补丁生成流程说明
本文说明当前精简版 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. 整体入口
当前程序有两个常见入口,但它们最终都走同一条核心生成链路:
GUI 程序
- 文件:RealismPatchGenerator.Gui/Form1.cs
- 在点击“生成”按钮后,会调用:
- new RealismPatchGenerator.Core.RealismPatchGenerator(repositoryRoot, seed?).Generate(outputPath)
临时控制台 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) 后,主流程可以概括成五步:
- 检查必要目录是否存在
- 加载全部模板
- 扫描 input 下所有 JSON 输入文件
- 逐个文件、逐个物品生成 patch
- 将 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 的职责是“以文件为单位组织处理”。
它会做这些事:
- 读取当前 input JSON 文件
- 把根对象里的每个条目视为一个物品
- 对每个物品调用 ProcessSingleItem
- 保留当前 sourceFile 的输出顺序信息,供最终写出时使用
只要某个文件成功进入当前支持的输入格式路径,最终写出时都会遵守两条固定约束:
- 条目顺序与输入源完全一致
- 只有 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 的提取方式
优先顺序大致是:
- 输入对象里的 Name
- locales 中的多语言名称
- 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 基底,缺失时再回退推断基底
它会做几件事:
- 先确定 patch 基底:直接使用输入对象,或先展开 clone/TemplateID 对应的基底
- 强制写入 ItemID
- 如果 Name 缺失,则回填提取出的名称
- 同步 ItemInfo 的大类标记
- 进入 FinalizePatch 做最终加工
这个设计的核心思想是:
- 最大限度保留输入里已经正确的字段
- 对克隆条目先补齐完整 Realism 结构,再对需要规范化的部分做修正
11. 第八步:FinalizePatch 的最终加工链
FinalizePatch 是整个生成逻辑最核心的一段,它按固定顺序执行下面几件事:
- MergeInputProperties
- EnsureBasicFields
- ApplyRealismSanityCheck
- ApplyItemException
- NormalizeStructuredOutput
- 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
这是“自动修正补丁内容”的核心步骤。
它的顺序是:
- 先补齐必需字段
- 先做一轮通用启发式修正
- 再按物品大类进入对应规则分支
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 会按这个顺序推断武器档位:
- 先看 parentId 是否命中 weaponParentGroups
- 再看模板文件名是否命中 TemplateFileToWeaponProfile
- 再看 sourceFile 文件名是否能映射 profile
- 最后根据名称和 WeapType 做关键词判断
可识别的典型 profile 包括:
- pistol
- smg
- launcher
- sniper
- machinegun
- shotgun
- assault
13.2 应用武器规则范围
确定 profile 后,程序会:
- 对应取出 weaponProfileRanges
- 先应用基础范围
- 再根据口径 profile 叠加 weapon caliber modifier
- 再根据枪托 profile 叠加 stock modifier
- 最后再做 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 确定后,程序会:
- 先做基础 clamp
- 对特殊类型做硬规则修正
- 应用该 profile 对应的数值范围
- 对少数字段保留源值但限制在规则范围内
- 再做一次 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 后,程序会:
- 先按 gearClampRules 做基础钳制
- 应用对应 gearProfileRanges
- 再次钳制
- 应用全局安全钳制
装备分支的原则是:
- 用 profile 控制主要数值区间
- 避免出现不符合装备类别的离谱字段
16. 弹药是如何生成和修正的
如果 patch 的 $type 被判定为 RealismMod.Ammo,就会进入弹药分支。
16.1 弹药 profile 推断
弹药修正会先推断三层信息:
- ammoProfile
- penetrationTier
- specialProfile
它们分别代表:
- 这是什么基础弹种
- 穿深处在哪个档位
- 是否属于特殊用途弹
16.2 叠加规则方式
弹药数值的生成逻辑是:
- 先取 ammoProfileRanges 里的基础范围
- 再叠加 ammoPenetrationModifiers
- 再叠加 ammoSpecialModifiers
- 对特殊字段再做额外限制
额外限制主要有:
- MalfMisfireChance / MisfireChance / MalfFeedChance 会被限制在非常小的区间
- ArmorDamage 会被限制在 1.0 到 1.2 之间
16.3 弹药结构规范化
弹药在最后输出前还会走 NormalizeAmmoOutputStructure。
这一步会按弹药模板字段顺序重建输出对象,目的有两个:
- 字段顺序稳定
- 输出结构与模板一致
17. 第十步:应用例外覆盖
当规则修正结束后,程序会检查当前物品是否在 item_exceptions.json 里存在启用中的例外项。
如果存在,就会把 overrides 里的字段逐项覆盖到 patch 上。
这一步的优先级高于自动规则,因此适合处理:
- 个别规则难以表达的特殊物品
- 不希望再被自动推断改动的字段
- 临时修复和人工校正
18. 第十一步:按来源文件分组缓存
每个 patch 生成后,不会立刻写文件,而是先通过 AddToFilePatches 放进一个按 sourceFile 分组的缓存结构。
这个结构保留了两个信息:
- 物品属于哪个输入文件
- 输入文件在本轮遍历中的顺序
这样做的好处是:
- 输出目录结构可以和 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。
如果后续要继续重构,最值得拆分的几个稳定阶段是:
- 输入识别与 ItemInfo 提取
- profile 推断
- 数值修正
- 例外覆盖
- 输出组织与写盘
这五层现在已经比较清晰,后续可以继续拆成独立服务,而不会改变现有生成结果。