Files
90CNServer/server/cmd/companionpvf/COMPANION-PVF-OVERLAY.md
T
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

265 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 支援兵 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 路径同样如此)。