NarraLeaf

清单

manifest.json 中的每一个字段——入口、完整的 contributes 接口,以及为什么大多数权限是派生出来的、而不是手写的

manifest.json 是 Studio 唯一会在不执行你的代码的情况下读取的文件。它声明插件提供了什么,而安装提示中显示的一切都由它计算得出

{
  "manifestVersion": 2,
  "id": "yourname.hello",
  "name": "Hello",
  "version": "1.0.0",
  "publisher": "Your Name",
  "description": "A starter plugin.",
  "entries": { "studio": "main.js", "runtime": "runtime.js" },
  "contributes": {
    "blueprintNodes": ["yourname.hello.log"],
    "runtimeCapabilities": ["store"]
  },
  "permissions": []
}

顶层字段

字段必填说明
manifestVersion是始终为 2。版本 1 会被拒绝
id是带命名空间:publisher.plugin-name,全小写,至少一个点
name是显示名称
version是语义化版本(1.0.0),可带预发布或构建后缀
publisher否显示在插件列表中
description否一行简介,安装时显示
entries是{ studio?, runtime? }——相对于包的 ESM 路径。至少一个;两个文件都必须存在
contributes否插件提供的一切。省略某个键等同于空列表
permissions否只包含作者声明的提权能力——见下文

入口路径不能是绝对路径,也不能包含 ..、.、空字节、? 或 #

contributes

contributes 是插件能做什么的唯一真相来源。Studio 无需运行插件代码就能据此校验一个项目,安装时的权限集合也由它派生而来

键类型声明了什么
blueprintNodesstring[]本插件提供的蓝图节点类型
widgetsstring[]本插件提供的控件元素类型
widgetTextobject本插件每种控件中存放玩家读到的文字的属性
localesobject[]Studio 语言包——插件新增或补全的语言
runtimeDatastring[]随游戏一并发布的插件存储命名空间
runtimeCapabilitiesstring[]runtime 入口可以使用的能力域
sidecarsobject[]随作者游戏一同发布的原生子进程
buildDependenciesobject[]构建时抓取并缓存的外部二进制文件
testsstring[]本插件提供的测试类型,供 Run > Test 使用
buildConfigobject[]本插件往构建对话框里加的构建配置字段
externalLinksstring[]runtime 入口可以在玩家浏览器里打开的地址模式
networkstring[]runtime 入口可以向其请求字节的主机模式
reservedSaveIdsstring[]插件留给自己用的存档 id,不出现在玩家的存档列表里
structsobject[]插件节点给出的行的字段
agentToolsobject[]studio 入口向 AI Agent 提供的工具,每项为 { name, write },见 Agent 工具
agentGuidestring包内的一个 Markdown 文件,作为本插件的指南章节提供给 Agent

每一个节点类型、控件类型、存储命名空间、sidecar id、构建时依赖 id、保留存档 id 和结构 id 都必须以你的插件 id 为前缀。contributes 下出现未知的键会被拒绝,而不是被忽略

blueprintNodes 与 widgets

注册一个你没有声明的类型会在加载时抛错,两个入口都是如此。正是这份声明,让 Studio 能在构建之前就告诉作者:项目里用到的某个节点没有 runtime 提供方——而不是发布一个带着无效节点的游戏

widgetText

控件里有玩家读到的文字(例如标题或说明)时,按控件类型列出存放这些文字的属性:

{
  "contributes": {
    "widgets": ["yourname.hello.badge"],
    "widgetText": {
      "yourname.hello.badge": [
        { "prop": "title", "label": "Title", "localized": { "zh": "标题", "ja": "タイトル" } },
        { "prop": "caption", "multiline": true }
      ]
    }
  }
}
字段说明
prop必填。存放文字的属性
keyProp作者从翻译键取字时,存放翻译键的属性。缺省为 <prop>LocalizationKey。由 Studio 写入,控件绘制时从不读取它
label属性面板中这段文字的名称。缺省为属性名
localized按编辑器语言给出的 label,以编辑器的语言代码为键,例如 zh 或 ja
multiline文字能否换行。缺省为单行

Studio 像对待自己的文本与按钮控件的文字那样处理每个声明的属性:属性面板在内容一节中提供直接输入或翻译键的选择,画布绘制键的文字,这些文字进入翻译表与导出,项目检查报告缺少的译文或键,字形检查也会读取它们。在游戏中,render 收到的属性已是玩家语言中的文字,控件自己从不解析键

