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 渲染方案

选择生涯插件

与指定实体交互时,可以选择自己的对话;与指定舰队开战时,也可以改用专用战场。CampaignPlugin 在这些请求发生时提供候选,本节用它处理 Foundry 和测试巡逻队,再在市场 Memory 读取时生成当前事实,说明选择规则怎样进入实际生涯功能。

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

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

学习目标

完成后,您将能够:

  • 使用稳定插件 ID 管理注册替换、读档重登和显式注销;
  • 根据目标对象返回 PluginPick,并用 null 把不匹配对象交给其他插件;
  • 使用 PickPriority 控制多个非空候选之间的最终选择;
  • 在 updateMarketFacts() 中使用传入 Memory 写入零过期时间的动态事实。

前置知识

  • [C7-S1] 的 BaseModPlugin.onGameLoad() 与新档、读档入口;
  • [C10-S4] 的实体与市场 Memory 读写;
  • [C11-S1] 的测试舰队创建和投放;
  • [C14-S2] 的回调注册、对象过滤和读档恢复边界。

登记本次生涯的选择规则

创建 src/data/scripts/gearin/campaign/GearInCampaignPlugin.java,继承 BaseCampaignPlugin。基类为全部选择与事实方法准备了默认实现,子类只覆写本次交互、战场和市场事实;其余请求继续返回默认结果。

在 GearInModPlugin 添加该类的 import,向已有 onGameLoad(...) 追加局部登记,沿用 sector:

sector.registerPlugin(new GearInCampaignPlugin());

完整类的 getId() 返回 GearIn_campaign_plugin,isTransient() 返回 true。选择规则没有需要保存的字段,目标状态留在实体与市场 Memory;每次载入生涯重新创建规则实例。

相同非空 ID 的登记会替换原插件。 当前入口重复执行时,注册表仍保留一份实现;游戏随后需要对话或战场,才调用该实例的选择方法。

标记 Foundry 与测试巡逻队

对话目标使用 Foundry 的主实体,市场事实使用 Foundry 的 MarketAPI,战场目标使用巡逻队。三个对象各保存自己的标记,选择方法读取实际传入对象。

在 [C12-S1] 的 GearInEconomyProbe.ProbeListener.economyUpdated() 已取得 market 后,加入局部片段,并添加 SectorEntityToken、MemoryAPI 与本节插件的 import:

SectorEntityToken dialogTarget = market.getPrimaryEntity();
dialogTarget.getMemoryWithoutUpdate().set(
        GearInCampaignPlugin.DIALOG_TARGET_KEY, true);

MemoryAPI marketMemory = market.getMemoryWithoutUpdate();
marketMemory.set(GearInCampaignPlugin.MARKET_FACTS_TARGET_KEY, true);
marketMemory.unset(GearInCampaignPlugin.MARKET_READY_KEY);
boolean readyBeforeRefresh = marketMemory.getBoolean(
        GearInCampaignPlugin.MARKET_READY_KEY);
boolean readyAfterRefresh = market.getMemory().getBoolean(
        GearInCampaignPlugin.MARKET_READY_KEY);
LOG.info("GearIn campaign facts: before=" + readyBeforeRefresh
        + " after=" + readyAfterRefresh);

在 [C11-S1] 的 GearInFleetGenerator 投放前再给当前 generatedFleet 写入战场标记,局部片段如下:

generatedFleet.getMemoryWithoutUpdate().set(GearInCampaignPlugin.BATTLE_TARGET_KEY, true);

实体标记决定使用专用对话,市场标记决定计算 ready 事实。片段先清除旧 ready 值,再分别通过普通读取与事实刷新取得结果,二者的区别在后文展开。巡逻队则在开战前取得独立战场标记。

为标记实体选择交互对话框

玩家开始交互时,pickInteractionDialogPlugin(...) 接收当前目标。本例从实体的 getMemoryWithoutUpdate() 读取 $GearIn_dialog_target,命中后返回专用对话与优先级:

if (interactionTarget == null
        || !interactionTarget.getMemoryWithoutUpdate()
                .getBoolean(DIALOG_TARGET_KEY)) {
    return null;
}
return new PluginPick<>(
        new GearInTargetDialog(interactionTarget),
        PickPriority.MOD_SPECIFIC);

不匹配的请求返回 null。 这样其他生涯插件继续提供自己的实现,当前规则只处理被标记的实体。标记在交互前写入,选择方法读取已有状态;调用 getMemoryWithoutUpdate() 也让判断保持在当前 Memory 上。

