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

使用 LazyLib 保存公共 JSON

本节用 LazyLib 记录跨生涯共享的加载次数与最近角色名。计数器从默认 JSON 建立初始结构,随后读回公共文件,在原对象上修改并保存;载入另一份存档时,次数和上次角色仍会延续。默认数据何时接管、对象何时写盘,都需要与这个结果对应起来。

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

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

学习目标

完成后,您将能够:

  • 区分 Memory、Sector PersistentData 与存档公共数据的生命周期;
  • 使用默认 JSON 创建可恢复的公共数据文件;
  • 使用 CommonDataJSONObject 读取、修改和保存字段;
  • 在游戏加载入口记录跨生涯共享的次数与角色名;
  • 使用 clear(...) 重置公共状态,并用 toColor(...) 读取颜色数组。

前置知识

  • [C7-S1] 的生涯加载回调;
  • [C16-S5] 的 MemoryAPI 与生涯保存;
  • [C17-S4] 的原版 common 文件路径和读写;
  • [C18-S1] 的 LazyLib 运行时依赖与编译 classpath;
  • 能够读取 JSON 对象中的字符串和整数。

准备默认 JSON

在 data/config/GearIn_common_defaults.json 中写入初始字段:

{
    "load_count": 0,
    "last_player": ""
}

load_count 保存累计成功记录的加载次数,last_player 保存最近一次角色名。默认资源随 MOD 发布,运行时公共文件则由玩家的实际使用过程产生;两者使用相同字段,使首次加载和后续读取进入同一套代码。

确定文件的共享范围

Memory 随提供它的生涯对象使用和保存,Sector PersistentData 属于当前生涯;**common 属于用户数据空间,跨生涯共享。**存档目录由启动配置决定,标准安装的默认位置是 Starsector/saves/common。换用另一份存档仍读取同名公共文件,加载次数和最近角色名由此延续。

本节使用的逻辑文件名是 GearIn_common_counter。游戏为它追加 .data 后缀,最终写入:

Starsector/saves/common/GearIn_common_counter.data

逻辑文件名应只包含当前 MOD 能稳定控制的名称。父目录跳转、绝对路径、反斜杠和驱动器路径会被 common 文件接口拒绝;传入 GearIn_common_counter.json 也不会得到普通 JSON 文件,而会生成 GearIn_common_counter.json.data。

common 文本单次最多写入 1,048,576 个 Java 字符。这个容量足够保存设置、计数器和事件状态,但公共文件并不适合承担日志归档或大规模对象快照。

从默认结构取得可修改对象

让默认资源接管首次加载

双参数重载先读取现有 common 文件。文件不存在、内容无法读取或 JSON 无法解析时,它改为加载默认资源,载入非空 JSON 后,包装对象在构造时保存一次;默认资源与已有文件都经过这一步:

try (CommonDataJSONObject data = JSONUtils.loadCommonJSON(
        COMMON_FILE_NAME, DEFAULT_JSON_PATH)) {
    int previousLoadCount = data.optInt(LOAD_COUNT_KEY, 0);
    String previousPlayerName = data.optString(LAST_PLAYER_KEY, "");
}

COMMON_FILE_NAME 交给 common 文件接口,DEFAULT_JSON_PATH 交给 MOD 资源加载器。局部片段先取得旧次数和角色名,非空对象此时已保存一次,try-with-resources 结束时还会再次保存。这是读改写入口的实际行为,纯读取继续使用 [C17-S4] 的原版方法。

默认资源也会接管解析失败的文件

现有公共文件无法读取或解析时,双参 loadCommonJSON 会采用默认资源,并将非空默认对象保存到同一个文件。原来的计数因此可能重新从 0 开始。

对这个练习计数器,编者接受失败后重新建立初始状态;若保存的是无法重新取得的玩家记录,就应保留读取失败的处理机会,再决定如何处置原内容。默认模板方便建立文件,同时也决定了失败时采用什么数据。

单参数 loadCommonJSON(filename) 适合已经能够自行补齐字段的数据。读取失败时,它返回与该文件名关联的空 CommonDataJSONObject,不会寻找 MOD 内的默认模板。

修改原对象中的字段

读取旧值后,在同一个对象上更新需要改变的键:

int currentLoadCount = previousLoadCount + 1;
String currentPlayerName = playerName;

data.put(LOAD_COUNT_KEY, currentLoadCount);
data.put(LAST_PLAYER_KEY, currentPlayerName);

optInt(...) 和 optString(...) 为缺失字段提供初始值,本节采用缺省 0 和空字符串初始化这两个字段。修改原对象还会保留当前代码并不认识的其他键,避免一次小改动顺手清空整份公共状态。

保存并关闭数据对象

CommonDataJSONObject 实现了 AutoCloseable。try-with-resources 离开作用域时调用 close(),而 close() 会调用 save(),因此载入非空数据后已保存原内容,正常修改路径在关闭时再保存更新:

