Sora
Sora 用来让游戏配置表保持易懂,同时让运行时代码获得强类型访问。
你先用 schema 描述表结构,再用 Excel、CSV、TOML、JSON 或 YAML 填行数据。Sora 会校验这些数据,校验通过后导出运行时数据包,并生成知道如何加载这个数据包的代码。
核心思路是:schema 是契约。Excel、CSV、TOML、生成代码和运行时数据包,都是这个契约的不同投影。策划可以在工作簿里编辑行数据,游戏代码则通过生成出来的强类型 API 读取配置。
一个小项目里的文件流通常是:
project.toml
-> schema/items.toml
-> data/Item.xlsx
-> generated/config.sora
-> generated/rust
通常手写 project.toml 和 schema 文件。策划或工具编辑 data/ 下的数据文件。generated/ 下的文件由 Sora 生成。
Sora 做什么
schema modules -> Excel/CSV/TOML/JSON/YAML data -> validation
|-> runtime bundle
|-> generated code
Sora 当前聚焦这些阶段:
- 用 schema 描述表、记录、枚举、联合、引用、索引和校验规则;
- 在内置的 Sora Studio UI 中查看和编辑 schema module;
- 根据 schema 生成 Excel 模板,避免表头和字段定义漂移;
- 从 TOML、JSON、YAML、CSV 或 Excel
.xlsx加载表格数据; - 按归一化 schema 校验数据和跨表引用;
- 导出 Sora binary、debug JSON、JSON bundle、CBOR bundle 或 Sora Protobuf bundle;
- 生成可以加载这些数据包的语言运行时代码。
常见术语
Sora 里 format 会出现在几个不同位置:
| 术语 | 含义 | 例子 |
|---|---|---|
| Schema format | schema/project 文件本身的格式。 | TOML、YAML、JSON、Lua |
| Source format | 可编辑表格数据的格式。 | Excel .xlsx、CSV、TOML、JSON、YAML |
| Export format | 校验后写出的数据包格式。 | binary、json、cbor |
| Runtime format | 生成代码期望加载的数据包格式。 | sora、json、cbor |
例如 Rust 代码生成使用 runtime_format = "sora" 时,需要匹配一个 binary export。源数据仍然可以来自 Excel。
适用场景
Sora 适合游戏配置和类似的数据密集型项目:
- 策划或工具需要编辑表格数据;
- 运行时代码希望读取强类型配置,而不是散乱字典;
- schema 变更需要进入源码审查;
- 项目方可能需要扩展自己的语言生成器或导出格式。
项目仍处于早期阶段,公共 API 可能继续调整。设计目标是让核心 schema 和 IR 独立于具体语言后端,方便下游用户增加生成器或导出器,而不用 patch 核心管线。
需要稳定输出的项目应该固定 sora CLI 版本。只有真实的生成 runtime 不兼容时,Sora 才会升级 runtime/export format version;当前不会用 edition flag 保留旧 schema 语义。见版本与兼容性。
推荐阅读顺序
先读快速开始,再读 Sora Studio、第一份配置和Excel 工作流。之后最常用的参考页是类型、表、单元格 Parser、引用和派生字段和版本与兼容性。
设计说明和扩展页面适合已经理解基本构建流程之后再读。
核心概念
Project
项目清单声明 package 名称、schema 模块、构建输出、代码生成目标和导出目标。sora check、sora build、sora gen 和 sora export 都以它为入口。
Schema
Schema 文件描述配置数据的形状。它定义枚举、结构体、联合、表、索引、引用和字段规则。Sora 会先把 schema 归一化成 IR,再进入校验、导出或代码生成。
Table
表是一组命名行。表可以是 list、按某个字段做 key 的 map,或者 singleton。source 元数据说明可编辑数据来自哪里。
表 schema 也会用于生成 Excel 表头这类编辑器投影。电子表格不是契约本身,它只是编辑符合契约的行数据的一种方式。
Value
Sora 会先把源数据单元格校验并转换成公共 value tree,再交给导出器。生成运行时从不同 runtime format 读取同一个形状,因此目标语言可以在 sora、json、cbor、sora-protobuf 之间切换,而不需要改 schema。
Runtime Format
Runtime format 是生成代码在运行时加载的数据格式。它通过 runtime_format 按语言目标选择。
Generator
Generator 是注册到 codegen registry 的语言后端。内置语言也是普通 registry entry,因此下游扩展可以复用同一条管线。
Exporter
Exporter 负责把校验后的数据写成运行时数据包。导出器 registry 和代码生成分离,因此数据格式和语言目标可以独立演进。
Scope
Schema 实体和字段归属于预先声明的 groups。具名 view 通过选择 group、稳定表 ID、
别名和目标语言 binding,形成 codegen 与 export 使用的外部契约。
快速开始
这一页会创建一个最小的道具表,生成 Excel 模板,导出运行时数据包,并生成可以读取它的 Rust 代码。
可以从 GitHub Releases 下载对应平台的压缩包,解压后把 sora 放到 PATH 中。
如果本机已有 Rust 工具链,也可以从 crates.io 安装已发布的 CLI:
cargo install sora-cli
本地开发时可以从源码安装:
cargo install --path crates/sora-cli
1. 创建项目
最快的方式是直接生成同一个最小项目:
sora init --out my-config --schema-format toml
cd my-config
--schema-format 支持 toml、yaml、json 和 lua。脚手架会生成这个目录结构:
| 路径 | 谁编辑 | 作用 |
|---|---|---|
project.toml | 你 | 项目入口、构建输出、默认数据目录。 |
schema/items.toml | 你 | Item 表的 schema。 |
data/Item.xlsx | 策划或工具 | 可编辑行数据。 |
generated/ | Sora | schema lock、Excel 模板、生成代码、导出数据。 |
本节后面的内容展示生成出来的文件,方便理解项目结构。project.toml 内容如下:
project = { id = "game_config" }
groups = { common = { default = true } }
views = { default = { contract = "game_config/default", groups = ["common"] } }
includes = ["schema/items.toml"]
[build]
default_source_format = "xlsx"
data_root = "data"
schema_lock = "generated/schema.lock"
excel_templates = "generated/excel"
[[build.codegen]]
target = "rust"
out = "generated/rust"
format = "auto"
[[build.exports]]
format = "binary"
out = "generated/config.sora"
这里 default_source_format = "xlsx" 表示表数据默认来自 Excel。data_root = "data" 表示导出和 build 时会从 data/Item.xlsx 读取 Item.xlsx。excel_templates = "generated/excel" 只是生成模板的输出目录。Sora 会在这里写入带 schema 表头的新 workbook;它不是源数据目录。把它和 data 分开,重新生成模板时才不会覆盖已经填写过的行数据。binary export 会写出 Rust 代码要加载的运行时数据包,因为 Rust 默认使用 runtime_format = "sora"。
创建 schema/items.toml:
[[enums]]
name = "ItemType"
values = [{ id = 0, name = "Weapon" }, { id = 1, name = "Armor" }, { id = 2, name = "Material" }, { id = 3, name = "Consumable" }]
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[tables.source]
format = "xlsx"
file = "Item.xlsx"
sheet = "Item"
[[tables.fields]]
name = "id"
type = "i32"
comment = "Item id"
[[tables.fields]]
name = "name"
type = "string"
comment = "Display name"
[[tables.fields]]
name = "item_type"
type = "enum<ItemType>"
comment = "Item category"
[[tables.fields]]
name = "max_stack"
type = "i32"
default = "1"
range = [1, 9999]
comment = "Stack limit"
2. 生成 Excel 模板
工作簿表头由 schema 生成:
sora excel-template --project project.toml --out generated/excel
这会生成 generated/excel/Item.xlsx。把这个文件当作 schema 变更后可以重新生成的模板产物。新建表时,可以把它复制到 data/Item.xlsx,然后在生成表头下面填写行数据:
| id | name | item_type | max_stack |
|---|---|---|---|
| 1001 | Iron Sword | Weapon | 1 |
| 2001 | Health Potion | Consumable | 99 |
当 data/Item.xlsx 里已经有真实数据后,不要运行 excel-template --out data,除非你就是想替换这些文件。空模板继续生成到 generated/excel;schema 变更后,用 excel-sync 原地更新已有数据 workbook。
对于已经有数据的 workbook,更推荐原地同步表头:
sora excel-sync --project project.toml --data-root data
sora excel-sync --project project.toml --data-root data --write
第一条命令会预览新增字段和 legacy 列。带 --write 的命令会刷新生成表头并保留数据行;从 schema 中删除的字段会继续留在 Excel 中,作为 Sora 忽略的 legacy 列。
3. 检查、导出和生成代码
只校验 schema:
sora check --project project.toml
运行 [build] 中声明的全部输出。这个过程会在写出导出文件前加载并校验源数据:
sora build --project project.toml
也可以用 CLI 内置的 Sora Studio 打开这个项目:
sora studio --project project.toml
命令会打印一个本地地址。用浏览器打开后,可以可视化 schema 关系、编辑 schema module、预览将要写入的变更,并保存回项目文件。
也可以分开执行:
sora gen --target rust --project project.toml --out generated/rust
sora export \
--format binary \
--default-source-format xlsx \
--project project.toml \
--data-root data \
--out generated/config.sora
4. 下一步
如果想用可视化方式编辑 schema,继续读 Sora Studio。也可以阅读第一份配置了解完整闭环,或者查看 examples/showcase/project.toml 作为多语言项目参考。
教程
教程从应用使用者的角度介绍 Sora。
先从第一份配置开始,完整跑通一个最小表。然后阅读 Excel 工作流了解生成式表头,再看加载生成代码把导出的数据接入运行时代码。
第一份配置
这个教程会创建一个小型道具配置表。实际项目中的大型配置也是同样模式:定义 schema、生成可编辑工作簿、填写行数据、导出运行时数据包、生成代码。
项目结构
project.toml
schema/items.toml
data/Item.xlsx
generated/
项目清单
project = { id = "game_config" }
groups = { common = { default = true } }
views = { default = { contract = "game_config/default", groups = ["common"] } }
includes = ["schema/items.toml"]
[build]
default_source_format = "xlsx"
data_root = "data"
schema_lock = "generated/schema.lock"
excel_templates = "generated/excel"
[[build.codegen]]
target = "rust"
out = "generated/rust"
format = "auto"
[[build.exports]]
format = "binary"
out = "generated/config.sora"
schema_lock 保存归一化 schema,excel_templates 写出带生成表头的工作簿,build.codegen 声明语言输出,build.exports 声明运行时数据输出。
Schema
[[enums]]
name = "ItemType"
values = [{ id = 0, name = "Weapon" }, { id = 1, name = "Armor" }, { id = 2, name = "Material" }, { id = 3, name = "Consumable" }]
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[tables.source]
format = "xlsx"
file = "Item.xlsx"
sheet = "Item"
[[tables.fields]]
name = "id"
type = "i32"
comment = "Item id"
[[tables.fields]]
name = "name"
type = "string"
comment = "Display name"
[[tables.fields]]
name = "item_type"
type = "enum<ItemType>"
comment = "Item category"
[[tables.fields]]
name = "max_stack"
type = "i32"
default = "1"
range = [1, 9999]
comment = "Stack limit"
这个表使用 mode = "map",因此生成运行时会提供按 id 查找的接口。
Excel 模板
生成工作簿:
sora excel-template --project project.toml --out generated/excel
生成出的 sheet 在可编辑数据区上方有多行元数据:
| #field | id | name | item_type | max_stack |
|---|---|---|---|---|
| #type | i32 | string | enum<ItemType> | i32 |
| #input | key | range=1..9999 | ||
| #desc | Item id | Display name | Item category | Stack limit |
数据行从生成表头之后开始:
| id | name | item_type | max_stack |
|---|---|---|---|
| 1001 | Iron Sword | Weapon | 1 |
| 2001 | Health Potion | Consumable | 99 |
生成后可以把工作簿复制到 data/Item.xlsx,或者在实验阶段直接让 source 指向生成位置。
构建
运行配置好的所有输出:
sora build --project project.toml
预期产物:
generated/schema.lockgenerated/excel/Item.xlsxgenerated/rustgenerated/config.sora
如果只想校验 schema,可以运行 sora check --project project.toml。
Excel 工作流
Excel 支持围绕生成模板设计。schema 拥有表结构,Excel 是这个 schema 的可编辑投影。
生成模板
有两种生成 Excel 模板的方式。
第一种是直接运行命令:
sora excel-template --project project.toml --out generated/excel
这条命令的意思是:读取 project.toml 里的 schema,然后把 Excel 模板写到 generated/excel 目录。这个目录应该只放模板产物,可以删除后重新生成,不应该放手工编辑的源数据。
第二种是把模板输出目录写进 project.toml,之后统一运行 sora build:
[build]
excel_templates = "generated/excel"
sora build --project project.toml
这两种方式生成的是同一类文件。区别只是:第一种只生成 Excel 模板;第二种会和 schema lock、codegen、export 等 build 输出一起执行。
模板目录和数据目录
excel_templates 不是输入数据目录,而是模板输出目录。真正的数据输入目录通常是 [build].data_root 或命令里的 --data-root。
推荐把两个目录分开:
| 路径 | 作用 | 是否可重新生成 |
|---|---|---|
generated/excel | 带 schema 表头的生成 workbook 模板。 | 是 |
data | export 和 build 读取的、已经填写行数据的文件。 | 否 |
不要把 excel-template --out 或 [build].excel_templates 指向已经有手工编辑数据 workbook 的目录,除非你明确想替换这些文件。生成模板用于新 workbook;已经有真实数据的 workbook 应该使用 excel-sync。
同步已有 Workbook
真实项目里已有数据通常很多,这时不要把数据行复制到新模板,而应该使用 excel-sync。它会根据当前 schema 更新 workbook 表头,同时保留数据行:
sora excel-sync --project project.toml --data-root data
不带 --write 时,命令只预览将要发生的变化。确认后再写入文件:
sora excel-sync --project project.toml --data-root data --write
写入已有 workbook 前,Sora 会先把旧文件复制到 data/.sora-backup/<timestamp>/ 下。
同步时按 #field 行匹配字段,而不是按列位置匹配:
- 仍然存在于 schema 中的字段会保留原数据;
- schema 新增字段会插入为空列;
- 类型、parser、groups、range、length、注释和表 metadata 变化会刷新生成表头;
- 从 schema 中删除的字段不会从 Excel 中删除,而是保留为 Sora 忽略的 legacy 列,由策划在合适的时候手动删除;
- 同一个 workbook 中不属于 schema 的 sheet 会作为 value-only sheet 保留下来。
每个表最终生成到哪个 workbook 和 sheet,由表自己的 source 决定:
[[tables]]
id = "item"
name = "Item"
[tables.source]
format = "xlsx"
file = "Core.xlsx"
sheet = "Item"
[[tables]]
id = "quest"
name = "Quest"
[tables.source]
format = "xlsx"
file = "Core.xlsx"
sheet = "Quest"
上面的配置会在 generated/excel/Core.xlsx 中生成两个 sheet:Item 和 Quest。
如果另一个表写成:
[tables.source]
format = "xlsx"
file = "Battle.xlsx"
sheet = "Skill"
它就会生成到另一个文件:generated/excel/Battle.xlsx 的 Skill sheet。
表头行
生成的 sheet 包含多行表头:
| Row | Purpose |
|---|---|
@table metadata | 表名、mode、key、groups 和 schema hash。 |
#name | 面向表格编辑的显示名行。 |
#field | Sora 读取的稳定 schema 字段名。 |
#type | 类型提示,例如 i32、enum<ItemType> 或 struct<Cost>(kind: enum<ResourceKind>, id: i32, count: i32)。 |
#groups | 每个字段所属的已声明 group。 |
#input | key、parser、range、length 或派生字段来源等输入提示。 |
#desc | 给编辑者和 reviewer 看的字段注释。 |
数据行从生成表头之后开始。
用户应该编辑什么
用户应该编辑数据行,不应该手工维护 Excel 里的字段名、类型、key 元数据、输入提示或校验规则。这些行会从 schema 重新生成。
如果某列的 #input 以 from= 开头,这个字段是从另一张表派生出来的。保留该列里的生成占位内容,去编辑对应的子表行。
schema 变更后,先运行 sora excel-sync --project project.toml --data-root data 预览表头变化,确认后再加 --write 写回。这样既保留了电子表格编辑体验,也避免 Excel 变成第二套 schema 语言。
常见字段形状
简单字段直接映射到单元格:
| id | name | max_stack |
|---|---|---|
| 1001 | Iron Sword | 1 |
结构化值可以用 parser 在单元格里写成紧凑形式:
[[tables.fields]]
name = "price"
type = "struct<ResourceCost>"
parser = { kind = "tuple" }
comment = "Tuple: kind,id,count"
示例单元格:
Item,1001,3
集合可以使用 JSON 或 map 风格 parser:
[[tables.fields]]
name = "tags"
type = "set<string>"
parser = { kind = "json" }
default = "[\"misc\"]"
[[tables.fields]]
name = "attributes"
type = "map<string,i32>"
parser = { kind = "map" }
comment = "Map pairs: key,value|key,value"
示例单元格:
["starter","melee"]
attack,12|speed,2
加载生成代码
生成代码包含强类型 row model、table container,以及所选 runtime format 的 config loader。
选择 Runtime Format
[codegen.rust]
runtime_format = "sora"
代码生成选择的 runtime format 必须有匹配的导出数据包:
[[build.exports]]
format = "binary"
out = "generated/config.sora"
runtime_format = "sora" 对应 binary 导出。json、cbor 和 sora-protobuf 分别对应同名导出格式。
Rust 示例
mod generated;
use generated::SoraConfig;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = std::fs::read("generated/config.sora")?;
let config = SoraConfig::from_sora_bytes(&bytes)?;
if let Some(item) = config.items.get(&1001) {
println!("{} stacks to {}", item.name, item.max_stack);
}
Ok(())
}
具体名称会由 schema 名称和目标语言命名习惯决定。例如 Item 表通常会生成 item row type 和 item table accessor。
Adapter Targets
有些目标语言会为部分格式暴露 adapter hook,因为具体生态依赖应由应用自己提供。例如 Lua、Erlang 和 Dart 可以传入 decode_cbor 或 decode_sora_protobuf 函数,而不是让生成代码绑定某个第三方解码库。
示例见运行时适配器。
Schema
schema module 是被项目清单 include 的 TOML、YAML、JSON 或 Lua 文件。
project = { id = "game_config" }
groups = { common = { default = true } }
views = { default = { contract = "game_config/default", groups = ["common"] } }
includes = ["schema/items.toml", "schema/skills.toml"]
Schema 是 Sora 的事实来源。它描述稳定的数据契约;Excel 工作簿等源文件只包含需要按契约校验的行数据。
支持的文件格式以及等价的 TOML/YAML/JSON/Lua 写法见 Schema 格式。
Enums
[[enums]]
name = "ItemType"
values = [{ id = 0, name = "Weapon" }, { id = 1, name = "Armor" }, { id = 2, name = "Material" }]
枚举在可编辑数据中用符号值表示,在支持的语言中会生成原生或接近原生的枚举结构。
Structs
[[structs]]
name = "Cost"
[[structs.fields]]
name = "gold"
type = "i32"
结构体适合复用的对象形状,比如消耗、奖励、坐标、属性修正等嵌套值。
Unions
[[unions]]
name = "RewardAction"
tag = "type"
[[unions.variants]]
name = "AddItem"
[[unions.variants.fields]]
name = "item_id"
type = "ref<Item.id>"
联合用于 tagged variant。tag 是源数据和运行时值里的判别字段名。
Tables
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[tables.source]
format = "xlsx"
file = "Item.xlsx"
sheet = "Item"
表定义带 source 的行集合。模式、key、source、索引和派生字段见表。
Field Types
常见字段类型包括 primitive、enum、struct、union、reference、list、set、fixed array、map 和 optional:
i32
string
enum<ItemType>
struct<Cost>
union<Reward>
ref<Item.id>
list<i32>
set<string>
array<i32,3>
map<string,i32>
optional<string>
完整说明见类型。
split、tuple、columns、tuple_list、map、json 等 Excel/CSV 紧凑单元格写法和列投影见单元格 Parser。
Schema 格式
Sora schema 文件可以写成 TOML、YAML、JSON 或 Lua。这些格式都会加载到同一个 schema model,后续生成的 IR、代码、Excel 模板、导出文件和 schema lock 都一致。
文件扩展名决定 parser:
| 扩展名 | 格式 |
|---|---|
.toml | TOML |
.yaml、.yml | YAML |
.json | JSON |
.lua | Lua |
include 文件按自己的扩展名解析,所以 YAML 项目可以 include TOML、JSON 或 Lua module,任意受支持的项目格式都可以混用受支持的 module 格式。
TOML
includes = ["schema/items.toml"]
project = { id = "game_config" }
groups = { common = { default = true } }
views = { default = { contract = "game_config/default", groups = ["common"] } }
[[enums]]
name = "ItemType"
values = [{ id = 0, name = "Weapon" }, { id = 1, name = "Armor" }]
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
YAML
project: { id: game_config }
groups: { common: { default: true } }
views: { default: { contract: game_config/default, groups: [common] } }
includes:
- schema/items.yaml
enums:
- name: ItemType
values: [{ id: 0, name: Weapon }, { id: 1, name: Armor }]
tables:
- id: item
name: Item
mode: map
key: id
fields:
- name: id
type: i32
JSON
{
"project": { "id": "game_config" },
"groups": { "common": { "default": true } },
"views": { "default": { "contract": "game_config/default", "groups": ["common"] } },
"includes": ["schema/items.json"],
"enums": [
{ "name": "ItemType", "values": [{ "id": 0, "name": "Weapon" }, { "id": 1, "name": "Armor" }] }
],
"tables": [
{
"id": "item",
"name": "Item",
"mode": "map",
"key": "id",
"fields": [
{ "name": "id", "type": "i32" }
]
}
]
}
Lua
Lua schema 文件必须 return 一个 table。这个 table 使用和 TOML/YAML/JSON 形状一致的字段名。Lua schema loader 面向数据配置;io、os 和 debug 不可用。
return {
project = { id = "game_config" },
groups = { common = { default = true } },
views = { default = { contract = "game_config/default", groups = { "common" } } },
includes = { "schema/items.lua" },
enums = {
{ name = "ItemType", values = { { id = 0, name = "Weapon" }, { id = 1, name = "Armor" } } },
},
tables = {
{
id = "item",
name = "Item",
mode = "map",
key = "id",
fields = {
{ name = "id", type = "i32" },
},
},
},
}
项目 Build 配置
项目文件里的 build 也可以使用 YAML、JSON 或 Lua:
project: { id: game_config }
groups: { common: { default: true } }
views: { default: { contract: game_config/default, groups: [common] } }
includes:
- schema/items.yaml
build:
default_source_format: xlsx
data_root: data
schema_lock: generated/schema.lock
excel_templates: generated/excel
codegen:
- target: rust
out: generated/rust
format: auto
exports:
- format: binary
out: generated/config.sora
{
"project": { "id": "game_config" },
"groups": { "common": { "default": true } },
"views": { "default": { "contract": "game_config/default", "groups": ["common"] } },
"includes": ["schema/items.json"],
"build": {
"default_source_format": "xlsx",
"data_root": "data",
"schema_lock": "generated/schema.lock",
"excel_templates": "generated/excel",
"codegen": [
{ "target": "rust", "out": "generated/rust", "format": "auto" }
],
"exports": [
{ "format": "binary", "out": "generated/config.sora" }
]
}
}
return {
project = { id = "game_config" },
groups = { common = { default = true } },
views = { default = { contract = "game_config/default", groups = { "common" } } },
includes = { "schema/items.lua" },
build = {
default_source_format = "xlsx",
data_root = "data",
schema_lock = "generated/schema.lock",
excel_templates = "generated/excel",
codegen = {
{ target = "rust", out = "generated/rust", format = "auto" },
},
exports = {
{ format = "binary", out = "generated/config.sora" },
},
},
}
表
表是带 source 的行集合。表 schema 声明表模式、source 位置、字段和可选索引。
Modes
| Mode | Shape | Typical Use |
|---|---|---|
map | 通过一个字段作为 key 的行集合。 | 道具、任务、等级、buff。 |
list | 没有 keyed lookup 的有序行集合。 | 掉落项、权重池、有序步骤。 |
singleton | 单行。 | 全局配置、调参常量。 |
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
对于 map 表,key 指定表的主键字段。Sora 会用它做行唯一性校验、生成 lookup API、生成 Excel 模板提示,并校验 ref<Table.key>。
表的 id 是 view 选择和表别名使用的稳定 schema 身份;修改 name 时不能跟着改变。
name 是数据文件和 schema 引用使用的 canonical source name。View 可以暴露不同的
消费方表名,而不改变稳定 ID 或源数据。
Source
[tables.source]
format = "xlsx"
file = "Core.xlsx"
sheet = "Item"
当项目或命令提供默认 source format 时,format 可以省略。导出和校验时,file 会基于命令的 --data-root 解析。
内置 source format 包括 xlsx、csv、toml、json 和 yaml。JSON 和 YAML 表文件是 row object 数组:
[
{ "id": 1001, "name": "Iron Sword" },
{ "id": 1002, "name": "Health Potion" }
]
对 JSON 和 YAML,file 也可以指向目录。这种情况下,Sora 会递归读取每个匹配的 .json、.yaml 或 .yml 文件,每个文件作为一条 row object,并按路径排序。
Indexes
索引是表上的额外查询入口。它和 mode = "map" 的 key 不一样:
| 概念 | 用途 |
|---|---|
table key | 表的主键。map 表必须靠它保证每行唯一,并生成主要的 get(id) 查询。 |
[[tables.indexes]] | 额外查询方式。比如按名字查、按类型分组、按关卡查掉落。 |
例如 Item 表的主键是 id,运行时代码通常会按 id 取一个道具:
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
[[tables.fields]]
name = "name"
type = "string"
[[tables.fields]]
name = "item_type"
type = "enum<ItemType>"
如果还希望按 name 查道具,可以加一个 unique index:
[[tables.indexes]]
name = "by_name"
fields = ["name"]
unique = true
对应数据可以长这样:
| id | name | item_type |
|---|---|---|
| 1001 | Iron Sword | Weapon |
| 1002 | Wood Shield | Armor |
unique = true 表示 name 不能重复。生成代码在支持该目标时会提供类似 get_by_name("Iron Sword") 的 helper,返回单行或空值。
如果希望按分类拿到多行,就用非 unique index:
[[tables.indexes]]
name = "by_item_type"
fields = ["item_type"]
unique = false
对应数据:
| id | name | item_type |
|---|---|---|
| 1001 | Iron Sword | Weapon |
| 1002 | Bronze Axe | Weapon |
| 2001 | Wood Shield | Armor |
unique = false 表示同一个 key 可以匹配多行。生成代码在支持该目标时会提供类似 get_by_item_type(ItemType::Weapon) 的 helper,返回匹配行列表。
fields 是列表,因此 unique index 也可以表达组合唯一性:
[[tables.indexes]]
name = "by_world_stage"
fields = ["world", "stage"]
unique = true
这会要求 (world, stage) 组合不能重复。例如 (1, 1) 只能出现一次,(1, 2) 可以再出现一次。当前生成 lookup helper 主要支持非 singleton 表上的单字段 index;组合 index 更适合先用于数据校验。
Validation
加载 source 数据后,Sora 会校验表行:
- 非 optional 字段必须存在,除非有 default;
- map 表的 key 字段必须唯一;
- enum value 必须有效;
- reference 必须指向已有行;
- numeric range 和 length range 必须通过;
- parser 输出必须匹配声明的字段类型。
类型
Sora 的类型表达式在 schema 字段中以字符串形式书写。
原始类型
| 类型 | 含义 |
|---|---|
bool | 布尔值。 |
i8 | 8 位有符号整数。 |
u8 | 8 位无符号整数。 |
i16 | 16 位有符号整数。 |
u16 | 16 位无符号整数。 |
i32 | 32 位有符号整数。 |
u32 | 32 位无符号整数。 |
i64 | 64 位有符号整数。 |
f32 | 32 位浮点数。 |
f64 | 64 位浮点数。 |
string | UTF-8 字符串。 |
duration | 非负时长,写作 500ms、30s、15m、2h、7d 或 1h 30m 这类单位组合。单位必须按从大到小排列:d、h、m、s、ms。运行时数据存储为毫秒。 |
text | 多语言文案 key。见多语言。 |
Sora 会在导出前校验整数宽度范围。部分目标语言没有 unsigned 小整数类型,生成代码可能使用更宽的 signed 类型承载,但 schema 范围语义仍然保留。
[[tables.fields]]
name = "level"
type = "u16"
range = [1, 100]
Named Types
| Type | Example |
|---|---|
| Enum | enum<ItemType> |
| Struct | struct<ResourceCost> |
| Union | union<RewardAction> |
| Reference | ref<Item.id> |
引用必须指向 mode = "map" 表的主键。容器可以包住引用,例如 list<ref<Item.id>>。
[[tables.fields]]
name = "item_type"
type = "enum<ItemType>"
[[tables.fields]]
name = "price"
type = "struct<ResourceCost>"
parser = { kind = "tuple" }
Collections
| Type | Meaning |
|---|---|
list<T> | 有序重复值。 |
set<T> | 唯一重复值。 |
array<T,N> | 固定长度重复值。 |
map<K,V> | 键值对。 |
optional<T> | 可空或可缺省值。 |
[[tables.fields]]
name = "tags"
type = "set<string>"
parser = { kind = "json" }
default = "[\"misc\"]"
[[tables.fields]]
name = "attributes"
type = "map<string,i32>"
parser = { kind = "map" }
单元格示例
这些例子展示策划在 Excel 或 CSV cell 里会填写什么:
| 字段类型 | Parser | Cell 值 |
|---|---|---|
u16 | 无 | 1001 |
enum<ItemType> | 无 | Weapon |
list<i32> | 无或 split | 1,2,3 |
duration | 无 | 1h 30m |
text | 无 | quest.1001.title |
set<string> | json | ["starter","melee"] |
struct<ResourceCost> | tuple | Gold,0,100 |
struct<ResourceCost> | columns | 展开到 cost_kind、cost_id、cost_count 多列 |
map<string,i32> | map | atk,10|hp,20 |
union<EventCondition> | json | {"type":"QuestCompleted","quest_id":5002} |
optional<ref<Item.id>> | 无 | 空 cell 或 1001 |
Field Rules
[[tables.fields]]、[[structs.fields]] 和 [[unions.variants.fields]] 共享通用字段属性。表字段额外拥有派生值相关属性;这些表专用属性不能写在 struct field 或 union variant field 上。表主键只在表本身用 key = "field_name" 声明一次。
字段是否可缺省由类型表达:optional<T> 表示值可以缺失或为空;其它类型都要求有值,除非 default 填充了缺失值。
对 TOML/JSON/YAML 这类 object 输入,字段可以不存在。对 Excel 和 CSV,表头列必须存在;某一行没有对应 cell、cell 为空,或者 CSV 行列数不够,都会按空 cell 处理。
| Schema 字段 | object 字段不存在 | Excel/CSV cell 为空 |
|---|---|---|
type = "i32" | 校验错误。 | 校验错误。 |
type = "optional<i32>" | null。 | null。 |
type = "i32" 加 default = "1" | 1。 | 1。 |
type = "optional<i32>" 加 default = "1" | 1。 | null。 |
| Property | 适用范围 | 作用 |
|---|---|---|
name | 所有字段 | 字段名。用于源数据、校验错误、生成代码和导出的运行时数据。 |
type | 所有字段 | 类型表达式,例如 i32、struct<ResourceCost> 或 list<union<RewardAction>>。 |
default | 除派生字段外的所有字段 | object 字段不存在,或 required Excel/CSV cell 为空时使用的字符串值。 |
comment | 所有字段 | 用于生成 Excel 表头说明。 |
range | 数值字段、duration,以及这些类型的集合元素 | 数值闭区间,写作 [min, max]。duration 的范围单位是毫秒。 |
length | string、list、set、array、map | 长度闭区间,写作 [min, max]。 |
parser | 单元格输入和 default | 单元格 parser 提示。见单元格 Parser。 |
groups | 所有字段 | 字段所属的已声明 group。省略时继承项目的默认 group。 |
from | 仅表字段 | 可选的子表来源,用来声明派生字段。 |
default 写成字符串,因为它会走和源数据相同的类型感知转换路径。
from 用来描述从另一张表匹配行得到的派生字段,详见引用和派生字段。派生字段可以是 list<T>、T 或 optional<T>,且不能声明 default。
枚举、结构体和联合
这些定义让 schema 可以表达超过扁平表格的数据结构。
Enums
[[enums]]
name = "Rarity"
values = [{ id = 0, name = "Common" }, { id = 1, name = "Uncommon" }, { id = 2, name = "Rare" }, { id = 3, name = "Epic" }, { id = 4, name = "Legendary" }]
枚举让源数据保持可读,同时让生成代码获得受约束的类型。
每个枚举值都必须声明一个显式、稳定的整数 id。同一个枚举内的 ID
不能重复,取值范围是 0 到 2147483647。调整枚举值顺序不会改变编码;
重命名枚举值时应保留原 ID。二进制格式、整数枚举表示、支持显式数值的
原生枚举以及生成的 Protobuf schema 都会使用这个 ID。
alias 可以保留导入数据或旧数据里的名称:
[[enums.aliases]]
name = "Epic"
alias = "Purple"
Structs
[[structs]]
name = "ResourceCost"
[[structs.fields]]
name = "kind"
type = "enum<ResourceKind>"
[[structs.fields]]
name = "id"
type = "i32"
[[structs.fields]]
name = "count"
type = "i32"
range = [1, 999999]
结构体适合多处复用的嵌套值。字段可以通过 type = "struct<ResourceCost>" 引用结构体。
Struct field 使用和 table field 相同的字段属性,包括 name、type、default、comment、range、length、parser 和 groups。key、from 这类表专用属性对普通 struct field 没有意义。完整字段参考见类型。
在 Excel、CSV 这类单元格输入中,struct 字段默认可以写成 JSON object 文本:
{"kind":"Gold","id":0,"count":100}
如果希望单元格更紧凑,可以在引用 struct 的字段上声明 parser = { kind = "tuple" }。Tuple 值按 struct 字段顺序书写:
Gold,0,100
Unions
当一个字段可能是几种不同形状之一时,使用 union。例如事件条件可能是“完成任务”,也可能是“拥有道具”:
{"type":"QuestCompleted","quest_id":5002}
{"type":"HasItem","item_id":1001,"count":2}
type 的值决定当前是哪一个 variant。剩余字段取决于这个 variant。
[[unions]]
name = "RewardAction"
tag = "type"
[[unions.variants]]
name = "AddItem"
[[unions.variants.fields]]
name = "item_id"
type = "ref<Item.id>"
[[unions.variants.fields]]
name = "count"
type = "i32"
[[unions.variants]]
name = "UnlockStage"
[[unions.variants.fields]]
name = "stage_id"
type = "ref<Stage.id>"
当一个字段可能是多个 tagged shape 之一时,使用 union。常见例子包括条件、奖励、触发器和脚本动作。
如果省略,union 的 tag 默认是 type。源数据必须包含这个 tag,值是 variant 名称。其余字段必须匹配被选中的 variant;未知字段和缺少非 optional 的 variant field 都会校验失败。
最直接的 Excel 或 CSV 写法是把单个 union 值写成 JSON object 文本:
| 字段类型 | 单元格值 |
|---|---|
union<RewardAction> | {"type":"AddItem","item_id":1001,"count":2} |
union 列表建议声明 parser = { kind = "json" },然后在单元格中写 JSON array:
[[tables.fields]]
name = "actions"
type = "list<union<RewardAction>>"
parser = { kind = "json" }
[
{"type":"AddItem","item_id":1001,"count":2},
{"type":"UnlockStage","stage_id":9002}
]
如果不想在 Excel/CSV 单元格里写 JSON,可以把一个 union<T> 字段展开成多列。下面的 action 字段是单个 union 值:
[[tables.fields]]
name = "action"
type = "union<RewardAction>"
parser = { kind = "tagged_columns" }
Excel 中会出现这些列:
| A | B | C | D | E | F |
|---|---|---|---|---|---|
id | name | action.type | action.item_id | action.count | action.stage_id |
1 | Give Sword | AddItem | 1001 | 2 | |
2 | Open Stage | UnlockStage | 9002 |
action.type 填 variant 名称。当前行如果是 AddItem,只填写 item_id 和 count;如果是 UnlockStage,只填写 stage_id。其它 variant 的列保持为空。
tagged_columns 只能用于类型正好是 union<T> 的字段,不能直接用于 list<union<T>>。如果一个父表字段需要多个 union 值,通常把每个 union 值拆成子表的一行,再由父表聚合回来:
[[tables.fields]]
name = "actions"
type = "list<union<RewardAction>>"
from = { table = "EventActionEntry", parent_key = "id", child_key = "event_id", field = "value", order_by = "seq" }
[[tables]]
id = "eventactionentry"
name = "EventActionEntry"
mode = "list"
[[tables.fields]]
name = "event_id"
type = "ref<EventRule.id>"
[[tables.fields]]
name = "seq"
type = "i32"
[[tables.fields]]
name = "value"
type = "union<RewardAction>"
parser = { kind = "tagged_columns", prefix = "" }
父表 EventRule 只保留普通字段:
| A | B |
|---|---|
id | name |
1 | First Event |
子表 EventActionEntry 每一行填写一个 action:
| A | B | C | D | E | F |
|---|---|---|---|---|---|
event_id | seq | type | item_id | count | stage_id |
1 | 1 | AddItem | 1001 | 2 | |
1 | 2 | UnlockStage | 9002 |
导出时,EventRule.actions 会得到两个 union 值,顺序由 seq 决定。这里的 prefix = "" 让子表列名直接叫 type、item_id、count、stage_id;如果这些列名会和同一张表的其它字段冲突,就不要使用空 prefix。
完整展开规则见单元格 Parser。
TOML 数据文件里可以直接用普通嵌套 table 写 union:
[[rows]]
id = 1
condition = { type = "QuestCompleted", quest_id = 5002 }
actions = [
{ type = "AddItem", item_id = 1001, count = 2 },
{ type = "UnlockStage", stage_id = 9002 },
]
引用和派生字段
引用让一张表指向另一张表的主键。派生字段则从另一张表匹配行,并把值复制或组装到当前表。
| 功能 | 源数据里存什么 | 运行时模型得到什么 |
|---|---|---|
ref<Item.id> | 目标行 id,例如 1001。 | id 值,或目标语言专用包装类型。 |
from = { ... } | 数据仍然放在子表行里。 | 父行得到复制或嵌套出来的字段。 |
当关系本身应该保留为 id 时,用 ref。当导出数据希望直接带有嵌套字段时,用 from。
ref 的目标表必须是 mode = "map",被引用字段必须是那张表的 key。
引用
[[tables.fields]]
name = "required_item"
type = "ref<Item.id>"
Sora 会校验每个值都指向被引用表中存在的行。
引用在源数据里仍然是普通值。生成的运行时代码可以根据目标语言,把它暴露为 key 值或目标语言专用的包装类型。
引用可以放在容器里,例如 list<ref<Item.id>>、set<ref<Item.id>> 或 optional<ref<Item.id>>。内部的 ref 仍然必须指向主键。
派生字段
派生字段不是从当前表的单元格读取,而是从另一张表中按 key 匹配行后生成。
这样可以让可编辑数据保持范式化,同时让生成的运行时模型暴露更方便的嵌套值。例如任务奖励可以拆成两张表:
Quest:
| id | name |
|---|---|
| 1001 | First Quest |
| 1002 | Second Quest |
QuestReward:
| quest_id | sort_order | item_id | count |
|---|---|---|---|
| 1001 | 1 | 2001 | 10 |
| 1001 | 2 | 2002 | 1 |
| 1002 | 1 | 2003 | 5 |
运行时如果希望 Quest 里直接有 rewards: list<Reward> 字段,可以声明这个字段来自 QuestReward:
[[structs]]
name = "Reward"
[[structs.fields]]
name = "item_id"
type = "ref<Item.id>"
[[structs.fields]]
name = "count"
type = "i32"
[[tables]]
id = "quest"
name = "Quest"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
[[tables.fields]]
name = "name"
type = "string"
[[tables.fields]]
name = "rewards"
type = "list<struct<Reward>>"
from = { table = "QuestReward", parent_key = "id", child_key = "quest_id", order_by = "sort_order" }
[[tables]]
id = "questreward"
name = "QuestReward"
mode = "list"
[[tables.fields]]
name = "quest_id"
type = "ref<Quest.id>"
[[tables.fields]]
name = "sort_order"
type = "i32"
[[tables.fields]]
name = "item_id"
type = "ref<Item.id>"
[[tables.fields]]
name = "count"
type = "i32"
含义是:
from.table = "QuestReward":从QuestReward子表读取匹配行。from.parent_key = "id":父行用自己的Quest.id值参与匹配。from.child_key = "quest_id":子行的QuestReward.quest_id等于父 key 时被选中。from.order_by = "sort_order":匹配到多行时,按子表里的sort_order字段升序排序。
用上面的示例数据,Quest.id = 1001 会得到两行奖励,顺序是 2001,然后 2002。
导出后的父行就像直接拥有了 rewards 字段:
{
"id": 1001,
"name": "First Quest",
"rewards": [
{"item_id": 2001, "count": 10},
{"item_id": 2002, "count": 1}
]
}
字段类型决定允许匹配多少行:
| 字段类型 | 匹配行数 | 没有匹配行时 |
|---|---|---|
list<T> | 0 到多行 | 空列表 |
optional<T> | 0 或 1 行 | null |
T | 必须正好 1 行 | 校验错误 |
如果 T 或 optional<T> 匹配到多行,Sora 会报错。
复制子表的单个字段
不写 from.field 时,Sora 会从子表的同名字段组装 struct。
如果父字段只需要接收子行中的某一个字段,设置 from.field:
[[unions]]
name = "EventCondition"
tag = "type"
[[unions.variants]]
name = "QuestCompleted"
[[unions.variants.fields]]
name = "quest_id"
type = "ref<Quest.id>"
[[unions.variants]]
name = "HasItem"
[[unions.variants.fields]]
name = "item_id"
type = "ref<Item.id>"
[[unions.variants.fields]]
name = "count"
type = "i32"
[[tables.fields]]
name = "condition"
type = "union<EventCondition>"
from = { table = "EventConditionEntry", parent_key = "id", child_key = "event_id", field = "value" }
[[tables]]
id = "eventconditionentry"
name = "EventConditionEntry"
mode = "list"
[[tables.fields]]
name = "event_id"
type = "ref<Event.id>"
[[tables.fields]]
name = "value"
type = "union<EventCondition>"
parser = { kind = "tagged_columns", prefix = "" }
含义是:Event.condition 接收 EventConditionEntry.value,前提是该子行的 event_id 等于 Event.id。子表里仍然可以有 id、event_id、备注、排序字段等辅助列;只有 from.field 指向的 value 会被复制到父表字段。
EventConditionEntry 在 Excel 中可以这样写:
| A | B | C | D | E |
|---|---|---|---|---|
event_id | type | quest_id | item_id | count |
1 | QuestCompleted | 5002 | ||
2 | HasItem | 1001 | 2 |
From 配置
from 对象有这些配置:
| 选项 | 必填 | 含义 |
|---|---|---|
table | 是 | 子表名。Sora 会从这张表扫描匹配行。 |
parent_key | 是 | 父表上的字段名。每个父行用这个字段值参与匹配。 |
child_key | 是 | 子表上的字段名。子行的这个字段值等于父 key 时,就会被选中。 |
field | 否 | 子表上的字段名。存在时,Sora 复制这个字段的值,而不是从整行组装 struct。 |
order_by | 否 | 子表上的字段名。存在时,匹配到的子行按这个字段升序排序。 |
order_by 是字段名,不是表达式。没有 desc、多字段排序、过滤条件或自定义排序语法。省略 order_by 时,匹配行保持源表读取顺序。
order_by 指向的字段必须存在于子表中。它通常会是 i32 这类排序字段,例如 sort_order、seq、rank。排序是升序。
不写 from.field 时,派生值类型必须是 struct,也就是 list<struct<...>>、struct<...> 或 optional<struct<...>>。结构体字段会从子表同名字段复制:
[[structs]]
name = "Reward"
[[structs.fields]]
name = "item_id"
type = "ref<Item.id>"
[[structs.fields]]
name = "count"
type = "i32"
这里 Reward.item_id 和 Reward.count 都必须在 QuestReward 上存在兼容字段。
写了 from.field 时,派生值类型必须和该子表字段兼容。例如 type = "union<EventCondition>" 可以从同样是 union<EventCondition> 的子表字段 value 派生。
派生字段不能同时声明 default。它的值来自匹配到的子行。
多个派生字段读取同一张子表
多张父表可以从同一张子表派生字段。这个过程不会消耗或移动子行,只是读取子表,并把匹配值复制到每个父字段。
例如 Quest 和 QuestPreview 都可以从 QuestReward 获取奖励:
[[tables]]
id = "quest"
name = "Quest"
mode = "map"
key = "id"
[[tables.fields]]
name = "rewards"
type = "list<struct<Reward>>"
from = { table = "QuestReward", parent_key = "id", child_key = "quest_id", order_by = "sort_order" }
[[tables]]
id = "questpreview"
name = "QuestPreview"
mode = "map"
key = "id"
[[tables.fields]]
name = "rewards"
type = "list<struct<Reward>>"
from = { table = "QuestReward", parent_key = "id", child_key = "quest_id", order_by = "sort_order" }
如果 Quest.id = 1001 和 QuestPreview.id = 1001 都存在,两张父表都会收到来自 QuestReward.quest_id = 1001 的奖励列表。Sora 不会把子行标记为已被 Quest 使用,也不会从 QuestReward 删除这行。
单元格 Parser
Parser 只用于 Excel、CSV 这类单元格输入。大多数 parser 告诉 Sora 如何把一个 cell 转成类型化值;columns、tagged_columns 这类投影 parser 告诉 Sora 一个字段如何映射到多列输入。字符串形式的 default 会走单 cell parser 的同一套路径。TOML 行数据通常可以直接使用 TOML array/table,不需要单元格 parser。
当默认 cell 写法太冗长或容易歧义时,再声明 parser:
[[tables.fields]]
name = "tags"
type = "list<string>"
parser = { kind = "split", separator = "|" }
对应 cell:
starter|melee|weapon
Parser option 都是字符串。未知 parser、当前 parser 不支持的 option、空 option value 都会在 schema normalization 阶段报错。例外是 columns.prefix、tagged_columns.prefix 这类投影前缀,"" 有明确含义。
自定义 Lua Parser
项目可以在 project.toml 里加载项目内的 Lua parser 脚本:
[parsers]
scripts = ["tools/parsers.lua"]
脚本路径按 project 文件所在目录解析。之后所有读取该 project 的命令都能使用这些自定义 parser,不需要反复写命令行参数:
sora build --project project.toml
sora export --project project.toml --data-root data --format json --out generated/config.json
CLI 命令也可以通过全局 --parser-script 临时追加 parser 脚本:
sora --parser-script tools/parsers.lua build --project project.toml
sora --parser-script tools/parsers.lua export --project project.toml --data-root data --format json --out generated/config.json
这个参数可以重复传,并追加在 project 配置的脚本之后。自定义 parser 属于项目可信代码。Sora 会用受限 Lua 标准库加载脚本,不暴露 io、os、package 或 debug。
Parser 脚本返回一个带 parsers 的 table。每个 parser 必须定义 parse(cell, ctx)。options 是支持的 parser option 列表。validate(field) 可选,会在 schema normalization 阶段执行。
return {
parsers = {
slug = {
options = { "prefix" },
validate = function(field)
if field.type ~= "string" then
error("slug parser requires string")
end
end,
parse = function(cell, ctx)
local text = string.lower(string.gsub(cell.text, "%s+", "-"))
if ctx.options.prefix ~= nil then
return ctx.options.prefix .. text
end
return text
end,
},
},
}
Schema 字段按名字使用自定义 parser:
[[tables.fields]]
name = "tag"
type = "string"
parser = { kind = "slug", prefix = "item-" }
cell 包含 kind、text,以及适用时的 value。ctx 包含 field、type、options、path,以及 row、column、worksheet 的 sheet 等位置信息。Lua 返回值会映射成 Sora 数据值:nil、bool、integer、float、string、array-like table 和 string-keyed table。
自定义 Lua parser 是单 cell parser。它不会替代 columns、tagged_columns 这类投影 parser,不能读取相邻 cell,也不会改变 schema、source loading 或生成 runtime 的行为。
默认解析
字段没有声明 parser 时,Sora 按字段类型做默认解析:
| 类型 | Cell 写法 |
|---|---|
bool | 布尔 cell、true、false,或数字 cell:0 为 false,非 0 为 true。 |
i32、i64、ref<Table.key> | 整数 cell、整数字符串,或无小数部分的 float cell。 |
duration | 带 d、h、m、s 或 ms 单位的时长文本,例如 500ms、30s 或 1h 30m。单位必须按从大到小排列。 |
f32、f64 | 数字 cell 或数字字符串。 |
string、enum<Name> | cell 展示文本。 |
struct<Name>、union<Name> | JSON object 文本。 |
list<T>、set<T>、array<T,N> | 逗号分隔文本。JSON array 请使用 json parser。 |
map<K,V> | JSON pair array,例如 [["atk",10],["hp",20]]。 |
optional<T> | 空 cell 解析成 null;非空时按内部 T 解析。 |
默认集合解析刻意保持简单。Primitive item 会按类型解析。struct 和 union 集合 item 必须写成 JSON object 文本。嵌套集合不能靠单个分隔符可靠表达,应该使用 parser = { kind = "json" }。
Parser 速查
| Parser | 适用类型 | Cell 形状 |
|---|---|---|
split | list<T>、set<T>、array<T,N>,或包在这些类型外的 optional | a,b,c |
tuple | struct<T> 或 optional<struct<T>> | Gold,0,100 |
columns | struct<T> 或 optional<struct<T>> | 多列 |
tuple_list | list<struct<T>>、set<struct<T>>、array<struct<T>,N>,或包在这些类型外的 optional | Gold,0,100|Gem,0,5 |
map | map<K,V> 或 optional<map<K,V>> | atk,10|hp,20 |
tagged_columns | 只能用于 union<T> | 多列 |
json | 任意类型 | 匹配字段类型的 JSON value |
array<T,N> 会检查 item 数量。tuple 会检查 value 数量是否等于被引用 struct 的字段数。
split
split 适合扁平集合,例如 primitive、enum、ref,或其它可以稳定用分隔符切开的简单值。
[[tables.fields]]
name = "starter_items"
type = "list<ref<Item.id>>"
parser = { kind = "split" }
Cell:
1001,1002,1003
解析结果:
[1001,1002,1003]
当逗号不适合作为分隔符时,声明 separator:
[[tables.fields]]
name = "tags"
type = "set<string>"
parser = { kind = "split", separator = "|" }
Cell:
starter|melee|weapon
tuple
tuple 适合把一个很小的 struct 写在一个 cell 里。值的顺序等于该 struct 的字段声明顺序。
[[structs]]
name = "ResourceCost"
[[structs.fields]]
name = "kind"
type = "enum<ResourceKind>"
[[structs.fields]]
name = "id"
type = "i32"
[[structs.fields]]
name = "count"
type = "i32"
[[tables.fields]]
name = "price"
type = "struct<ResourceCost>"
parser = { kind = "tuple" }
Cell:
Gold,0,100
解析结果:
{"kind":"Gold","id":0,"count":100}
如果 struct 字段值里经常出现逗号,可以换分隔符:
parser = { kind = "tuple", separator = "|" }
Cell:
Gold|0|100
columns
columns 适合把一个 struct 展开成普通 Excel/CSV 列来编辑,而不是写 JSON 或一个紧凑 tuple cell。它只能用于 table field 上的 struct<T> 或 optional<struct<T>>。
[[structs]]
name = "ResourceCost"
[[structs.fields]]
name = "kind"
type = "enum<ResourceKind>"
[[structs.fields]]
name = "id"
type = "i32"
[[structs.fields]]
name = "count"
type = "i32"
[[tables.fields]]
name = "price"
type = "struct<ResourceCost>"
parser = { kind = "columns", prefix = "price_" }
CSV header 和行:
id,name,price_kind,price_id,price_count
1,Iron Sword,Gold,0,100
解析出的 price:
{"kind":"Gold","id":0,"count":100}
默认 prefix 会使用字段名,例如字段 price 会投影出 price.kind、price.id、price.count。只有当 struct 字段名应该直接位于当前表顶层时,才使用 prefix = ""。Sora 会拒绝投影列名冲突。
columns 不会递归展开嵌套 struct 或 union。如果被展开出来的 struct 字段本身仍然是复杂类型,可以给这个子字段声明 tuple、split、map、json 这类单 cell parser;如果嵌套数据很大或需要重复出现,应该拆成独立表,再用 ref 或派生字段连接。这样可以避免表变得很宽,也让复杂记录更容易复用。
生成 XLSX 模板时,同一个 columns 字段投影出来的列会使用同一组表头颜色。
tuple_list
tuple_list 适合一组小 struct。separator 用来切一个 struct 内部的字段,item_separator 用来切 list item。
[[tables.fields]]
name = "materials"
type = "list<struct<ResourceCost>>"
parser = { kind = "tuple_list" }
Cell:
Item,2003,4|Gold,0,1000
解析结果:
[
{"kind":"Item","id":2003,"count":4},
{"kind":"Gold","id":0,"count":1000}
]
自定义分隔符:
parser = { kind = "tuple_list", separator = ":", item_separator = ";" }
Cell:
Item:2003:4;Gold:0:1000
map
map 适合简单 key/value pair。separator 用来切 key 和 value,item_separator 用来切 map entry。
[[tables.fields]]
name = "attributes"
type = "map<string,i32>"
parser = { kind = "map" }
Cell:
atk,10|hp,20
解析结果:
[["atk",10],["hp",20]]
Sora 导出 map 时使用 pair array,这样非 string key 也不会有歧义。如果你更想在 cell 里写 JSON,也可以使用 parser = { kind = "json" },然后写同样的 pair-array 形状:
[["atk",10],["hp",20]]
tagged_columns
tagged_columns 用来把一个 union<T> 值展开到多列 Excel/CSV 中编辑。它只能用于类型正好是 union<T> 的 table field。它不能用于 optional<union<T>>、list<union<T>>、set<union<T>> 或其它容器。
[[unions]]
name = "EventCondition"
tag = "type"
[[unions.variants]]
name = "QuestCompleted"
[[unions.variants.fields]]
name = "quest_id"
type = "ref<Quest.id>"
[[unions.variants]]
name = "HasItem"
[[unions.variants.fields]]
name = "item_id"
type = "ref<Item.id>"
[[unions.variants.fields]]
name = "count"
type = "i32"
[[tables.fields]]
name = "value"
type = "union<EventCondition>"
parser = { kind = "tagged_columns", prefix = "" }
CSV header 和行:
id,type,quest_id,item_id,count
1,QuestCompleted,5002,,
2,HasItem,,1001,2
tag 列填写 union variant 名称。只有被选中的 variant 的字段列可以填写值。默认 prefix 会使用字段名,例如字段 condition 会投影出 condition.type、condition.quest_id、condition.item_id。只有当这些列应该直接位于当前表顶层时,才使用 prefix = ""。
Sora 会拒绝投影列名冲突。例如表里已经有普通字段 type,同时又对 tag 也是 type 的 union 使用 prefix = ""。
tagged_columns 也不会递归展开 variant 字段里的嵌套 struct 或嵌套 union。Variant 字段仍然可以使用 tuple、split、map、json 这类单 cell parser。如果某个 variant 需要很大的嵌套对象或重复嵌套对象,应该把那部分数据建成独立表,再通过引用或派生字段组合,而不是继续把 union 行横向展开。
生成 XLSX 模板时,同一个 tagged_columns 字段投影出来的列会使用同一组表头颜色,tag 列会在同色组里更醒目。
json
json 适合嵌套值、容器里的 union、嵌套集合,以及任何需要明确转义的复杂形状。
[[tables.fields]]
name = "actions"
type = "list<union<RewardAction>>"
parser = { kind = "json" }
Cell:
[
{"type":"AddItem","item_id":1007,"count":3},
{"type":"UnlockStage","stage_id":9002}
]
单个 union 值:
[[tables.fields]]
name = "condition"
type = "union<EventCondition>"
parser = { kind = "json" }
Cell:
{"type":"QuestCompleted","quest_id":5002}
map<K,V> 的 JSON 写法是 pair array,不是 JSON object:
[["atk",10],["hp",20]]
怎么选
| 需求 | 推荐 |
|---|---|
| 扁平 primitive 列表 | split |
| 一个紧凑 struct | tuple |
| 一个 struct 展开成多列 | columns |
| 一组紧凑 struct | tuple_list |
| 简单 key/value pair | map |
| 一个 union 展开成多列 | tagged_columns |
| 嵌套值、容器里的 union、需要转义、或想直接写 JSON | json |
项目配置
项目清单既可以只是 schema root,也可以是完整的构建描述。它可以写成 TOML、YAML、JSON 或 Lua;本页示例使用 TOML。
includes = ["schema/items.toml"]
[project]
id = "game_config"
views = ["views/client.toml", "views/server.toml"]
[groups.common]
default = true
[groups.server]
[parsers]
scripts = ["tools/parsers.lua"]
[type_mappings]
scripts = ["tools/type_mappings.lua"]
[build]
default_source_format = "xlsx"
data_root = "data"
view = "client"
schema_lock = "generated/schema.lock"
excel_templates = "generated/excel"
[[build.codegen]]
target = "rust"
out = "rust/src/generated"
format = "auto"
[[build.exports]]
format = "binary"
out = "generated/config.sora"
project.id 是 canonical schema 的稳定身份,不会再被拿来推导任何语言的
package 或 namespace。每张表也必须声明稳定 id;重命名表不会改变这个身份。
Group 在项目中集中声明。Schema 实体和字段通过 groups = [...] 归组;省略时
继承所有 default = true 的 group。
View 是具名的外部契约。它选择 group 和表,可以按稳定表 ID 修改导出表名,并 持有各目标语言自己的 binding:
# views/client.toml
name = "client"
contract = "game/client-v1"
groups = ["common"]
[tables]
include = ["item", "settings"]
[names.tables]
item = "ClientItem"
[bindings.kotlin]
package = "com.example.game.config"
[bindings.csharp]
namespace = "Example.Game.Config"
[bindings.go]
package = "gameconfig"
[bindings.c]
prefix = "game_config"
[bindings.cpp]
namespace = "game::config"
[bindings.proto-schema]
package = "game.config"
表选择和别名始终使用稳定表 ID,而不是显示名称。不同 manifest 可以引用不同 view 文件,让每个消费方拥有独立版本的契约,同时共享同一份 canonical schema。
运行所有配置好的输出:
sora build --project project.toml
data_root 和 excel_templates 的用途不同。data_root 是 export 和 build 读取的输入目录,里面放已经填写过行数据的文件。excel_templates 是生成 workbook 模板的输出目录,schema 变更后可以删除并重新生成。不要把 excel_templates 指向已经编辑过的数据目录,除非你明确想替换那些 workbook。
[parsers].scripts 列出 CLI 读取该 project 时使用的自定义 Lua 单元格 parser 脚本。路径相对 project 文件所在目录。脚本 API 见单元格 Parser。
[type_mappings].scripts 列出用于自定义生成语言类型的 Lua 脚本。路径相对 project 文件所在目录。类型映射只影响 codegen:schema 仍然使用 struct<Vec3> 这类语言无关的 Sora 类型,映射脚本可以把这个命名类型映射到目标语言自己的类型。
多语言通过 project root 的 [localization] 声明。它的 sources 独立于普通 [[tables]];见多语言。
只运行一个配置好的 codegen target:
sora build --project project.toml --target rust
Target Options
语言相关的运行时选项放在 [codegen.<target>] 下。Package、namespace、
module 和符号前缀归各个 view 的 binding 所有:
[codegen.rust]
runtime_format = "sora"
[codegen.typescript]
runtime_format = "json"
enum_repr = "string"
[codegen.lua]
runtime_format = "cbor"
lua_version = "5.4"
这些选项由对应生成器消费。归一化 IR 保持语言无关。
类型映射脚本返回带 type_mappings 的 table。每条映射对应一个目标语言和一个命名 schema 类型:
return {
type_mappings = {
{
target = "csharp",
schema_type = "Vec3",
type_name = "Vector3",
nullable_type_name = "Vector3?",
decode = "GameMappings.ToVector3({value})",
value_decode = "GameMappings.ToVector3({value})",
imports = { "UnityEngine" },
},
},
}
nullable_type_name 是可选字段。当 optional<schema_type> 需要不同于后端默认 nullable wrapper 的目标语言类型表达式时使用它。
decode 包裹默认的 binary runtime decode 表达式,value_decode 包裹 JSON/CBOR/protobuf 风格的 value decode 表达式。{value} 会替换成生成器默认生成的表达式。
C target 使用写入目标指针的 decode 函数,所以 C 映射应使用 decode_into,而不是 decode。{target} 会替换成输出指针表达式。C 映射也可以提供 free,其中 {target} 会替换成需要释放的指针:
{
target = "c",
schema_type = "Vec3",
type_name = "game_vector3",
decode_into = "game_vector3_decode(reader, {target})",
free = "game_vector3_free({target});",
imports = { "#include \"vector3.h\"" },
}
imports 是目标语言相关的,只由需要它的语言生成器输出。C#、Java、Kotlin、Scala 期望不带关键字的 namespace/path;Go 期望类似 "example.com/game/vector" 的 import spec;Python、TypeScript、JavaScript、Dart、Godot、C、C++、Rust 期望完整 import/include/use/preload 行。
runtime_format 可以是 sora、json、cbor 或 sora-protobuf,但不是每个 target 都支持所有 runtime format。支持矩阵见运行时格式。
内置 Target Options
| Target | Options |
|---|---|
rust | runtime_format 默认 sora;map_type = "std" 或 "fx_hash_map",默认 std;string_storage = "owned" 或 "arc",默认 owned。 |
kotlin | runtime_format 默认 sora;view binding 必须提供 package。 |
csharp | runtime_format 默认 sora;view binding 必须提供 namespace。 |
java | runtime_format 默认 sora;view binding 必须提供 package;nullable_annotation 默认 SoraNullable,也可以设置成 org.jetbrains.annotations.Nullable 这类 annotation class,或设置为 "" 禁用 annotation。 |
scala | runtime_format 默认 sora;view binding 必须提供 package;scala_version = "2.12"、"2.13" 或 "3",默认 3。 |
go | runtime_format 默认 sora;view binding 必须提供 package。 |
dart | runtime_format = "json"、"cbor" 或 "sora-protobuf"。建议显式设置;Dart 不支持 sora。 |
godot | runtime_format = "json"。建议显式设置;这是 Godot 唯一支持的 runtime format。 |
c | runtime_format = "sora";c_standard = "c99"、"c11"、"c17" 或 "c23",默认 c11;view binding 必须提供 prefix。 |
cpp | runtime_format = "sora";cpp_standard = "c++11"、"c++14"、"c++17"、"c++20" 或 "c++23",默认 c++17;view binding 必须提供 namespace。 |
typescript | runtime_format 默认 sora;enum_repr = "string" 或 "integer",默认 string。 |
javascript | runtime_format 默认 sora;enum_repr = "string" 或 "integer",默认 string;emit_dts 是 boolean,默认 true。 |
erlang | runtime_format 默认 sora;enum_repr = "atom" 或 "integer",默认 atom。 |
lua | runtime_format 默认 sora;module 是可选 require/import 前缀;lua_version = "5.1"、"5.2"、"5.3"、"5.4" 或 "luajit",默认 5.4;enum_repr = "string" 或 "integer",默认 string。 |
python | runtime_format 默认 sora。 |
proto-schema | View binding 必须提供 package。它生成 .proto schema 文件,不生成 runtime loader。 |
包含多种语言选项的示例:
[codegen.rust]
runtime_format = "sora"
map_type = "fx_hash_map"
string_storage = "arc"
[codegen.cpp]
runtime_format = "sora"
cpp_standard = "c++20"
[codegen.javascript]
runtime_format = "json"
enum_repr = "integer"
emit_dts = true
多语言
Sora 将翻译文本作为独立的 locale catalog 处理,而不是普通业务表。
业务配置只保存 text 类型的文案 key。localization source 表提供这些 key 的各语言翻译。运行时先加载普通配置包,再单独挂载一个或多个语言包。
业务表 -> config bundle
localization sources -> LocaleCatalog -> i18n locale packs
Text Key
需要本地化的字段使用 text:
[[tables.fields]]
name = "title_key"
type = "text"
[[tables.fields]]
name = "body_keys"
type = "list<text>"
text 保存的是 key,不是实际翻译文本。源数据里应填写 quest.1001.title、ui.confirm 这类值。目标语言支持独立生成运行时类型时,生成代码会暴露为 TextKey。
catalog 校验会扫描业务数据里的所有 text 值。key 不存在或翻译为空都会直接构建失败。
Catalog Sources
在 project schema root 声明 localization:
[localization]
locales = ["zh_cn", "en_us"]
default_locale = "zh_cn"
fallback_locale = "en_us"
[[localization.sources]]
name = "ui"
file = "Core.xlsx"
sheet = "UILocalization"
[[localization.sources]]
name = "quest"
file = "Quest.xlsx"
sheet = "QuestLocalization"
每个 source 是宽表。默认 key 列名是 key:
| key | zh_cn | en_us | note |
|---|---|---|---|
ui.confirm | 确认 | Confirm | button label |
quest.1001.title | 第一章 | Chapter One | quest title |
locales 里声明的语言列会进入语言包。其它列,例如 note,只作为编辑和诊断元数据,不进入运行时包。
规则:
| 规则 | 行为 |
|---|---|
source.name | 必须是 ASCII 标识符风格。它用于诊断和组织,不作为 key 前缀。 |
key 值 | 可以使用 quest.1001.title 这类 dotted key。 |
| 多个 source | 合并成一个逻辑 catalog。 |
| 重复 key | 构建失败。key 在所有 source 里全局唯一。 |
| 缺少 locale 列 | 构建失败。 |
| 翻译为空 | 构建失败。 |
如果 key 列不叫 key,可以在 source 上指定:
[[localization.sources]]
name = "ui"
file = "Core.xlsx"
sheet = "UILocalization"
key = "id"
导出语言包
普通导出格式(binary、json、cbor、sora-protobuf、proto)只包含业务数据和 text key,不包含实际翻译文本。
在 build manifest 里添加 i18n 导出:
[[build.exports]]
format = "binary"
out = "generated/config.sora"
[[build.exports]]
format = "i18n-binary"
out = "generated/i18n/zh_cn.sora-i18n"
locale = "zh_cn"
[[build.exports]]
format = "i18n-json"
out = "generated/i18n/en_us.json"
locale = "en_us"
i18n-binary 面向生产语言包。i18n-json 面向检查、外包翻译交付和测试。
运行时挂载
生成运行时会分开加载配置包和语言包。Rust 示例:
#![allow(unused)]
fn main() {
let config = SoraConfig::from_bytes(config_bytes)?;
let pack = generated::runtime::LocalePack::from_bytes(locale_bytes)?;
let mut i18n = generated::SoraI18n::new();
i18n.mount(&config, pack)?;
i18n.set_locale("zh_cn")?;
let mail = config.mail_template().get(&1001).unwrap();
let title = i18n.text(&mail.title_key);
let body = i18n.format(&mail.body_key, [("count", 100)])?;
}
挂载时会校验:
| 校验 | 作用 |
|---|---|
schema_fingerprint | 防止加载另一个 schema 生成的语言包。 |
| locale 声明 | 拒绝 [localization].locales 未声明的语言包。 |
| text keys | 拒绝缺少当前配置使用的 key 或包含空文本的语言包。 |
| 已挂载语言 | set_locale 只能切到已经 mount 的语言。 |
业务代码不感知 key 来自哪张 source 表,只用已挂载的 i18n runtime 查询 TextKey。
Sora Studio
Sora Studio 是内置在 sora CLI 里的浏览器 schema 编辑器。它用于查看和编辑项目 schema,不需要用户单独启动前端开发服务器。
用项目文件启动:
sora studio --project project.toml
默认监听 127.0.0.1:5174,并在终端打印本地地址。如果需要换地址,可以使用 --host 或 --port:
sora studio --project project.toml --port 5180
可以编辑什么
Studio 会加载项目文件,以及 includes 中列出的每个 schema module。项目文件和 schema module 可以使用 TOML、YAML、JSON 或 Lua,同一个项目里也可以混用这些格式。
编辑器可以修改:
- 项目 package 名称和 schema include 列表;
- schema module 文件,包括新增和删除 include 文件;
- 表、结构体、枚举和联合;
- 表字段、结构体字段、枚举值和联合分支;
- 表模式、主键、数据源设置、parser 设置、默认值、备注、范围和长度约束;
- 引用字段和从子表派生出来的字段。
Studio 是 schema 编辑器,不是行数据编辑器。Excel、CSV、TOML、JSON 和 YAML 表数据仍然在各自的源文件中编辑,并通过 sora check、sora export 或 sora build 校验。
可视化能力
主画布会展示 schema 节点和它们之间的关系:
- 字段使用枚举、结构体或联合时产生 type edge;
ref<Table>字段产生 reference edge;- 从其他表组装出的子表字段产生 derived edge。
侧边栏可以按名称过滤 schema,展示项目统计,并按类型组织节点。诊断信息会显示在 UI 中,所以某个 schema 出错时,可以在 Studio 中定位错误,而不是让整个编辑器不可用。
预览和保存
保存前可以先预览 Studio 将要写入的文件。Studio 会按每个项目文件或 schema 文件自己的格式输出:
.toml文件写成 TOML;.yaml和.yml文件写成 YAML;.json文件写成格式化 JSON;.lua文件写成返回数据表的 Lua。
保存会用 Studio 的 renderer 归一化被修改的文件。这是有意的:Studio 保持 schema 数据模型稳定,但不会保留注释、精确空白或编辑文件中的手写排序。提交前应该先看预览。
交付方式
发布版会把 Studio 前端资源嵌入 sora 二进制。最终用户只需要从 GitHub Releases 或 crates.io 安装 CLI,不需要 Node.js,也不需要本地 Vite server。
发布维护者在构建 CLI 前需要先构建前端:
cd apps/studio
npm run build
cd ../..
cargo build -p sora-cli --release
如果嵌入资源缺失,sora studio 会提示需要先构建 apps/studio 再构建 CLI。
模型上下文协议(MCP)
Sora 内置面向 AI 客户端的 MCP 2025-11-25 服务。MCP、CLI 与 Studio 共用同一套
project、schema、data、Excel 和 build 应用服务;generated code 与 runtime bundle
始终只是输出,不能作为修改入口。
安装与连接
从仓库构建或安装 sora:
cargo install --path crates/sora-cli
本地客户端优先使用 stdio,并使用绝对路径:
{
"mcpServers": {
"sora": {
"command": "/absolute/path/to/sora",
"args": [
"mcp",
"--project",
"/absolute/path/to/project.toml"
]
}
}
}
stdout 只包含 JSON-RPC,审计日志写入 stderr。项目声明的 Lua parser 或 type-mapping
脚本默认不受信任。只有客户端完成 form Elicitation,或本地启动命令显式加入
--trust-project-scripts,Sora 才会执行它们。
支持 MCP Roots 的客户端可以省略 --project,随后调用 sora_project_list 和
sora_project_open。不支持 form Elicitation 的客户端必须显式传入 root_id 与
relative_manifest。
Streamable HTTP
本地监听:
sora mcp --transport http --host 127.0.0.1 --port 8000
客户端连接 http://127.0.0.1:8000/mcp。若浏览器客户端来自其他 Origin,必须逐项加入:
sora mcp --transport http \
--allowed-origin http://localhost:6274
非 loopback 监听必须提供 HTTPS public URL 与 OAuth resource server 配置:
sora mcp --transport http \
--host 0.0.0.0 \
--port 8443 \
--public-url https://sora.example.com/mcp \
--allowed-origin https://client.example.com \
--oauth-issuer https://id.example.com \
--oauth-audience https://sora.example.com/mcp \
--oauth-scope sora:mcp
Sora 默认通过 RFC 8414 发现 JWKS;只有部署需要固定端点时才使用
--oauth-jwks-uri。Access token 必须是非对称签名 JWT,并通过签名、iss、aud、
sub、exp、可选 nbf 及全部 scope 校验。RFC 9728 protected resource metadata
位于 /.well-known/oauth-protected-resource。
HTTP 层限制 Host、Origin、body 大小、速率、并发、授权主体数量、session 数量、idle 时间和续传 event 保留时间。session 与不透明 event cursor 都绑定授权主体。Sora 不保留 旧式独立 HTTP+SSE transport。
推荐工作流
- 列出并打开项目。
- 读取 summary、revision、diagnostics 与 normalized schema resources。
- 校验 schema 与 data。
- 所有 schema、row、project、Excel 修改先调用对应 preview。
- 审核 diagnostics、changes、affected files 与输入 revision。
- 使用返回的
plan_id和新的稳定idempotency_key调用 apply。 - 运行 manifest 声明的 build,并读取返回的 immutable artifact resource。
Plan 有 TTL,绑定授权主体与 revision。输入 revision 变化后 plan 自动失效。commit 前 取消不会留下持久化修改;进入原子 commit 后要么完成,要么回滚。
Tools 参考
所有 tool 都拒绝未知字段并返回严格结构化结果。业务失败会设置 MCP isError: true,
同时保留统一 diagnostics。每个 tool 都声明 read-only、destructive、idempotent 和
open-world hints。
| Tool | 主要输入 | 行为 |
|---|---|---|
sora_server_info | {} | 协议与 workspace 摘要。 |
sora_project_list | {} | 在允许的 Roots 内发现项目。 |
sora_project_open | root_id?, relative_manifest? | 打开项目;缺失选择时可 Elicit。 |
sora_project_inspect | project_id | 查看 schema identity、group、view、source、target 与 format。 |
sora_project_init | root_id, relative_directory, schema_id | 预览新项目 scaffold。 |
sora_project_init_apply | plan_id, idempotency_key | 原子创建 scaffold。 |
sora_schema_validate | project_id | normalize 并校验 schema。 |
sora_schema_search | project_id 加 kind/name/field/type/reference 过滤 | 搜索规范化实体。 |
sora_schema_preview | project_id、期望 schema/manifest revision、operations | 生成 schema plan 与 text diff。 |
sora_schema_apply | project_id, plan_id, idempotency_key | 原子应用 schema plan。 |
sora_data_validate | project_id、可选 view 与 tables | 校验项目数据。 |
sora_table_query | project_id, table 及可选 filter/key/index/select/order/cursor/limit/locale | 查询类型化数据;limit 为 1–500。 |
sora_data_diff | project_id, other_data_root | 比较项目内 baseline。 |
sora_data_preview | project_id、期望 schema/data revision、operations | 预览 row 与 localization 变更。 |
sora_data_apply | project_id, plan_id, idempotency_key | 原子应用 data plan。 |
sora_excel_sync_preview | project_id、期望 schema/data revision | 预览 workbook 同步。 |
sora_excel_sync_apply | project_id, plan_id, idempotency_key | 原子同步 XLSX。 |
sora_build | project_id、project revision、可选 view/output/target/format/clean | 执行声明的 build graph。 |
sora_codegen | project_id, revision, target、可选 view/clean | 执行一个 generator。 |
sora_export | project_id, revision, format、可选 view/clean | 执行一个 exporter。 |
sora_schema_lock | project_id, revision、可选 view/clean | 构建 schema lock。 |
sora_excel_template | project_id, revision、可选 view/clean | 构建 Excel template。 |
build、codegen、export、schema-lock、Excel-template 与 Excel sync 支持可选 MCP Tasks。 客户端不支持 Tasks 时仍可通过普通 tool call、progress 与 cancellation 完成。
Resources 参考
固定资源:
sora://server/infosora://workspace/projectssora://project/{project_id}/summarysora://project/{project_id}/manifestsora://project/{project_id}/capabilitiessora://project/{project_id}/schemasora://project/{project_id}/diagnosticssora://project/{project_id}/revision
模板资源:
sora://project/{project_id}/schema/{kind}/{name}sora://project/{project_id}/table/{table}/schemasora://project/{project_id}/table/{table}/rows?cursor=&limit=&select=sora://project/{project_id}/artifact/{artifact_id}sora://project/{project_id}/task/{task_id}sora://docs/overview与sora://docs/safety
row cursor 绑定 revision;artifact 与 task 有 TTL,并绑定授权主体和 project。
Prompts 与 Completion
标准 Prompts:
sora_create_tablesora_add_field_with_migrationsora_rename_entity_safelysora_fix_validation_errorssora_add_codegen_targetsora_prepare_config_releasesora_review_schema
每个 Prompt 都要求 project_id,嵌入有界 project summary,并链接 schema 与 diagnostics。
Completion 覆盖 project、entity kind/name、table、field、enum、group、view、source、codegen
target、runtime format、export format、locale、artifact 和 mode。
安全与 trust
- Roots 是显式 capability;所有路径先 canonicalize,再检查 traversal 与外部 symlink。
- cell、comment、schema、prompt 和文档内容都是不可信数据,永远不会作为命令执行。
- Lua trust 精确绑定 script path 与 SHA-256 digest,并按授权主体隔离。
- formatter 只能来自内置 allowlist,使用参数数组,不经过 shell。
clean只能删除 manifest 声明且位于安全 output root 内的 generated outputs。- mutation plan 绑定 authorization、project、revision、TTL 与 idempotency。
- audit event 记录 tool、project、授权指纹、revision、结果、耗时和有界 change count; 不记录 bearer token 与请求 body。
MCP Inspector 操作
MCP Inspector 需要当前 Node.js。
cargo build -p sora-cli
npx @modelcontextprotocol/inspector \
target/debug/sora mcp \
--project "$(pwd)/examples/showcase/project.toml"
在 UI 中依次:
- 确认协议为
2025-11-25,查看 Tools、Resources 与 Prompts。 - 调用
sora_project_inspect。 - 读取项目的
revision、schema和diagnostics。 - 对
Item表调用sora_table_query,设置limit: 2。 - 调用 preview tool,确认文件没有变化。
- 只在临时项目应用 plan,并用同一 idempotency key 重试。
- 启动 build,观察 progress、可选 Tasks 与 artifact links。
CLI smoke test:
npx @modelcontextprotocol/inspector --cli \
target/debug/sora mcp --project "$(pwd)/examples/showcase/project.toml" \
--method tools/list
本地 Streamable HTTP 要允许 Inspector Origin:
sora mcp --transport http \
--allowed-origin http://localhost:6274
npx @modelcontextprotocol/inspector
在 Inspector 选择 Streamable HTTP,连接 http://127.0.0.1:8000/mcp。
Showcase 闭环验证
cargo test -p sora-mcp --test read_only_workflow
cargo test -p sora-mcp --test build_workflow
第一个测试打开 showcase、检查项目、查询 Excel-backed validated data,并确认 revision 不变化;第二个测试执行事务 build、接收 progress,并通过 MCP 读取 immutable artifact。
CLI 参考
已安装二进制的精确帮助文本以 sora --help 为准;单个命令的参数可以用 sora <command> --help 查看。本页集中整理常用工作流命令、alias 和短参数。
全局参数
全局参数可以放在子命令前,也可以放在子命令后。
| 参数 | 说明 |
|---|---|
-j, --jobs <N> | 最大工作线程数。必须大于 0。 |
--serial | 禁用并行执行。 |
--parser-script <PATH> | 加载自定义 Lua 单元格 parser 脚本。可以重复传。项目级 parser 脚本也可以配置在 project.toml 的 [parsers].scripts 中。 |
--type-mapping-script <PATH> | 加载自定义 Lua 类型映射脚本。可以重复传。项目级脚本也可以配置在 project.toml 的 [type_mappings].scripts 中。 |
-h, --help | 打印帮助。 |
-V, --version | 打印 CLI 版本。 |
命令 Alias
| 命令 | Alias |
|---|---|
build | b |
check | c |
init | i |
gen | g |
export | e |
diff | d |
excel-template | template, et |
excel-sync | sync, es |
schema-lock | lock, sl |
studio | st |
常用短参数
| 短参数 | 长参数 | 使用命令 |
|---|---|---|
-p | --project | 读取 project 的命令。 |
-o | --out | init、gen、export、diff、excel-template、schema-lock。 |
-v | --view | build、gen、export、diff、excel-template、excel-sync、schema-lock。 |
-t | --target | build、gen。 |
-f | --format | export。 |
-d | --data-root | build、export、excel-sync。 |
-l | --lock、--left-root | check、diff。 |
-r | --right-root | diff。 |
-c | --clean | build。 |
-w | --write | excel-sync。 |
命令
init
创建新的项目脚手架。
sora init --out my-config --schema-format toml
sora i -o my-config --schema-format yaml
| 参数 | 说明 |
|---|---|
-o, --out <DIR> | 脚手架输出目录。 |
| `–schema-format <toml | yaml |
--force | 允许写入已有脚手架路径。 |
check
校验项目 schema,也可以和已有 schema lock 对比。
sora check --project project.toml
sora c -p project.toml -l generated/schema.lock
| 参数 | 说明 |
|---|---|
-p, --project <PATH> | 项目清单路径。 |
-l, --lock <PATH> | 用于校验的已有 schema lock。 |
build
运行 project.toml 中 [build] 声明的输出,例如 schema lock、Excel 模板、codegen 和 export。
sora build --project project.toml
sora b -p project.toml -t rust -c
| 参数 | 说明 |
|---|---|
-p, --project <PATH> | 项目清单路径。 |
| `–default-source-format <csv | json |
-d, --data-root <DIR> | 数据输入根目录。覆盖 [build].data_root。 |
-v, --view <NAME> | 只构建 view 声明的 schema 投影。 |
-t, --target <NAME> | 要运行的 codegen target。可以重复传。 |
-c, --clean | 重建前删除选中的生成输出。 |
gen
直接为某个 target 生成代码,不依赖 [build.codegen]。
sora gen --target rust --project project.toml --out generated/rust
sora g -t typescript -p project.toml -o generated/typescript
| 参数 | 说明 |
|---|---|
-t, --target <NAME> | Codegen target,例如 rust、typescript 或 python。 |
-p, --project <PATH> | 项目清单路径。 |
-o, --out <DIR> | 输出目录。 |
| `–format-code <never | auto |
-v, --view <NAME> | 只生成 view 声明的 schema 投影。 |
export
读取表数据并导出运行时数据。
sora export --project project.toml --data-root data --format json --out generated/config.json
sora e -p project.toml -d data -f binary -o generated/config.sora
| 参数 | 说明 |
|---|---|
-f, --format <NAME> | 导出格式,例如 binary、json、debug-json、cbor、sora-protobuf 或 typed-protobuf。 |
| `–default-source-format <csv | json |
-p, --project <PATH> | 项目清单路径。 |
-d, --data-root <DIR> | 数据输入根目录。 |
-o, --out <PATH> | 输出文件或目录,取决于导出格式。 |
-v, --view <NAME> | 只导出 view 声明的 schema 投影。 |
| `–compression <none | zstd>` |
--compression-level <N> | 压缩导出的压缩等级。 |
diff
使用同一份项目 schema 比较两个数据根目录。
sora diff --project project.toml --left-root old-data --right-root data --out generated/diff.json
sora d -p project.toml -l old-data -r data -o generated/diff.json
| 参数 | 说明 |
|---|---|
| `–default-source-format <csv | json |
-p, --project <PATH> | 项目清单路径。 |
-l, --left-root <DIR> | 基准数据根目录。 |
-r, --right-root <DIR> | 变更后的数据根目录。 |
-o, --out <PATH> | Diff 输出路径。 |
-v, --view <NAME> | 只比较 view 声明的 schema 投影。 |
excel-template
根据 schema 生成空 Excel workbook。它适合新建 workbook,不适合覆盖已有数据文件。
sora excel-template --project project.toml --out generated/excel
sora et -p project.toml -o generated/excel
| 参数 | 说明 |
|---|---|
-p, --project <PATH> | 项目清单路径。 |
-o, --out <DIR> | 生成 workbook 的输出目录。 |
-v, --view <NAME> | 只为 view 声明的 schema 投影生成模板。 |
excel-sync
预览或应用已有 Excel 数据 workbook 的 schema 表头更新,同时保留数据行。从 schema 中删除的字段会保留为被忽略的 legacy 列。
sora excel-sync --project project.toml --data-root data
sora es -p project.toml -d data -w
| 参数 | 说明 |
|---|---|
-p, --project <PATH> | 项目清单路径。 |
-d, --data-root <DIR> | 数据 workbook 根目录。 |
-v, --view <NAME> | 只同步 view 声明的 schema 投影。 |
-w, --write | 写入 workbook 变更。不带这个参数时只预览变化。 |
schema-lock
为当前归一化 schema 写出 schema lock。
sora schema-lock --project project.toml --out generated/schema.lock
sora sl -p project.toml -o generated/schema.lock
| 参数 | 说明 |
|---|---|
-p, --project <PATH> | 项目清单路径。 |
-o, --out <PATH> | Schema lock 输出路径。 |
-v, --view <NAME> | 只锁定 view 声明的 schema 投影。 |
studio
启动内置的 Sora Studio schema 编辑器。
sora studio --project project.toml
sora st -p project.toml --port 5180
| 参数 | 说明 |
|---|---|
-p, --project <PATH> | 项目清单路径。 |
--host <IP> | 绑定地址。默认 127.0.0.1。 |
--port <PORT> | 端口。默认 5174。 |
数据导出
Sora 将数据导出和语言代码生成分离。
Exporter 接收已经校验的数据,并写出运行时数据包。生成代码随后读取这些数据包格式。这允许同一份 schema 和数据服务多个语言或不同运行时存储选择。
简化来看:
source data -> export format -> generated code runtime_format
例如生成的 Rust 代码使用 runtime_format = "sora" 时,构建配置里也必须写出一个 binary export。代码生成决定“怎么读”,数据导出负责“写出要读的文件”。
内置导出
| Format | Purpose |
|---|---|
binary | 原生 sectioned Sora binary bundle。 |
json-debug | 便于检查的人类可读 debug 输出。 |
json | Runtime JSON bundle。 |
cbor | Runtime CBOR bundle。 |
sora-protobuf | 使用 Sora value model 的 runtime Protobuf bundle。 |
proto | 使用生成出的业务 schema 的 typed Protobuf bundle。 |
i18n-binary | 单个 locale 的二进制语言包。 |
i18n-json | 单个 locale 的 JSON 语言包。 |
codegen 中的 runtime_format = "sora" 对应 binary 导出。
命令示例
sora export \
--format binary \
--default-source-format xlsx \
--project project.toml \
--data-root data \
--out generated/config.sora
Build Manifest 示例
构建清单可以声明多个导出:
[[build.exports]]
format = "binary"
out = "generated/config.sora"
[[build.exports]]
format = "json-debug"
out = "generated/debug-json"
[[build.exports]]
format = "i18n-binary"
out = "generated/i18n/zh_cn.sora-i18n"
locale = "zh_cn"
sora build 运行时会检查配置的 codegen target 是否有匹配的 runtime format 导出。
语言包是独立运行时资源,由生成的 i18n runtime 挂载。见多语言。
导出格式
导出格式是运行时数据包格式。它和 Excel、CSV、TOML、JSON、YAML 这类 source format 是两回事。
| Export | Codegen Runtime Format | Output Shape | Use When |
|---|---|---|---|
binary | sora | 原生 sectioned binary bundle。 | 需要紧凑、自包含的 Sora runtime。 |
json | json | Runtime JSON bundle。 | 需要易检查、易接入平台工具。 |
cbor | cbor | Runtime CBOR bundle。 | 需要通用紧凑二进制 value format。 |
sora-protobuf | sora-protobuf | 用 Protobuf 编码的 Sora value model。 | 想使用 Protobuf transport,但不想为每个游戏维护 .proto model。 |
proto | none | 使用生成出的业务 schema 的 typed Protobuf bundle。 | 需要面向外部工具的业务 .proto 契约。 |
json-debug | none | 按表输出的 debug JSON。 | 用于检查、review 或测试。 |
i18n-binary | none | 单个 locale 的原生二进制语言包。 | 需要和配置包分开挂载的生产多语言资源。 |
i18n-json | none | 单个 locale 的 debug JSON 语言包。 | 需要可 review 的文本,用于外包翻译交付或测试。 |
构建输出示例:
[[build.exports]]
format = "binary"
out = "generated/config.sora"
[[build.exports]]
format = "json"
out = "generated/config.json"
[[build.exports]]
format = "json-debug"
out = "generated/debug-json"
[[build.exports]]
format = "i18n-binary"
out = "generated/i18n/zh_cn.sora-i18n"
locale = "zh_cn"
生成运行时只加载它支持的 runtime format。json-debug 面向人和工具,不用于 generated runtime loading。
多语言导出需要 [localization],并在 build manifest 中为 export 指定 locale。见多语言。
代码生成
代码生成会把归一化 schema IR 转成目标语言的 row type、table container 和 config loader。
它由语言生成器 registry 驱动。
每个生成器声明:
- target id 和 alias;
- 展示元数据;
- 支持的 runtime format;
- 可选 formatter 集成;
CodeGenerator实现。
因此内置语言和下游生成器可以使用同一种管线形状。
schema files -> schema model -> normalized IR -> generator registry -> target generator -> files
直接生成一个目标:
sora gen --target typescript --project project.toml --out generated/typescript
也可以在构建清单中声明:
[[build.codegen]]
target = "typescript"
out = "typescript/generated"
format = "auto"
format 可以是 never、auto 或 required。auto 会在 formatter 可用时运行;required 会在 formatter 缺失或失败时报错。
Runtime Format
每个目标可以选择 runtime format:
[codegen.typescript]
runtime_format = "json"
runtime format 只控制该目标生成的 loader 代码,不改变 schema 或源数据。
生成代码形状
生成代码通常包含:
- schema enum 对应的枚举;
- struct、union variant 和 table row 对应的 record type;
map、list、singletontable container;- key 和 index lookup helper;
- 选定 runtime format 的顶层 config loader。
生成标识符遵循目标语言命名习惯,但运行时数据查找仍然使用原始 schema 名称。见标识符命名。
Schema optional<T> 会映射成目标语言中能表达的最强可空形式。见空值表达。
标识符命名
Schema 名称是事实来源。生成代码会把这些名称转换成目标语言习惯的标识符,但运行时数据查找仍然使用原始 schema 名称。
例如,schema 字段 max_stack 在 TypeScript 中可能生成 maxStack,在 C# 中生成 MaxStack,在 Rust 中仍然是 max_stack。生成的 decoder 读取运行时数据包时,字段名仍然是 max_stack。
命名流程
Sora 会先从每个 schema 名称派生通用命名形式,再交给语言生成器:
| 形式 | max_stack 的例子 | 常见用途 |
|---|---|---|
| Raw | max_stack | 运行时 table 名、field 名、enum 文本值、union tag。 |
| Pascal | MaxStack | 类型、类、enum variant、导出符号。 |
| Camel | maxStack | camel-case 语言中的字段、属性、参数、方法。 |
| Snake | max_stack | snake-case 语言中的文件、模块、字段、函数。 |
语言生成器会选择合适的形式,并可以继续做语言相关的合法化处理,例如处理非法字符或保留字。
语言约定
内置生成器遵循目标语言常见的公开 API 风格:
| 目标 | 类型 | 字段和访问器 | 文件/模块 |
|---|---|---|---|
| Rust | PascalCase | snake_case | snake_case.rs |
| C | 带前缀的 snake_case | snake_case | snake_case.h, snake_case.c |
| C++ | PascalCase | snake_case | snake_case.hpp |
| C# | PascalCase | PascalCase property | PascalCase.cs |
| Go | PascalCase 导出名称 | PascalCase 导出字段 | snake_case.go |
| Java | PascalCase | lowerCamelCase | PascalCase.java |
| Kotlin | PascalCase | lowerCamelCase | target layout |
| Scala | PascalCase | lowerCamelCase | PascalCase.scala |
| TypeScript | PascalCase | lowerCamelCase | snake_case.ts |
| JavaScript | PascalCase | lowerCamelCase | snake_case.js, snake_case.d.ts |
| Python | PascalCase | snake_case | snake_case.py |
| Dart | PascalCase | lowerCamelCase | snake_case.dart |
| Lua | PascalCase table-like 类型 | lowerCamelCase | snake_case.lua |
| Erlang | snake_case 模块 | snake_case map key/function | snake_case.erl |
| Godot | PascalCase class | snake_case | snake_case.gd |
这个表描述的是生成代码里的标识符,不是运行时数据名。
运行时名称保持 Raw
下面这些值保留原始 schema 拼写:
- 数据包和 table metadata 中的 table 名;
- 从运行时 row 中读取的 field 名;
- enum string value;
- union variant tag value;
- schema lock 和 fingerprint 的输入。
修改 schema 名称会修改数据契约。仅修改某个目标语言的生成标识符风格,不应该修改数据契约。
自定义类型映射
自定义类型映射不会重命名生成的 schema 标识符。它只控制目标语言类型表达式、import/include,以及可选的转换 hook。
映射函数名是用户为目标语言编写的原生代码,所以应该遵循目标语言自己的命名习惯。映射 key 仍然是命名 schema 类型,例如 Vec3。
空值表达
Schema 使用 optional<T> 表达可空性。代码生成器会把这个 schema 类型映射成目标语言中能表达的最强可空形式。
运行时数据包会显式编码 optional presence。生成代码的公开 API 应该保留这个区别,而不是依赖没有文档说明的 null 约定。
内置表示
| Target | optional<T> 表示 |
|---|---|
| Rust | Option<T> |
| C# | 启用 nullable reference types 的 T? |
| Kotlin | T? |
| Dart | T? |
| Scala | Option[T] |
| TypeScript | `T |
| JavaScript d.ts | `T |
| Python | `T |
| C++ | C++17 及更新版本使用 std::optional<T>;旧标准使用 SoraOptional<T> |
| C | 带 presence state 的生成 optional wrapper type |
| Go | *T |
| Erlang | `T |
| Lua | T? EmmyLua annotation |
| Godot | 支持 null 的 Variant |
| Java | 可空 value type 加 annotation |
JavaScript、Lua、Godot 这类动态目标主要只能为工具链标注可空性。静态类型目标会尽量在生成类型中表达。
Java Annotation
Java 没有标准的可空类型语法。Sora 会用 annotation 标记可空 Java 字段、构造参数和可能返回 null 的 lookup 结果。
默认情况下,Java 生成代码使用自包含的 package-local SoraNullable annotation:
@SoraNullable
public final String nickname;
如果项目使用特定 annotation 包,可以配置:
[codegen.java]
nullable_annotation = "org.jetbrains.annotations.Nullable"
设置 nullable_annotation = "" 可以保留可空 Java 值,但不生成 annotation。
自定义类型映射
当目标语言中 optional<YourType> 需要不同类型表达式时,类型映射脚本可以提供 nullable_type_name:
{
target = "java",
schema_type = "UserId",
type_name = "int",
nullable_type_name = "Integer",
}
这个字段只改变生成的类型表达式。optional presence 的 decode 方式仍然由对应语言后端控制。
运行时格式
按 codegen target 选择 runtime format:
[codegen.rust]
runtime_format = "sora"
Runtime format 是生成代码能加载的数据格式。它们对应导出格式:
Codegen runtime_format | Required Export |
|---|---|
sora | binary |
json | json |
cbor | cbor |
sora-protobuf | sora-protobuf |
这个设置不会改变 Excel、CSV、TOML、JSON、YAML 或 schema 文件。它只决定目标语言生成什么 loader。选定的 runtime format 必须在项目 build 中有匹配的 export。
支持矩阵
| Target | sora | json | cbor | sora-protobuf |
|---|---|---|---|---|
| Rust | self-contained | managed dependency | managed dependency | managed dependency |
| Kotlin | self-contained | managed dependency | managed dependency | managed dependency |
| C# | self-contained | managed dependency | managed dependency | managed dependency |
| Java | self-contained | managed dependency | managed dependency | managed dependency |
| Scala | self-contained | managed dependency | managed dependency | managed dependency |
| Go | self-contained | managed dependency | managed dependency | managed dependency |
| TypeScript | self-contained | managed dependency | managed dependency | managed dependency |
| JavaScript | self-contained | managed dependency | managed dependency | managed dependency |
| Python | self-contained | managed dependency | managed dependency | managed dependency |
| Dart | not supported | standard library | user adapter | user adapter |
| Godot | not supported | standard library | not supported | not supported |
| C | self-contained | not supported | not supported | not supported |
| C++ | self-contained | not supported | not supported | not supported |
| Erlang | self-contained | user adapter | user adapter | user adapter |
| Lua | self-contained | user adapter | user adapter | user adapter |
依赖类型含义:
| Kind | Meaning |
|---|---|
| self-contained | 生成 runtime 内置 decoder。 |
| standard library | 生成 runtime 使用语言标准库。 |
| managed dependency | 生成 runtime 预期使用该生态的常规 package dependency。 |
| user adapter | 生成 runtime 暴露 adapter hook,由应用提供具体 decoder。 |
如何选择
目标支持时,优先用 sora 获得原生 Sora binary bundle。
需要更强可检查性、工具友好性或平台接入简单性时,用 json。
已有 CBOR 依赖,并希望使用通用紧凑二进制 value format 时,用 cbor。
运行环境偏好 Protobuf transport,但仍想保留 Sora 的 schema-driven value model 时,用 sora-protobuf。
CI runtime matrix 会生成此表中每个支持组合,并对轻量可检查的语言做语法检查。
运行时适配器
有些语言并没有适合每个 runtime format 的内置依赖方案。这些目标会使用 adapter hook,而不是在生成代码中嵌入某个第三方 decoder。
生成 runtime 负责 Sora value model 和 table loading 逻辑。应用只需要提供一个小函数,把 bytes 转成 runtime 期望的 decoded value tree。
这让生成代码不绑定具体依赖。游戏可以使用自己已经信任的 CBOR、Protobuf 或压缩库。
Lua
local config = SoraConfig.from_cbor(bytes, {
decode_cbor = function(payload)
return my_cbor.decode(payload)
end,
})
Erlang
Options = #{
decode_cbor => fun my_cbor:decode/1
},
Config = sora_config:from_cbor(Bytes, Options).
Dart
final config = SoraConfig.fromCbor(
bytes,
decodeCbor: (payload) => myCborDecode(payload),
);
Adapter 让生成代码独立于依赖选择,同时仍能使用相同的导出数据格式。
Adapter 返回什么
adapter 应该返回生成 runtime 期望的目标语言 Sora value tree。它不负责构造 typed row;解码后的类型化构造由生成代码处理。
如果某个 target 对某个格式有 self-contained decoder,就不需要 adapter。
版本与兼容性
Sora 仍处于早期阶段。项目不提供类似 Rust edition 的旧 schema 语义兼容模式。需要稳定输出的项目应该固定使用的 sora CLI 版本,并把 CLI 升级视为一次显式迁移。
需要固定什么
在项目工具链里固定 CLI 二进制或 crate 版本:
- 下载指定版本的 GitHub Release 资源,并在 CI 中持续使用这个版本;
- 用
cargo install sora-cli --version X.Y.Z安装指定 crates.io 版本; - 在项目搭建文档或构建脚本中记录期望的
sora --version。
同一次项目构建中的生成代码、生成 Excel 模板、schema lock 和导出的运行时数据包,都应该来自同一个固定的 CLI 版本。
运行时数据包版本
导出的运行时数据包会携带格式版本。Sora binary bundle 也有文件头版本,生成的 runtime 会拒绝读取不支持的版本。
只有当生成 runtime 无法安全读取旧布局写出的数据时,Sora 才会升级这些 runtime/export format version。例如:
.sora二进制 section 布局发生变化;- 生成 runtime 依赖的 manifest 字段发生破坏性变化;
- JSON、CBOR 或 Protobuf bundle 结构变化,导致旧生成代码无法读取;
- 导出的运行时数据包中的值编码规则发生变化。
在早期开发阶段,普通实现变化不会自动升级 format_version。版本升级是手动动作,只保留给真实的 runtime/export 不兼容。
Schema 和 Codegen 语义
项目还年轻时,schema 语法、parser 行为、校验规则、Studio 渲染和生成语言 API 都可能继续调整。Sora 不会用 edition flag 或其他兼容模式保留旧行为。
如果新版 CLI 改变了 schema 或 codegen 语义,用户应该:
- 有意识地升级 CLI;
- 重新生成 schema lock、模板、导出数据和代码;
- 审查 diff;
- 按需要更新 schema/data/project 文件。
Schema fingerprint 和 schema lock 可以帮助发现生成代码、schema 和数据之间的不匹配,但它们不是迁移工具。它们负责避免静默不兼容,不负责保留旧语义。
扩展 Sora
Sora 设计上可以作为库使用,便于项目方增加自己的语言或数据格式支持。
扩展边界被刻意拆开:
input adapter -> schema model -> normalized IR -> data validation
|-> exporter
|-> code generator
添加 Code Generator
实现 generator trait:
#![allow(unused)]
fn main() {
pub trait CodeGenerator: Send + Sync {
fn generate(&self, context: CodegenContext<'_>, out_dir: &Path) -> Result<()>;
}
}
注册 target id、alias、runtime capability 和可选 formatter 配置。
更完整的说明见生成器。
保持 IR 中立
语言相关配置应该放在 target options 和 generator 代码里。归一化 IR 只描述 schema 语义:package、table、field、type、key、index、union 和 validation metadata。
项目自己的语言类型映射应该通过 codegen type mapping provider 实现,而不是写进 schema 字段。这样数据语义和目标语言表示保持分离,例如把 struct<Vec3> 映射到 UnityEngine.Vector3。
添加 Exporter
Exporter 和 generator 是分离的。如果需要新的运行时数据包格式,就添加 data exporter。如果需要新的语言目标,就添加 code generator。
导出器边界见导出器。
生成器
generator 把归一化 IR 转成某个语言目标的文件。
Registration
生成器注册时包含:
- canonical target id;
- alias;
- display metadata;
- 支持的 runtime format;
- 可选 formatter 集成;
CodeGenerator实现。
这样内置生成器和下游生成器都能使用同一条管线。
Implementation Shape
#![allow(unused)]
fn main() {
pub trait CodeGenerator: Send + Sync {
fn generate(&self, context: CodegenContext<'_>, out_dir: &Path) -> Result<()>;
}
}
generator 接收:
- 归一化 IR;
- 解析后的 target options;
- 已注册的类型映射 providers;
- 输出目录;
- runtime format 选择。
它不应该修改 IR,也不应该依赖 IR 中存在语言相关字段。
类型映射
语言生成器可以先查询 context.type_mappings,再回退到内置类型映射。provider 按 target 加命名 schema 类型匹配,例如 struct<Vec3>,并返回生成类型名、可选 nullable 类型名和可选 decode 包裹表达式。容器和 optional 类型应该递归走同一个 mapper,因此 list<struct<Vec3>> 与 optional<struct<Vec3>> 会自动使用目标语言中的映射类型。
schema 保持语言无关。项目自己的映射规则应该放在库注册代码或 CLI Lua 类型映射脚本里,不放在字段定义里。
Target Options
语言相关选项放在 [codegen.<target>] 下:
[codegen.rust]
runtime_format = "sora"
map_type = "btree"
string_storage = "owned"
这些选项的解释权属于对应 generator。
导出器
exporter 把校验后的配置数据写成运行时数据包。
Exporter 和 code generator 是分离的,因为同一份导出数据可以被多种语言消费。
什么时候添加 Exporter
当需要这些能力时添加 exporter:
- 新的运行时 wire format;
- 平台特定 asset package;
- 不同压缩或 section layout;
- 面向工具的检查格式。
不要为了支持一种新编程语言而添加 exporter。那应该添加 code generator。
Expected Boundary
exporter 应该消费:
- 归一化 schema IR;
- 校验后的 config data;
- exporter options;
- output target。
它不应该依赖某个具体语言生成器。
设计说明
这些页面解释 Sora 背后的架构选择。
简短版本是:schema 文件是事实来源。Excel 表头、运行时数据包、生成代码和扩展点,都是归一化 schema 与校验后数据的投影。
Schema 是事实来源
Sora 是 schema-first 的。TOML schema 是配置数据的契约;源文件和生成产物都是这个契约的投影。
schema modules
-> normalized IR
-> Excel headers
-> validation
-> runtime exports
-> generated language code
这个设计避免常见问题:电子表格、手写 parser 和运行时代码各自定义了一套略有差异的数据形状。
结果
- 字段名、类型、key、default、reference 和 validation rule 都在 schema 中定义。
- Excel 和 CSV 文件提供值,而不是第二套 schema。
- runtime export format 不改变数据模型。
- 语言选项属于 codegen target,不属于 IR。
- 下游用户可以添加 generator 或 exporter,而不改变 schema 语义。
schema 仍然可以包含编辑提示,例如 comment、parser hint、range 和 length limit。这些提示属于数据契约的一部分,因为它们会影响校验或生成投影。
Excel 表头投影
Excel 模板由归一化 schema 生成。表头是投影,不是独立的格式定义。
为什么生成表头
手工维护的 spreadsheet header 很容易和代码漂移:
- 字段在代码里改名了,但 Excel 没改;
- 类型变了,但旧行看起来仍然有效;
- 策划新增一列,但运行时没人读取;
- 校验规则只写在注释里,而没有被执行。
Sora 通过从 schema 生成工作簿结构来避免这些问题。
表头包含什么
生成行包含:
- 表元数据:表名、mode、key、groups 和 schema hash;
- 稳定字段名;
- 类型提示;
- group 提示;
- validation 和 parser rule;
- 给编辑者看的注释。
只有行数据应该被视为作者内容。表头行可以在 schema 变更后重新生成。
实际工作流
- 修改 schema。
- 重新生成 Excel 模板。
- 把已有数据行移动或粘贴到更新后的模板中。
- 运行
sora build或sora export校验值和引用。 - 生成导出数据和代码。
这样 Excel 仍然适合编辑,但 schema 始终保持权威。
IR 边界
归一化 IR 描述 schema 语义,不应该编码语言相关的 codegen 选择。
属于 IR 的内容
- package 和 include 的 schema module;
- enum、struct、union、table、field 和 index;
- table mode 和 key;
- source metadata;
- field type、default、parser、range、length 和 comment;
- reference 和派生子表字段 metadata;
- group 与 view 投影。
不属于 IR 的内容
- Rust map 实现选择;
- TypeScript enum 表示方式;
- Lua module 名称;
- runtime decoder 依赖选择;
- formatter 设置;
- target-specific 文件布局。
这些设置应该放在 [codegen.<target>] 或 generator registration metadata 中。
扩展边界
schema input -> normalized IR -> validation
|-> exporter registry
|-> codegen registry
新的语言生成器应该消费 IR 和自己的 target options。新的运行时数据格式应该作为 exporter 添加。除非实际数据语义发生变化,否则二者都不应该要求修改 schema model。