Files
90CNServer/pvf/docs/PVF变体架构.md
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

189 lines
13 KiB
Markdown
Raw Permalink 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.
# PVF 变体架构
## 分层
- `internal/pvf/detect.go`:已知标准格式、替代密钥格式、Paged110 解封依次探测,最后尝试未知格式的头部恢复。结果包含逻辑字节、头部、格式规则以及写回所需状态。
- `internal/pvf/archive.go`:公共逻辑容器解析,不负责配套文件发现或分页解封。字符串池和 GRPI 无法解析时返回 `ErrInvalidSection`;HASH 无法恢复时保留读取能力并限制结构修改。
- `internal/pvf/format.go`:格式标识、容器类型、内容规则和写入能力。格式标识用于诊断,功能分支依据内容规则。
- `internal/pvf/save.go`:统一校验、字符串池修复、重建和状态接纳流程;`writeContainer` 仅负责普通写出或分页包装。
- `services/list_registration.go`:依据 `RequiresIndexHash` 决定伴随索引维护及 uint32 ID 约束。界面依据内容能力控制注册和字符串引用入口。
目前标准与替代密钥格式共享内容规则;110US / 115US 使用分页容器,并启用 UTF-16 字符串策略、indexhash 注册和字符串引用入口。100CN 同样使用分页容器与 indexhash、字符串引用,但保留 ASCII / UTF-16 双池。容器与这些内容规则可以独立组合。
`IsPaged110` 和信息快照中的 `paged110` 保留兼容用途。新业务功能不应依据格式名称判断是否可用。
## 写入约束
`WriteCapabilities` 描述归档的基础写入能力。未知 HASH 时,已有内容替换仍可保存,但新增、删除、字符串池扩展或重映射会在保存前拒绝;`CanSave` 不意味着任意待保存修改都被支持。缺少分页写回状态时不可保存。
HASH 是否已知通过独立状态记录,数值为零的有效 seed 不再代表未知。格式规则及分页状态必须随批处理、脚本工作区暂存克隆一起保留。
未修改归档保持原有字节往返约定;启用 UTF-16-only 规则的历史异常归档仍会执行已有字符串池修复,这类修复可能改变输出。保存的结构限制也适用于该修复。
解析错误明确拒绝不可解码的必需段。当前只在打开时验证首个正文分块,其余分块仍按需解压;成功打开不等于完成整个归档的完整性扫描。
## 新变体接入
1. 确定差异属于段参数、物理容器还是内容规则。
2. 只有段参数变化时添加已知探测器及参数,复用公共解析、编辑和重建。
3. 容器变化时扩展解封和 `writeContainer`,保持公共解析接收逻辑字节。
4. 内容行为变化时在 `ContentRules` 中明确表达,并集中配置;不要在服务、编辑器、字符串池中散布格式名称判断。
5. 克隆必须复制新增的可变写回状态;不可把配套文件依赖带入普通内容编辑。
6. 更新生成的前端绑定必须使用 Wails 生成命令。
未知密钥恢复只支持当前共享布局,不能自动推断未知容器结构或客户端内容规则。新变体仍需要样本确认,不能因头部恢复成功就声明完整支持。没有配套文件时,被封装的未知头部目前仍可能返回 `ErrBadSignature`,尚不能可靠区分所有缺失依赖与不支持格式。
## 验证
`format_test.go` 使用合成数据覆盖:
- 标准与替代格式的有/无 guard 探测、未修改字节一致性、增删后的保存重开。
- Paged110 分页包装、解封后的公共解析、暂存编辑、新增及再次保存;使用合成分页状态,不覆盖真实配套文件解封。
- 未知 HASH 的内容保存与结构修改拒绝、已知零值 seed。
- 克隆格式状态、字符串池策略与容器的独立组合。
- 字符串池长度错误的显式拒绝。
常规验证:`go test ./...`、`cd frontend && npm run build`。
真实客户端兼容性仍需使用对应变体样本运行 `PVF_TESTFILE=/path/to/Script.pvf go test ./...`,未匹配内置参数的分页版本还需对应配套文件。测试日志和示例不得包含配套文件内容或密钥;授权内置的客户端解封参数仅存于专用资源。
## 100CN 样本调查
100CN 样本的页首保护边界同样是每 10 MiB 页的前 10,240 字节。配套国服
`DNF.exe` 的 PVF 加载流程明确读取同目录的 `sk.dat`,先解封页表,再解密各页
保护区,最后按宽字符公式处理 `iNfO` 头部。程序内的 `sk.dat` 文件名经过
字符串混淆,不能以普通字符串搜索未找到它作为不需要配套文件的依据。
分页解封现在依次尝试 `DFO.exe` 和 `DNF.exe`;前一个程序不存在或不能解封时
仍会继续尝试后一个。只在配套文件和归档头部完整校验通过后接受结果,不凭
可执行文件名认定客户端版本。100CN 按成功解封候选的公钥身份登记为
`paged100cn`,`ClientVersion()` 返回 `100CN`;未登记身份继续使用通用 `paged`。
合成测试覆盖两种程序名、两种 PEM 编码、错误 `DFO.exe` 与有效 `DNF.exe`
同时存在、无修改字节一致,以及内容编辑和新增文件后的保存重开。
100CN 的 ASCII 池约 145 MB,非 ASCII 文本使用 UTF-16 池。不能对它执行
110US 的 ASCII 池迁移,否则未修改保存也会重建整个归档。装备 indexhash
的 227,634 条原始记录均使用 ASCII 池数字引用,数值符合现有哈希算法;
更新和新增记录沿用双池策略,现有 ASCII 引用不再被误判为需要修复。
真实样本验证通过:2,522,676 个文件、30,402 个分块全部解压和数据范围检查,
未修改输出 SHA-256 与源文件一致,编辑、新增、删除后保存重开,重建 HASH
覆盖当前文件表,未编辑载荷抽样保持一致,并验证新增装备的 LST/indexhash
注册。验证范围为离线读写,未进行游戏客户端实机加载。
真实测试可通过 `PVF_TEST_SIDECAR_DIR` 指定配套目录,避免移动源文件或将
配套文件放进仓库。`PVF_TEST_FULL_SCAN=1` 启用全分块及文件数据范围检查:
```sh
PVF_TESTFILE=/path/to/100CN.pvf \
PVF_TEST_SIDECAR_DIR=/path/to/client \
PVF_TEST_CLIENT_VERSION=100CN PVF_TEST_FULL_SCAN=1 \
go test ./internal/pvf -run '^TestRealPagedClient(Compatibility|EditRoundTrip)$' -count=1
```
110US、100CN 和 115US 的完整解封数据统一放在 `internal/pvf/assets/clients/`,
各用一个 `go:embed` 二进制资源,包含 PKCS#8 客户端参数、metadata 和加密
`sk.dat`。格式为 `PVFC` 签名、uint32 版本号(当前为 1)、uint32 DER 长度、
uint32 密封表长度、DER、32 字节 metadata、密封表;整数均为小端。
110US 原先的源码参数常量、独立 `sk.dat` 和专用加载函数已移除。
`paged_embed.go` 使用共用解码器校验版本、精确长度、公钥身份与密封表,
统一注册三种客户端,并以 `sync.Once` 缓存校验后的客户端和 RSA 解封结果。
身份识别只比较公钥指纹,不再依赖私钥加载。参数不会出现在诊断错误或测试
输出中;每次打开仍需校验归档签名与完整布局,不能仅凭资源身份采用内容规则。
打开时先依次尝试完整的内置 110US、100CN、115US 参数,无需读取同目录文件;
内置参数不匹配才查找归档目录中的 `sk.dat`,并尝试 `DFO.exe` / `DNF.exe`
内的候选。外部 `sk.dat` 仅作为兜底表,不会阻止已匹配的内置客户端打开。
后端也可调用 `OpenWithSidecars` 指定客户端目录。所有候选失败后,错误提示
用户将该 PVF 对应版本的 `sk.dat` 和客户端程序与归档放在同一目录后重试。
真实测试额外覆盖完全没有外部配套文件的打开、从内存 `Parse` 打开、末尾
分块读取,以及错误外部文件不覆盖有效内置参数。合成测试覆盖内置参数不
匹配时的外部客户端兜底和缺少配套文件的中文提示。
115US 样本的内置数据包含 81 页的解封参数;已验证独立打开、抽样读取、
未修改保存的字节一致性,以及编辑、新增、删除和 LST/indexhash 注册后重开。
默认压缩重建字符串池可能让归档超过页表容量,保存时会在超限情况下尝试
更高压缩率的字符串池,并保持字符串偏移与未编辑分块不变。仍然超限时,
返回 `ErrPagedCapacity` 和所需/可用页数,在写出前拒绝保存;`SaveAs` 保留
原目标文件及待保存修改,不扩展客户端配套页表。
## TW 独立目录格式
TW 的格式标识为 `tw`,客户端代号为 `TW`。它使用长度前缀 UUID、版本号、
目录长度/校验值/文件数,以及变长目录记录;目录和每个文件独立进行
CRC 派生的 XOR 与 32 位旋转加解密,没有 S4A21 的 HASH、字符串双池和
压缩正文分组。`tw.go` 将它适配为公共 Archive、字符串池和脚本表示,
服务层可继续使用现有目录树、搜索、编辑、批处理和字符串引用接口。
目录路径沿用参考读取器的 CP949 编码;游戏字符串表、`.str` 文本和 `.nut`
源码按 TW
配置使用 Big5。脚本带 `B0 D0` 前缀,原生 token 编号在读写边界转换;
`9 + 10` 字符串引用对适配为公共 `<index::key>` 引用。打开时校验目录和
非空文件 CRC、字符串索引及脚本尾部对齐,并建立不可变引用映射。
未修改保存逐字节保留原包;修改保存复制未编辑文件的密文及已有路径字节,
保留 UUID、版本和原有文件编号,向 `stringtable.bin` 追加新字符串,已有
字符串编号不变。新增、删除和 `.str` / `.nut` 编辑均写回 TW 格式。字符串表由编辑器
维护,不支持直接替换;Big5 无法表示的新字符会在写出前报错。
`tw_test.go` 覆盖合成归档的繁体文本、引用、二进制文件、增删、克隆、保存
重开和损坏拒绝。真实样本及服务层验证使用独立变量,避免其他格式专用测试
对 TW 产生不适用的假设:
```sh
PVF_TW_TESTFILE="$PWD/testdata/TW.pvf" \
go test ./internal/pvf ./services -run '^TestTW' -count=1
```
真实样本含 951,559 个文件、1,137,629 个字符串;验证范围为离线读写,
未进行游戏客户端实机加载。
### 基础掉率的版本兼容
“基础掉率”工具支持 90US、90CN、TW、110US 和 100CN。五种版本使用
相同的四个掉率文件及 `[basis of rarity dicision]`;TW 每组 6 个值,
90 系列常见每组 7 个值,100CN 每组 8 个值,110US 每组 9 个值。
读写均按各文件的 section 总长度验证分组宽度,只接受完整的 6/7/8/9 值分组。
调整概率只更新前四个累计阈值,保留地狱文件前置值、终止阈值和附加字段。
真实 TW 的四类掉率读取、调整、TW 格式保存重开和附加字段保留已验证。
可使用 `PVF_TW_TESTFILE` 运行 `TestDropRateServiceTWRealRoundTrip`。
110US 样本的翻牌、小怪、精英段长度分别为 9、36、36,地狱段为
1 个前置值加 2 组 9 个值,共 19 个值。100CN 对应长度为 8、32、32、17。
本次仍编辑五档基础概率,后续新增阈值不重新解释;前五档终止阈值不在
既有兼容范围内时继续拒绝,避免把其他档位的份额归入现有五档。
`item drop rarity control`、其他 section 和其他组的内容均原样保留。
真实高版本验证包括四个文件的读取、第一组概率调整、逐 token 确认仅四个
累计阈值变化,以及分页格式保存重开后的完整载荷一致性。仍保留 90US、
90CN 和 TW 的原有支持,未验证的 115US 和通用分页格式暂不开放。
```sh
PVF_TESTFILE="$PWD/testdata/110US.pvf" \
go test ./services -run '^TestDropRateServicePagedRealRoundTrip$' -count=1
PVF_TESTFILE="$PWD/testdata/100CN.pvf" \
go test ./services -run '^TestDropRateServicePagedRealRoundTrip$' -count=1
```
### 掉率字段按版本换行
`config/rendering.json` 中现有的 `section.basis of rarity dicision` 和
`section.basis of rarity dicision.hell` 增加 `format.tokensPerLineByVersion`:
90US/90CN 为 7、TW 为 6、100CN 为 8、110US 为 9。深渊规则继续使用
`offset: 1`,首个数量值独占一行。未配置或未知版本保留 `tokensPerLine: 7`
的回退行为,不推测尚未验证的 115US 分组宽度。
`tokensPerLineByVersion` 是可选的版本→正整数行宽映射;文件级和 section
级规则均可使用。解析时先选择现有优先级规则,再按 `ClientVersion()` 取得
行宽;如另有 `tokensPerLineIndex`,token 中的动态宽度仍优先。旧规则无需
迁移,规则管理界面可通过 JSON 字段调整此映射。
五个版本的真实样本已检查四类掉率的行数和每行宽度;合成 token 测试覆盖
版本映射、未知版本回退、深渊首值单行、普通段分组及原始 token 字节往返。
仓库规则修改后,运行中的应用仍需重新加载渲染规则;生产应用须包含更新后
的内置配置,不会自动读取当前仓库的 JSON。