服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
15 KiB
设计一个类似标注的功能,核心目的是解决“哪个配置配什么东西”的问题:
-
标注走规则配置,而非针对具体内容的单独标注
-
标注内容包括 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和 contextualcontextPaths,装备、道具、技能等列表不再在索引器内重复配置。 - section/token 标注只在打开文件或编辑当前文件时计算,不在打开归档时解析所有
.equ文件。 - reference 索引按 relation 类型懒加载并缓存。
- 当前文件标注按文件索引和精确编辑器文本缓存;切换归档或修改文本时失效。
- 对不支持的
TypeUnicode文件,第一版只提供路径标注;section/token 标注先限定为TypeScript。
实施阶段
- 增加内置 JSON 加载、版本校验和规则错误提示。
- 实现独立 HTML 规则编辑器和 JSON 导入导出。
- 重构 PVF 反编译流程,同时输出 token/section 位置。
- 实现路径标注和文件树 Tag。
- 实现编辑器 Tag、Tooltip、复制排除和 Tag 显示设置。
- 实现 enum 标注、规则冲突聚合和编辑后刷新。
- 实现 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。