Skip to content

补丁生成流程说明

字数
5627 字
阅读时间
23 分钟

本文说明当前精简版 Realism Patch Generator 是怎样从输入 JSON 一步步生成 output 补丁的。

当前文档对应的是当前实际实现,核心特征如下:

  • 当前核心直接支持 RealismStandardTemplate、WttArmory_templates、Epic_templates、ConsortiumOfThings_templates、Requisitions_templates、EcoAttachment_templates、Artem_templates、WttStandalone_templates、SptBattlepass_templates、RaidOverhaul_templates、Moxo_Template 和 Mixed_templates 十二种输入格式
  • Moxo_Template 使用 clone + item/items 结构,并且输出 Name 优先取 locales.Name
  • Mixed_templates 允许同一个文件里同时出现 clone + item/items 和 direct item/items 条目
  • 只有 input/attatchments、input/gear、input/weapons 下的 RealismStandardTemplate 输出保持原文件名
  • 其他当前支持的输出文件写为“源文件名 + _realism_patch.json”
  • 输出条目顺序严格保留输入源文件中的物品顺序
  • 生成主入口统一收敛到 RealismPatchGenerator.Core.RealismPatchGenerator
  • 输入字段是否会被读取,不等于该字段允许进入最终输出;最终 output 只保留 Realism 标准补丁字段

1. 整体入口

当前程序有两个常见入口,但它们最终都走同一条核心生成链路:

  1. GUI 程序

    • 文件:RealismPatchGenerator.Gui/Form1.cs
    • 在点击“生成”按钮后,会调用:
      • new RealismPatchGenerator.Core.RealismPatchGenerator(repositoryRoot, seed?).Generate(outputPath)
  2. 临时控制台 runner

    • 文件:artifacts/moxo-check-runner/Program.cs
    • 本质上也是调用同一个 Core 生成器

也就是说,无论你是从 GUI 运行,还是从临时 runner 运行,真正负责生成补丁的都是:

  • RealismPatchGenerator.Core/RealismPatchGenerator.cs

2. 程序启动后先做什么

在创建 RealismPatchGenerator 实例时,构造函数会先准备生成所需的运行环境。

2.1 解析基础路径

构造函数接收一个 basePath,随后派生出几个关键目录:

  • input:输入物品目录
  • RealismItemTemplates:模板目录
  • RealismItemRules:规则目录

对应代码位于:

  • RealismPatchGenerator.Core/RealismPatchGenerator.cs
  • RealismPatchGenerator.Core/RuleWorkspace.cs

当前仓库里,真正参与运行的是这两个目录:

  • RealismItemTemplates
  • RealismItemRules

它们不是 docs 里早期中文目录名的替代说明,而是当前代码实际查找的目录。

2.2 准备随机种子

生成器会保存一个 generationSeed,并据此创建 CompatibleRandom。

作用:

  • 某些范围字段在生成时需要在规则范围内取值
  • 相同 seed 下可以复现同一轮随机结果

如果外部没有传 seed,程序会自动生成一个运行时 seed。

2.3 加载规则

构造函数里会调用:

  • RuleSetLoader.Load(basePath, Log)

这一步会从 RealismItemRules 中加载四类规则:

  • weapon_rules.json
  • attachment_rules.json
  • ammo_rules.json
  • gear_rules.json

如果这些文件不存在,程序会先写入默认规则,再继续加载。

规则加载后会形成一个内存对象 RuleSet,其中包含:

  • 武器分组、数值范围、口径修正、枪托修正
  • 配件父类到基础 profile 的映射,以及各 profile 的数值范围
  • 弹药 profile、穿深档位修正、特殊弹修正
  • 装备 profile 及其数值范围

2.4 加载例外覆盖

构造函数里还会调用:

  • ItemExceptionStore.Load(basePath)

它会从下面这个文件读取手工例外覆盖:

  • RealismItemRules/item_exceptions.json

这份文件的作用是:

  • 对个别物品做人工修正
  • 在自动生成之后,强制覆盖指定字段

