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

13 KiB
Raw Permalink Blame History

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 启用全分块及文件数据范围检查:

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 产生不适用的假设:

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 和通用分页格式暂不开放。

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。