服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
11 KiB
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。