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

147 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`。