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

注册市场商品

本节注册回火合金,使它能作为货物加入 Cargo,并在悬停时显示描述。随后到 Foundry 市场读取它的库存、供需与买卖报价,再通过势力规则将它列为违禁品。同一种商品在不同市场会有不同结果,规格与市场状态也要分别处理。

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

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

学习目标

完成后,您将能够:

  • 在 commodities.csv 中注册稳定的商品 ID;
  • 解释 CSV 字段如何进入 CommoditySpecAPI;
  • 区分全局商品规格和市场商品状态;
  • 读取库存、供需、贸易调节、合法性和市场价格;
  • 判断商品注册、描述挂接和势力规则是否分别生效。

前置知识

  • [C1-S1] 的 MOD 目录与数据合并;
  • [C1-S7] 的 CSV 表头、列与转义;
  • [C3-S1] 的势力文件与 gear_industry;
  • [C3-S5] 的 RESOURCE 描述类型;
  • [C12-S1] 的市场对象和市场查询入口;
  • 能够启用 Console Commands 并进入生涯市场。

为回火合金建立规格

在 MOD 的 data/campaign/commodities.csv 注册本节商品。已有文件时只追加数据行;同 ID 行参与数据合并,因此新商品使用独立 ID GearIn_tempered_alloy。文件内容如下:

name,id,demand class,base price,export value,price variability,utility,origin,tags,stack size,cargo space,icon,sound id,sound id drop,order,economyTier,econUnitOrigUnused,econUnit,baseRaidDanger,eU * base value,iconWidthMult,plugin,desc
Tempered Alloy,GearIn_tempered_alloy,heavy_machinery,180,700,3,1,,"industrial, expensive",1000,1,graphics/icons/cargo/materials.png,ui_cargo_supplies,ui_cargo_supplies_drop,2.9,2.2,200,600,,108000,1,,

显示名采用 Tempered Alloy,需求类别沿用 heavy_machinery。新商品身份与已有经济类别分别设置,市场才能按这个类别处理需求;本节先说明商品如何接入,生产规则留给产业实现。

基础价格为 180,出口价值为 700,价格波动级别为 3,效用为 1。它们是经济计算的输入,实际报价还要结合市场。economyTier 取 2.2,econUnit 取 600,参与经济规模与物品数量换算;货舱空间每件为 1,规格单栈上限为 1000,栈是否采用这个上限由 [C12-S2] 的 Cargo 设置决定。

图标沿用原版金属图标,两列声音使用已注册的补给拾取与放下声音。tags 的 industrial, expensive 是两个独立标签,CSV 按 [C1-S7] 的规则给含逗号字段加引号。标签供分类与筛选使用,具体用途取决于读取它的功能。

商品字段速查表

字段当前职责
name / id显示名与代码、规则使用的稳定 ID
demand class经济需求类别,本例使用已建立的 heavy_machinery
base price / export value基础价格与出口价值
price variability / utility价格波动级别与商品效用
origin / tags来源标识与逗号分隔的标签集合
stack size / cargo space规格单栈上限与单件空间
icon / iconWidthMult图标路径与显示宽度倍率
sound id / sound id drop物品拾取与放下声音 ID
order列表显示顺序
economyTier / econUnit经济规模参数与数量换算基准
baseRaidDanger突袭危险度
econUnitOrigUnused / eU * base value表中的辅助数据列,保留原列位
plugin / desc保留表头位置;商品插件和货舱长描述不从这两列接入

登记货舱描述

货舱长描述沿 [C3-S5] 的 RESOURCE 类型注册,键使用商品 ID。把下面两行写入 data/strings/descriptions.csv,已有表头时只追加记录:

id,type,text1,text2,text3,text4,text5,notes
GearIn_tempered_alloy,RESOURCE,经过回火处理的工业合金,可用于需要耐磨与韧性的结构部件。,,,,,

text1 在货舱与交易界面的悬停说明中显示。商品规格让货物可以被识别,描述行给它提供正文,两处 ID 逐字符对应。启动后先确认规格能够取得,再通过控制台加入货物,观察图标、声音与悬停结果。

读取全局商品规格

通过设置或经济对象取得 CommoditySpecAPI,可以读取刚才的全局定义。以下局部片段记录回火合金的身份、经济类别、基础价格与货舱规格:

CommoditySpecAPI spec = Global.getSettings().getCommoditySpec("GearIn_tempered_alloy");
LOG.info(String.format("spec id=%s name=%s demand=%s base=%.0f stack=%d space=%.1f",
        spec.getId(), spec.getName(), spec.getDemandClass(), spec.getBasePrice(),
        spec.getStackSize(), spec.getCargoSpace()));

