Files
90CNServer/pvf/docs/脚本工作区设计.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

11 KiB
Raw Permalink Blame History

JavaScript 脚本工作区

脚本工作区使用 Goja 在进程内运行 .pvf.js。脚本只能操作当前打开归档的虚拟文件,运行阶段写入隔离事务;运行成功后生成预览,只有点击“应用选中”才会写入当前工作区 Overlay。应用后仍需通过归档编辑器的保存操作写回 PVF 文件。

最小示例

for (const file of pvf.glob("equipment/**/*.equ")) {
  const document = file.parse();
  const price = document.section("price");
  if (price && price.get() < 10) {
    price.set(10);
    file.write(document);
  }
}

sections(path) 返回精确路径的全部 section;section(path) 和 get/set/delete 默认只访问第一个匹配项。重复 section 必须显式遍历 sections() 或传入 occurrence。set 接收字符串时,110US 自动注册为 type 8 字符串表引用,其它版本写入旧式 type 6 文本;数字替换保留已有类型,新增数字按整数/浮点范围推断。精确值对象始终按声明类型写入。append / setValues 的类型推断规则不变。

需要精确控制 token 类型时传入值对象:

document.set("price", { type: "float", value: 3.5 });
section.set({ type: "quoted", value: "展示名", pool: "utf16" });

字符串引用

110US 的 type 8/10 与旧式字符串类型均可读写。get / getValue 保留引用本身; getOriStrValue 将 <表号::键名> 解析为字符串表中的文本,也支持文本中嵌入的多个引用。 旧式文本和无法解析的引用原样返回;document/section 的值不存在时返回 undefined, 值不是字符串时报错。解析使用当前脚本暂存状态,包含同次运行对 .str 和映射列表的修改。

const file = pvf.find("equipment/item.equ");
const document = file.parse();
const name = document.section("name");
const text = name.getOriStrValue(); // 等同于 document.getOriStrValue("name")
const original = pvf.getOriStrValue("<31::equip_name_1>");
name.set("新的展示名"); // 110US 自动注册 .str 条目,写入 type 8
document.set("description", "新的描述", { create: true });
name.setStrValue("旧式内联文本"); // 所有版本固定写入 type 6
file.write(document);

section.setStrValue(text, index?) 和 document.setStrValue(path, text, options?) 始终写入旧式 type 6,后者的选项和返回值与 set 相同,不注册或修改 .str 表。 110US 的 set 优先选用当前值引用的可写字符串表,否则使用基础映射中第一个可写表。 自动生成的内容键可复用相同文本,不覆盖原有共享引用。没有可写表、文本包含换行或 NUL 时报错, 可改用 setStrValue。自动注册产生的 .str 变更也会进入预览,应用引用文件时必须一并选择依赖表。 精确控制已有引用时可传入 { type: "block8", value: "<31::key>" },或使用 "block10"。

移除子 Section

const count = document.section("parent").removeChildren("child");
file.write(document);

removeChildren(children_name) 删除全部同名直接子 section 及其子树,返回删除数量; 无匹配返回 0。名称支持 "child" 或 "[child]",只处理直接子节点,不影响其它 section 和父节点的直接值。被删除子树的旧句柄不可再使用。

新建、复制与删除文件

const created = pvf.createFile("equipment/new.equ", pvf.types.script, "[price]\n100");
created.setText("[price]\n200"); // 之后可继续编辑

pvf.copyFile("equipment/a.equ", "equipment/b.equ");        // 目标已存在时报错
pvf.copyFile("equipment/a.equ", "equipment/b.equ", true);   // 显式覆盖

pvf.deleteFile("equipment/old.equ"); // 返回是否删除了条目
  • createFile(path, dataType?, text?):dataType 省略时默认为 pvf.types.script,text 是脚本可读的初始内容。路径必须非空且不含 .、.. 或空段;路径中不存在的目录会随文件一起创建;路径已存在时报错,不会静默覆盖。
  • copyFile(from, to, overwrite?):复制当前暂存状态的最终内容与数据类型,因此同一脚本中先编辑再复制会带上新内容。目标已存在时必须显式传 overwrite。
  • deleteFile(path):只删除文件本体,不修改 .lst 等注册表文件。路径不存在时返回 false。需要同步维护列表时用下面的 pvf.lst。

结构变更与句柄失效

createFile / copyFile / deleteFile 会改变归档的文件表,删除之后的条目索引会被重新编号。因此:

  • 删除发生后再使用之前通过 pvf.find / pvf.glob / pvf.files 取得的旧句柄会报错,需要重新查询。
  • 预览面板按路径(changeKey)而不是文件索引标识变更,行状态区分 changed / added / deleted。
  • 应用后需要重新加载归档视图,不能按旧索引原地刷新。

.lst 列表操作

.lst 文件按固定两 token 记录(id、路径)存放资源注册表。pvf.lst(path) 返回一个列表句柄:

const lst = pvf.lst("equipment/equipment.lst");

