Starsector Nihilism Guidance
首页
GitHub 项目
首页
GitHub 项目
  • 首页
  • 基础及舰船相关
    • MOD 骨架和入口文件
    • 制作一艘基本舰船
    • 制作一艘航母与战机
    • 制作一艘相位舰
    • 制作火炮、光束与导弹
    • 为舰船加入内置武器、船插与战机
    • 补充描述文本
  • 无需入门的代码修改
    • 以船插为例初步熟悉代码
    • 制作完整船插
    • 制作最小战术系统
    • 为武器加入命中与光束特效
  • 势力与生涯星系
    • 注册一个势力
    • 配置势力舰队
    • 制作 MOD 入口类的最基础适配
    • 制作一个生涯星系
    • 补完描述文本
  • 战役与无代码对象
    • 制作测试战役装配
    • 为模拟战与标题画面加入装配
    • 为舰船加入外观风格
    • 制作一艘模块船
    • 制作一种星体类型
    • 制作一个自定义实体
  • 音频资源与播放
    • 准备 OGG 音频资源
    • 注册音频资源
    • 为游戏对象加入音频
  • Java 工程化
    • 确认 Java 与编译依赖
    • 迁移代码文件到 src
    • 配置 Gradle 工程
    • 构建 JAR 并声明产物
  • 全局服务
    • 制作 MOD 入口类
    • 使用 Global 访问游戏服务
    • 读取设置
    • 调用声音播放器
    • 记录和定位 MOD 日志
  • 战斗运行时
    • 读取战斗引擎状态
    • 读取战斗对象
    • 制作每帧战斗插件
    • 修改舰船数值
    • 配置武器脚本
    • 配置战斗监听器
    • 制作分层渲染插件
  • 战斗内容扩展
    • 详解舰船插件
    • 详解战术系统
    • 详解武器动画
  • 生涯世界与基础服务
    • 读取生涯世界状态
    • 构建生涯地点
    • 配置生涯势力
    • 放置生涯实体
  • 生涯舰队与舰队成员
    • 生成生涯舰队
    • 管理舰队中的成员
    • 舰队状态的重算过程
    • 维护舰队成员状态
    • 读取并修改舰船装配
  • 市场与商品
    • 创建生涯市场
    • 读写货舱内容
    • 注册市场商品
    • 实现专用子市场
    • 制作专用产业
    • 注册特殊物品
    • 实现市场条件
  • 人物与技能
    • 配置人物基础
    • 让人物承担角色
    • 制作角色技能
  • 生涯脚本与情报事件
    • 编写生涯逐帧脚本
    • 监听生涯事件
    • 选择生涯插件
    • 管理舰队活动情报
  • 酒吧事件与任务
    • 组织生涯交互对话
    • 实现对话插件
    • 接入酒吧事件
    • 构建阶段任务
    • 接入酒吧任务
  • Rules 系统、命令与内存
    • 组织 Rules 规则表
    • 编排 Rules 对话
    • 接入 Rules 生涯场景
    • 编写 Rules 命令
    • 使用 MemoryAPI 保存状态
  • 文件读取与写入
    • 读取 MOD 内文本
    • 读取 MOD 内 JSON 文件
    • 读取 MOD 内 CSV
    • 读写 common 数据
    • 实现本地化机制
  • 常用依赖库
    • 接入常用依赖库
    • 使用 LazyLib 几何工具
    • 使用 LazyLib 查询战场
    • 使用 LazyLib 保存公共 JSON
    • 注册 GraphicsLib 光照数据
    • 创建 GraphicsLib 动态光源
    • 制作 GraphicsLib 纹理映射
    • 配置 MagicLib 自动尾迹
    • 创建 MagicLib 运行时尾迹
    • 配置 MagicLib 数据赏金
    • 使用 MagicLib 辅助生涯开发
    • 使用 MagicLib 战斗小工具
  • 可选依赖与 Nexerelin
    • 接入可选依赖
    • 登记 Nexerelin 势力与外交配置
    • 配置 Nexerelin 的两种星区开局
    • 配置 Nexerelin 战略 AI 与扩张规模
    • 登记 Nexerelin 地面战单位与产业
    • 为 Nexerelin 特遣队添加命名与事件响应
    • 制作即时生效的 LunaLib 设置面板
  • 着色器与渲染
    • 使用 Java OpenGL
    • 使用 GLSL 进阶
    • 使用 VBO 和 VAO 保存顶点
    • 使用 SSBO 传递参数
    • 使用实例化进行绘制
    • 选择 BoxUtil 渲染方案

制作 MOD 入口类

