服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
11 KiB
支援兵 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.goArchive.EnsureANSIString/Archive.EnsureUnicodeStringArchive.RepackRawAndStringPools← 关键:RepackRaw只重写文件体、 不写回被追加过的字符串池,补丁引用会解析成空串;本方法会用当前strA/strW重新编码 name 段。Archive.ansiOffsets字段(ANSI 池偏移缓存)
二、生成
profiles.json(必须无 BOM,否则 invalid character '\ufeff'):
{
"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 路径。
命令:
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 这个固定文件名,
所以是替换而不是追加:
- 备份客户端
D:\90server\Client\Script.pvf(110,794,637 B) - 用
Script.companion.pvf覆盖D:\90server\Client\Script.pvf - 服务端
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" 会让支援兵站着不打
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 完全一致的两个循环:
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 direction100time50 [input]a30time 100 |
flame_hands2/key/*.key |
25 B | 80 B | ... [input] skill:511100time 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 路径同样如此)。