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

接入酒吧事件

本节把金属交易放进市场酒吧:工人收购 100 金属,支付 5000 信用点,玩家可以接受、拒绝或暂时离开。交易在一次对话中完成,邀请却可能继续留在酒吧中。我们分别处理事件生成、对话结束和活动期限,让暂时离开后的再次交互继续使用原事件。

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

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

学习目标

完成后,您将能够:

  • 说明 BarEventManager、creator、PortsideBarEvent 与 BarCMD 的职责;
  • 注册一个 Java creator 并控制事件权重和有效期;
  • 让事件按照市场条件出现在酒吧;
  • 通过稳定 optionData 区分接受、拒绝和离开;
  • 用货舱金属和 credits 完成一次性交易;
  • 区分拒绝结束、离开保留和未交互过期三种状态。

前置知识

  • [C14-S3] 的生涯实体、市场对象和交互对话入口;
  • [C15-S1] 的 InteractionDialogAPI、目标对象和关闭语义;
  • Java 接口实现、继承和布尔状态;
  • CargoAPI 的商品数量、移除商品和 credits 读写。

登记生成器,让管理器创建事件

创建 src/data/scripts/gearin/campaign/GearInBarEvent.java 与 GearInBarEventCreator.java。事件类实现当前交易,creator 类返回事件并提供权重与期限。在 GearInModPlugin 添加对应类及 BarEventManager 的 import,向已有 onGameLoad(...) 追加局部登记:

BarEventManager manager = BarEventManager.getInstance();
if (!manager.hasEventCreator(GearInBarEventCreator.class)) {
    manager.addEventCreator(new GearInBarEventCreator());
}

getInstance() 取得存档中的管理器,hasEventCreator(...) 按类型查询。已有生成器继续保留,缺失时才添加。addEventCreator(...) 直接追加候选项,重复登记会改变抽取机会。 因此登记放在生涯入口,事件每次对话只处理自己的状态。

creator 提供抽取参数

管理器在生涯推进中按权重选择可用 creator,调用 createBarEvent(),将返回的 PortsideBarEvent 放入活动跟踪器与酒吧事件集合。局部创建与权重如下:

@Override
public PortsideBarEvent createBarEvent() {
    return new GearInBarEvent();
}

@Override
public float getBarEventFrequencyWeight() {
    return 10f;
}

权重 10 只参与当前候选中的相对抽取,市场显示还要经过酒吧筛选。本例采用普通候选,保留基类默认优先级。活跃期限为 30 生涯日,未处理过期后的冷却为 30 日,通知已交互后的冷却为 60 日,完整 creator 分别返回这三个值。

活动期限与 creator 冷却分别计时

30 日活动期限跟随生成的事件实例。事件过期或被处理后,creator 再进入对应冷却;冷却期间停止生成下一份事件。

在太空港市场显示邀请

玩家进入市场酒吧时,BarCMD 从酒吧事件集合取得候选,调用 shouldShowAtMarket(...)。本例要求有太空港,并沿用基类 shownAt 约束,局部判断如下:

@Override
public boolean shouldShowAtMarket(MarketAPI market) {
    return market != null && market.hasSpaceport() && super.shouldShowAtMarket(market);
}

首次实际展示后,BarCMD 调用 wasShownAtMarket(...),基类保存该市场。同一实例此后留在这里,所以玩家补充货物后返回同一酒吧,可以继续原邀请。

筛选通过后,用 addPromptAndOption(...) 写描述与选项。这里的 optionData 使用事件对象本身,酒吧从这个对象识别需要交接的事件:

@Override
public void addPromptAndOption(InteractionDialogAPI dialog,
        Map<String, MemoryAPI> memoryMap) {
    dialog.getTextPanel().addPara(
            "A dockworker offers a credit payment for a shipment of refined metals.");
    dialog.getOptionPanel().addOption("Trade 100 metals for 5000 credits", this);
}

显示文字提供交易内容,this 把选择对应到当前实例。字符串选项会进入酒吧的另一条规则路径,[C15-S5] 使用那种入口接任务;这里让 Java 事件接收玩家的选择。