try (CommonDataJSONObject data = JSONUtils.loadCommonJSON(
        COMMON_FILE_NAME, DEFAULT_JSON_PATH)) {
    data.put(LOAD_COUNT_KEY, currentLoadCount);
    data.put(LAST_PLAYER_KEY, currentPlayerName);
}

save() 用于需要在作用域结束前立即写盘的场景。读改写在同一作用域完成时,载入非空对象、显式 save() 与关闭分别保存;省略显式调用,仍会保存原内容和关闭后的修改。

直接调用 new CommonDataJSONObject(filename) 得到与文件名关联的空对象,适合明确创建或整体覆盖。增量更新继续加载现有对象,才能保留原字段;新建对象后关闭,保存的便只有这个对象的内容。所谓 "重新整理结构",如果只剩自己刚写的几个键,整理掉的还有用户原来的值。

接到生涯加载入口

保存本节的 GearInCommonDataCounter 到 src/data/scripts/gearin/lib/GearInCommonDataCounter.java。在既有 GearInModPlugin.onGameLoad(...) 追加局部调用,保留原初始化,并导入计数器及其嵌套类型 GearInCommonDataCounter.LoadSnapshot:

@Override
public void onGameLoad(boolean newGame) {
    try {
        String playerName = Global.getSector().getPlayerPerson().getNameString();
        LoadSnapshot snapshot = GearInCommonDataCounter.recordLoad(playerName);
        String previousPlayer = snapshot.getPreviousPlayerName().isEmpty()
                ? "no record"
                : snapshot.getPreviousPlayerName();

        Global.getSector().getCampaignUI().addMessage(
                "Common loads: " + snapshot.getPreviousLoadCount()
                        + " -> " + snapshot.getCurrentLoadCount()
                        + "; current: " + snapshot.getCurrentPlayerName()
                        + "; previous: " + previousPlayer);
    } catch (Exception ex) {
        Global.getLogger(GearInModPlugin.class).error(
                "Failed to update GearIn common data", ex);
    }
}

recordLoad(...) 读回旧值、更新字段并完成关闭保存,返回的 LoadSnapshot 同时保留旧值与本次值,入口据此生成消息。第一次成功记录显示 0 -> 1 和 previous: no record;载入另一份生涯后,当前角色改变,旧角色与累计次数继续来自公共文件。

保存等操作抛出的异常由入口记录,当前处理块结束后,其余生涯初始化继续执行。LazyLib 已捕获并转向默认资源的读取错误,则按前面的初始结构处理。

重置公共状态

重置需要清除未知字段,再恢复默认结构。JSONUtils.clear(...) 原地移除 JSONObject 的全部键,随后重新写入两个默认值:

public static void reset() throws Exception {
    try (CommonDataJSONObject data = JSONUtils.loadCommonJSON(
            COMMON_FILE_NAME, DEFAULT_JSON_PATH)) {
        JSONUtils.clear(data);
        data.put(LOAD_COUNT_KEY, 0);
        data.put(LAST_PLAYER_KEY, "");
    }
}

通过 Console Commands 执行以下命令,可以在不切换存档的情况下恢复公共计数器:

runcode data.scripts.gearin.lib.GearInCommonDataCounter.reset();

命令结束时对象已经关闭并保存。下一次载入生涯时,旧次数为 0,本次次数变为 1,上次角色名为空。

读取颜色数组

JSONUtils.toColor(...) 把 JSON 整数数组转换为 Color。这个方法适合读取数据驱动的界面色或特效色,不要求把颜色拆成四个独立字段:

Color opaque = JSONUtils.toColor(new JSONArray("[80, 200, 255]"));
Color translucent = JSONUtils.toColor(new JSONArray("[80, 200, 255, 160]"));

三个整数依次表示红、绿、蓝,alpha 默认为 255;数组恰有四个分量时采用第四个整数作为 alpha。少于三个分量时无法组成颜色,方法会抛出 JSONException。

常用方法速查表

方法读取或写入结果适用场景
loadCommonJSON(filename, defaultPath)读取公共对象,失败时采用默认 JSON;非空对象构造时保存拥有固定初始结构的数据
loadCommonJSON(filename)读取公共对象;非空对象构造时保存,失败时返回空对象代码能够自行补齐全部字段的数据
new CommonDataJSONObject(filename)创建与文件名关联的空对象明确创建或覆盖全部内容
save()立即把当前对象写入 common 文件作用域尚未结束但需要立即写盘
close()调用 save() 并结束当前对象的使用try-with-resources 自动保存
clear(object)原地移除对象中的全部键重置公共状态
toColor(array)把 RGB 或 RGBA 整数数组转换为 Color数据驱动颜色

完整最小例

默认 JSON 提供首次加载结构,静态工具类负责读取、更新、保存和重置同一个公共文件。

{
    "load_count": 0,
    "last_player": ""
}
完整成品:GearInCommonDataCounter
package data.scripts.gearin.lib;

import org.lazywizard.lazylib.JSONUtils;
import org.lazywizard.lazylib.JSONUtils.CommonDataJSONObject;