const entries = lst.get();                  // => { "1008": "character/a.equ", ... }
lst.forEach((id, path) => { /* 按文件顺序遍历 */ });
lst.set("1009", "character/b.equ");         // id 已存在则改,不存在则追加
lst.mset({ "1010": "character/c.equ" });    // 批量写入
lst.unset("1008");                          // => true / false
lst.getId("character/b.equ");               // => "1009",未注册时返回 null
  • get():返回普通对象,键是 id、值是路径;同一 id 有多条记录时只保留第一条。
  • forEach(callback):按 .lst 文件顺序调用 callback(id, path)。因为 JS 对象会把整数形式的键按数值重排,需要按文件顺序处理时应当用 forEach,而不是遍历 get() 的结果。回调里可以修改列表,遍历基于进入时的快照。
  • set(id, path) / mset(entries):id 已存在则原地改写它的路径(保留记录位置与 id 的 token 编码),不存在则追加到末尾。id 可以传字符串或整数(如 set(1010, ...)),内部统一按字符串处理。写入某个 id 时,该 id 的重复记录会被合并为一条,因此一个 id 始终只对应一个条目。
  • unset(id):删除该 id 的全部记录,返回是否真的删除了条目。
  • getId(path):反向查询路径注册的 id,未注册时返回 null。它只做路径换算,不要求目标文件存在。
  • 旧布局按 .lst 所在目录存储相对路径,例如 equipment/equipment.lst 里的条目写作 character/a.equ;110US 和 list/ 集中布局按归档根存储完整路径,例如 list/equipment.lst 里的条目写作 equipment/character/a.equ。这里的完整路径是 PVF 内部路径,不是操作系统路径。set / mset 同时接受列表目录相对路径和完整归档路径,按布局自动换算;有歧义时优先采用列表原生存储形式。目标文件必须存在,否则报错且不写入,避免登记出悬空条目。getId 使用相同的路径换算,但不要求目标存在;get / forEach 返回实际存储路径。
  • pvf.lst(path) 兼容两种列表位置:旧路径 equipment/equipment.lst 可打开集中布局的 list/equipment.lst,反向也支持;如果指定路径确实存在,优先使用它。

列表编辑属于普通内容修改,会以 changed 行进入预览,不需要重新加载归档视图。

文件集操作

文件集是用户配置目录下 file-sets.json 里的路径集合,与当前归档无关。pvf.fileset(name) 按名称取得句柄,不存在时返回 null;pvf.createFileset(name, paths?) 新建一个集合并返回句柄。

内置的「默认文件集」与侧边栏一致,始终存在(即使从未保存过),所以可以直接读改它,不需要先创建:

const set = pvf.fileset("默认文件集");   // 一定存在,初值为空
if (set) {
  const paths = set.getAll();                       // => ["equipment/a.equ", ...]
  set.setAll(paths.filter((path) => path.endsWith(".equ")));
}

pvf.createFileset("武器", ["equipment/character/sword.equ"]);
pvf.createFileset("空集合");                         // paths 省略即空集
pvf.fileset("不存在");                               // => null
  • getAll():返回集合内的路径列表,顺序与存储一致。同一脚本内先 createFileset 再 fileset 也能查到。
  • setAll(paths):用给定列表整体替换集合内容,返回替换后的条目数。路径会规范化(反斜杠转正斜杠、去首尾斜杠)并去重,空路径被忽略;不校验路径是否存在于当前归档,所以可以预先录入尚未创建的文件,未命中的条目在侧边栏显示为失效。已存在路径会保留其名称与 id 等元数据。
  • createFileset(name, paths?):名称首尾空白会被忽略,与已有集合重名时报错(包括内置的「默认文件集」),不会静默覆盖。paths 规则与 setAll 相同。
  • 文件集是用户配置,不是归档内容,因此它的变更不改动 PVF:预览面板里以「文件集 · <名称>」单列一组(整组共用一个勾选框,显示 +/- 的路径增减),点“应用选中”才写入 file-sets.json。只改文件集的脚本没有归档变更,modifiedFiles 为 0,但依然可以应用;反之只勾选归档行时文件集不会被写入。
  • 应用后前端广播 fileset:changed,侧边栏重新读取磁盘内容。若侧边栏此刻有未保存的改动,不会自动刷新而是提示冲突,避免脚本写入被下一次保存覆盖。

运行边界

  • file.parse() 只接受 TypeScript 文件;text() / setText() 也支持 Unicode 文本文件。
  • 结构化文档保留 token 顺序、类型和字符串池编码,不保留空白、注释或原始排版。
  • 新建、复制、删除、.lst 列表编辑、文件集修改与内容修改一样进入隔离事务,只有点击“应用选中”才会写入当前工作区 Overlay(文件集例外,它写入 file-sets.json);可用 pvf.modifiedCount 查看包括结构变更在内的待应用数量。
  • 每次运行最多 5 分钟,同时只能有一个活动运行实例;停止、超时、归档切换和运行异常都会丢弃事务。
  • Goja 沙箱不注册 require、process、fetch、Node.js 模块、本地文件、网络或 shell 能力。
  • 脚本目录固定为 os.UserConfigDir()/pvfine/scripts/,只接受目录内 basename 格式的 .pvf.js 文件。

脚本服务事件:script:state、script:log、script:progress。