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

读取生涯世界状态

本节通过 SectorAPI 读取当前生涯,用每五日一次的记录比较地点、玩家舰队、势力与日期。采样器在读档后重新计时,存档标记则继续保留,我们将利用这个区别说明运行进度和生涯数据各自怎样保存。

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

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

学习目标

完成后,您将能够:

  • 取得当前星区并定位常用全局服务入口;
  • 读取生涯日期、时间戳和暂停状态;
  • 区分 persistent data、星区 memory 和脚本字段;
  • 按固定生涯天数输出状态采样日志;
  • 根据暂停、读档和对象延迟取得现象定位读取错误。

前置知识

  • [C7-S1] 的 MOD 入口和生涯回调;
  • [C7-S2] 的 Global.getSector() 对象取得方式;
  • [C7-S5] 的日志位置、前缀与上下文;
  • 能够区分存档数据、运行时对象和当前帧状态。

给生涯入口接入采样脚本

本节每隔五个生涯日记录当前地点、玩家舰队与势力,再比较暂停和读档后的变化。创建 src/data/scripts/gearin/campaign/GearInSectorStateSampler.java,在 GearInModPlugin 添加类的 import,将下面局部登记加入已有 onGameLoad(...),沿用其中的 sector:

if (!sector.hasTransientScript(GearInSectorStateSampler.class)) {
    sector.addTransientScript(new GearInSectorStateSampler());
}

hasTransientScript(...) 查询当前列表,缺少实例时才加入。脚本随后持续接收生涯推进,暂停时返回 false 的 runWhilePaused() 使它停止调用。

这份采样器只保存当前运行阶段的进度。 addTransientScript(...) 登记的对象不进入存档,每次读档由入口重新建立实例。第十四章会完整展开脚本生命周期,这里先用它定期读取世界。

从当前存档取得世界对象

进入生涯之后,Global.getSector() 返回当前存档的 SectorAPI。从它取得地点、舰队、势力与时钟,局部读取如下:

SectorAPI sector = Global.getSector();
LocationAPI currentLocation = sector.getCurrentLocation();
CampaignFleetAPI playerFleet = sector.getPlayerFleet();
FactionAPI playerFaction = sector.getPlayerFaction();
CampaignClockAPI clock = sector.getClock();

这些对象都来自当前星区。玩家切换地点后,getCurrentLocation() 返回新的地点;读档后,访问当前 Sector 取得恢复的生涯。应用资源加载时还没有这份世界,因此读取放在 onGameLoad(...) 和它登记的脚本中。

静态工具方法也可以访问当前 Global.getSector(),数据的保存范围仍取决于实际所属对象。需要长期进度时,写入相应存档数据,不能仅凭访问器是静态方法就把对象保存在进程字段中。

生涯服务入口速查表

服务入口当前用途
地点getCurrentLocation()、getAllLocations()取得玩家当前地点与全星区地点集合
势力getPlayerFaction()、getFaction(id)取得玩家势力与指定势力
玩家getPlayerFleet()、getPlayerPerson()、getPlayerStats()取得玩家舰队、人物与统计对象
经济getEconomy()查询市场与经济集合
事件与情报getEventManager()、getIntelManager()访问生涯事件与情报服务

本节将地点、舰队和势力的 ID 写入日志。后续专用页面再从相同入口展开各自的对象,读取来源继续保持一致。

按生涯日安排记录

采样使用 CampaignClockAPI 的时间戳。getTimestamp() 保存当前时刻,getElapsedDaysSince(timestamp) 计算从指定时刻经过的生涯天数,日期方法则提供可显示文本:

CampaignClockAPI clock = sector.getClock();
long sampleTimestamp = clock.getTimestamp();
float elapsedDays = clock.getElapsedDaysSince(sampleTimestamp);
String date = clock.getDateString();
String shortDate = clock.getShortDate();

脚本第一次推进时,把当前时间戳保存为 lastSampleTimestamp。以后每次比较距离该时刻的天数,达到 SAMPLE_INTERVAL_DAYS 的 5 日后输出,再以当前时刻开始下一段间隔:

