Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

11 KiB
Raw Permalink Blame History

支援兵 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'):

{
  "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 这个固定文件名, 所以是替换而不是追加:

  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" 会让支援兵站着不打

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 路径同样如此)。