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

生成生涯舰队

本节生成两支巡逻队,并将它们放到来源市场两侧。基础舰队说明市场、预算、工厂与地点怎样共同完成生成,高级舰队再指定质量、旗舰和军官。实际成员与最终装备从返回对象取得,生成参数的完整字段留作按需配置的速查。

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

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

学习目标

完成后,您将能够:

  • 解释 FleetParamsV3 在舰队生成过程中的职责;
  • 为六类舰船预算选择合适的点数;
  • 使用市场来源构造参数并调用 FleetFactoryV3.createFleet(...);
  • 检查空结果、开启 transponder,并把舰队投放到地点;
  • 使用质量、旗舰、军官和随机源字段补充高级生成要求;
  • 在读取最终装配前执行装配随机化;
  • 比较请求 FP、实际 FP 和工厂记录的生成比例。

前置知识

  • [C10-S1] 的星区与经济入口;
  • [C10-S2] 的地点容器、实体位置与归属;
  • [C7-S1] 的新档生成与经济建立后的回调;
  • 能够构建 Java 17 MOD;
  • 能够在 starsector.log 中按类名或日志前缀查找输出。

准备一次新档生成

本节的两支巡逻队都从来源市场生成,一支采用基础预算,另一支再指定质量、旗舰和军官。创建 src/data/scripts/gearin/campaign/GearInFleetGenerator.java 与 GearInFleetGeneratorAdvanced.java,在 GearInModPlugin 添加两类的 import。

生成需要已经建立的市场,将下面的局部方法加入入口:

@Override
public void onNewGameAfterEconomyLoad() {
    SectorAPI sector = Global.getSector();
    sector.addTransientScript(new GearInFleetGenerator());
    sector.addTransientScript(new GearInFleetGeneratorAdvanced());
}

经济加载完成后登记两份一轮脚本,进入生涯时各生成一次。星系与初始势力关系继续由原来的新档回调处理,生成器在这里只使用已经存在的市场和地点。

从市场取得生成条件与投放位置

基础生成器优先取得 jangala。存档没有这个市场时,遍历当前经济中的市场,选择第一项具有开放市场、地点和主实体的来源。完整类的 pickSourceMarket() 承担这个选择,调用方使用如下局部片段:

MarketAPI source = pickSourceMarket();
if (source == null) {
    LOG.warn("GearIn fleet generator: no source market");
    return;
}

LocationAPI location = source.getContainingLocation();
SectorEntityToken anchor = source.getPrimaryEntity();
if (location == null || anchor == null) {
    LOG.warn("GearIn fleet generator: source has no location or anchor");
    return;
}

来源市场提供势力和质量条件,getContainingLocation() 提供投放地点,getPrimaryEntity() 提供坐标锚点。它们在生成和投放中承担不同作用,所以这里分别取得,无法取得时记录原因并结束本次生成。

市场来源并不会自动让舰队出现在市场附近。 参数只记录来源条件,工厂创建舰队,地点最后接收它。下面顺着这几个动作完成一支基础巡逻队。

分配这支舰队的预算

FleetParamsV3 保存本次生成请求。市场构造器带入市场势力、超空间位置、当前生涯时间戳与来源质量,调用方再指定舰队类型、各类点数和质量修正:

FleetParamsV3 params = new FleetParamsV3(
        source,
        FleetTypes.PATROL_MEDIUM,
        40f,
        12f,
        6f,
        0f,
        0f,
        0f,
        0f);

这里为战斗舰分配 40 FP,货船 12 FP,油船 6 FP,运输舰、客轮和辅助舰都为 0。qualityMod 也为 0,保留来源质量。后勤预算让本例同时生成战斗与物流成员,用于后面的集合观察。

市场构造参数速查表

构造参数本例值生成作用
source来源市场提供势力、位置、时间戳与质量来源
fleetTypePATROL_MEDIUM说明舰队用途并参与生成配置
combatPts40f战斗舰目标 FP
freighterPts12f货船目标 FP
tankerPts6f油船目标 FP
transportPts0f运输舰目标 FP
linerPts0f客轮目标 FP
utilityPts0f辅助舰目标 FP
qualityMod0f在来源质量上附加的修正