两个入口都读取这份声明:studio 入口的控件按它编辑,runtime 入口的控件按它绘制。这里列出的每个类型也必须在 contributes.widgets 中,且至少有一项。prop 与 keyProp 须是标识符,不能是 runtimeTextOrigin 或 runtimeTextUnit,同一控件内名称不能重复。widgetText 不派生任何安装权限

runtimeData

插件存储位于项目的编辑器目录下,而那个目录永远不会被打包。如果你的 runtime 入口需要作者在 Studio 中编写的数据——一份目录表、一张查找表——就把这些命名空间列在这里,并用 app.game.data.readJson(namespace) 读取

{ "contributes": { "runtimeData": ["yourname.hello.catalog"] } }

这个列表是一份显式白名单,这样纯编辑器的插件状态就不会意外泄漏进发布的游戏。readJson 是同步的——数据随包一起分发——当命名空间未被声明、项目从未写入过,或游戏早于该数据被发布时,它返回 null。请优雅降级;不要假定作者数据一定存在

runtimeCapabilities

十三个能力域,每一个对应 app.game 的一个命名空间。未声明的域在对象上根本不存在,而不是一个会抛错的方法。声明了它却没有 entries.runtime 是清单错误

{ "contributes": { "runtimeCapabilities": ["store", "events", "state.read"] } }

store · events · state.read · state.write · saves.read · saves.write · ui.overlay · assets · locale · menu · story.compile · diagnostics · process.memory

完整模型、每个能力具体授予什么,以及各环境如何进一步收窄它:Runtime API

sidecars 与 buildDependencies

两个重量级声明。sidecar 是随作者构建的游戏一起发布的原生子进程;构建时依赖 是 Studio 在构建时下载、校验并缓存的外部二进制文件。两者都以 <platform>-<arch> 为键,也都必须提供 sha256

locales

一个 Studio 语言包——插件新增的语言,或它为内置语言补上的缺口。参见 创建第一个插件。只包含 contributes.locales、没有任何入口代码的清单也是一个有效的插件

reservedSaveIds

插件以自己的 id 写下的存档(例如快速存档位、检查点)和玩家的存档放在同一个存档库里。把这些 id 列在这里,它们就不会出现在玩家的存档列表里:List Saves、所有基于它的存档界面以及 Dev Mode 的存档面板都会略过它们,和略过自动存档一样。其余途径照常能用到它们:插件自己的 app.game.saves(它的 listIds 原样返回存档库),以及接收 id 的存档节点,例如 Load Save、Delete Save 和各个存档读取节点

{ "contributes": { "reservedSaveIds": ["yourname.quicksave.slot"] } }

每个 id 都必须以你的插件 id 开头,所以插件藏不掉作者自己用 Save Game 写下的存档位。只有插件在游戏里运行时这些 id 才是保留的;在不运行该插件的游戏里,这些存档会和其他存档一样被列出

structs

节点给出一组记录(画廊条目、成就、商店商品等)时,在这里声明记录的字段,并把输出引脚的类型写成该结构:

{
  "contributes": {
    "blueprintNodes": ["yourname.shop.getGoods"],
    "structs": [
      {
        "id": "yourname.shop.good",
        "name": "Shop item",
        "localized": { "zh": "商品", "ja": "商品" },
        "fields": [
          { "key": "id", "type": "string" },
          { "key": "name", "type": "string" },
          { "key": "price", "type": "number" },
          { "key": "icon", "type": "image" },
          { "key": "owned", "type": "boolean" }
        ]
      }
    ]
  }
}
{ id: "goods", kind: "output", semantic: "data", valueType: "array<struct:yourname.shop.good>", label: "Goods" }
字段说明
id必填。以插件 id 为前缀。携带一条记录的引脚类型写作 struct:<id>,携带一组记录的写作 array<struct:<id>>
name必填。编辑器中该结构的名称,显示在引脚上和列表的结构选项中
localized各编辑器语言的 name,按编辑器语言代码(如 zh、ja)为键
fields必填,至少一项。每个字段写作 { key, type },顺序即列表中显示的顺序。type 为 string、number、boolean、image、color 或 json。同一个 key 不能出现两次