这些值说明商品注册成了什么。即使规格的基础价格为 180,当前市场报价也可以不同;库存与供需从市场对象继续取得。

规格读取入口速查表

读取目的入口
身份与名称getId()、getName()、getLowerCaseName()、getOrigin()
图标与声音getIconName()、getIconLargeName()、getIconWidthMult()、getSoundId()、getSoundIdDrop()
经济参数getBasePrice()、getExportValue()、getPriceVariability()、getDemandClass()、getUtility()、getEconomyTier()、getEconUnit()
堆叠与空间getStackSize()、getCargoSpace()
标签与经济分类getTags()、hasTag()、isPrimary()、isExotic()、isMeta()、isNonEcon()
物流资源身份isPersonnel()、isFuel()、isSupplies()
数据来源来源 MOD 查询,供注册冲突与加载来源定位

isPrimary() 判断商品 ID 是否也是它自己的需求类别。本例使用 heavy_machinery 类别,因此回火合金不是这个类别的主商品。exotic、meta 与 nonecon 标签分别影响对应经济分类,人员、燃料与补给判断则供物流功能使用。分类、库存与违禁规则各有读取入口,标签本身不会自动建立市场货物或势力限制。

setName()、setIconName()、setDemandClass()、setBasePrice()、setExportValue() 与 setBaseDanger() 会改变运行时全局规格,随后读取它的对象也会取得新值。本例只读规格,商品数据由 CSV 提供,市场即时状态按下面的对象处理。

到 Foundry 读取商品状态

创建 src/data/scripts/gearin/campaign/GearInCommodityProbe.java。在 [C12-S1] 的 GearInEconomyProbe.ProbeListener.economyUpdated() 中,将以下局部调用放在市场日志之后、expired = true 之前:

GearInCommodityProbe.log(market);

两类位于同一包,监听器已经保存 Foundry 市场引用。完整观察器对回火合金、补给与毒品逐项读取,零库存也可以记录当前经济状态。它从 market.getCommodityData(id) 取得 CommodityOnMarketAPI,再通过 getCommodity() 回到规格、getMarket() 确认市场,局部片段如下:

CommodityOnMarketAPI onMarket = market.getCommodityData("GearIn_tempered_alloy");
CommoditySpecAPI spec = onMarket.getCommodity();
LOG.info("market=" + onMarket.getMarket().getId() + " spec=" + spec.getId());

区分经济档位与货物件数

getStockpile() 读取库存状态,getAvailable() 读取经济可得量档位。getMaxSupply()、getMaxDemand() 是产业供需档位,getDemand() 返回需求对象,getDemandValue() 读取需求值。经济更新通过 updateMaxSupplyAndDemand() 重新取得当前产业的供需,观察器在更新结束后读取。

产业的供需档位与 Cargo 中的件数采用不同单位。 getDeficitQuantity() 返回可按较高价格向市场出售的件数,getExcessQuantity() 返回可按较低价格从市场购买的件数,适合与本次交易数量比较。换算还受到 econUnit 与当前经济规模影响,档位 3 不能直接理解为库存 3 件。

需要进一步解释当前市场时,getUtilityOnMarket() 已考虑当地商品效用,getExportIncome() 读取出口收入,getCommodityMarketData() 取得商品的市场汇总数据。市场商品也提供 isPersonnel()、isFuel()、isNonEcon() 与 isMeta() 等分类,用于相应计算。

读取近期贸易带来的调节

贸易和经济事件通过带期限的修饰改变可得状态。getTradeMod() 允许正负值,getTradeModPlus() 只取正向部分,getTradeModMinus() 只取负向部分,getCombinedTradeModQuantity() 返回三者合并后的物品数量影响。下面两段局部日志分别记录总结果与各通道:

float tradeQuantity = onMarket.getCombinedTradeModQuantity();
float modValue = onMarket.getTradeMod().getModifiedValue();
LOG.info(String.format("trade quantity=%.1f modified value=%.1f",
        tradeQuantity, modValue));
LOG.info(String.format("trade mod=%.1f plus=%.1f minus=%.1f combined=%.1f",
        onMarket.getTradeMod().getModifiedValue(),
        onMarket.getTradeModPlus().getModifiedValue(),
        onMarket.getTradeModMinus().getModifiedValue(),
        onMarket.getCombinedTradeModQuantity()));