GearInTargetDialog 保存命中的目标,在初始化时显示 ID、标记与市场 ready,再提供关闭选项。Foundry 有市场,因此读取其当前事实;其他无市场目标显示 false。选项用稳定 optionData 判断,显示文字用于界面。

这个类只展示选择结果怎样进入对话。[C15-S1] 再展开三个面板、选项与关闭过程,本节先让玩家实际打开被选中的实现。

为标记舰队选择战场创建插件

战斗建立时,pickBattleCreationPlugin(...) 同样接收生涯实体。先限定为 CampaignFleetAPI,再检查 $GearIn_battle_target,命中后返回 GearInObjectiveBattleCreationPlugin:

if (!(opponent instanceof CampaignFleetAPI)
        || !opponent.getMemoryWithoutUpdate().getBoolean(BATTLE_TARGET_KEY)) {
    return null;
}
return new PluginPick<>(
        new GearInObjectiveBattleCreationPlugin(),
        PickPriority.MOD_SPECIFIC);

这个战场类继承原版 BattleCreationPluginImpl。在 super.initBattle(...) 前把 context.forceObjectivesOnMap 设为 true,让父类创建地图时采用这个条件,双方舰队装入、地图尺寸与部署时间继续沿用父类处理。

objectivesAllowed 为 true 时,小规模战斗也生成战术目标;为 false 时仍按许可跳过。当前只改变这项生成条件。标记在战斗建立前准备,战场已经选定后再改 Memory,对这场已建立的战斗没有追溯作用。

比较候选优先级与插件 ID

每个选择请求都会调用当前注册插件的相应方法,收集非空 PluginPick<T>。它保存 plugin 与 priority:没有候选时返回 null,一份候选直接采用,多份则选择枚举序号最大的优先级。

选择优先级速查表

优先级适用范围选择关系
CORE_GENERAL原版通用实现枚举序号最低
MOD_GENERALMOD 全局替换高于通用原版实现
CORE_SET原版对象集合规则高于 general 级
MOD_SETMOD 对一组对象的规则高于 CORE_SET
CORE_SPECIFIC原版特定遭遇高于 set 级
MOD_SPECIFICMOD 特定对象或遭遇高于 CORE_SPECIFIC
HIGHEST有依据的冲突裁决枚举序号最高

本节只处理标记的特定对象,采用 MOD_SPECIFIC。下面的局部片段把同一对话包装为 set 与 specific 候选,后者优先:

PluginPick<InteractionDialogPlugin> setPick = new PluginPick<>(
        new GearInTargetDialog(dialogTarget), PickPriority.MOD_SET);
PluginPick<InteractionDialogPlugin> specificPick = new PluginPick<>(
        new GearInTargetDialog(dialogTarget), PickPriority.MOD_SPECIFIC);

相同优先级的竞争没有公开稳定的胜者约定,需要唯一结果时,让命中条件互斥或明确区分优先级。编者认为优先级应当表达接管范围:全局替换给特定遭遇保留位置,剧情对象再由专用规则处理。使用 HIGHEST 时,当前候选还会压过同一目标上的其他专用规则,因此选择它要有实际冲突与明确的裁决依据。

ID 决定注册替换

插件 ID 与优先级处理不同关系。ID 在登记时决定是否替换旧对象,优先级在请求时决定采用哪份候选。连续登记同一 ID 的局部实验如下:

sector.registerPlugin(new GearInCampaignPlugin());
sector.registerPlugin(new GearInCampaignPlugin());

第二次登记成为当前实例。基类默认 ID 为 null,这时每次登记追加对象,所有实例都会参与选择与事实更新;本例显式提供稳定 ID,便于替换和注销。

isTransient() 决定插件是否进入存档。基类默认 true,本例每次 onGameLoad() 重登;确实保存插件字段的实现可以返回 false,在新档时登记并随档恢复。长期对象状态放在哪里,要结合功能实际持有的对象决定。

在读取 Memory 时更新市场 facts

生涯插件还提供读取时计算的 facts。对象的 getMemory() 调用当前插件,传入对象与 Memory;getMemoryWithoutUpdate() 则取得已有值。本节的 updateMarketFacts(...) 检查市场标记,向传入 memory 写入 $GearIn_campaign_plugin_ready。

因此准备片段在 unset(...) 后,readyBeforeRefresh 为 false;调用 market.getMemory() 后,更新方法写入 ready,readyAfterRefresh 为 true。对话也用目标市场的 getMemory(),显示本次读取后的事实。