这些点数是各角色的目标 FP,而非成员数量。 战斗舰形成主体,货船、油船与运输舰分别补充货运、燃料和人员能力,客轮与辅助角色再提供对应成员。候选舰船的 FP 是离散值,实际组合还受市场倍率、学说与生成限制影响。

getTotalPts() 将六项相加,本例请求为 58 FP。工厂返回后再读 getFleetPoints(),得到实际舰队规模,两个值无需相等。

创建舰队并投放到地图

将参数交给 FleetFactoryV3.createFleet(...),得到 CampaignFleetAPI。后续要使用成员与旗舰,先处理没有对象和没有成员这两种结果:

generatedFleet = FleetFactoryV3.createFleet(params);
if (generatedFleet == null || generatedFleet.isEmpty()) {
    LOG.warn("GearIn fleet generator: factory returned an empty fleet");
    return;
}

参数描述请求,返回对象保存生成结果。调用后继续修改 params,不会追溯改变这支舰队;要改变实际成员或装配,使用后续的舰队 API。

返回舰队尚未属于任何地点。本例开启 transponder,再以市场主实体为锚点,向右偏移 300 个地图单位投放:

generatedFleet.setTransponderOn(true);
location.spawnFleet(
        anchor,
        300f,
        0f,
        generatedFleet);

spawnFleet(...) 将舰队加入地点,最终位置为锚点坐标加偏移。此后 getContainingLocation() 应对应 location,地点会继续管理这支地图舰队。

记录本次来源与实际结果:

LOG.info("GearIn fleet generator: market=" + source.getId()
        + " name=" + generatedFleet.getName()
        + " members=" + generatedFleet.getFleetData().getNumMembers()
        + " fleetPoints=" + generatedFleet.getFleetPoints()
        + " location=" + location.getId()
        + " alive=" + generatedFleet.isAlive()
        + " transponder=" + generatedFleet.isTransponderOn());

日志中的市场与地点用于找到它,成员数和实际 FP 用于确认生成内容。后面的高级舰队放到市场左侧,两个实例也就可以分别观察。

验证方案

前置数据:测试存档保留原版核心世界,存在 Jangala 或其他具有开放市场、地点和主实体的市场。

  1. 在 MOD 根目录执行 gradle jar,保存新档登记,重启游戏并创建测试存档。
  2. 进入生涯推进一帧,搜索 GearIn fleet generator,按日志中的市场 ID 前往对应星系。
  3. 在市场主实体右侧查找基础巡逻队,比较名称、成员数、实际 FP、地点、存活状态与 transponder。

预期结果:

  • 基础巡逻队属于来源市场势力,位于市场主实体右侧 300 个地图单位处。
  • 成员数大于 0,transponder 开启,日志中的 alive=true。
  • 日志地点与来源市场所在地点一致,实际 FP 来自生成后的舰队。

为另一支舰队指定生成要求

高级生成器仍使用同一个市场,增加战斗与后勤预算,并在调用工厂前填写质量、旗舰、军官与数量条件:

FleetParamsV3 params = new FleetParamsV3(
        source,
        FleetTypes.PATROL_LARGE,
        80f,
        20f,
        12f,
        0f,
        0f,
        0f,
        0f);
params.qualityOverride = 0.8f;
params.averageSMods = 1;
params.flagshipVariantId = "tempest_Attack";
params.officerLevelBonus = 1;
params.maxNumShips = 18;
params.random = new Random(104729L);
params.withOfficers = true;

qualityOverride 设为 0.8,直接选择本次质量;averageSMods 设为 1,提供装配随机化的平均内置船插目标。flagshipVariantId 使用已注册的 tempest_Attack,工厂在生成出旗舰后将对应装配应用到它。

军官等级额外加 1,withOfficers=true 保留人物生成,最终数量以 maxNumShips=18 参与常规裁剪。固定随机源使用 104729L,便于在相同数据环境中比较结果。