public final class GearInCommonDataCounter {
    private static final String COMMON_FILE_NAME = "GearIn_common_counter";
    private static final String DEFAULT_JSON_PATH =
            "data/config/GearIn_common_defaults.json";
    private static final String LOAD_COUNT_KEY = "load_count";
    private static final String LAST_PLAYER_KEY = "last_player";

    private GearInCommonDataCounter() {
    }

    public static LoadSnapshot recordLoad(String playerName) throws Exception {
        try (CommonDataJSONObject data = JSONUtils.loadCommonJSON(
                COMMON_FILE_NAME, DEFAULT_JSON_PATH)) {
            int previousLoadCount = data.optInt(LOAD_COUNT_KEY, 0);
            String previousPlayerName = data.optString(LAST_PLAYER_KEY, "");
            int currentLoadCount = previousLoadCount + 1;
            String currentPlayerName = playerName;

            data.put(LOAD_COUNT_KEY, currentLoadCount);
            data.put(LAST_PLAYER_KEY, currentPlayerName);

            return new LoadSnapshot(
                    previousLoadCount,
                    currentLoadCount,
                    previousPlayerName,
                    currentPlayerName);
        }
    }

    public static void reset() throws Exception {
        try (CommonDataJSONObject data = JSONUtils.loadCommonJSON(
                COMMON_FILE_NAME, DEFAULT_JSON_PATH)) {
            JSONUtils.clear(data);
            data.put(LOAD_COUNT_KEY, 0);
            data.put(LAST_PLAYER_KEY, "");
        }
    }

    public static final class LoadSnapshot {
        private final int previousLoadCount;
        private final int currentLoadCount;
        private final String previousPlayerName;
        private final String currentPlayerName;

        private LoadSnapshot(int previousLoadCount, int currentLoadCount,
                String previousPlayerName, String currentPlayerName) {
            this.previousLoadCount = previousLoadCount;
            this.currentLoadCount = currentLoadCount;
            this.previousPlayerName = previousPlayerName;
            this.currentPlayerName = currentPlayerName;
        }

        public int getPreviousLoadCount() {
            return previousLoadCount;
        }

        public int getCurrentLoadCount() {
            return currentLoadCount;
        }

        public String getPreviousPlayerName() {
            return previousPlayerName;
        }

        public String getCurrentPlayerName() {
            return currentPlayerName;
        }
    }
}

验证方案

前置数据:启用 LazyLib 与 Console Commands,并准备两份角色名不同的生涯存档。

  1. 执行 gradle jar,将构建产物放入 MOD 的 jars 目录,并确认默认 JSON 位于 data/config。
  2. 在实际存档公共目录核对计数器。首次实验尚未有该文件时,载入第一份生涯;已有计数器时记录原值,再通过正文重置命令从 0 开始比较。
  3. 再次载入第一份生涯,比较消息中的旧次数、本次次数和角色名。
  4. 载入第二份生涯,确认消息保留累计次数,并把第一份生涯的角色名显示为上次角色名。
  5. 打开实际公共目录中的 GearIn_common_counter.data,核对 load_count 与 last_player。
  6. 执行 runcode data.scripts.gearin.lib.GearInCommonDataCounter.reset();,核对文件中的默认字段。
  7. 再次载入任意生涯,确认次数从 0 增加到 1。
  8. 将测试颜色数组分别设为 RGB、RGBA 和少于三个分量,调用 toColor(...) 并观察解析结果与异常日志。

预期结果:

  • 首次加载从默认资源建立公共文件,消息显示 0 -> 1;
  • 同一生涯再次加载时次数连续增加,上次与本次角色名相同;
  • 另一份生涯读取相同累计次数,并显示上一份生涯的角色名;
  • 公共文件始终包含最新的 load_count 与 last_player;
  • 重置后两个字段分别为 0 和空字符串,下一次加载显示 0 -> 1;
  • RGB 数组得到 alpha 为 255 的颜色,RGBA 数组保留给定 alpha,分量不足时产生 JSONException。

故障分析

  • 每次载入都显示 0 -> 1:数据对象没有正常关闭,或每次调用使用了不同的公共文件名。
    • 解决方案:使用固定的 GearIn_common_counter,并让 try-with-resources 完成 close() 与保存。

总结

计数器在同一个公共对象上读取旧值、更新字段,再由关闭操作保存结果,加载入口只使用返回的快照显示变化。文件跨生涯共享,默认资源负责建立初始结构,也接管读取失败的情况;这项选择与载入、关闭的写入时机一起决定了数据如何延续。

建议参考项

  • org.lazywizard.lazylib.JSONUtils:加载、清空和转换公共 JSON 数据;
  • org.lazywizard.lazylib.JSONUtils.CommonDataJSONObject:关联公共文件并在关闭时保存对象;
  • com.fs.starfarer.api.SettingsAPI:LazyLib 公共 JSON 最终使用的 common 文件读写入口;
  • Starsector/saves/common:运行时存档公共数据目录;
  • data/config/GearIn_common_defaults.json:公共计数器的首次加载结构。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
上一页
使用 LazyLib 查询战场
下一页
注册 GraphicsLib 光照数据