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

配置人物基础

本节让 Aster Vance 留在 Foundry 的通信目录中,供后续脚本按固定身份找到她。姓名、头像与职务组成界面资料,重要人物服务保存可查询的对象,市场和目录提供实际接触入口。创建、登记与关联分别完成,读档后才能继续使用同一位人物。

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

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

学习目标

完成后,您将能够:

  • 选择固定剧情人物的创建入口;
  • 配置人物身份、标签和 Memory;
  • 使用 ImportantPeopleAPI 登记、查询和借还人物;
  • 区分市场成员、人物市场字段和通信目录;
  • 让注册流程在重复调用和读档后复用对象。

前置知识

  • [C7-S1] 的 BaseModPlugin 与 onGameLoad;
  • [C7-S2] 的 Global.getFactory();
  • [C10-S1] 的星区服务与日志;
  • [C12-S1] 创建的 GearIn_market_foundry。

把人物注册接到现有市场

创建 src/data/scripts/gearin/campaign/GearInPersonScript.java。把下面的局部调用加入 [C12-S1] 的 GearInEconomyProbe.ProbeListener.economyUpdated(),当前市场已建立,调用与其他观察顺序保留:

new GearInPersonScript().run();

两类在同一个包。每次载入存档,经济观察器等待一轮更新后调用人物流程,当前市场已经建立。Aster 与关联关系会随存档保存,这个入口因此既要处理首次创建,也要处理已有的人物。

选择固定人物的创建入口

固定人物采用 createPerson(),由调用方填写资料。需要随机背景人物时,createRandomPerson() 从势力资料池生成姓名与头像。以下局部片段对照两个入口:

PersonAPI blank = Global.getFactory().createPerson();
PersonAPI random = Global.getSector().getFaction("gear_industry").createRandomPerson();

两者得到的是人物对象,重要人物登记与市场关联由调用方继续完成。本节把创建放在固定 ID 查询之后,首次运行才建立 Aster。

先按 ID 取得人物,再配置首次资料

从 Global.getSector().getImportantPeople() 取得重要人物服务,查询 GearIn_aster_vance。已有对象直接返回,只有首次调用才创建并登记。getOrCreatePerson() 的局部结构如下:

ImportantPeopleAPI importantPeople = Global.getSector().getImportantPeople();
PersonAPI person = importantPeople.getPerson(PERSON_ID);
if (person != null) {
    return person;
}
person = Global.getFactory().createPerson();
// 写入资料与 Memory
importantPeople.addPerson(person);
return person;

这样,已有对话进度和人物状态继续留在原对象上,读取存档不会再用一份新建资料覆盖它。完整类在创建分支写完身份、标签与 Memory,最后调用 addPerson(...)。

写入 Aster 的身份

人物 ID 先设为 GearIn_aster_vance,然后填写 Aster Vance、女性、齿轮工业公民与商人职务。头像采用 graphics/portraits/portrait_hegemony01.png,标签使用同一前缀。局部配置如下:

person.setId(PERSON_ID);
person.setName(new FullName("Aster", "Vance", FullName.Gender.FEMALE));
person.setFaction("gear_industry");
person.setRankId(Ranks.CITIZEN);
person.setPostId(Ranks.POST_TRADER);
person.setPortraitSprite("graphics/portraits/portrait_hegemony01.png");
person.addTag(PERSON_TAG);

固定 ID 在登记前确定,后续脚本继续使用它。 重要人物服务按 ID 建立查询,姓名与头像则供玩家辨认。rank 表示身份层级,post 表示职务;getRankId()、getPostId() 返回稳定值,getRank()、getPost() 按人物势力取得显示文本。

标签是分类集合,供 MOD 查询使用。完整类还写入 $GearIn_aster_vance_ready,标记这份基础资料已经完成;状态含义与人物 ID 分别保存。

人物资料速查表

资料写入读取用途
IDsetIdgetId跨脚本定位
姓名setNamegetNameString界面显示
性别FullName.GendergetGender人物文本
势力setFactiongetFaction归属
ranksetRankIdgetRankId身份层级
postsetPostIdgetPostId职务
头像setPortraitSpritegetPortraitSprite人物图像
标签addTaghasTag分类

