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