固定种子仍依赖输入

市场状态、势力候选与学说都会参与抽选。种子相同而这些输入改变,结果也可能改变;复验时保留相同版本、存档状态与候选数据。

取得真正完成的成员装配

舰队已经生成,不代表武器、战机和船插都已完成最后一次选择。工厂可以为它挂载 inflater,将装配随机化延迟到需要完整装备的时刻。

本例在统计高级成员之前,明确完成这一过程:

boolean inflatedBefore = generatedFleet.isInflated();
boolean inflaterAttachedBefore = generatedFleet.getInflater() != null;
generatedFleet.inflateIfNeeded();

List<FleetMemberAPI> members =
        generatedFleet.getFleetData().getMembersListCopy();

编者建议将依赖最终装备的判断放在 inflateIfNeeded() 之后。 生成返回值已经有成员,装配选择却还可能等待处理;调用后从成员重新取得装配,才能使用这次生成的最终武器、来源和 D 插。

然后统计当前装配的 REFIT 来源与 D 插:

int refitCount = 0;
int dModCount = 0;
for (FleetMemberAPI member : members) {
    if (member.getVariant().getSource() == VariantSource.REFIT) {
        refitCount++;
    }
    dModCount += DModManager.getNumDMods(member.getVariant());
}

REFIT 数量描述持有相应独立装配的成员,D 插总数描述这支舰队的结果。它们都是汇总观察项,不能据此认定每名成员经历相同处理。装配随机化也可能替换成员持有的装配引用,后续继续从 member.getVariant() 读取。

对照请求规模与实际规模

高级日志同时记录请求 FP、实际 FP、装配随机化前后状态与人物信息。指挥官和旗舰可能缺席,记录时保留空值,例如:

float spawnFPMult = Misc.getSpawnFPMult(generatedFleet);
String commanderName = generatedFleet.getCommander() == null
        ? "none"
        : generatedFleet.getCommander().getNameString();

FleetFactoryV3.KEY_SPAWN_FP_MULT 对应 $spawnFPMult,工厂记录实际 FP 与请求 FP 的比例,Misc.getSpawnFPMult(...) 读取它。高级示例从 params.getTotalPts() 取得请求,从生成舰队取得实际规模,再投放到市场左侧 300 个地图单位。

完整最小例

两类保存到前述源码目录,保留经济加载后的登记。来源选择、参数准备、生成与投放在各自一轮脚本中完成。

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

import org.apache.log4j.Logger;

import com.fs.starfarer.api.EveryFrameScript;
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.CampaignFleetAPI;
import com.fs.starfarer.api.campaign.LocationAPI;
import com.fs.starfarer.api.campaign.SectorEntityToken;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.impl.campaign.fleets.FleetFactoryV3;
import com.fs.starfarer.api.impl.campaign.fleets.FleetParamsV3;
import com.fs.starfarer.api.impl.campaign.ids.FleetTypes;
import com.fs.starfarer.api.impl.campaign.ids.Submarkets;

/** 生成一支常规巡逻舰队并投放到来源市场附近。 */
public class GearInFleetGenerator implements EveryFrameScript {

    private static final Logger LOG = Global.getLogger(GearInFleetGenerator.class);

    private boolean done;
    private CampaignFleetAPI generatedFleet;

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

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