getRelToPlayer() 返回人物与玩家的关系对象,参与对话等显示。本节读取身份与归属,关系沿用已有状态。

关联市场和通信目录

人物登记后,run() 将她关联到 [C12-S1] 的 GearIn_market_foundry。market.addPerson(person) 同时加入人物集合并设置人物的市场字段,局部操作如下:

MarketAPI market = Global.getSector().getEconomy().getMarket(MARKET_ID);
if (!market.getPeopleCopy().contains(person)) {
    market.addPerson(person);
}

单独调用 person.setMarket(market) 只保存人物的反查字段。这里从集合入口加入,随后可以从 getPeopleCopy() 与 person.getMarket() 两端确认归属。

再将人物放进通信目录。目录另有自己的条目,按人物查询后添加:

CommDirectoryAPI directory = market.getCommDirectory();
int entriesBefore = directory.getEntriesCopy().size();
if (directory.getEntryForPerson(person) == null) {
    directory.addPerson(person);
}
int entriesAfter = directory.getEntriesCopy().size();

ImportantPeopleAPI 的附加数据也保存位置,完整类调用 getData(person).getLocation().setMarket(market)。这样后续人物服务既能按 ID 找到她,也能知道她位于 Foundry。目录出现人物,说明界面入口已接好;市场集合、人物字段和服务位置仍各有职责。

人物关联速查表

对象写入检查结果
重要人物服务addPersongetPerson可按 ID 查询
市场集合MarketAPI.addPersongetPeopleCopy市场集合拥有成员
人物市场字段MarketAPI.addPerson 同步人物字段getMarket人物反查市场
通信目录CommDirectoryAPI.addPersongetEntryForPerson界面出现条目

让重复调用继续使用原人物

getOrCreatePerson() 处理固定 ID 查询与首次登记,run() 处理市场、目录、服务位置和日志,两者传递同一个人物。首次运行时 created=true;同存档再次调用或读档后,人物服务返回已有对象,created=false,入口只补齐缺失的关联。

containsPerson(person) 按人物 ID 检查登记,getData(person).getPerson() == person 则确认服务保存的具体对象。因此日志分别记录 important 与 dataLinked,再记录 market、personMarket、directory 和目录数量前后值。姓名相同的两个对象,仍可能在这些关系中得到不同结果。

本例的 important=true 表示已登记到重要人物服务。任务中的重要标志另由 Memory 管理,二者的作用在下面区分。跨读档继续按 ID 查询,当前一次调用中再用对象引用核对关联。

用 Memory 保存人物状态

人物有自己的 MemoryAPI。当前 $GearIn_aster_vance_ready 使用无期限值,表示首次资料已经写完,随人物保存。局部读写如下:

person.getMemoryWithoutUpdate().set(PERSON_READY, true);
boolean ready = person.getMemoryWithoutUpdate().getBoolean(PERSON_READY);

标签表达分类,Memory 表达当前运行状态;带期限值适合短期事件。本例的最终代码只写自己的资料标记,原版 MemFlags 还供任务与军官功能使用,按实际读取方选择。

人物相关 Memory 速查表

常量字符串值人物上的数据类型具体语义
ENTITY_MISSION_IMPORTANT$missionImportantreason 组合布尔标记任务等流程标为重要,通信目录与重要目标逻辑读取
MEMORY_KEY_MISSION_IMPORTANT$missionImportantreason 组合布尔标记与前一常量共用键,通过 Misc.makeImportant 管理
EXCEPTIONAL_SLEEPER_POD_OFFICER$exceptionalSleeperPodOfficer布尔值特殊睡眠舱军官身份,技能变更逻辑据此处理
SUSPECTED_AI$suspectedAI布尔值疑似 AI 身份,原版生命周期逻辑会写入
OFFICER_SKILL_PICKS_PER_LEVEL$officerSkillPicksPerLevel整数军官每级的技能候选数量
OFFICER_MAX_LEVEL$officerMaxLevel整数军官最高等级
OFFICER_MAX_ELITE_SKILLS$officerMaxEliteSkills整数军官精英技能上限