进入事件,重新建立本次选项

选择邀请后,BarCMD 创建 BarEventDialogPlugin 并接管当前会话,显式初始化。外壳保存原酒吧插件与事件,把初始化和选项回调转发给这份事件;事件结束后,再交回原插件。

事件的初始化只负责建立当前选项集合:

@Override
public void init(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap) {
    super.init(dialog, memoryMap);
    done = false;
    noContinue = false;
    options.clearOptions();
    options.addOption("Provide 100 metals", OPTION_ACCEPT);
    options.addOption("Refuse the trade", OPTION_DECLINE);
    options.addOption("Leave for now", OPTION_LEAVE);
}

super.init(...) 保存这次对话、面板与 Memory map,子类清理旧选项并建立交易分支。每次进入都将 done 与 noContinue 重置。 暂时离开保留事件实例,原对话已经完成,下一次进入要建立新的活动会话;实例中的 rewardGranted 则继续表示交易结果。

接受时检查货物,再完成交易

接受分支从玩家舰队取得货舱,确认 metals 至少为 100。满足条件后扣除货物、增加资金,并通知管理器;局部操作如下:

if (OPTION_ACCEPT.equals(optionData)) {
    CargoAPI cargo = Global.getSector().getPlayerFleet().getCargo();
    if (cargo.getCommodityQuantity(METALS) < METALS_REQUIRED) {
        text.addPara("You need 100 metals before this trade can be accepted.");
        return;
    }
    if (!rewardGranted) {
        cargo.removeCommodity(METALS, METALS_REQUIRED);
        cargo.getCredits().add(CREDIT_REWARD);
        rewardGranted = true;
        text.addPara("The dockworker takes the metals and pays you 5000 credits.");
        done = true;
        BarEventManager.getInstance().notifyWasInteractedWith(this);
    }
}

getCommodityQuantity(...) 取得当前数量,removeCommodity(...) 扣除 100,信用通过独立 MutableValue 增加 5000。[C12-S2] 已说明移除不足时可以部分扣除,所以数量判断放在写入前。货物不足只追加说明,交易与事件仍可继续。

成功后设置 rewardGranted,本次货物、资金和管理器通知只执行一次。notifyWasInteractedWith(this) 移除活动实例,并让对应 creator 进入交互后的冷却。外壳读取 done=true 后交还酒吧,当前成功分支保留默认的继续选项,让玩家读完结果再返回。

拒绝与暂时离开产生不同结果

拒绝表示邀请已经处理,写入结果后通知管理器并进入 60 日冷却,资金与货物保持原值。局部分支如下:

if (OPTION_DECLINE.equals(optionData)) {
    text.addPara("You refuse the trade and leave the dockworker behind.");
    done = true;
    noContinue = true;
    BarEventManager.getInstance().notifyWasInteractedWith(this);
}

暂时离开只结束这次会话,活动事件仍留在管理器中。这个分支没有已交互通知,局部处理如下:

if (OPTION_LEAVE.equals(optionData)) {
    text.addPara("You leave before making a decision.");
    done = true;
    noContinue = true;
}

noContinue=true 让外壳直接重建酒吧选项,省去继续按钮。它控制会话收尾,是否开始交互后冷却则取决于管理器通知。所以两种离开都能返回酒吧,后续邀请是否保留却不同。

让活动期限负责未处理的邀请

本例 shouldRemoveEvent() 沿用默认的 false,期限由管理器的 active 跟踪器处理。30 日耗尽后,实例离开活动集合,管理器回收对应关系并开始普通的 30 日冷却。事件自己的 advance(...) 与移除判断可用于额外状态,本次沿用管理器时限即可。

完成对话、完成交易与结束活动分别记录。 done 交还当前对话,rewardGranted 保存交易已经写入,活动与冷却跟踪器控制邀请还能存在多久。暂时离开后重入,要重置会话状态,交易状态与剩余活动期限则继续保留。

酒吧接入速查表