这些通道使用 MutableStatWithTempMods,日志从 getModifiedValue() 取得修饰后的值。addTradeMod(...)、addTradeModPlus(...) 与 addTradeModMinus(...) 接受来源、数量与持续生涯日,写入后再换算成可得量档位修饰。

因此近期交易可以改变可得量、赤字或盈余和报价,而库存并未按同样数量直接改变。需要直接设置当前市场库存时使用 setStockpile(...),设置供需上限时使用 setMaxSupply(...)、setMaxDemand(...);它们与带期限的事件效果分别处理。本节的观察器只读当前结果。

为指定数量取得买卖报价

有了规格与当前市场状态,再从 MarketAPI 取得报价。getDemandPrice(...) 表示市场买入,即玩家卖出;getSupplyPrice(...) 表示市场卖出,即玩家买入。本例固定 100 件,并采用玩家价格:

float demandPrice = market.getDemandPrice("GearIn_tempered_alloy", 100, true);
float supplyPrice = market.getSupplyPrice("GearIn_tempered_alloy", 100, true);
LOG.info(String.format("price for 100: demand=%.0f supply=%.0f", demandPrice, supplyPrice));

返回值是这 100 件商品的金额,子市场税额另计。 isPlayerPrice 决定是否应用玩家价格修饰,库存、供需与近期贸易决定当前报价,市场刷新后结果可能改变。规格基础价格与出口价值仍是各自的参数,显示名称也没有参与 ID 查询。

两个方向可以有不同金额。需要让日志直接对应玩家操作时,用下面的局部写法保留方向名称:

String id = onMarket.getId();
float sellToMarket = market.getDemandPrice(id, 100, true);
float buyFromMarket = market.getSupplyPrice(id, 100, true);
LOG.info(String.format("%s sell=%.0f buy=%.0f", id, sellToMarket, buyFromMarket));

判断商品交易合法性

把回火合金列为齿轮工业的违禁品。打开 [C3-S1] 的 data/world/factions/gear_industry.faction,在已有 illegalCommodities 数组中追加商品 ID,其余条目保留。局部形式如下:

"illegalCommodities":[
    "drugs",
    "organs",
    "GearIn_tempered_alloy"
],

CommodityOnMarketAPI.isIllegal() 读取当前市场判断,普通势力规则同时考虑商品 ID 与需求类别,自由港还会改变结果。isIllegalAssumePrimary() 直接检查势力违禁列表中的商品 ID,适合取得这项直接声明。比较两者时,同时确认市场势力与自由港状态。

isSupplyLegal()、isDemandLegal() 则保留产业供给与需求的合法性结果,经济更新从提供相应供需的产业读取。具体子市场是否允许一笔买卖,由 [C12-S4] 的交易插件决定。产业合法性、市场违禁判断与子市场交易许可各自有用途,当前日志分别记录。

最后让日志保留市场上下文。完整观察器的 log(...) 已在循环前加入以下局部行:

LOG.info("GearIn commodity context: market=" + market.getId());

市场 ID 与商品 ID 一起定位结果。控制台能加入回火合金后,继续悬停确认描述、进入目标市场确认违禁标记,再按相同数量比较报价与税额。

完整最小例

保存为 src/data/scripts/gearin/campaign/GearInCommodityProbe.java,调用入口采用前面的经济观察器。

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

import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.campaign.econ.CommodityOnMarketAPI;
import com.fs.starfarer.api.campaign.econ.CommoditySpecAPI;
import com.fs.starfarer.api.campaign.econ.MarketAPI;
import org.apache.log4j.Logger;

public class GearInCommodityProbe {
    private static final Logger LOG = Global.getLogger(GearInCommodityProbe.class);
    private static final String[] COMMODITY_IDS = {
            "GearIn_tempered_alloy",
            "supplies",
            "drugs"
    };

    private GearInCommodityProbe() {
        // 工具类:不实例化
    }