Facts 更新入口速查表

更新方法触发对象典型用途
updateEntityFacts(entity, memory)生涯实体位置、标签和交互条件
updatePersonFacts(person, memory)人物身份、关系与角色状态
updateFactionFacts(faction, memory)势力势力关系与全局规则
updateGlobalFacts(memory)星区全局 Memory全局进度与环境条件
updatePlayerFacts(memory)玩家角色数据玩家能力与任务条件
updateMarketFacts(market, memory)市场规模、产业、管理与市场交互条件

其他对象各有对应事实入口,仍使用当前方法收到的对象与 Memory。动态键采用 MOD 前缀,并用 memory.set(key, value, 0f) 写本次事实,下一次推进过期,后续读取时重新计算。

facts 更新中使用传入的 Memory

在 updateMarketFacts() 中再次调用 market.getMemory(),会递归触发相同更新。当前值和输入条件都从方法参数 memory 读取,写入也使用这份对象。

注销插件并观察恢复结果

调用 Global.getSector().unregisterPlugin(GearInCampaignPlugin.PLUGIN_ID) 按稳定 ID 注销。当前插件退出后续选择与事实刷新,已经打开的对话和已经建立的战斗各自继续原流程。

再次交互时,其他候选重新处理目标。验证 facts 时先从 getMemoryWithoutUpdate() 清除 ready,再调用 getMemory(),才能确认本插件没有重新写入;现有 Memory 值与插件是否登记分别观察。

完整最小例

三个类保存到前述路径,登记和三类标记分别采用上面的当前入口。

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

import com.fs.starfarer.api.PluginPick;
import com.fs.starfarer.api.campaign.BaseCampaignPlugin;
import com.fs.starfarer.api.campaign.BattleCreationPlugin;
import com.fs.starfarer.api.campaign.CampaignFleetAPI;
import com.fs.starfarer.api.campaign.InteractionDialogPlugin;
import com.fs.starfarer.api.campaign.SectorEntityToken;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.campaign.rules.MemoryAPI;

/**
 * 根据对象 Memory 选择交互与战场插件,并在市场 Memory 刷新时写入动态事实。
 */
public class GearInCampaignPlugin extends BaseCampaignPlugin {

    public static final String PLUGIN_ID = "GearIn_campaign_plugin";
    public static final String DIALOG_TARGET_KEY = "$GearIn_dialog_target";
    public static final String BATTLE_TARGET_KEY = "$GearIn_battle_target";
    public static final String MARKET_FACTS_TARGET_KEY = "$GearIn_market_facts_target";
    public static final String MARKET_READY_KEY = "$GearIn_campaign_plugin_ready";

    @Override
    public String getId() {
        return PLUGIN_ID;
    }

    @Override
    public boolean isTransient() {
        return true;
    }

    @Override
    public PluginPick<InteractionDialogPlugin> pickInteractionDialogPlugin(
            SectorEntityToken interactionTarget) {
        if (interactionTarget == null
                || !interactionTarget.getMemoryWithoutUpdate().getBoolean(DIALOG_TARGET_KEY)) {
            return null;
        }
        return new PluginPick<>(
                new GearInTargetDialog(interactionTarget),
                PickPriority.MOD_SPECIFIC);
    }

    @Override
    public PluginPick<BattleCreationPlugin> pickBattleCreationPlugin(
            SectorEntityToken opponent) {
        if (!(opponent instanceof CampaignFleetAPI)
                || !opponent.getMemoryWithoutUpdate().getBoolean(BATTLE_TARGET_KEY)) {
            return null;
        }
        return new PluginPick<>(
                new GearInObjectiveBattleCreationPlugin(),
                PickPriority.MOD_SPECIFIC);
    }

    @Override
    public void updateMarketFacts(MarketAPI market, MemoryAPI memory) {
        if (memory.getBoolean(MARKET_FACTS_TARGET_KEY)) {
            memory.set(MARKET_READY_KEY, true, 0f);
        }
    }
}
完整成品:GearInTargetDialog
package data.scripts.gearin.campaign;

import java.util.Collections;
import java.util.Map;

import com.fs.starfarer.api.campaign.InteractionDialogAPI;
import com.fs.starfarer.api.campaign.InteractionDialogPlugin;
import com.fs.starfarer.api.campaign.SectorEntityToken;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.campaign.rules.MemoryAPI;
import com.fs.starfarer.api.combat.EngagementResultAPI;

/**
 * 显示 Campaign 插件选中的交互目标及其市场事实。
 */