    @Override
    public void advance(float amount) {
        if (done) {
            return;
        }
        done = true;

        MarketAPI source = pickSourceMarket();
        if (source == null) {
            LOG.warn("GearIn fleet generator: no source market");
            return;
        }

        LocationAPI location = source.getContainingLocation();
        SectorEntityToken anchor = source.getPrimaryEntity();
        if (location == null || anchor == null) {
            LOG.warn("GearIn fleet generator: source has no location or anchor");
            return;
        }

        FleetParamsV3 params = new FleetParamsV3(
                source,
                FleetTypes.PATROL_MEDIUM,
                40f,
                12f,
                6f,
                0f,
                0f,
                0f,
                0f);

        generatedFleet = FleetFactoryV3.createFleet(params);
        if (generatedFleet == null || generatedFleet.isEmpty()) {
            LOG.warn("GearIn fleet generator: factory returned an empty fleet");
            return;
        }

        generatedFleet.setTransponderOn(true);
        location.spawnFleet(
                anchor,
                300f,
                0f,
                generatedFleet);

        LOG.info("GearIn fleet generator: market=" + source.getId()
                + " name=" + generatedFleet.getName()
                + " members=" + generatedFleet.getFleetData().getNumMembers()
                + " fleetPoints=" + generatedFleet.getFleetPoints()
                + " location=" + location.getId()
                + " alive=" + generatedFleet.isAlive()
                + " transponder=" + generatedFleet.isTransponderOn());
    }

    private MarketAPI pickSourceMarket() {
        MarketAPI jangala = Global.getSector().getEconomy().getMarket("jangala");
        if (jangala != null) {
            return jangala;
        }

        for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) {
            if (market.hasSubmarket(Submarkets.SUBMARKET_OPEN)
                    && market.getContainingLocation() != null
                    && market.getPrimaryEntity() != null) {
                return market;
            }
        }
        return null;
    }
}

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

import java.util.List;
import java.util.Random;

import org.apache.log4j.Logger;

import com.fs.starfarer.api.EveryFrameScript;
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.CampaignFleetAPI;
import com.fs.starfarer.api.campaign.LocationAPI;
import com.fs.starfarer.api.campaign.SectorEntityToken;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import com.fs.starfarer.api.fleet.FleetMemberAPI;
import com.fs.starfarer.api.impl.campaign.DModManager;
import com.fs.starfarer.api.impl.campaign.fleets.FleetFactoryV3;
import com.fs.starfarer.api.impl.campaign.fleets.FleetParamsV3;
import com.fs.starfarer.api.impl.campaign.ids.FleetTypes;
import com.fs.starfarer.api.impl.campaign.ids.Submarkets;
import com.fs.starfarer.api.loading.VariantSource;
import com.fs.starfarer.api.util.Misc;

/** 生成并投放巡逻舰队,记录高级参数与装配随机化结果。 */
public class GearInFleetGeneratorAdvanced implements EveryFrameScript {

    private static final Logger LOG = Global.getLogger(GearInFleetGeneratorAdvanced.class);

    private boolean done;
    private CampaignFleetAPI generatedFleet;

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

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

    @Override
    public void advance(float amount) {
        if (done) {
            return;
        }
        done = true;

        MarketAPI source = pickSourceMarket();
        if (source == null) {
            LOG.warn("GearIn advanced fleet: no source market");
            return;
        }

        LocationAPI location = source.getContainingLocation();
        SectorEntityToken anchor = source.getPrimaryEntity();
        if (location == null || anchor == null) {
            LOG.warn("GearIn advanced fleet: source has no location or anchor");
            return;
        }

        FleetParamsV3 params = new FleetParamsV3(
                source,
                FleetTypes.PATROL_LARGE,
                80f,
                20f,
                12f,
                0f,
                0f,
                0f,
                0f);
        params.qualityOverride = 0.8f;
        params.averageSMods = 1;
        params.flagshipVariantId = "tempest_Attack";
        params.officerLevelBonus = 1;
        params.maxNumShips = 18;
        params.random = new Random(104729L);
        params.withOfficers = true;

        generatedFleet = FleetFactoryV3.createFleet(params);
        if (generatedFleet == null || generatedFleet.isEmpty()) {
            LOG.warn("GearIn advanced fleet: factory returned an empty fleet");
            return;
        }

        boolean inflatedBefore = generatedFleet.isInflated();
        boolean inflaterAttachedBefore = generatedFleet.getInflater() != null;
        generatedFleet.inflateIfNeeded();

        List<FleetMemberAPI> members = generatedFleet.getFleetData().getMembersListCopy();
        int refitCount = 0;
        int dModCount = 0;
        for (FleetMemberAPI member : members) {
            if (member.getVariant().getSource() == VariantSource.REFIT) {
                refitCount++;
            }
            dModCount += DModManager.getNumDMods(member.getVariant());
        }

        float spawnFPMult = Misc.getSpawnFPMult(generatedFleet);
        String commanderName = generatedFleet.getCommander() == null
                ? "none"
                : generatedFleet.getCommander().getNameString();

        generatedFleet.setTransponderOn(true);
        location.spawnFleet(
                anchor,
                -300f,
                0f,
                generatedFleet);

        LOG.info("GearIn advanced fleet: market=" + source.getId()
                + " requestedPoints=" + params.getTotalPts()
                + " actualPoints=" + generatedFleet.getFleetPoints()
                + " spawnFPMult=" + spawnFPMult
                + " inflatedBefore=" + inflatedBefore
                + " inflatedAfter=" + generatedFleet.isInflated()
                + " inflaterBefore=" + inflaterAttachedBefore
                + " inflaterAfter=" + (generatedFleet.getInflater() != null)
                + " members=" + members.size()
                + " refitMembers=" + refitCount
                + " dMods=" + dModCount
                + " commander=" + commanderName
                + " flagship=" + (generatedFleet.getFlagship() == null
                        ? "none"
                        : generatedFleet.getFlagship().getHullId())
                + " location=" + location.getId()
                + " alive=" + generatedFleet.isAlive());
    }