对象或方法作用本例中的结果
BarEventManager抽取、跟踪和回收事件管理 active 与 timeout
BaseBarEventCreator提供事件实例和抽取参数生成 GearInBarEvent
shouldShowAtMarket判断市场是否显示事件仅太空港市场显示
addPromptAndOption写酒吧文本和选项optionData 使用事件实例
notifyWasInteractedWith移除已处理事件并设置冷却接受、拒绝调用
BarEventDialogPlugin在 BarCMD 与事件之间转发回调结束后交还 BarCMD

完整最小例

两个类保存到前述路径,生涯入口登记 creator,酒吧再取得对应事件。

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

import java.util.Map;

import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.CargoAPI;
import com.fs.starfarer.api.campaign.InteractionDialogAPI;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.campaign.rules.MemoryAPI;
import com.fs.starfarer.api.impl.campaign.intel.bar.events.BarEventManager;
import com.fs.starfarer.api.impl.campaign.intel.bar.events.BaseBarEvent;

public class GearInBarEvent extends BaseBarEvent {

    private static final String OPTION_ACCEPT = "GEARIN_BAR_ACCEPT";
    private static final String OPTION_DECLINE = "GEARIN_BAR_DECLINE";
    private static final String OPTION_LEAVE = "GEARIN_BAR_LEAVE";
    private static final String METALS = "metals";
    private static final float METALS_REQUIRED = 100f;
    private static final int CREDIT_REWARD = 5000;

    private boolean rewardGranted;

    @Override
    public boolean shouldShowAtMarket(MarketAPI market) {
        return market != null && market.hasSpaceport() && super.shouldShowAtMarket(market);
    }

    @Override
    public void addPromptAndOption(InteractionDialogAPI dialog,
            Map<String, MemoryAPI> memoryMap) {
        dialog.getTextPanel().addPara(
                "A dockworker offers a credit payment for a shipment of refined metals.");
        dialog.getOptionPanel().addOption("Trade 100 metals for 5000 credits", this);
    }

    @Override
    public void init(InteractionDialogAPI dialog, Map<String, MemoryAPI> memoryMap) {
        super.init(dialog, memoryMap);
        done = false;
        noContinue = false;
        options.clearOptions();
        options.addOption("Provide 100 metals", OPTION_ACCEPT);
        options.addOption("Refuse the trade", OPTION_DECLINE);
        options.addOption("Leave for now", OPTION_LEAVE);
    }

    @Override
    public void optionSelected(String optionText, Object optionData) {
        if (OPTION_ACCEPT.equals(optionData)) {
            CargoAPI cargo = Global.getSector().getPlayerFleet().getCargo();
            if (cargo.getCommodityQuantity(METALS) < METALS_REQUIRED) {
                text.addPara("You need 100 metals before this trade can be accepted.");
                return;
            }
            if (!rewardGranted) {
                cargo.removeCommodity(METALS, METALS_REQUIRED);
                cargo.getCredits().add(CREDIT_REWARD);
                rewardGranted = true;
                text.addPara("The dockworker takes the metals and pays you 5000 credits.");
                done = true;
                BarEventManager.getInstance().notifyWasInteractedWith(this);
            }
        } else if (OPTION_DECLINE.equals(optionData)) {
            text.addPara("You refuse the trade and leave the dockworker behind.");
            done = true;
            noContinue = true;
            BarEventManager.getInstance().notifyWasInteractedWith(this);
        } else if (OPTION_LEAVE.equals(optionData)) {
            text.addPara("You leave before making a decision.");
            done = true;
            noContinue = true;
        }
    }
}
完整成品:GearInBarEventCreator
package data.scripts.gearin.campaign;

import com.fs.starfarer.api.impl.campaign.intel.bar.PortsideBarEvent;
import com.fs.starfarer.api.impl.campaign.intel.bar.events.BaseBarEventCreator;

public class GearInBarEventCreator extends BaseBarEventCreator {

    @Override
    public PortsideBarEvent createBarEvent() {
        return new GearInBarEvent();
    }

    @Override
    public float getBarEventFrequencyWeight() {
        return 10f;
    }

    @Override
    public float getBarEventActiveDuration() {
        return 30f;
    }

    @Override
    public float getBarEventTimeoutDuration() {
        return 30f;
    }