这一步的定位是“最后兜底”,不是主规则本身的一部分。


3. 开始生成:Generate 的主流程

调用 Generate(outputDirectory) 后,主流程可以概括成五步:

  1. 检查必要目录是否存在
  2. 加载全部模板
  3. 扫描 input 下所有 JSON 输入文件
  4. 逐个文件、逐个物品生成 patch
  5. 将 patch 按源文件分组写入 output

在这五步之间,生成器始终执行一条硬边界规则:

  • 输入阶段可以读取第三方源字段做 clone、fallback、modType 推断和容量推断
  • 输出阶段会统一经过字段白名单裁剪
  • 不属于 Realism 标准字段的源输入字段,即使参与了推断,也不会进入最终补丁

下面按代码顺序展开。


4. 第一步:检查目录

Generate 一开始会调用 EnsureRequiredDirectories()。

它会验证两个目录必须存在:

  • input
  • RealismItemTemplates

如果缺失,直接抛异常,不继续生成。

这是为了保证:

  • 输入数据一定可读
  • 模板和字段顺序信息一定可用

5. 第二步:加载全部模板

接下来调用 LoadAllTemplates()。

这一步会遍历模板目录中的几个子目录:

  • weapons
  • attatchments
  • ammo
  • gear
  • consumables

然后把每个模板 JSON 文件读入内存,建立几套索引:

5.1 templateById

按物品 ID 索引模板对象。

用途:

  • 需要按 ItemId 精确找模板时可直接取

5.2 templates

按模板文件名索引一整份模板表。

用途:

  • 通过模板文件名推断字段结构
  • 某些分类逻辑需要知道某个模板文件的内容

5.3 templateParentIndex

按模板文件名索引它可能对应的 parentId 列表。

用途:

  • 当输入里没给出 parentId 时,可以从模板文件反推一个 parentId

这一步本质上是在做“静态世界知识预加载”,让后面的物品分类和输出规范化能基于模板完成。


6. 第三步:扫描 input 下的所有输入文件

Generate 会把 input 下所有 json 文件递归找出来,并按相对路径排序。

随后依次调用:

  • ProcessItemFile(filePath)

这里的设计目标是:

  • 输出顺序稳定
  • 同一个 input 文件中的物品,最终仍然写回到对应的 output 文件
  • 输出命名按输入格式和来源目录区分,避免把标准模板文件和第三方源补丁混为一类

7. 第四步:处理单个输入文件

ProcessItemFile 的职责是“以文件为单位组织处理”。

它会做这些事:

  1. 读取当前 input JSON 文件
  2. 把根对象里的每个条目视为一个物品
  3. 对每个物品调用 ProcessSingleItem
  4. 保留当前 sourceFile 的输出顺序信息,供最终写出时使用

只要某个文件成功进入当前支持的输入格式路径,最终写出时都会遵守两条固定约束:

  1. 条目顺序与输入源完全一致
  2. 只有 input/attatchments、input/gear、input/weapons 下的 RealismStandardTemplate 输出保持原文件名,其他支持的输出文件追加 _realism_patch

例如:

  • input/weapons/xxx.json
  • output/weapons/xxx.json

另一个例子:

  • input/user_templates/foo.json
  • output/user_templates/foo_realism_patch.json

如果某个文件的旧命名形式仍残留在 output 中,保存阶段会删除与当前规则不一致的另一种命名文件,避免混用。

Moxo_Template 还会在这一阶段支持两种 clone 来源:

  • clone 到模板库已有物品
  • clone 到同一源文件里前面已经成功生成的物品

8. 第五步:识别单个物品是否可处理

真正进入物品级逻辑的是 ProcessSingleItem。

它会先做几层筛选。

8.1 去重

如果某个 itemId 在当前文件里已经处理过,就直接跳过。

8.2 跳过显式禁用项

如果输入里有:

  • enable = false

则直接跳过,不生成补丁。

8.3 当前支持的输入识别