    private MarketAPI pickSourceMarket() {
        MarketAPI jangala = Global.getSector().getEconomy().getMarket("jangala");
        if (jangala != null) {
            return jangala;
        }

        for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) {
            if (market.hasSubmarket(Submarkets.SUBMARKET_OPEN)
                    && market.getContainingLocation() != null
                    && market.getPrimaryEntity() != null) {
                return market;
            }
        }
        return null;
    }
}

观察高级舰队

验证方案

前置数据:前面的新档已执行两份生成脚本,基础巡逻队验证完成。

  1. 在市场主实体左侧查找高级舰队,搜索 GearIn advanced fleet。
  2. 比较 requestedPoints、actualPoints 与 spawnFPMult,确认比例来自工厂返回结果。
  3. 比较 inflatedBefore、inflatedAfter、inflater 状态,以及 members、refitMembers 和 dMods。
  4. 读取日志中的指挥官与旗舰,查看成员装配,将装备与来源对应到同一高级舰队。

预期结果:

  • 高级舰队位于市场主实体左侧 300 个地图单位,两支示例舰队同时保留在地图上。
  • inflatedAfter=true,成员装配与 D 插统计在装配随机化后取得。
  • 日志保留请求 FP、实际 FP、比例、指挥官和旗舰,能够对应生成结果。

根据需要配置其他生成字段

基础与高级示例已经完成,其他要求从下列字段选择。FleetParamsV3 公开声明 47 个字段,其中 44 个用于调用方配置,另 3 个 transient 字段供工厂传递状态。

默认值采用字段初始化。市场构造器另外写入来源、位置、势力、时间戳与质量;如果替换来源,setSource(source, true) 会同时重算质量,直接给 source 赋值则由调用方安排。

无参构造器适合逐字段组装,超空间坐标构造器适合没有市场来源的任务,同时接受市场、坐标与显式势力的构造器适合来源与生成势力不同的任务。调用工厂前,生成势力、舰队类型与预算要对应当前要求。

来源、势力与质量速查表

字段类型默认值生成阶段用途与边界
sourceMarketAPInull参数准备来源市场;直接赋值后要自行决定是否重算质量
locInHyperVector2fnull参数准备超空间位置;无市场来源时用于建立附近环境
qualityfloat0f参数准备当前来源质量;市场构造器会更新
factionIdStringnull成员抽选生成势力 ID;工厂需要取得对应势力
fleetTypeStringnull舰队初始化舰队用途,如巡逻、贸易或袭击
factionOverrideFactionAPInull成员抽选临时替换势力对象,可携带不同 学说 或候选集合
qualityModfloat0f质量计算在来源质量上附加修正
qualityOverrideFloatnull质量计算直接指定质量;非空时作为明确覆盖
averageSModsIntegernull装配随机化指定平均内置船插数量
timestampLongnull内容可用性判断市场构造器写入当前时间戳;影响按时间判断的已知内容
allWeaponsBooleannull装配随机化放宽武器选择范围;会改变最终装配候选