float daysSinceSample = clock.getElapsedDaysSince(lastSampleTimestamp);
if (daysSinceSample >= SAMPLE_INTERVAL_DAYS) {
    writeStateLog(sector, clock);
    lastSampleTimestamp = clock.getTimestamp();
}

这里记录的是生涯经过时间。advance(float amount) 提供秒数,不能直接当作生涯日;当前示例采用时钟时间戳比较,需要累计步长的功能则通过生涯时钟换算。

暂停时生涯时钟冻结,采样器也停止推进,因此真实时间经过多久,都不会增加这一段五日间隔。恢复后继续从暂停前的时间戳比较。

区分采样进度与存档标记

现在考虑读档:脚本是新实例,lastSampleTimestamp 重新建立,五日采样会从加载后的时刻重新计算。但 "这个存档已经采样过" 应当继续保留,所以它写入 Sector 的 persistent data。

同时读取星区 Memory 中的观察键,比较两种数据入口:

Map<String, Object> persistentData = sector.getPersistentData();
MemoryAPI sectorMemory = sector.getMemoryWithoutUpdate();

boolean hasSampled = Boolean.TRUE.equals(
        persistentData.get("GearIn_sector_sampled"));
Object probeValue = sectorMemory.get("$GearIn_sector_probe");

GearIn_sector_sampled 是 persistent data 的键,完成采样后写入 true;$GearIn_sector_probe 属于星区 Memory,本节只读取,尚未设置时为 null。getMemoryWithoutUpdate() 返回现有事实,Memory 还提供过期时间等语义。

脚本字段、persistent data 与 Memory 各自保存所需状态。 此例中,脚本字段记录本次采样间隔,persistent data 记录该存档是否采样过,星区 Memory 提供规则状态的读取入口。需要跨读档延续计时的功能,则把时间戳保存到普通脚本或持久数据中,再按那份基准继续。

将日志对应到实际操作

每次采样重新取得当前对象,记录日期、时间戳、地点数量、玩家所在地点、舰队、势力与两种数据值。切换地点时只改变相关对象与记录,采样仍按原有五日间隔执行。

标记在输出之后写入。 因此新档第一次日志为 persistent=false,第二次为 true。保存第一次采样后的存档再读档,标记恢复为 true,而脚本重新计五日。这个差异正好说明了两者的保存范围。

完整类只保存时间戳与初始化状态,所有世界对象在采样时取得。日志中的空值反映当时引用状态,具体读取继续放在已建立生涯的回调内。

完整最小例

保存下列类,并保留前面的生涯登记片段,再构建 JAR。

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

import java.util.Map;

import org.apache.log4j.Logger;

import com.fs.starfarer.api.EveryFrameScript;
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.CampaignClockAPI;
import com.fs.starfarer.api.campaign.CampaignFleetAPI;
import com.fs.starfarer.api.campaign.FactionAPI;
import com.fs.starfarer.api.campaign.LocationAPI;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.fs.starfarer.api.campaign.rules.MemoryAPI;

public class GearInSectorStateSampler implements EveryFrameScript {

    public static final String REGISTER_MARK = "GearIn_SectorStateSampler";

    private static final String PERSISTENT_SAMPLE_KEY = "GearIn_sector_sampled";
    private static final String MEMORY_PROBE_KEY = "$GearIn_sector_probe";
    private static final float SAMPLE_INTERVAL_DAYS = 5f;
    private static final Logger LOG = Global.getLogger(GearInSectorStateSampler.class);

    private long lastSampleTimestamp;
    private boolean initialized;

    @Override
    public boolean isDone() {
        return false;
    }

    @Override
    public boolean runWhilePaused() {
        return false;
    }

    @Override
    public void advance(float amount) {
        SectorAPI sector = Global.getSector();
        CampaignClockAPI clock = sector.getClock();
        if (!initialized) {
            lastSampleTimestamp = clock.getTimestamp();
            initialized = true;
        }

        float daysSinceSample = clock.getElapsedDaysSince(lastSampleTimestamp);
        if (daysSinceSample < SAMPLE_INTERVAL_DAYS) {
            return;
        }

        writeStateLog(sector, clock);
        lastSampleTimestamp = clock.getTimestamp();
        sector.getPersistentData().put(PERSISTENT_SAMPLE_KEY, true);
    }

