# JavaScript 脚本工作区 脚本工作区使用 Goja 在进程内运行 `.pvf.js`。脚本只能操作当前打开归档的虚拟文件,运行阶段写入隔离事务;运行成功后生成预览,只有点击“应用选中”才会写入当前工作区 Overlay。应用后仍需通过归档编辑器的保存操作写回 PVF 文件。 ## 最小示例 ```js 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 类型时传入值对象: ```js 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` 和映射列表的修改。 ```js 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 ```js const count = document.section("parent").removeChildren("child"); file.write(document); ``` `removeChildren(children_name)` 删除全部同名直接子 section 及其子树,返回删除数量; 无匹配返回 `0`。名称支持 `"child"` 或 `"[child]"`,只处理直接子节点,不影响其它 section 和父节点的直接值。被删除子树的旧句柄不可再使用。 ## 新建、复制与删除文件 ```js 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)` 返回一个列表句柄: ```js 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?)` 新建一个集合并返回句柄。 内置的「默认文件集」与侧边栏一致,始终存在(即使从未保存过),所以可以直接读改它,不需要先创建: ```js 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`。