三个 OFFICER_ 键由军官成长功能读取,Aster 当前只是普通剧情人物。MemFlags 也集中定义市场、舰队与全局键,使用时仍要对应实际对象和类型。

任务重要标志按 reason 管理

$missionImportant 由多个 reason 共同决定。使用 Misc.makeImportant(...) 标记,Misc.makeUnimportant(...) 解除自己的原因,才能保留其他任务的标记。人物服务登记继续调用 ImportantPeopleAPI.addPerson(...)。

筛选人物与登记占用

固定人物按 ID 查询。需要取得同类人物时,使用 getMatching(...) 按人物字段筛选。下面的局部片段按 Ranks.POST_TRADER 读取商人,需要 java.util.List:

List<ImportantPeopleAPI.PersonDataAPI> traders = importantPeople.getMatching(
        data -> Ranks.POST_TRADER.equals(data.getPerson().getPostId()));

getPeopleWithRank(...) 使用 rank 显示文本。当前 getPeopleWithPost(...) 也不能据传入 post ID 完成这里的职务筛选,因此本节明确比较 getPostId(),数据 ID 与界面文本分别使用。

任务还可以登记人物占用。checkOutPerson(person, reason) 将原因加入附加数据的集合,returnPerson(...) 移除同一原因,市场与目录关系继续保留。以下局部片段登记后立即归还,需要 java.util.Set:

String reason = "GearIn_person_example";
if (importantPeople.canCheckOutPerson(person, reason)) {
    importantPeople.checkOutPerson(person, reason);
    Set<String> during = importantPeople.getData(person).getCheckedOutFor();
    importantPeople.returnPerson(person, reason);
}

canCheckOutPerson 检查当前 reason

返回 true 表示当前原因尚未登记,人物仍可能被其他原因占用。需要独占使用时,再结合 isCheckedOutForAnything() 或 getCheckedOutFor() 确认全部占用记录。

完整最小例

保存下列类,调用入口保留在经济更新结束后的市场观察中。

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

import org.apache.log4j.Logger;

import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.CommDirectoryAPI;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.characters.FullName;
import com.fs.starfarer.api.characters.ImportantPeopleAPI;
import com.fs.starfarer.api.characters.PersonAPI;
import com.fs.starfarer.api.impl.campaign.ids.Ranks;

/**
 * 创建或读取 Aster Vance,并把人物加入 GearIn Foundry 的通信目录。
 * 固定人物 ID 与目录查询使重复调用保持同一个人物和目录条目。
 */
public class GearInPersonScript {

    public static final String PERSON_ID = "GearIn_aster_vance";

    private static final Logger LOG = Global.getLogger(GearInPersonScript.class);
    private static final String MARKET_ID = "GearIn_market_foundry";
    private static final String PERSON_TAG = "GearIn_aster_vance";
    private static final String PERSON_READY = "$GearIn_aster_vance_ready";

    public void run() {
        ImportantPeopleAPI importantPeople = Global.getSector().getImportantPeople();
        boolean existed = importantPeople.getPerson(PERSON_ID) != null;
        PersonAPI person = getOrCreatePerson();

        MarketAPI market = Global.getSector().getEconomy().getMarket(MARKET_ID);
        if (!market.getPeopleCopy().contains(person)) {
            market.addPerson(person);
        }
        CommDirectoryAPI directory = market.getCommDirectory();
        int entriesBefore = directory.getEntriesCopy().size();
        if (directory.getEntryForPerson(person) == null) {
            directory.addPerson(person);
        }
        int entriesAfter = directory.getEntriesCopy().size();

        ImportantPeopleAPI.PersonDataAPI data = importantPeople.getData(person);
        data.getLocation().setMarket(market);
        LOG.info(String.format(
                "GearIn person: id=%s created=%s name=%s gender=%s faction=%s rank=%s post=%s "
                        + "tag=%s ready=%s important=%s dataLinked=%s market=%s personMarket=%s directory=%s entries=%d->%d",
                person.getId(), !existed, person.getNameString(), person.getGender(),
                person.getFaction().getId(), person.getRankId(), person.getPostId(),
                person.hasTag(PERSON_TAG), person.getMemoryWithoutUpdate().getBoolean(PERSON_READY),
                importantPeople.containsPerson(person), data.getPerson() == person,
                market.getPeopleCopy().contains(person), person.getMarket() == market,
                directory.getEntryForPerson(person) != null,
                entriesBefore, entriesAfter));
    }

