Files
90CNServer/pvf/docs/标注功能设计.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

15 KiB
Raw Blame History

设计一个类似标注的功能,核心目的是解决“哪个配置配什么东西”的问题:

  • 标注走规则配置,而非针对具体内容的单独标注

  • 标注内容包括 title/content

  • 标注在编辑器内的显示:

    • 在被标注内容旁边直接加上一个 tag 显示标题,hover tag 显示 tooltip
    • 复制内容的时候不要复制标注
  • 标注规则:

    • 可指定生效的文件后缀
      • 如:.equ 文件下的 [rarity],标注为“装备品级”
      • 如果不指定后缀,则全局生效
    • 可对 section 本身进行标注
    • 可对 section 内的数据进行标注
      • 指定 section 内第几个 token(或者范围)
      • 标注类型
        • 枚举,如稀有度的 3-神器、4-史诗
        • 关联 id,可指定关联的类型,标注显示其 id 对应的 name,且可通过 cmd/ctrl + 单击跳转到对应文件
    • 可对路径,包括目录和文件进行标注,此标注显示在现有文件树的 name tag 位(一般不会对有 name 的做标注,但设计上可以共存)
  • 标注规则第一版用 json 文件定义,设计好 json 结构。

已确认的产品约束

  • 第一版规则为系统内置规则,不在应用内提供用户编辑入口。
  • 规则通过单独的 HTML 可视化编辑器维护,由 Go 本地服务提供页面、校验、预览和实时写盘能力。
  • 配置统一放在根目录 config/:annotations.json 保存手写规则,paths.json 保存批量目录/文件树注释,sections.json 保存批量 section 注释,lists.json 保存固定的 lst/关联索引。HTML 编辑器只写入 config/annotations.json,写入前将旧内容保存为 annotations.json.bak。
  • 规则和列表配置通过 go:embed 提供启动默认值;应用工具栏提供手动重载按钮,可在运行中校验并切换到磁盘上的最新配置。
  • section 名称重复时,规则匹配全部同名 section,不配置 occurrence。
  • Tag 的显示位置通过设置面板切换,设置面板在标注功能之前完成。
  • 多条规则命中同一位置时,Tag 只显示规则列表中的第一条,Tooltip 显示全部命中规则。
  • 引用 ID 的 Cmd/Ctrl + 单击只负责打开目标文件,不要求定位到目标 section 或 token。

可行性结论

整体可行性较高,但这是一个跨越 PVF 解析、规则匹配、编辑器渲染和文件跳转的功能,不是简单的文本后缀处理。

当前 PVF 脚本由固定 5 字节 token 流组成,section 和 token 类型已经明确,适合实现结构化匹配。现有反编译器只返回纯文本,没有保留 token 对应的文本范围;CodeMirror 目前也只接收纯文本文档。因此最关键的工作是建立“语义 token 到编辑器文本范围”的映射。

文件树已经有 Tag 展示能力,现有搜索索引也具备 ID、名称和路径关联的基础。路径标注可以较低成本复用这部分能力;section/token 标注和引用跳转需要新增通用的标注引擎。

推荐总体方案

1. 内置规则

建议使用以下结构:

config/annotations.json
config/paths.json
config/sections.json
config/lists.json
internal/annotations/loader.go
tools/annotation-editor.html

Go 通过 go:embed 加载这些配置文件,应用启动时完成规则和列表配置解析、合并与校验。配置文件不写入 PVF。