六类点数速查表

字段类型默认值生成阶段用途与边界
combatPtsfloat0f成员抽选战斗舰目标 FP
freighterPtsfloat0f成员抽选货船目标 FP
tankerPtsfloat0f成员抽选油船目标 FP
transportPtsfloat0f成员抽选运输舰目标 FP
linerPtsfloat0f成员抽选客轮目标 FP
utilityPtsfloat0f成员抽选辅助舰目标 FP

舰体限制、数量和裁剪速查表

字段类型默认值生成阶段用途与边界
maxShipSizeint1000成员抽选最大舰体级别;常用级别为 0 至 4
minShipSizeint0战斗舰抽选战斗舰最小级别;剩余 FP 仍可能加入更小舰船
ignoreMarketFleetSizeMultBooleannull预算调整跳过市场舰队规模倍率
onlyApplyFleetSizeToCombatShipsBooleannull预算调整只对战斗舰预算应用规模倍率
doNotPruneBooleannull结果裁剪跳过常规裁剪,结果可能更大
doNotAddShipsBeforePruningBooleannull结果裁剪裁剪前不补充成员
maxNumShipsIntegernull结果裁剪限制本次生成的最终舰船数量
onlyRetainFlagshipBooleannull结果裁剪最终只保留旗舰
treatCombatFreighterSettingAsFractionBooleannull角色预算按比例解释 学说 的战斗货船设置
addShipsList<String>null成员抽选优先加入指定装配 ID,再消费其他预算

抽选模式与学说速查表

字段类型默认值生成阶段用途与边界
modeOverrideShipPickModenull成员抽选覆盖势力提供的舰船抽选模式
forceAllowPhaseShipsEtcBooleannull候选过滤放宽相位舰等特殊候选限制
doctrineOverrideFactionDoctrineAPInull预算与抽选替换势力 学说;影响舰体比例、军官与候选
randomRandomnull整个生成过程提供随机源;固定种子便于在同一数据环境中复验

军官、指挥官与 AI 核心速查表

字段类型默认值生成阶段用途与边界
withOfficersbooleantrue人物生成控制是否生成军官
officerLevelBonusint0人物生成增加生成军官的等级
officerNumberBonusint0人物生成增加生成军官的数量
maxOfficersToAddIntegernull人物生成限制本次新增军官数
officerNumberMultfloat1f人物生成缩放计算出的军官数量
officerLevelLimitint0人物生成覆盖军官等级上限;0 表示不覆盖
commanderLevelLimitint0人物生成限制新指挥官等级;0 使用生成逻辑
commanderPersonAPInull人物生成直接提供现有指挥官
noCommanderSkillsBooleannull人物生成跳过指挥官技能生成
aiCoresOfficerQualitynull人物生成为适用势力指定 AI 核心质量
doNotIntegrateAICoresbooleanfalse人物生成保持 AI 核心未整合状态

旗舰与显式装配速查表

字段类型默认值生成阶段用途与边界
flagshipVariantIdStringnull旗舰配置用注册表装配 ID 指定旗舰;ID 必须存在
flagshipVariantShipVariantAPInull旗舰配置直接提供旗舰装配对象
onlyRetainFlagshipBooleannull结果裁剪只保留旗舰;与数量策略共同核对
addShipsList<String>null成员抽选在常规抽选前加入指定装配

flagshipVariantId 与 flagshipVariant 用于配置已生成的旗舰。指定 ID 时,装配必须存在,返回舰队也需要有可用旗舰;它们并不直接在候选预算中购买这艘舰船。onlyRetainFlagship 与 addShips 在数量表中已经计入,重复列出用于说明相关生成要求。

工厂传递状态速查表