    @Override
    public float getBarEventAcceptedTimeoutDuration() {
        return 60f;
    }
}

验证方案

前置数据:启用包含两个 Java 类和注册片段的 MOD,进入一个有太空港的市场酒吧,并准备一份金属不足 100 的存档和一份至少有 100 金属的存档。

  1. 保存两类和生涯登记,构建重启,载入存档;重复读档后核对 creator 仍只有一份。
  2. 进入有太空港的市场酒吧,确认出现 "Trade 100 metals for 5000 credits"。
  3. 在金属不足的存档中选择接受,确认文本提示需要 100 金属,货舱和 credits 数值不变。
  4. 在至少有 100 金属的存档中选择接受,确认金属减少 100、credits 增加 5000,事件结束并离开活动列表。
  5. 观察交易成功后回到酒吧,当前事件已处理,比较货舱与信用仅发生一次本次交易。
  6. 在另一份事件实例中选择拒绝,确认事件结束并进入交互后 timeout。
  7. 在另一份事件实例中选择暂时离开,再从同一市场进入,确认重新建立选项;补足金属后完成接受。
  8. 等待未交互事件的 active 时长耗尽,确认事件从活动列表移除并进入普通 timeout。
  9. 返回 BarCMD 后确认酒吧选项恢复,事件对话没有残留选项。

预期结果:

  • creator 只注册一次;
  • 太空港市场显示事件;
  • 金属不足不会完成接受分支;
  • 接受成功只扣除 100 金属并增加 5000 credits;
  • 拒绝结束事件并触发交互后冷却;
  • 离开结束当前对话但保留事件;
  • 未交互过期事件进入普通冷却;
  • 返回酒吧后回调归属恢复到 BarCMD。

故障分析

  • 事件从未进入酒吧:市场没有太空港或事件实例已经被 shownAt 锁定到另一市场。

    • 解决方案:检查市场条件和事件是否已经在其他市场展示。
  • 同一事件重复注册:注册入口没有先调用 hasEventCreator(...)。

    • 解决方案:把 addEventCreator(...) 放在类型去重判断之后。
  • 接受分支没有奖励:货舱中的 metals 少于 100。

    • 解决方案:补充金属后重新进入事件。
  • 拒绝后事件再次出现:拒绝分支没有通知 BarEventManager。

    • 解决方案:在设置 done 后调用 notifyWasInteractedWith(this)。

总结

creator 生成金属交易,酒吧按市场筛选并展示邀请,外壳将当前会话交给事件。接受先确认货物再完成资金写入,接受与拒绝通知管理器进入冷却,暂时离开则保留原实例。

会话与活动有各自的寿命,交易结果也单独保存。重入恢复对话状态,活动期限继续推进,玩家才能在暂时离开、正式拒绝与交易完成之后得到对应结果。

建议参考项

  • com.fs.starfarer.api.impl.campaign.intel.bar.PortsideBarEvent:本页事件的声明类型,createBarEvent 返回它并被 BarCMD 识别;
  • com.fs.starfarer.api.impl.campaign.intel.bar.events.BaseBarEvent:GearInBarEvent 的基类,init 已把对话与面板引用保存到受保护字段;
  • com.fs.starfarer.api.impl.campaign.intel.bar.events.BaseBarEventCreator:GearInBarEventCreator 的基类,只负责生成事件实例和抽取参数;
  • com.fs.starfarer.api.impl.campaign.intel.bar.events.BarEventManager:从存档取得的管理器,负责 creator 注册、活动跟踪和交互后冷却;
  • com.fs.starfarer.api.impl.campaign.intel.bar.BarEventDialogPlugin:在 BarCMD 与事件对象之间转发初始化和选项回调;
  • com.fs.starfarer.api.impl.campaign.rulecmd.missions.BarCMD:酒吧场景入口,检查市场显示条件并把回调归属交还酒吧;
  • com.fs.starfarer.api.campaign.CargoAPI:玩家货舱接口,接受分支用它核对 metals 数量并写入 credits。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
上一页
实现对话插件
下一页
构建阶段任务