    public static void log(MarketAPI market) {
        LOG.info("GearIn commodity context: market=" + market.getId());
        for (String commodityId : COMMODITY_IDS) {
            CommoditySpecAPI spec = Global.getSettings().getCommoditySpec(commodityId);
            CommodityOnMarketAPI onMarket = market.getCommodityData(commodityId);
            LOG.info(String.format(
                    "GearIn commodity %s: name=%s demand=%s base=%.0f stack=%d space=%.1f tags=%s stockpile=%.1f available=%d maxSupply=%d maxDemand=%d utility=%.2f deficit=%d excess=%d tradeMod=%.1f illegal=%s supplyLegal=%s demandLegal=%s demandPrice(100)=%.0f supplyPrice(100)=%.0f",
                    spec.getId(), spec.getName(), spec.getDemandClass(), spec.getBasePrice(),
                    spec.getStackSize(), spec.getCargoSpace(), spec.getTags(),
                    onMarket.getStockpile(), onMarket.getAvailable(), onMarket.getMaxSupply(),
                    onMarket.getMaxDemand(), onMarket.getUtilityOnMarket(),
                    onMarket.getDeficitQuantity(), onMarket.getExcessQuantity(),
                    onMarket.getCombinedTradeModQuantity(), onMarket.isIllegal(),
                    onMarket.isSupplyLegal(), onMarket.isDemandLegal(),
                    market.getDemandPrice(commodityId, 100, true),
                    market.getSupplyPrice(commodityId, 100, true)));
        }
    }
}

验证方案

前置数据:启用包含本节 commodities.csv 注册行和 GearInCommodityProbe 类的 GearIn MOD,并使用拥有 gear_industry 市场且势力 illegalCommodities 已含 GearIn_tempered_alloy 的测试存档,同时已启用 Console Commands 控制台。

  1. 启动游戏并进入 GearIn 市场,确认 commodities.csv 已经加载。
  2. 执行 additem GearIn_tempered_alloy 10,打开货舱和交易界面。
  3. 查看 GearIn commodity 日志,按市场 ID 对应回火合金、补给与毒品,核对规格、经济档位、物品数量和两个价格方向。
  4. 悬停回火合金,查看 RESOURCE 正文与违禁品标记;比较界面的商品金额与税额,日志给出 100 件的报价,金额比较采用同数量、同市场状态。

预期结果:

  • 日志出现 GearIn_tempered_alloy 的名称、需求类别、基础价格、堆叠和货舱空间;
  • 市场商品日志同时出现库存、可得量、供需上限、赤字/盈余和贸易调节;
  • demandPrice(100) 对应市场买入、玩家卖出,supplyPrice(100) 对应市场卖出、玩家买入;
  • 市场合法性状态与 illegalCommodities 中的商品 ID 一致;
  • 货舱显示 Tempered Alloy ×10,悬停正文来自本节 RESOURCE 行;价格方向与税费分别观察。

故障分析

  • 商品规格为空:commodities.csv 路径错误、列位错位或商品 ID 重复。

    • 解决方案:核对文件路径、表头顺序和 GearIn_tempered_alloy,重启后查看加载日志。
  • 取得商品对象时发生加载错误:商品 ID 未注册或需求类别没有建立。

    • 解决方案:先用设置取得商品规格,再核对 CSV 的 ID 与 demand class,重启后从目标市场查询。
  • 基础价格与交易价格不同:把 CommoditySpecAPI.getBasePrice() 当成了玩家当前交易价。

    • 解决方案:使用 MarketAPI.getDemandPrice() 和 getSupplyPrice(),同时固定数量与玩家价格标志。
  • 违禁品标记没有出现:势力文件中的 ID 与商品注册 ID 不一致。

    • 解决方案:逐字核对 illegalCommodities 中的 GearIn_tempered_alloy,确认当前市场属于 gear_industry 且未启用自由港,再读取 isIllegal() 并核对所用子市场的交易规则。
  • 价格方向记录反了:把需求价写成玩家买入,或把供给价写成玩家卖出。

    • 解决方案:按需求方向是市场买入、供给方向是市场卖出的规则重新标注日志和验证结果。

总结

回火合金的规格由 CSV 提供,货舱正文用同一 ID 登记为 RESOURCE,市场再按当前供需和贸易计算状态。经济档位、物品件数与交易金额各有单位,报价也区分市场买入和卖出。

势力违禁列表提供商品规则,市场条件影响当前判断,子市场决定实际交易许可。记录市场、商品与数量,就能把这些结果对应到玩家正在进行的交易。

建议参考项

  • data/campaign/commodities.csv:商品 ID、显示、经济和货舱字段的注册来源;
  • com.fs.starfarer.api.campaign.econ.CommoditySpecAPI:全局商品规格和分类属性;
  • com.fs.starfarer.api.campaign.econ.CommodityOnMarketAPI:市场商品库存、供需、调节和合法性状态;
  • com.fs.starfarer.api.campaign.econ.MarketAPI:市场价格、商品接入和合法性查询;
  • Starsector/starsector-core/starsector.log:确认商品加载和运行时错误。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
上一页
读写货舱内容
下一页
实现专用子市场