当前版本会先对整份输入文件做格式识别。

目前支持 4 条路径:

  • RealismStandardTemplate:条目包含 $type 和 ItemID,且不带旧格式标记字段;条目可以额外带 TemplateID,表示这是标准模板内部的克隆引用,而不是 legacy 输入

  • Moxo_Template:条目包含 clone,且同时包含 item 或 items

  • Mixed_templates:整个文件中的条目都属于 legacy item/items 体系;其中既允许 clone + item/items,也允许 direct item/items

  • RaidOverhaul_templates:条目包含 ItemToClone,属性主要来自 OverrideProperties

旧格式标记字段包括:

  • itemTplToClone

  • clone

  • ItemToClone

  • OverrideProperties

  • overrideProperties

  • item

  • items

  • 如果文件无法整体落入以上任一支持路径,程序才会把该文件判定为“暂不支持的输入结构文件”并跳过。


9. 第六步:提取物品上下文信息

对于通过筛选的物品,程序会调用:

  • ExtractItemInfo(itemId, itemData, sourceFile)

它会组装一个 ItemInfo 对象,里面保存后续生成需要的上下文。

9.1 直接从输入读取的信息

包括:

  • ItemId
  • Name
  • $type 对应的 ItemType
  • parentId
  • 输入中的原始属性集合
  • sourceFile

9.2 Name 的提取方式

优先顺序大致是:

  1. 输入对象里的 Name
  2. locales 中的多语言名称
  3. LocalePush 中的名称

如果前面都拿不到,后面还会有兜底名称。

9.3 parentId 标准化

如果 parentId 是大写枚举名或带下划线的逻辑名,程序会把它映射成实际 ID。

这一步靠的是 StaticData.ItemTypeNameToId。

9.4 模板文件推断

如果已有 parentId,程序会先通过 parentId 找模板文件。

如果还没有模板文件,就尝试从 sourceFile 反推模板文件名。

9.5 分类补强

程序会结合以下信息补强物品类别:

  • $type
  • parentId
  • sourceFile 所在目录

从而判断这个物品是不是:

  • 武器
  • 装备
  • 消耗品

这一层补强的意义是:

  • 输入可能不完全规范
  • 但后续数值修正必须知道它属于哪一大类

10. 第七步:以输入对象为基础创建 patch

当前版本不是“从空模板完全重建”,而是采用更保守的策略:

  • 先深拷贝输入对象
  • 再在这份拷贝上做规范化和修正

对应逻辑在当前实现里体现为:

  • RealismStandardTemplate:普通条目直接深拷贝;若带 TemplateID,则先展开对应基底模板再并入当前条目的显式字段
  • Moxo_Template:先找 clone 基底,再并入有效输入字段
  • Mixed_templates:clone 条目优先走 Moxo 路径,direct 条目按 parent/category 推断基底
  • RaidOverhaul_templates:优先复用模板库 clone 基底,缺失时再回退推断基底

它会做几件事:

  1. 先确定 patch 基底:直接使用输入对象,或先展开 clone/TemplateID 对应的基底
  2. 强制写入 ItemID
  3. 如果 Name 缺失,则回填提取出的名称
  4. 同步 ItemInfo 的大类标记
  5. 进入 FinalizePatch 做最终加工

这个设计的核心思想是:

  • 最大限度保留输入里已经正确的字段
  • 对克隆条目先补齐完整 Realism 结构,再对需要规范化的部分做修正

11. 第八步:FinalizePatch 的最终加工链

FinalizePatch 是整个生成逻辑最核心的一段,它按固定顺序执行下面几件事:

  1. MergeInputProperties
  2. EnsureBasicFields
  3. ApplyRealismSanityCheck
  4. ApplyItemException
  5. NormalizeStructuredOutput
  6. AddToFilePatches

下面分别说明。

11.1 MergeInputProperties

这一步会把输入对象里抽取出的属性重新并入 patch。