声明的结构与引擎自己的结构(例如结局、存档条目)同等对待:

  • 列表可以在编辑内容中把它选作结构。字段由插件提供,不能编辑,作者不必照着你的文档抄写字段名
  • 从该引脚拖出连线时,添加节点菜单列出每个字段;Get Field 按名称读取字段,输出带有字段的类型
  • 连入 Set List Content 的行缺少列表结构的字段时,画布上和项目检查中都会报告

字段的 key 同时就是它的 id,因此作者无法给插件结构的字段改名。编辑器、运行中的游戏和命令行检查都在执行插件代码之前从清单读取这些声明。插件未加载(被禁用或未安装)时,它的结构与它的节点一样不可用:使用该结构的列表不显示字段,项目的依赖检查会指出是哪个插件。structs 不派生安装权限

权限是派生的

安装权限分为两类,这个划分正是重点所在

作者声明的——由你写在 permissions[] 里。它们是与 contributes 无关的 Studio 提权控制项:

{
  "permissions": [
    { "kind": "filesystem", "path": "/absolute/path", "mode": "readwrite", "recursive": true },
    { "kind": "api", "capability": "bash.execute" }
  ]
}
类别形状
filesystem{ path, mode: "read" | "write" | "readwrite", recursive }——一个真实的路径字符串
api{ capability }——目前插件只能使用 bash.execute

这些只影响 studio 入口。授权按 pluginId@version 记录,所以提升版本号就需要作者重新批准一次

派生的——由 Studio 从 contributes 计算得出,绝不由你手写:

类别派生自
runtimecontributes.runtimeCapabilities 中的每一项
sidecar每个 contributes.sidecars 条目,以及它所发布的平台键
buildDependency每个 contributes.buildDependencies 条目,以及它下载所用的主机名
externalLinkcontributes.externalLinks,用作者自己写的模式原样列出 —— 不改写,好让提示与清单是同一份文件
networkcontributes.network。与 externalLink 是两个问题,永远不合并:打开一个页面只是把地址交给浏览器、什么都不会回来,而这个是取字节,取回来的东西会在游戏里跑
agentToolscontributes.agentTools:全部工具,以及其中修改项目的工具。新增工具,或把只读工具改为写入工具,会再次询问

手写 runtime、sidecar、buildDependency、externalLink、network 或 agentTools 权限是清单错误——插件会安装失败,并提示 "permission kind … is derived from contributes and must not be declared by hand."。在 contributes 中把能力声明一次,权限自然随之而来

这正是安装提示能保持诚实的原因:一项能力只在一个地方声明,因此提示列出的内容与插件真正能触及的内容不可能漂移开来。它也让更新的行为变得正常——新增一项能力会扩大派生出的权限集,于是自动重新向作者索取批准;而没有扩大任何范围的版本则沿用已有的授权

校验失败

症状检查
安装被拒绝manifest.json 不是合法 JSON,或 id / version / entries 未通过校验
"requires a runtime entry"声明了 runtimeCapabilities 或 sidecars,却没有 entries.runtime
"must not be declared by hand"把某个派生类别的权限写进了 permissions[]
"Unknown plugin runtime capability"runtimeCapabilities 里有拼写错误。这个列表是封闭的
加载时注册抛错某个节点或控件类型不在 contributes 中,或没有以插件 id 为前缀
预览报 "Plugin validation failed"项目用到的某个节点或控件没有 runtime 提供方——插件被禁用、缺失、没有 entries.runtime,或该类型未被声明
安装时摘要不匹配某个 sidecar 文件的字节与它的 sha256 不符

不派生任何权限的贡献

tests、buildConfig、reservedSaveIds 和 structs 是不会产生安装权限的贡献,且每一种都是裁定而不是漏掉。

其余每一种带代码的贡献,在插件装载那一刻就拿到了一项能力。而测试只有在作者打开 Run > Test、在对话框里挑中它、再按下 Start 时才会跑;安装时根本没有一项“环境里一直在”的能力需要同意,而测试跑起来之后能碰什么,由它自己的 requires 单独把关。而构建配置字段只是一个留给作者填的空,填了也不会让插件多碰到任何东西。而保留存档 id 只是把插件自己命名空间里的 id 从玩家的存档列表里拿掉。结构只描述插件节点本来就会给出的行有哪些字段。

本页目录