字段类型默认值内部职责
modetransient ShipPickModenull保存工厂当前采用的抽选模式
banPhaseShipsEtctransient booleanfalse保存特殊舰船过滤状态
blockFallbacktransient Booleannull保存候选回退是否被阻止

mode、banPhaseShipsEtc 与 blockFallback 在工厂方法之间传递状态。调用方通过前面的配置字段表达要求,保留它们的内部用途。

字段数量速查表

分组字段数
来源、势力与质量11
六类点数6
舰体限制、数量和裁剪10
抽选模式与 学说4
军官、指挥官与 AI 核心11
旗舰专属字段2
调用方字段合计44
工厂内部传递字段3
公开字段总计47

旗舰专属字段只计 flagshipVariantId 与 flagshipVariant,重复展示的 onlyRetainFlagship 与 addShips 已计入数量组。各表合起来覆盖调用方字段与工厂传递字段。

故障分析

  • 日志显示来源市场不可用:测试存档中没有 Jangala,且经济中没有同时具备开放市场、地点和主实体的市场。

    • 解决方案:改用当前存档中存在的市场,并在构造参数前分别检查 getContainingLocation() 与 getPrimaryEntity()。
  • 工厂返回 null 或空舰队:势力、舰队类型、预算与候选内容没有形成有效结果。

    • 解决方案:先使用存在的势力、标准 FleetTypes 常量和正数战斗预算,确认基础生成成立后再增加限制。
  • 实际 FP 与请求 FP 不相等:角色候选、离散舰船 FP、市场倍率、数量限制或裁剪改变了结果。

    • 解决方案:同时记录 getTotalPts()、getFleetPoints() 与 $spawnFPMult,按参数表逐项检查规模相关字段。
  • 指定旗舰没有出现:装配 ID 无效,或生成结果没有可供替换装配的旗舰。

    • 解决方案:核对 flagshipVariantId 与注册装配,检查返回舰队和当前旗舰,再读取应用后的装配。
  • 装配随机化后的统计仍为空:统计发生在 inflateIfNeeded() 之前,或代码继续使用调用前保存的成员与装配引用。

    • 解决方案:先执行装配随机化,再从 FleetDataAPI 重新取得成员,并逐个重新读取装配。
  • 固定种子仍产生不同结果:市场状态、势力候选、学说 或游戏数据发生变化。

    • 解决方案:在相同版本和相同存档状态下比较固定种子结果,并把候选数据变化作为输入变化记录。
  • 地图中找不到舰队:工厂只创建了对象,舰队尚未投放到地点。

    • 解决方案:调用 location.spawnFleet(...),随后核对 getContainingLocation() 和舰队坐标。
  • $spawnFPMult 缺失或结果异常:读取发生在工厂生成前,或使用了错误的 Memory 值类型。

    • 解决方案:只在 createFleet(...) 成功后通过 Misc.getSpawnFPMult(fleet) 读取浮点结果。

总结

两支巡逻队已经从市场条件与角色预算生成,并由地点投放到地图。高级参数参与质量、人物与旗舰配置,装配随机化完成后再提供最终装备。

请求 FP 说明生成要求,实际 FP 与成员说明结果。参数不会代替返回舰队,工厂也不会代替地点投放,后续修改继续从已经生成的对象展开。

建议参考项

  • com.fs.starfarer.api.impl.campaign.fleets.FleetParamsV3:舰队生成参数与字段定义;
  • com.fs.starfarer.api.impl.campaign.fleets.FleetFactoryV3:舰队工厂和生成规模记录;
  • com.fs.starfarer.api.campaign.CampaignFleetAPI:生成后的舰队状态;
  • com.fs.starfarer.api.campaign.LocationAPI:舰队投放入口;
  • com.fs.starfarer.api.fleet.FleetMemberAPI:装配随机化后的成员读取;
  • com.fs.starfarer.api.loading.VariantSource:装配来源枚举;
  • Starsector/starsector-core/starsector.log:地图投放与生成结果日志。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
下一页
管理舰队中的成员