项目数据库 API 让扩展通过稳定契约使用项目数据集合。扩展只认识自己在 manifest 中声明的逻辑别名,不接触项目文件路径或真实集合 id;Studio 负责创建、选择、绑定和版本兼容检查。
声明数据依赖
在 extension.json 的 dataDependencies 中为每项依赖声明别名、契约和版本范围:
{
"id": "example.inventory",
"name": "背包系统",
"version": "1.0.0",
"sdkVersion": "^1.9.8",
"entry": "dist/index.js",
"dataDependencies": {
"items": {
"name": "道具目录",
"description": "背包和商店共同读取的道具定义",
"contract": "community.avg.item-catalog",
"version": "^1.0.0",
"access": "read",
"autoCreate": {
"name": "道具目录",
"contractVersion": "1.0.0",
"runtimePolicy": "readonly",
"schema": {
"type": "object",
"required": ["name", "price"],
"columns": [
{ "key": "name", "label": "名称", "type": "text" },
{ "key": "price", "label": "价格", "type": "number" },
{ "key": "icon", "label": "图标", "type": "image" }
]
}
}
}
}
}
| 字段 | 说明 |
|---|
contract | 社区或团队约定的稳定契约 id |
version | 扩展接受的 semver 范围 |
access | read 或 runtime-write |
optional | 可选依赖;缺失时不阻止扩展继续使用其它能力 |
autoCreate | 缺少兼容集合时的创建模板;设为 false 时必须由创作者手工绑定 |
同一契约可以由多个扩展共享。契约升级时用新的 contractVersion 表示实际创建版本,并让消费方逐步扩大或调整接受范围。
获取集合
在剧本方法或其它已注入上下文的位置,通过依赖别名获取集合:
static listAffordableItems = method({
title: "查询可购买道具",
async run(ctx) {
const items = ctx.database.collection("items");
return items.find(
{ price: { $lte: 100 } },
{ sort: { price: 1 }, limit: 20 },
);
},
});
collection() 接收的是 manifest 中的 items 别名,不是 Studio 数据集合页面里的表名或内部 id。依赖完成绑定后,集合始终可用,不需要连接或关闭数据库。
const items = ctx.database.collection("items");
const one = await items.getById("item-id");
const result = await items.find(
{
$and: [
{ category: "medicine" },
{ price: { $gte: 10, $lte: 100 } },
],
},
{ sort: { price: 1 }, skip: 0, limit: 50 },
);
字段过滤支持 $eq、$ne、$gt、$gte、$lt、$lte、$in、$contains 和 $exists,也可以用 $and / $or 组合。所有方法均为异步 API,不应依赖当前 JSON 文件实现或同步读取项目目录。
新增、更新和删除
需要写入的依赖应声明 access: "runtime-write",数据表运行时策略也必须允许写入:
const inventory = ctx.database.collection("inventory");
const created = await inventory.insertOne({ itemId: "key", count: 1 });
await inventory.updateOne(
{ id: created.id },
{ $inc: { count: 1 }, $set: { equipped: true } },
);
await inventory.deleteOne({ id: created.id });
记录 id 由宿主生成且不可修改。更新操作支持 $set、$unset、$inc、$push 和 $pull;返回值包含命中与实际修改的记录数。
监听变化
const stopWatching = ctx.database.collection("inventory").watch(() => {
// 重新查询并刷新界面
});
// 界面卸载或扩展停止时
stopWatching();
监听器只通知集合有效数据发生变化,不直接携带完整记录。收到通知后重新调用 find(),可以避免事件载荷和当前数据不同步。
运行时策略与权限
本页上方的 ctx.database 是游戏运行时 API。它的写入保存在会话或玩家存档覆盖层,不修改作者在 Studio 中维护的原始资料。
| 策略 | 扩展写入结果 |
|---|
readonly | 拒绝运行时写入,只允许查询 |
session | 只保留到本次游戏关闭 |
archive | 保存到当前玩家存档 |
profile | 保存到跨存档共享档案 |
扩展身份彼此隔离,只能通过自己声明并完成绑定的别名访问集合。不要绕过 API 读取 databases/ 文件,也不要假设另一扩展的别名、真实集合 id 或底层存储格式。
删除字段或收窄契约前,先发布兼容迁移并确认所有消费扩展已经升级。Studio 会检查契约版本,但不会替业务数据自动推断复杂迁移规则。
创作者如何建立和维护数据表,见项目数据集合。
在 Studio 编辑界面里维护原始资料
编辑期入口 src/studio.tsx 可以通过 @avg-studio/studio-ui 的 useDataCollection(alias),在自定义设置页或自定义检查器里直接维护本扩展声明并绑定的数据集合。先在 Studio 中执行“同步 SDK”,获取当前组件包类型。
import { Button, Panel, useDataCollection } from "@avg-studio/studio-ui";
type Item = { id: string; name: string; price: number };
function ItemsPage() {
const items = useDataCollection<Item>("items");
async function addItem() {
try {
await items.insertOne({ name: "药水", price: 20 });
} catch (error) {
// 替换成扩展自己的错误提示。
console.error(error);
}
}
return (
<Panel>
<Button disabled={items.revision === null} onClick={() => void addItem()}>
新增道具
</Button>
</Panel>
);
}
export const settingsPage = ItemsPage;
items 是 manifest.dataDependencies 中的别名,不能传表名、路径或其他扩展的别名。这个 API 不能在游戏运行时调用;设置页仍遵循原有入口要求,包括声明至少一项设置。
| 成员 | 作用 |
|---|
documents | 原始记录的独立副本;Studio 表格保存、绑定变化或撤销重做后自动刷新。直接修改副本不会保存 |
revision | 当前资料和表结构的版本;集合不可用时为 null |
error | 项目未就绪、别名未声明或未绑定、契约不兼容、集合不可用时的诊断;正常时为 null |
insertOne(document) | 新增并返回记录,由 Studio 生成 id,输入不能携带 id |
updateOne(id, patch, baseRevision?) | 按 id 浅合并指定字段;不能修改 id,记录不存在返回 false |
deleteOne(id, baseRevision?) | 按 id 删除,记录不存在返回 false |
saveDocuments(documents, baseRevision) | 整表保存,支持排序和删行,必须保留已有 id;新增记录使用 insertOne |
所有写操作均返回 Promise,失败时 reject。保存会校验 JSON 值、字段类型、必填项和 id。扩展和 Studio 数据表格页读写同一份资料,任一边保存后另一边都会刷新。
access: "read" 和 runtimePolicy: "readonly" 只限制游戏运行时,作者仍可通过设置页修改原始资料。这个 API 不能读取或修改玩家存档覆盖层,也不能修改表结构。
在方法检查器里使用
检查器组件和设置页的用法相同,有两点不同:
- 资料由整个工程共用,保存后影响所有读取它的指令,剧本的撤销也不会撤回这次保存。
useInspectorEnvironment().readOnly 为 true 时能读取,但所有写入都会 reject。请把保存按钮置灰。
草稿与并发修改
开始编辑草稿时,同时保留 documents 和当时的 revision:
const draft = {
documents: structuredClone(items.documents),
revision: items.revision,
};
// 作者编辑 draft.documents 后,在保存回调中执行:
if (draft.revision !== null) {
await items.saveDocuments(draft.documents, draft.revision);
}
不能保留旧草稿,却用最新渲染中的 items.revision 保存它。版本冲突后应重新读取并合并修改,再保存;直接换成最新版本号重试会覆盖其他编辑。
逐条更新和删除默认读取最新资料。如果编辑过程跨越多次渲染,也应把草稿开始时的版本传给 updateOne 或 deleteOne。项目切换或依赖重新绑定后,旧回调会拒绝写入,避免改到新的集合。只在作者操作时保存,不要在组件挂载的 effect 中自动修改资料。