config/paths.json 存放批量生成的 target.kind: "path" 规则(目录树与文件树注释),glob 都是精确路径:目录注释只描述该目录本身,不向子孙继承,因此没有自己规则的路径不显示标注。加载器把它追加在 annotations.json 的规则之后,因此手写规则仍决定同一路径的首个标题;两个文件出现相同 id 时保留 annotations.json 中的那条并跳过重复项。它由外部数据整理生成,不由 HTML 编辑器维护。手写规则仍可使用 /** 递归 glob,批量 path 规则在编译期按字面 glob 和 <dir>/** 前缀建索引,避免每个树节点都全量扫描规则。

config/sections.json 存放批量生成的 target.kind: "section" 规则,来自 pvfUtility PvfComments(按扩展名限定,title 为中文注释名,content 保留原始说明与官方示例;只保留能在实际 PVF 数据中确认存在的段)。合并与去重规则同上,手写 section 规则仍决定同名段的首个标题。

HTML 编辑器负责规则列表管理、规则排序、表单编辑、JSON 导入导出、错误展示和示例预览。它不直接依赖浏览器文件系统权限,而是由 cmd/annotation-editor 启动仅监听 loopback 的 Go HTTP 服务,并自动打开系统浏览器。

启动命令为:

task annotation:editor

服务监听 127.0.0.1:0,使用系统分配的随机端口,并为 API 请求附加当前会话令牌。提供以下接口:

  • GET /api/rules:读取并校验当前规则(只含 annotations.json,不含批量 paths.json / sections.json),同时提供只读的列表关联供规则选择。
  • PUT /api/rules:只校验并写入 annotations.json;通过后创建 .bak,再使用临时文件和 rename 原子替换源文件,lists.json、paths.json 与 sections.json 不由规则编辑器修改。
  • POST /api/preview:使用提交的当前规则、示例路径和反编译文本调用 Go 规则引擎,返回编辑器标注和路径标注。

表单的有效修改会在短暂防抖后实时保存。校验失败时展示具体错误,且不能覆盖现有 JSON。导入和导出继续保留,便于批量维护和迁移规则。

每次有效保存还会同步运行时规则和列表配置到 os.UserConfigDir()/pvfine/annotations.json、lists.json。应用内“重载标注规则”按钮在开发环境优先读取仓库源文件,发布环境读取运行时副本;paths.json、sections.json 取同一目录下的同名文件,不存在时回退到内置数据。重载会先完整校验这些文件;失败时保留当前规则引擎,成功后重建路径标注、清空 relation/编辑器标注缓存、搜索索引,并刷新已打开文件和当前文件树。

规则文件中应保留 version 和可选的 description 字段,便于后续升级格式和说明规则用途。

2. JSON 结构

建议第一版只使用扩展名匹配、路径 glob 匹配和精确 section 名称匹配,不引入正则表达式。

{
  "version": 1,
  "rules": [
    {
      "id": "equ.rarity",
      "match": {
        "extensions": [".equ"]
      },
      "target": {
        "kind": "token",
        "section": "rarity",
        "index": 0
      },
      "annotation": {
        "title": "装备品级",
        "type": "enum",
        "values": {
          "3": "神器",
          "4": "史诗"
        }
      }
    },
    {
      "id": "equ.related-equipment",
      "match": {
        "extensions": [".equ"]
      },
      "target": {
        "kind": "token",
        "section": "related",
        "index": 0
      },
      "annotation": {
        "title": "关联装备",
        "type": "reference",
        "relation": "equipment"
      }
    },
    {
      "id": "path.equipment",
      "match": {
        "glob": "equipment/**/*.equ"
      },
      "target": {
        "kind": "path"
      },
      "annotation": {
        "title": "装备配置",
        "type": "text",
        "content": "装备相关配置文件"
      }
    }
  ]
}

固定列表和关联索引单独放在 config/lists.json:

{
  "version": 1,
  "relations": {
    "装备": {
      "listPath": "equipment/equipment.lst",
      "idToken": 0,
      "pathToken": 1,
      "recordTokens": 2,
      "nameSection": "name"
    }
  }
}

结构约定如下:

  • target.kind 支持 path、section、token。
  • section 目标匹配所有同名 section。
  • token 下标使用 0-based;范围使用 start 和 endExclusive。
  • token 目标可以配置 recordTokens,按固定数量的 token 重复匹配每条记录;offset 会跳过 section 开头的若干 token,再开始重复匹配;tokensPerLineIndex 指定 section 内直接 token 的 0-based 下标,其正整数值会覆盖 recordTokens,无效或缺失时回退到 recordTokens;contextIndex 可覆盖关联类型指定的上下文 token 下标。
  • 默认只计算 section 的直接 token,不递归计算嵌套 section。
  • nameSection(relation 取名)、itemshop 的 [NPC] 取值,以及 [name] 名字索引只认一级 section;嵌套 section 里的同名 [name] 属于子记录,不会被当作文件自身的名字。
  • annotation.type 支持 text、image、enum、reference、path。
  • path 用单个 token 的路径值查找归档内文件;annotation.pathRoot 可填写归档内相对目录作为前缀,留空时从归档根目录查找。路径中的反斜杠会按正斜杠处理;找到目标后可 Cmd/Ctrl+单击跳转。
  • annotation.title 可以留空;留空时不显示标注 Tag,已有的正文标注交互(例如路径跳转)仍然可用。
  • enum 使用 token 的规范化值作为 key。
  • reference 通过 config/lists.json 中的 relation 类型解析目标文件、目标 ID 和 name。
  • kind: "union" 的 relation 可以组合多个固定列表;默认提供 物品,按 装备、道具 的顺序查找,适合 ID 可混用的字段。
  • 普通 relation 使用固定 listPath;kind: "contextual" 的 relation 使用 contextToken 指定的同一条记录上下文值,从 contextPaths 选择对应的 lst,再用目标 ID 查找技能文件。例如 [skill levelup] 每条记录有 3 个 token,目标为 index: 1、recordTokens: 3,关联类型可把 [fighter] 映射到 skill/fighterskill.lst。
  • 规则数组顺序就是优先级。

3. 编辑器标注模型

后端解析当前文件时,同时返回编辑器文本和标注范围:

text
annotations: [
  {
    start,
    end,
    title,
    content,
    type,
    targetFileIndex
  }
]

start/end 使用 JavaScript UTF-16 code unit 偏移,避免中文和代理对字符导致 CodeMirror 定位错误;CRLF 和 CR 按 CodeMirror 的规则各占一个换行单元。