MOD 入口接收游戏加载、新档、读档与保存时的回调。代码放在哪个方法里,决定它何时执行,也决定它能使用哪些游戏对象。本节整理 GearInModPlugin,保留已有星系、关系和声音,在这些回调中安排一次性初始化与阶段日志。

本节预期阅读时间:~10 分钟。

本节预期操作时间:~15 分钟。

学习目标

完成后,您将能够:

  • 在 JAR 和 mod_info 中提供有效入口;
  • 按应用、新档、读档、启用与保存阶段选择回调;
  • 用 persistent data 记录每个存档的初始化;
  • 通过日志和保存/读取结果核对调用。

前置知识

  • [C3-S3] 的入口与势力关系;
  • [C3-S4] 的 GearInSystemGenerator;
  • [C5-S3] 的 UI 反馈;
  • [C6-S2] 至 [C6-S4] 的源码目录、构建与加载;
  • Java 继承、方法覆写、Map 与字符串。

让游戏加载入口类

入口仍使用 GearInModPlugin,源码保存在 src/data/scripts/gearin/GearInModPlugin.java。构建之后,游戏通过 mod_info.json 的 modPlugin 字段找到它。已有 MOD 信息保留实际内容,加载声明采用下面的关系:

{
    "id": "gear_in",
    "name": "齿轮工业",
    "author": "YourName",
    "version": {"major": 0, "minor": 1, "patch": 0},
    "description": "Starsector MOD 教程的练习项目。",
    "gameVersion": "0.98a-RC8",
    "jars": ["jars/GearIndustry.jar"],
    "modPlugin": "data.scripts.gearin.GearInModPlugin"
}

jars 提供包含代码的文件,modPlugin 指定其中的入口类。 填写实际作者、版本等信息,保留已经登记的数据。游戏取得这个类后,将生命周期回调交给它。

入口继承 BaseModPlugin。基类已经实现未使用的方法,我们只覆写本节需要的回调。接下来按游戏实际运行的过程安排代码;完整类在后面集中给出。

应用加载后读取配置

onApplicationLoad() 在核心加载与脚本编译完成后执行,这时可以使用设置服务。将下面的局部方法放入入口类,记录这次回调与游戏版本:

@Override
public void onApplicationLoad() throws Exception {
    log("onApplicationLoad version=" + Global.getSettings().getGameVersion());
}

这条日志用于确认回调已经发生。MOD 信息中的 gameVersion 与工程编译目标仍按第六章配置;这里读取设置,不改变它们。

应用加载时还没有当前生涯。星系、势力关系和存档数据要等新档创建或读档完成后处理,因此生成器也不能放在这个方法里。

新档生成与读档分别处理

创建新的生涯时,onNewGame() 负责生成本 MOD 的星系并设置初始关系。这里沿用第三章的生成器和关系方法:

@Override
public void onNewGame() {
    new GearInSystemGenerator().generate(Global.getSector());
    seedRelations(Global.getSector());
    log("onNewGame");
}

星系与关系随后进入存档,读档时继续使用保存的结果。若把生成调用放在每次读档都会执行的方法里,同一个世界便会反复收到创建请求。

onGameLoad(boolean newGame) 在新档和读档时都会执行,newGame 表示本次是否为新档。进入生涯的声音、需要重新取得的对象,以及每次加载都要登记的运行期功能,都可以在这里处理。新档生成使用 onNewGame(),每次进入存档的处理使用 onGameLoad(...)。

为每个存档记录初始化结果

还有一种操作:它需要在当前存档中执行一次,之后读档继续保留结果。本例让齿轮工业参与人物赏金事件,将以下局部片段放入 onGameLoad(...):

SectorAPI sector = Global.getSector();
if (!sector.getPersistentData().containsKey(CLASS_MARK)) {
    SharedData.getData().getPersonBountyEventData().addParticipatingFaction("gear_industry");
    sector.getPersistentData().put(CLASS_MARK, true);
    log("first-entry initialization (CLASS_MARK gate opened)");
}

CLASS_MARK 的值为 GearInModPlugin。首次进入时,persistent data 中没有这个键,于是加入参与势力,再写入完成标记。标记随存档保存,后续加载同一存档时就会跳过初始化。

这里保存的是某个生涯的完成状态,因此使用 SectorAPI.getPersistentData()。类的 static 字段跟随游戏进程:同一进程可以先后加载不同存档,重新启动又会重新建立静态状态。每存档一次的条件,要由该存档保存。