当前各支持路径在进入 FinalizePatch 之前,都会先完成各自的基底补丁构造与输入字段提取;MergeInputProperties 的职责是把已经筛选过的有效字段重新并入 patch。

11.2 EnsureBasicFields

这一步负责补齐生成补丁最基本的结构字段:

  • ItemID
  • Name
  • $type
  • 某些情况下的 ConflictingItems

如果 $type 缺失,会根据上下文自动推断:

  • 武器 -> RealismMod.Gun, RealismMod
  • 弹药 -> RealismMod.Ammo, RealismMod
  • 装备 -> RealismMod.Gear, RealismMod
  • 消耗品 -> RealismMod.Consumable, RealismMod
  • 其它 -> RealismMod.WeaponMod, RealismMod

如果名称还是为空,则根据物品类型生成一个兜底名,比如:

  • weapon_物品ID
  • ammo_物品ID
  • mod_物品ID

11.3 ApplyRealismSanityCheck

这是“自动修正补丁内容”的核心步骤。

它的顺序是:

  1. 先补齐必需字段
  2. 先做一轮通用启发式修正
  3. 再按物品大类进入对应规则分支

11.4 ApplyItemException

在规则修正完成之后,再应用 item_exceptions.json 里的手工覆盖。

顺序上放在这里很重要,因为这保证了:

  • 规则系统先跑完
  • 手工例外拥有最终优先级

11.5 NormalizeStructuredOutput

当前主要对弹药做结构规范化。

弹药 patch 会按模板字段顺序重新整理输出,确保结构统一。

11.6 AddToFilePatches

最后把 patch 按 sourceFile 分组缓存起来,等待统一落盘。

这一步决定了最后 output 的文件组织方式。


12. 第九步:规则修正具体怎么做

12.1 通用预处理启发式

在进入具体类别规则前,程序会先基于名称做几类通用启发式:

  • 材料启发式
    • 比如 titanium、carbon、steel
  • 尺寸启发式
    • 比如 compact、mini、short、long、extended
  • 枪管长度启发式
    • 从名称中提取枪管长度,推断 Velocity

这些启发式的作用不是最终定值,而是先把明显不合理的数值拉回更可信的范围。


13. 武器是如何生成和修正的

如果 patch 的 $type 被判定为 RealismMod.Gun,程序会进入武器分支。

13.1 武器 profile 推断

InferWeaponProfile 会按这个顺序推断武器档位:

  1. 先看 parentId 是否命中 weaponParentGroups
  2. 再看模板文件名是否命中 TemplateFileToWeaponProfile
  3. 再看 sourceFile 文件名是否能映射 profile
  4. 最后根据名称和 WeapType 做关键词判断

可识别的典型 profile 包括:

  • pistol
  • smg
  • launcher
  • sniper
  • machinegun
  • shotgun
  • assault

13.2 应用武器规则范围

确定 profile 后,程序会:

  1. 对应取出 weaponProfileRanges
  2. 先应用基础范围
  3. 再根据口径 profile 叠加 weapon caliber modifier
  4. 再根据枪托 profile 叠加 stock modifier
  5. 最后再做 clamp

也就是说,武器值不是只看“武器大类”,而是:

  • 武器基础档位
  • 口径修正
  • 枪托结构修正

三层叠加的结果。

13.3 武器额外规则

例如:

  • 如果识别为 pistol,会强制 HasShoulderContact = false
  • RecoilAngle 超过合理范围会被拉回默认值

最后还会经过全局安全钳制,避免出现过大或过小的异常值。


14. 配件是如何生成和修正的

如果 patch 的 $type 被判定为 RealismMod.WeaponMod,就会进入配件分支。

14.1 配件 profile 推断

InferModProfile 是当前代码里最复杂的一段分类逻辑之一。

它会综合:

  • ModType
  • 名称关键词
  • parentId 对应的基础 profile
  • 模板文件名

来判定配件属于哪一类。