    private PersonAPI getOrCreatePerson() {
        ImportantPeopleAPI importantPeople = Global.getSector().getImportantPeople();
        PersonAPI person = importantPeople.getPerson(PERSON_ID);
        if (person != null) {
            return person;
        }

        person = Global.getFactory().createPerson();
        person.setId(PERSON_ID);
        person.setName(new FullName("Aster", "Vance", FullName.Gender.FEMALE));
        person.setFaction("gear_industry");
        person.setRankId(Ranks.CITIZEN);
        person.setPostId(Ranks.POST_TRADER);
        person.setPortraitSprite("graphics/portraits/portrait_hegemony01.png");
        person.addTag(PERSON_TAG);
        person.getMemoryWithoutUpdate().set(PERSON_READY, true);
        importantPeople.addPerson(person);
        return person;
    }
}

验证方案

前置数据:测试 MOD 已包含 [C12-S1] 创建的市场,并准备可重新载入的存档。

  1. 执行 gradle jar,确认 MOD 构建完成。
  2. 载入存档并推进一轮经济更新,触发现有观察器中的 run()。
  3. 在 starsector.log 查找 GearIn person:。
  4. 打开 GearIn Foundry 通信目录,确认姓名、头像和职务。
  5. 退出并重新载入同一存档,再推进经济更新,比较 created 与目录数量。
  6. 保存并重新载入,再次检查固定 ID 和目录条目。

预期结果:

  • 首次运行日志包含 id=GearIn_aster_vance 和 created=true;
  • 人物姓名为 Aster Vance,性别为 FEMALE,势力为 gear_industry;
  • rank ID 为 citizen,post ID 为 trader;
  • tag=true 且 ready=true;
  • important=true 且 dataLinked=true;
  • market=true 且 personMarket=true;
  • directory=true,通信目录能取得 Aster Vance;
  • 同存档再次调用时 created=false,entriesBefore 与 entriesAfter 相等;
  • 重新载入存档后固定 ID 仍返回 Aster Vance。

故障分析

  • 每次调用都创建人物:查询 ID 与写入 ID 不一致,或登记没有执行。

    • 解决方案:统一使用 PERSON_ID,并在返回前调用 importantPeople.addPerson(person)。
  • 市场存在但目录没有人物:只维护了市场集合。

    • 解决方案:查询 getEntryForPerson(person),缺失时调用 directory.addPerson(person)。
  • 市场集合中没有人物:只写了人物反查字段。

    • 解决方案:通过 market.addPerson(person) 加入集合,再从 person.getMarket() 与 getPeopleCopy() 两端读取。
  • 目录出现同名人物的多个条目:反复创建了不同人物对象,再分别加入目录。

    • 解决方案:按固定 ID 从人物服务复用对象,添加目录前用 getEntryForPerson(person) 查询当前条目。

总结

Aster 的首次资料保存在人物对象中,重要人物服务按固定 ID 查找,市场集合与通信目录让她出现在 Foundry。重复调用取得原人物并补齐关联,读档后也沿同一身份继续使用。

人物状态、任务重要标志与占用原因分别管理。后续任务可以找到这位商人,也能判断当前关系与占用;姓名和头像完成了界面资料,固定身份与实际关联完成了接入。

建议参考项

  • com.fs.starfarer.api.characters.PersonAPI:人物资料、标签和 Memory;
  • com.fs.starfarer.api.characters.ImportantPeopleAPI:重要人物登记、查询和借还;
  • com.fs.starfarer.api.campaign.CommDirectoryAPI:通信目录条目;
  • com.fs.starfarer.api.impl.campaign.ids.MemFlags:原版人物 Memory 键;
  • com.fs.starfarer.api.util.Misc:按 reason 管理重要标志。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
下一页
让人物承担角色