    private void writeStateLog(SectorAPI sector, CampaignClockAPI clock) {
        LocationAPI currentLocation = sector.getCurrentLocation();
        CampaignFleetAPI playerFleet = sector.getPlayerFleet();
        FactionAPI playerFaction = sector.getPlayerFaction();
        Map<String, Object> persistentData = sector.getPersistentData();
        MemoryAPI sectorMemory = sector.getMemoryWithoutUpdate();
        Object probeValue = sectorMemory.get(MEMORY_PROBE_KEY);

        String locationId = currentLocation == null
                ? "null"
                : currentLocation.getId();
        String fleetId = playerFleet == null
                ? "null"
                : playerFleet.getId();
        String factionId = playerFaction == null
                ? "null"
                : playerFaction.getId();

        LOG.info(String.format(
                "GearIn sector sample: date=%s timestamp=%d paused=%s locations=%d location=%s fleet=%s faction=%s persistent=%s probe=%s",
                clock.getDateString(),
                clock.getTimestamp(),
                sector.isPaused(),
                sector.getAllLocations().size(),
                locationId,
                fleetId,
                factionId,
                Boolean.TRUE.equals(persistentData.get(PERSISTENT_SAMPLE_KEY)),
                probeValue));
    }
}

验证方案

前置数据:包含本节采样脚本类的 GearIn MOD 已构建并启用,并准备一个新档。

  1. 保存类与 onGameLoad(...) 登记,构建 JAR,重启游戏并进入新档。
  2. 推进至少 5 个生涯天数,搜索 GearIn sector sample。
  3. 暂停游戏并保持一段时间,确认没有新的普通采样日志。
  4. 恢复运行并切换地点,确认 date、timestamp 和 location 发生变化。
  5. 完成一次采样后保存并退出,重新读档,再推进五日,比较 persistent 标记与当前地点、玩家舰队。

预期结果:

  • 生涯天数达到采样阈值后,日志出现 GearIn sector sample;
  • 暂停游戏期间没有新的普通采样日志;
  • 恢复运行并切换地点后,date、timestamp 和 location 发生变化;
  • 首次采样时 persistent=false,之后及读档后的采样为 true;probe 未设置时为 null。
  • 读档后脚本重新计五日,地点与玩家舰队从当前星区取得。

故障分析

  • 日志一直没有采样:生涯登记缺失、五日阈值尚未达到,或当前处于暂停。

    • 解决方案:核对 onGameLoad(...) 的 transient 登记,恢复运行并推进五日,再比较时间戳与 runWhilePaused()。
  • 读取地点或舰队时发生空引用:读取代码放在生涯尚未建立的应用回调中。

    • 解决方案:将当前世界读取放入 onGameLoad(...) 登记的生涯脚本,并在采样时取得当前星区对象。
  • persistent 标记读档后仍为 false:写入使用了脚本字段或不同的键名。

    • 解决方案:统一使用 getPersistentData() 和稳定键 GearIn_sector_sampled,不要把 memory 值当作持久 Map 值读取。

总结

五日采样从当前星区取得对象,用生涯时钟比较间隔,再记录结果。暂停不增加生涯天数,读档重新建立 transient 计时,已经写入的 persistent 标记却继续属于这个存档。

后续的地点、势力与实体同样从当前 Sector 取得。运行进度怎样延续,取决于它保存在哪个对象与容器中。

建议参考项

  • com.fs.starfarer.api.campaign.SectorAPI:生涯世界根对象与全局服务入口,暂停推进、脚本推进和星区 memory 由其承接;
  • com.fs.starfarer.api.campaign.CampaignClockAPI:时间戳、日期和生涯天数换算;
  • com.fs.starfarer.api.campaign.rules.MemoryAPI:memory 键值和过期状态读取;
  • com.fs.starfarer.api.EveryFrameScript:采样器所需的最小调用接口。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
下一页
构建生涯地点