典型结果包括:

  • muzzle_adapter
  • muzzle_brake
  • muzzle_flashhider
  • muzzle_suppressor_*
  • barrel_*
  • handguard_*
  • magazine_*
  • stock_*
  • pistol_grip
  • receiver
  • gasblock
  • mount
  • iron_sight
  • scope_red_dot
  • scope_magnified
  • flashlight_laser
  • foregrip
  • bipod
  • ubgl

14.2 应用配件规则范围

配件 profile 确定后,程序会:

  1. 先做基础 clamp
  2. 对特殊类型做硬规则修正
  3. 应用该 profile 对应的数值范围
  4. 对少数字段保留源值但限制在规则范围内
  5. 再做一次 clamp 和全局安全钳制

14.3 配件特殊硬规则

当前代码里有一些明确的结构性修正,例如:

  • barrel_2slot 的 ModShotDispersion 会被压到 0
  • bipod 不应该拥有 AutoROF、SemiROF、ReloadSpeed 之类的异常值,会被归零
  • 非枪管类配件的 Velocity 上限比枪管更低
  • suppressor 类 profile 会补 CanCycleSubs = true

14.4 源字段保留策略

某些 profile 不完全重采样,而是优先保留源值并限制到规则范围内。

例如:

  • gasblock 的 Loudness、Velocity
  • iron_sight 的 Accuracy
  • handguard 的 Accuracy、Dispersion

这个策略的目的是:

  • 保留输入数据中原本已经合理的特征
  • 避免所有同类物品被规则系统洗成过度平均化的结果

15. 装备是如何生成和修正的

如果 patch 的 $type 被判定为 RealismMod.Gear,就会进入装备分支。

15.1 装备 profile 推断

InferGearProfile 会综合这些信息判断装备属于哪一类:

  • parentId
  • 模板文件名
  • Name
  • ArmorClass
  • 一些防护相关字段

典型 profile 包括:

  • armor_plate_hard
  • armor_plate_soft
  • armor_plate_helmet
  • armor_vest_light
  • armor_vest_heavy
  • armor_chest_rig_light
  • armor_chest_rig_heavy
  • chest_rig_light
  • chest_rig_heavy
  • helmet_light
  • helmet_heavy
  • armor_mask_ballistic
  • armor_mask_decorative
  • backpack_compact
  • backpack_full
  • cosmetic_gasmask
  • protective_eyewear_ballistic
  • protective_eyewear_standard

15.2 应用装备规则范围

确定 profile 后,程序会:

  1. 先按 gearClampRules 做基础钳制
  2. 应用对应 gearProfileRanges
  3. 再次钳制
  4. 应用全局安全钳制

装备分支的原则是:

  • 用 profile 控制主要数值区间
  • 避免出现不符合装备类别的离谱字段

16. 弹药是如何生成和修正的

如果 patch 的 $type 被判定为 RealismMod.Ammo,就会进入弹药分支。

16.1 弹药 profile 推断

弹药修正会先推断三层信息:

  1. ammoProfile
  2. penetrationTier
  3. specialProfile

它们分别代表:

  • 这是什么基础弹种
  • 穿深处在哪个档位
  • 是否属于特殊用途弹

16.2 叠加规则方式

弹药数值的生成逻辑是:

  1. 先取 ammoProfileRanges 里的基础范围
  2. 再叠加 ammoPenetrationModifiers
  3. 再叠加 ammoSpecialModifiers
  4. 对特殊字段再做额外限制

额外限制主要有:

  • MalfMisfireChance / MisfireChance / MalfFeedChance 会被限制在非常小的区间
  • ArmorDamage 会被限制在 1.0 到 1.2 之间

16.3 弹药结构规范化

弹药在最后输出前还会走 NormalizeAmmoOutputStructure。

这一步会按弹药模板字段顺序重建输出对象,目的有两个:

  1. 字段顺序稳定
  2. 输出结构与模板一致

17. 第十步:应用例外覆盖

当规则修正结束后,程序会检查当前物品是否在 item_exceptions.json 里存在启用中的例外项。

如果存在,就会把 overrides 里的字段逐项覆盖到 patch 上。