第五章的 UI 反馈仍在每次 onGameLoad(...) 中播放。把它放在标记判断之外,首次进入与后续读档都能听到;初始化则继续只执行一次。两项功能在同一个回调里,却有不同的执行条件。

接收启用与保存通知

onEnabled(boolean wasEnabledBefore) 用于新档或首次向当前存档启用 MOD 的处理,参数说明这个存档以前是否曾启用过本 MOD。已有启用状态的正常读档继续使用 onGameLoad(...)。完整例在启用回调中记录参数,以便与读档日志对应。

保存过程则有自己的通知:写入前执行 beforeGameSave(),成功后执行 afterGameSave(),失败时执行 onGameSaveFailed()。加入以下局部方法,记录保存前后的调用:

@Override
public void beforeGameSave() {
    log("beforeGameSave");
}

@Override
public void afterGameSave() {
    log("afterGameSave");
}

失败回调也保留相同的日志写法。日志用于确认实际调用,初始化标记用于控制功能是否执行;保存通知本身不会替代这个标记。后续入口增加内容时,仍按各项操作所需的对象与时机安排。

完整最小例

保存入口类并继续使用 [C3-S4] 的生成器源码。src 中的两个包路径参与同一个 MOD 构建,关系设置与 UI 反馈由当前入口保留。

完整成品:GearInModPlugin
package data.scripts.gearin;

import com.fs.starfarer.api.BaseModPlugin;
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.FactionAPI;
import com.fs.starfarer.api.campaign.RepLevel;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.fs.starfarer.api.impl.campaign.shared.SharedData;

import data.scripts.gearin.campaign.GearInSystemGenerator;

/** 接收MOD生命周期回调,生成新档内容并记录存档初始化与保存。 */
public class GearInModPlugin extends BaseModPlugin {

    public static final String CLASS_MARK = "GearInModPlugin";

    @Override
    public void onApplicationLoad() throws Exception {
        log("onApplicationLoad version=" + Global.getSettings().getGameVersion());
    }

    @Override
    public void onEnabled(boolean wasEnabledBefore) {
        log("onEnabled wasEnabledBefore=" + wasEnabledBefore);
    }

    @Override
    public void onNewGame() {
        new GearInSystemGenerator().generate(Global.getSector());
        seedRelations(Global.getSector());
        log("onNewGame");
    }

    @Override
    public void onGameLoad(boolean newGame) {
        SectorAPI sector = Global.getSector();
        if (!sector.getPersistentData().containsKey(CLASS_MARK)) {
            SharedData.getData().getPersonBountyEventData().addParticipatingFaction("gear_industry");
            sector.getPersistentData().put(CLASS_MARK, true);
            log("first-entry initialization (CLASS_MARK gate opened)");
        }
        Global.getSoundPlayer().playUISound("GearIn_ui_feedback", 1f, 1f);
        log("onGameLoad newGame=" + newGame);
    }

    @Override
    public void beforeGameSave() {
        log("beforeGameSave");
    }

    @Override
    public void afterGameSave() {
        log("afterGameSave");
    }

    @Override
    public void onGameSaveFailed() {
        log("onGameSaveFailed");
    }

    private void seedRelations(SectorAPI sector) {
        FactionAPI gear = sector.getFaction("gear_industry");
        gear.setRelationship("player", 0f);
        gear.setRelationship("pirates", RepLevel.HOSTILE);
        gear.setRelationship("hegemony", RepLevel.FRIENDLY);
        gear.setRelationship("tritachyon", RepLevel.HOSTILE);
    }

    private static void log(String message) {
        Global.getLogger(GearInModPlugin.class).info("GearIn lifecycle: " + message);
    }
}

下面是当前生成器的完整形态,保存为 src/data/scripts/gearin/campaign/GearInSystemGenerator.java。它承接第三章的 Second 描述绑定和第四章的余烬世界、导航锚;对应数据继续使用前文已经登记的内容。

完整成品:GearInSystemGenerator
package data.scripts.gearin.campaign;

import java.awt.Color;

import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.JumpPointAPI;
import com.fs.starfarer.api.campaign.PlanetAPI;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.fs.starfarer.api.campaign.SectorEntityToken;
import com.fs.starfarer.api.campaign.SectorGeneratorPlugin;
import com.fs.starfarer.api.campaign.StarSystemAPI;
import com.fs.starfarer.api.campaign.econ.MarketAPI;

/** 创建齿轮工业星系,使用自定义行星、导航锚与独立描述。 */
public class GearInSystemGenerator implements SectorGeneratorPlugin {