标注不能写入编辑器文档。前端使用 CodeMirror Decoration/Widget 在 token 或 section 旁边渲染 Tag,Tooltip 使用聚合后的全部命中规则。由于 Widget 不属于 state.doc,复制内容时不会复制标注;复制行为仍需要通过前端测试验证。

同一文件、同一 section/token 范围的多条命中应聚合成一个 anchor:

  • 可见 Tag 使用第一条规则的 title。
  • Tooltip 按规则顺序显示全部 title/content。
  • 引用 Tag 携带 targetFileIndex,Cmd/Ctrl + 单击调用现有文件打开流程。

编辑文本后只重新计算当前文件的标注,不能通过整体替换 CodeMirror 文档刷新,否则可能导致光标和选择范围跳动。

4. 文件树标注

路径标注需要同时支持目录和文件。当前文件树标签主要按文件索引保存,应扩展为按归档路径保存,例如:

map[string][]AnnotationTag

ListChildren 返回目录或文件节点时,根据完整路径附加标注。文件原有的 name、ID 关联标签与路径标注可以同时存在。

5. 显示设置

Tag 的显示位置属于前端设置,不放入规则 JSON。设置面板完成后建议预留以下设置:

annotation.tagPlacement:
  after-target
  line-end
  hidden

显示设置只影响 Tag 的渲染位置和可见性,不影响 Tooltip 中的完整标注内容。

设置通过 Wails SettingsService 读写,不由前端直接操作文件。设置文件位于:

os.UserConfigDir()/pvfine/settings.json

默认值为 after-target,保存时同样使用临时文件和 rename 原子替换。第一版设置面板只提供标注显示位置,保留后续扩展通用设置的结构。

性能策略

  • 路径标注在建立目录索引时计算。
  • 搜索索引从 relations 中读取固定 listPath 和 contextual contextPaths,装备、道具、技能等列表不再在索引器内重复配置。
  • section/token 标注只在打开文件或编辑当前文件时计算,不在打开归档时解析所有 .equ 文件。
  • reference 索引按 relation 类型懒加载并缓存。
  • 当前文件标注按文件索引和精确编辑器文本缓存;切换归档或修改文本时失效。
  • 对不支持的 TypeUnicode 文件,第一版只提供路径标注;section/token 标注先限定为 TypeScript。

实施阶段

  1. 增加内置 JSON 加载、版本校验和规则错误提示。
  2. 实现独立 HTML 规则编辑器和 JSON 导入导出。
  3. 重构 PVF 反编译流程,同时输出 token/section 位置。
  4. 实现路径标注和文件树 Tag。
  5. 实现编辑器 Tag、Tooltip、复制排除和 Tag 显示设置。
  6. 实现 enum 标注、规则冲突聚合和编辑后刷新。
  7. 实现 reference relation、name 解析和 Cmd/Ctrl 点击打开文件。

主要测试

  • JSON 版本、字段和规则错误校验。
  • 扩展名、路径 glob、section 和 token 范围匹配。
  • 重复 section 全部命中。
  • 中文、反引号、嵌套 section 和不同 token 类型的范围偏移。
  • 多条规则命中时只显示第一条 Tag、Tooltip 显示全部内容。
  • 编辑文本后标注刷新且光标不跳动。
  • 复制文本不包含 Tag。
  • reference ID 不存在、重复和跨类型时的降级行为。
  • 目录和文件路径标注同时存在时的文件树显示。

适用 PVF 版本

规则 rules[] 和共享字段 fields[] 均支持可选的 pvfVersions 数组:

{
  "id": "equ.rarity",
  "pvfVersions": ["90US", "110US"],
  "match": {"extensions": [".equ"]},
  "target": {"kind": "token", "section": "rarity", "index": 0},
  "annotation": {"title": "装备品级", "type": "enum", "values": {"4": "史诗"}}
}
  • 只接受 90US、90CN、110US、115US;省略属性或空数组表示全版本可用,旧配置无需迁移,顶层 version 仍为 1。110US 和 115US 独立匹配,共用规则须显式列出两个版本或不限定版本。
  • 当前已知格式映射为 standard → 90US、alternate → 90CN、paged110 → 110US、paged115 → 115US。paged(未登记客户端的分页容器)、recovered 及其他未知格式只启用未限定版本的配置。映射用于配置适用性,不决定解析或写入能力。
  • 规则引用共享字段时,两处版本限制同时生效。交集为空时规则不生效;字段自身若适用,仍可生成隐式标注。先筛选,再判断显式规则是否覆盖字段。
  • 共享字段的版本限制同时影响标注和结构化预览;关联列表配置不受影响。
  • 后端保留完整规则文档,按当前归档建立运行视图,统一作用于编辑器、目录树和字段预览。切换归档、恢复状态及热重载时更新运行视图与相关缓存。
  • 配置编辑器表单末尾提供版本复选框;试运行区域提供四个版本及“未知版本”单选,默认 90US。POST /api/preview 可传 pvfVersion,省略或空字符串表示未知版本,非法值返回 HTTP 400。