这一步的优先级高于自动规则,因此适合处理:

  • 个别规则难以表达的特殊物品
  • 不希望再被自动推断改动的字段
  • 临时修复和人工校正

18. 第十一步:按来源文件分组缓存

每个 patch 生成后,不会立刻写文件,而是先通过 AddToFilePatches 放进一个按 sourceFile 分组的缓存结构。

这个结构保留了两个信息:

  1. 物品属于哪个输入文件
  2. 输入文件在本轮遍历中的顺序

这样做的好处是:

  • 输出目录结构可以和 input 对齐
  • 输出顺序稳定
  • 最终写文件只做一次集中落盘

19. 第十二步:统一写入 output

Generate 的最后一步是 SavePatches(outputPath)。

它会遍历之前缓存好的 fileBasedPatchOrder,并逐组写出 JSON 文件。

19.1 输出路径规则

如果调用 Generate 时传了 outputDirectory,就写到那个目录。

如果没传,则默认写到:

  • basePath/output

19.2 输出文件命名规则

当前流程按输入格式和来源目录决定文件名:

  • input/attatchments、input/gear、input/weapons 下的 RealismStandardTemplate 输出为原文件名 + .json
  • 其他当前支持的输出为原文件名 + _realism_patch.json

例如:

  • input/attatchments/foo.json

  • output/attatchments/foo.json

  • input/user_templates/foo.json

  • output/user_templates/foo_realism_patch.json

如果 output 中还残留另一种旧命名文件,保存阶段会删除旧文件,避免把旧输出和新输出混在一起。

19.3 输出内容组织方式

每个输出文件内部仍然是:

  • 以 itemId 为 key
  • 以 patch 对象为 value

也就是说,程序并不会把一个物品拆成单独文件,而是保持“按来源文件聚合”的结构。

同时,每个输出文件中的条目顺序会严格沿用输入源文件中的物品顺序,便于逐项人工核对。


20. 第十三步:返回统计结果

全部写完后,Generate 会构造 GenerationResult 返回给调用方,其中包含:

  • BasePath
  • OutputPath
  • UsedSeed
  • Statistics
  • Logs

Statistics 会统计以下几类数量:

  • WeaponCount
  • AttachmentCount
  • AmmoCount
  • GearCount
  • ConsumableCount
  • TotalCount

GUI 就是根据这份结果把日志和统计显示给用户。


21. 当前精简版的边界

为了后续继续重构,当前生成器有几个非常明确的边界:

21.1 当前只接受 3 类已实现输入

也就是:

  • RealismStandardTemplate
  • Moxo_Template
  • Mixed_templates

其他历史输入结构仍会跳过。

21.2 生成逻辑是“基于输入修正”,不是“从零重建”

当前主策略是:

  • 先保留输入结构
  • 再做字段补齐、规则修正、例外覆盖、结构规范化

21.3 例外覆盖永远在自动规则之后执行

因此 item_exceptions.json 是最终裁定层。

21.4 output 的组织单位是“源文件”,不是“物品”

所以定位某个 patch 时,最好优先从它来自哪个 input 文件去追。


22. 一句话总结当前流程

当前程序的实际生成逻辑可以概括为:

先加载规则、模板和例外表,再遍历 input 中所有已实现支持的输入文件;每个物品先识别输入路径并提取上下文,再基于 clone 基底、模板基底或源对象构造补丁,随后按武器/配件/弹药/装备规则修正、应用人工例外覆盖,最后按原始 sourceFile 分组写回 output。

如果后续要继续重构,最值得拆分的几个稳定阶段是:

  1. 输入识别与 ItemInfo 提取
  2. profile 推断
  3. 数值修正
  4. 例外覆盖
  5. 输出组织与写盘

这五层现在已经比较清晰,后续可以继续拆成独立服务,而不会改变现有生成结果。

贡献者

The avatar of contributor named as SamuelNOTCuriousMeow SamuelNOTCuriousMeow

文件历史

撰写