    @Override
    public void generate(SectorAPI sector) {
        StarSystemAPI system = sector.createStarSystem("GearIn");
        system.setBackgroundTextureFilename("graphics/backgrounds/background2.jpg");
        system.setLightColor(new Color(200, 200, 210));

        PlanetAPI star = system.initStar("gearin", "star_yellow", 600f, 550f, 3f, 0.75f, 1f);
        system.getLocation().set(-16000f, -9000f);

        PlanetAPI prime = system.addPlanet(
                "GearIn_planet1", star, "GearIn Prime", "GearIn_ember_world", 55, 150, 3200, 120);
        PlanetAPI second = system.addPlanet(
                "GearIn_planet2", star, "GearIn Second", "barren", 200, 90, 5200, 200);
        second.setCustomDescriptionId("GearIn_description_second");

        SectorEntityToken stable = system.addCustomEntity(
                "GearIn_stable", "GearIn 导航锚", "GearIn_navigation_anchor", "gear_industry");
        stable.setCircularOrbitPointingDown(star, 90, 2200, 100);

        MarketAPI market = Global.getFactory().createMarket(prime.getId(), prime.getName(), 4);
        market.setFactionId("gear_industry");
        market.setPrimaryEntity(prime);
        market.setSurveyLevel(MarketAPI.SurveyLevel.FULL);
        market.addCondition("population_4");
        market.addIndustry("population");
        market.addIndustry("spaceport");
        market.addSubmarket("open_market");
        market.addSubmarket("storage");
        sector.getEconomy().addMarket(market, true);
        prime.setMarket(market);
        prime.setFaction("gear_industry");

        JumpPointAPI jump = Global.getFactory().createJumpPoint("GearIn_jump", "GearIn 跳跃点");
        jump.setCircularOrbit(star, 300, 1800, 100);
        jump.setRelatedPlanet(prime);
        system.addEntity(jump);
        system.autogenerateHyperspaceJumpPoints(true, true);
    }
}

验证方案

  1. 构建 JAR,核对入口与生成器 class 条目及 mod_info 声明。
  2. 启动游戏,查看 GearIn lifecycle 的应用加载日志和版本。
  3. 创建生涯,核对 GearIn 星系、初始关系、onNewGame、启用、首次初始化与 onGameLoad newGame=true。
  4. 保存后读取同一存档,核对 onGameLoad newGame=false 与 UI 反馈。
  5. 手动保存,比较 beforeGameSave 和 afterGameSave 记录。

预期结果:

  • 入口类有效,应用加载阶段记录实际版本。
  • 新档创建星系和关系,首次初始化写入持久标记。
  • 保存后的同一存档读取时继续使用标记,一次性初始化日志保持一次。
  • 每次进入生涯播放 UI 反馈并记录当前 newGame 值。
  • 保存成功记录前后回调,失败路径由 onGameSaveFailed 记录。

故障分析

  • 入口类加载失败:JAR 条目、package、class 或 modPlugin 不同。

    • 解决方案:按完整类名核对产物与加载值,重新构建。
  • 入口转换时报 ClassCastException:登记的类没有实现 MOD 入口接口。

    • 解决方案:使用 BaseModPlugin 或正确的 ModPlugin 实现,再构建。
  • 应用加载时访问生涯抛空指针:当前 Sector 尚未建立。

    • 解决方案:将生涯操作安排到创建或读档回调。
  • 每次读档重复初始化:标记未写入存档数据,或当前读取的是不同存档。

    • 解决方案:核对 CLASS_MARK 键和 getPersistentData,在同一保存档测试。
  • 新档缺少原有星系:替换入口时遗漏生成器调用。

    • 解决方案:在 onNewGame 保留 generate 和关系设置,再创建生涯。

总结

同一个入口现在接收应用、新档、读档、启用与保存通知。已有星系只在新档生成,UI 反馈随每次进入播放,赏金参与状态则由存档标记控制。回调决定调用时机,功能自己的状态决定本次要不要执行。

后面的生涯功能仍从这个入口接入。生成过程继续交给生成器,入口负责在合适的阶段调用它,日志记录实际发生的过程。

建议参考项

  • mod_info.json:modPlugin 与 jars。
  • com.fs.starfarer.api.BaseModPlugin:默认入口实现。
  • com.fs.starfarer.api.ModPlugin:生命周期签名与调用约定。
  • com.fs.starfarer.api.campaign.SectorAPI:getPersistentData 存档数据。
  • com.fs.starfarer.api.impl.campaign.shared.SharedData:人物赏金参与势力。
  • Starsector/starsector-core/starsector.log:入口和回调结果。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
下一页
使用 Global 访问游戏服务