public class GearInTargetDialog implements InteractionDialogPlugin {

    private static final String OPTION_CLOSE = "GearIn_close_campaign_plugin_dialog";

    private final SectorEntityToken target;
    private InteractionDialogAPI dialog;

    public GearInTargetDialog(SectorEntityToken target) {
        this.target = target;
    }

    @Override
    public void init(InteractionDialogAPI dialog) {
        this.dialog = dialog;
        boolean dialogTarget = target.getMemoryWithoutUpdate()
                .getBoolean(GearInCampaignPlugin.DIALOG_TARGET_KEY);
        MarketAPI market = target.getMarket();
        boolean marketReady = market != null
                && market.getMemory().getBoolean(GearInCampaignPlugin.MARKET_READY_KEY);

        dialog.getTextPanel().addPara("GearIn CampaignPlugin 选择了这个交互对话框。");
        dialog.getTextPanel().addPara("目标 ID:" + target.getId());
        dialog.getTextPanel().addPara("对话标记:" + dialogTarget);
        dialog.getTextPanel().addPara("市场 ready fact:" + marketReady);
        dialog.getOptionPanel().addOption("关闭", OPTION_CLOSE);
        dialog.setOptionOnEscape("关闭", OPTION_CLOSE);
    }

    @Override
    public void optionSelected(String optionText, Object optionData) {
        if (OPTION_CLOSE.equals(optionData)) {
            dialog.dismiss();
        }
    }

    @Override
    public void optionMousedOver(String optionText, Object optionData) {
    }

    @Override
    public void advance(float amount) {
    }

    @Override
    public void backFromEngagement(EngagementResultAPI battleResult) {
    }

    @Override
    public Object getContext() {
        return target;
    }

    @Override
    public Map<String, MemoryAPI> getMemoryMap() {
        return Collections.emptyMap();
    }
}
完整成品:GearInObjectiveBattleCreationPlugin
package data.scripts.gearin.campaign;

import com.fs.starfarer.api.combat.BattleCreationContext;
import com.fs.starfarer.api.impl.combat.BattleCreationPluginImpl;
import com.fs.starfarer.api.mission.MissionDefinitionAPI;

/**
 * 沿用原版战场创建流程,并为匹配的战斗强制生成战术目标。
 */
public class GearInObjectiveBattleCreationPlugin extends BattleCreationPluginImpl {

    @Override
    public void initBattle(BattleCreationContext context, MissionDefinitionAPI api) {
        context.forceObjectivesOnMap = true;
        super.initBattle(context, api);
    }
}

验证方案

前置数据:已启用包含 [C14-S3] 三个完整类的测试 MOD;准备一个可交互实体、一支可开战的敌舰队和一个市场,并能够在三者 Memory 中写入测试标记。

  1. 编译 GearInCampaignPlugin、GearInTargetDialog 和 GearInObjectiveBattleCreationPlugin。
  2. 在 onGameLoad() 注册 GearInCampaignPlugin,核对 getId() 返回 GearIn_campaign_plugin 且 isTransient() 返回 true。
  3. 在同一次 onGameLoad(...) 使用前面的两次同 ID 登记,比较交互与 facts 行为仍执行一份当前实现。
  4. 给交互实体写入 $GearIn_dialog_target 并开始交互,核对对话框显示的目标 ID、标记和市场 ready 状态。
  5. 与未标记实体交互,确认当前插件返回 null,并观察其他插件提供的交互结果。
  6. 给测试敌舰队写入 $GearIn_battle_target 并开战,观察小规模战场中的战术目标。
  7. 与未标记敌舰队开战,确认战场采用其他 Campaign 插件提供的规则。
  8. 给市场写入 $GearIn_market_facts_target,依次读取 getMemoryWithoutUpdate() 和 getMemory(),比较 ready 键。
  9. 阅读 set 与 specific 的局部候选,按优先级表比较最终选择;实际完整插件采用 MOD_SPECIFIC,匹配与未匹配对象各自观察。
  10. 保存并读档,确认 onGameLoad() 重新登记 transient 插件,插件集合中只有一个 GearIn_campaign_plugin。
  11. 调用 unregisterPlugin(GearInCampaignPlugin.PLUGIN_ID),再次测试对话与战场选择。
  12. 注销后从市场 Memory 清除 ready 键,再调用 market.getMemory(),确认本插件不再写入该事实。

