# 支援兵 AIC 覆盖(companionpvf)使用说明 本文件说明如何重新生成、校验、启用支援兵的 **PVF AIC 覆盖**。 覆盖的作用:把服务端编译出的 AI 程序真正写进 `aicharacter/**.aic`, 让支援兵的行为与 GM 工具里的 AI 档位一致;只调 GM 滑块而不做覆盖时, 支援兵跑的是客户端 PVF 里的**原版 AIC**。 ## 一、工具位置 | 项 | 路径 | |---|---| | 源码 | `go-server/cmd/companionpvf/` | | 可执行 | `runtime/bin/companionpvf.exe` | | 暂存目录 | `runtime/companion-overlay/` | 移植时补上的两处基础设施(原树没有): - `internal/platform/pvf/ensure_string.go` - `Archive.EnsureANSIString` / `Archive.EnsureUnicodeString` - `Archive.RepackRawAndStringPools` ← **关键**:`RepackRaw` 只重写文件体、 不写回被追加过的字符串池,补丁引用会解析成空串;本方法会用当前 `strA/strW` 重新编码 name 段。 - `Archive.ansiOffsets` 字段(ANSI 池偏移缓存) ## 二、生成 `profiles.json`(**必须无 BOM**,否则 `invalid character '\ufeff'`): ```json { "profiles": [ { "behavior": "aggressive", "class_id": 0, "buff_policy": "leader_first", "combo_preset": "auto", "combo_enabled": true, "aoe_min_targets": 3, "protect_leader": true } ], "jobs": [0] } ``` `jobs[i]` 与 `profiles[i]` 一一对应。**同一个 job 只能有一个 profile** —— 两个 profile 指向同一个 `.aic` 且内容不同会直接报错 (`two profiles target ... with different artifacts`),因为一个人物职业只有 一条 AIC 路径。 命令: ```bat companionpvf.exe --profile-overlay ^ D:\90server\Server\runtime\data\dnf\Script.pvf ^ D:\90server\Server\runtime\companion-overlay\Script.companion.pvf ^ D:\90server\Server\runtime\companion-overlay\profiles.json ``` 输出 JSON 里带 `Manifest`,包含每个 artifact 的 `apc_code`/`aic_path`/ `aic_text`/`sha256`/`strategy_status`/`missing_capabilities`。 ## 三、职业 → APC / AIC 映射 来自 `internal/modules/dnf/companion/template.go` `aiTemplatePairs`: | job | 职业 | APC | AIC 路径 | |---|---|---|---| | 0 | swordman 剑士 | **900** | `aicharacter/_bizarre/swordman/mirror_swordman/mirror_swordman.aic` | | 1 | fighter 格斗 | 5405 | `aicharacter/fighter/araf_fighter/araf_fighter_doll.aic` | | 2 | gunner 枪手 | 10903 | `aicharacter/_bizarre/gunner/mirror_gunner/mirror_gunner.aic` | | 3 | mage 法师 | 15902 | `aicharacter/_bizarre/magician/mirror_magician/mirror_magician.aic` | 本部署的支援兵「测试3」是 **job 0**,所以覆盖的是 APC **900** 的 `mirror_swordman.aic`。 ## 四、校验(独立于工具自身的回读) 工具内部已经回读校验过;需要再次人工确认时,用 `platformpvf` 打开两个归档: - 文件数必须相同(只替换已有文件,不增删路径) - 目标 `.aic` 的字节必须不同 - 目标 `.aic` 必须含 `CompanionSlot00`…`CompanionSlot11`、 `CompanionDirectPool00`…、`CompanionBasicAttack` - 五个标量标签必须等于 `aic_text` 里的值: `[think term]` `[destination change term]` `[keep distance with target]` `[warlike]` `[vision]` ## 五、已知缺口(工具自己会报) 最近一次生成(job 0 / aggressive)结果: ``` strategy_status = compiled_with_gaps missing_capabilities = [stuck_detour, target_sticky] ``` 含义:卡住绕行与目标粘性这两步**没有可用的原生能力回执**,不会被编译进 AIC。其余(思考间隔、目的地切换、保持距离、好战度、视野)都已写入。 ## 六、启用(**未自动执行,需人工确认**) 覆盖要生效,客户端必须加载这份 PVF —— DNF 只读 `Script.pvf` 这个固定文件名, 所以是**替换**而不是追加: 1. 备份客户端 `D:\90server\Client\Script.pvf`(110,794,637 B) 2. 用 `Script.companion.pvf` 覆盖 `D:\90server\Client\Script.pvf` 3. 服务端 `runtime\data\dnf\Script.pvf` 也要一致 (`Server\START.bat` / `Client\开始游戏.bat` 会在启动时按哈希把客户端 PVF 同步到服务端,所以第 2 步做完通常会自动处理) **回滚**:把备份的 `Script.pvf` 拷回去即可。 ### 风险(务必知悉) - 覆盖改的是 `aicharacter/_bizarre/swordman/mirror_swordman/mirror_swordman.aic`。 `_bizarre` 目录下的 AIC 属于"镜像/特殊"类资源,**如果还有别的地方复用这条 AIC**(例如某些副本的镜像敌人),它们的行为会一起改变。 - PVF 是 105 MB 级文件,替换前确认磁盘余量。 - 覆盖只影响客户端表现;服务端 `0x7F41` 投影里的档位参数仍然照常下发。 ## 七、全职业覆盖(推荐做法) `profiles.json` 的 `jobs` 可以一次给多个职业,工具会逐条编译并回读校验。 当前部署已覆盖 **job 0–14 共 15 个职业**(`jobs: [0..14]`,全部 aggressive): | job | 职业 | APC | AIC | |---|---|---|---| | 0 | swordman | 900 | `_bizarre/swordman/mirror_swordman/mirror_swordman.aic` | | 1 | fighter | 5405 | `fighter/arad_fighter/arad_fighter_doll.aic` | | 2 | gunner | 10903 | `_bizarre/gunner/mirror_gunner/mirror_gunner.aic` | | 3 | mage | 15902 | `_bizarre/magician/mirror_magician/mirror_magician.aic` | | 4 | priest | 20902 | `_bizarre/priest/mirror_priest/mirror_priest.aic` | | 5 | atgunner | 25900 | `_bizarre/atgunner/mirror_atgunner/mirror_atgunner.aic` | | 6 | thief | 30901 | `_bizarre/Thief/mirror_Thief/mirror_Thief.aic` | | 7 | atfighter | 35903 | `_bizarre/atfighter/mirror_atfighter/mirror_atfighter.aic` | | 8 | atmage | 40800 | `autocharacter/atmage/auto1/auto_atmage1.aic` | | 9 | demonic_swordman | 902 | `_bizarre/swordman/despair_darkknight/despair_darkknight.aic` | | 10 | creator | 15903 | `_bizarre/magician/despair_creator/despair_creator.aic` | | 11 | atswordman | 56901 | `_bizarre/atswordman/despair_atswordman/despair_atswordman.aic` | | 12 | knight | 60901 | `_bizarre/knight/despair_knight/despair_knight.aic` | | 13 | demonic_lancer | 65001 | `demoniclancer/mirror_demonicLancer/mirror_demonicLancer.aic` | | 14 | atpriest | 70003 | `atpriest/despair_atpriest/despair_atpriest.aic` | | **15** | **gunblader** | 75001 | **`gunblader/mirror_gunblader/mirror_gunblader.aic` —— 无法覆盖** | 16 条 AIC 路径互不相同,不会触发"两个 profile 指向同一 AIC"的冲突。 ### job 15(gunblader)为什么覆盖不了 `companionAIRefs` 要求 `[ai pattern]` 段里同时存在 `action` / `destination` / `move` 三个 `.ai` 程序引用。实际内容对比: ``` job 14 atpriest : `Event.ai` `DestinationSelect.ai` `Action.ai` `MoveMethod.ai` job 15 gunblader: `Action.h` ``` gunblader 用的是**另一代 AIC 格式**:只引用一个 `.h` 头文件,没有独立的 action/destination/move 程序槽,因此 AI 程序无处可写。这是格式代差,不是 补丁器 bug —— 强行写入会破坏该 AIC。它的支援兵仍然能召唤并自带原版 AI, 只是拿不到本工具的 12 格快捷栏/普攻程序。 ## 八、两个必须知道的坑 ### 1. `behavior: "assist"` 会让支援兵**站着不打** ```go func aicWarlike(profile AIProfile) int { if profile.Behavior == "aggressive" { return 80 } return 0 // assist 及其它一律 0 } ``` `[warlike] = 0` 就是不主动攻击。而且 assist 想要的能力(`leader_buff`、 `leader_follow`)当前都缺回执,实测等于"不动"。**想让支援兵主动作战必须用 `aggressive`。** ### 2. GM 克隆出来的角色,快照可能无效 GM 工具的"克隆角色"生成的记录不完全满足投影约束,preflight 会报: ``` reason = snapshot_invalid message = 所选角色的装备/技能快照不完整或不符合投影约束 ai_template = @{key=; apc_code=0; aic_path=; ...} ← 全空 snapshot_at = 0001-01-01T00:00:00Z ``` 这类角色**不能**当支援兵。换一个正常创建的角色即可(`preflight` 会显示 每个源的 `ready` / `reason`)。 ## 九、按键程序必须重写("支援兵不放技能"的根因) `--profile-overlay` 原来**只替换 AIC + 4 个 `.ai` 程序**,**没有重写 key 文件本体**; legacy 路径(`main.go` 里 `keyFiles` / `directPoolFiles` 两个循环)一直在重写。 这个差异导致支援兵能召唤、能跟随、却不放技能。 ### 症状(客户端 `90CN_companion_p0.log`) ``` runtime-key projection blocked reason=direct-bytecode-shape slot=0 ... size=128 projection APC registered ... runtime_keys_applied=0 ``` ### 原因 | 项 | 值 | |---|---| | 客户端默认绑定表要求 | `kRuntimeDirectKeyBytecodeSize = 0x180`(384 字节)| | 客户端 DB4 表要求 | `0x1BC`(444 字节)| | DB4 那批 25 字节旧 key 编译后 | **128 字节 (0x80)** | | 客户端占位技能码 | `kSkillCodeBias(0x100) + kPlaceholderSkillId(511)` = **0x2FF** | AIC 的 `[key stream]` 引用的是 `magician/biske2` 与 `magician/flea_inflame_hands2` 下那批 25 字节 key —— 它们是给 DB4 客户端做的。零售客户端把它编译成 128 字节, 尺寸校验直接拒绝,于是 `runtime_keys_applied=0`,12 格快捷栏没有绑定,AIC 无键可按。 ### 修复 在 `BuildProfileOverlay` 重打包前,补上与 legacy 完全一致的两个循环: ```go for index, path := range companionFrontKeyFiles { appendReplacement(RawReplacement{Path: path, Data: buildQuickSlotKey(archive, companionQuickSlotKeys[index])}) } for _, path := range companionDirectKeyFiles { appendReplacement(RawReplacement{Path: path, Data: buildDirectPlaceholderKey(archive)}) } ``` `companionQuickSlotKeys = a s d f g h q w e r t y`(前段 12 格要按的键)。 生成结果: | key | 原始 | 重写后 | 内容 | |---|---|---|---| | `biske2/key/*.key` | 25 B | 50 B | `[input] `target current direction` 100 `time` 50 [input] `a` 30 `time` 100` | | `flame_hands2/key/*.key` | 25 B | 80 B | `... [input] `skill:511` 100 `time` 200` | ### 验证成功的标志 ``` projection APC registered key=1 ... runtime_keys_applied=1 projection APC registered key=3 ... runtime_keys_applied=1 ``` `runtime_keys_applied` 从 0 变 1,且日志里不再出现 `direct-bytecode-shape`。 ### 覆盖范围 `已覆盖职业: 15 / 16` —— job 0–14 全部 25 组键流 + `CompanionSlot00`。 **job 15(gunblader 枪剑士)无法覆盖**:它的 `[ai pattern]` 只有 `` `Action.h` `` 单头文件,没有 action/destination/move 三个程序槽,`companionAIRefs` 直接报错, `companionpvf` 不会为它生成任何东西。它的支援兵仍可召唤,但拿不到支援兵 AI 与 12 格快捷栏程序。 ### key 文件是共享资产 重写的 24 个 key 文件是**全局共享**的,所以一次重写对所有已覆盖职业同时生效; 代价是 `mirror_magician` 等原本用这批 key 的 APC 的按键行为也会跟着变(官方 legacy 路径同样如此)。