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 Samuel是不是好奇猫 Samuel是不是好奇猫
The avatar of contributor named as SamuelNOTCuriousMeow SamuelNOTCuriousMeow

文件历史