预期结果:

  • 三个 Java 类编译完成,加载期没有类缺失或方法签名错误;
  • 相同非空 ID 的新插件替换旧实例,集合中只有当前实例;
  • 标记实体使用 GearInTargetDialog,对话框显示正确目标 ID、对话标记和市场 ready 状态;
  • 未标记实体使当前 picker 返回 null,其他插件仍可提供交互;
  • 标记敌舰队使用 GearInObjectiveBattleCreationPlugin,战场生成战术目标;
  • 未标记敌舰队不使用本节战场插件;
  • getMemoryWithoutUpdate() 不触发 ready 写入,getMemory() 刷新后 ready 为 true;
  • MOD_SPECIFIC 候选高于 MOD_SET 候选并成为最终结果;
  • 读档后 transient 插件重新登记,稳定 ID 避免重复实例;
  • 注销后本插件停止参与新的对话、战场和市场 facts 更新。

故障分析

  • 读档后所有自定义选择都消失:插件返回 transient,但 onGameLoad() 没有重新登记实例。

    • 解决方案:每次 onGameLoad() 调用 registerPlugin(new GearInCampaignPlugin())。
  • 相同插件 ID 的两个实现没有并存:注册器把非空 ID 作为替换键,新实例登记前会移除旧实例。

    • 解决方案:需要竞争的插件使用不同稳定 ID;同一功能的新版本继续使用相同 ID 完成替换。
  • 每次读档后 picker 调用次数增加:getId() 返回 null,每次登记都向集合追加新实例。

    • 解决方案:让 getId() 始终返回 GearIn_campaign_plugin。
  • 未标记实体也打开自定义对话框:交互 picker 在条件不匹配时仍返回了 PluginPick。

    • 解决方案:先判空并检查 $GearIn_dialog_target,任一条件不成立时返回 null。
  • MOD_SPECIFIC 候选没有获选:另一个插件返回了枚举序号更高的优先级,或本插件因过滤条件返回 null。

    • 解决方案:记录每个候选的命中条件和 PickPriority,核对目标 Memory 与全部非空候选。
  • 标记敌舰队的小规模战斗没有战术目标:战场插件在父类创建地图之后才修改 forceObjectivesOnMap。

    • 解决方案:在 super.initBattle(context, api) 之前设置 context.forceObjectivesOnMap = true。
  • 读取 getMemoryWithoutUpdate() 后 ready 仍为 false:该入口只返回当前 Memory,不执行 Campaign 插件的 facts 更新。

    • 解决方案:需要刷新事实时调用 market.getMemory(),再从返回对象读取 ready 键。
  • 读取市场 Memory 时发生递归调用:updateMarketFacts() 内再次调用了 market.getMemory()。

    • 解决方案:只读取和写入方法参数 memory,需要市场属性时直接使用参数 market。
  • 动态 facts 在 Memory 中形成长期过期项:更新方法为每次读取都重算的键设置了正数期限。

    • 解决方案:使用 memory.set(MARKET_READY_KEY, true, 0f) 写入当前刷新事实。
  • 调用注销后插件仍在参与选择:传给 unregisterPlugin() 的字符串与插件 getId() 不一致。

    • 解决方案:注销时统一使用 GearInCampaignPlugin.PLUGIN_ID。

总结

Foundry 的交互与巡逻队战场在实际请求中取得专用候选,目标标记决定命中范围,优先级决定候选竞争。稳定插件 ID 负责登记替换与注销,选择结果再进入对话或地图创建。

市场 facts 在读取 Memory 时更新,使用传入对象计算当前值。长期目标状态与读取时事实分别保存,注册、匹配与刷新各自明确,生涯插件就能与其他功能共同处理当前请求。

建议参考项

  • com.fs.starfarer.api.campaign.CampaignPlugin:插件 ID、存档性质、选择优先级、picker 和 facts 更新合同;
  • com.fs.starfarer.api.campaign.BaseCampaignPlugin:全部 Campaign 插件方法的默认实现;
  • com.fs.starfarer.api.PluginPick:候选插件与 PickPriority 的组合对象;
  • com.fs.starfarer.api.campaign.SectorAPI:Campaign 插件注册与注销入口;
  • com.fs.starfarer.api.campaign.InteractionDialogPlugin:交互对话框的初始化、选项和状态接口;
  • com.fs.starfarer.api.impl.combat.BattleCreationPluginImpl:原版舰队装载、地图创建和部署规则实现;
  • com.fs.starfarer.api.campaign.rules.MemoryAPI:目标标记、动态 facts 和过期时间读写。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
上一页
监听生涯事件
下一页
管理舰队活动情报