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、字段和表可以声明 scope。构建时可以选择 scope,只生成或导出某个运行环境需要的部分。
快速开始
这一页会创建一个最小的道具表,生成 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 内容如下:
package = "game_config"
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 = ["Weapon", "Armor", "Material", "Consumable"]
[[tables]]
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/
项目清单
package = "game_config"
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 = ["Weapon", "Armor", "Material", "Consumable"]
[[tables]]
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、scope、range、length、注释和表 metadata 变化会刷新生成表头;
- 从 schema 中删除的字段不会从 Excel 中删除,而是保留为 Sora 忽略的 legacy 列,由策划在合适的时候手动删除;
- 同一个 workbook 中不属于 schema 的 sheet 会作为 value-only sheet 保留下来。
每个表最终生成到哪个 workbook 和 sheet,由表自己的 source 决定:
[[tables]]
name = "Item"
[tables.source]
format = "xlsx"
file = "Core.xlsx"
sheet = "Item"
[[tables]]
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、scope 和 schema hash。 |
#name | 面向表格编辑的显示名行。 |
#field | Sora 读取的稳定 schema 字段名。 |
#type | 类型提示,例如 i32、enum<ItemType> 或 struct<Cost>(kind: enum<ResourceKind>, id: i32, count: i32)。 |
#scope | 每个字段的 scope 信息。 |
#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 文件。
package = "game_config"
includes = ["schema/items.toml", "schema/skills.toml"]
Schema 是 Sora 的事实来源。它描述稳定的数据契约;Excel 工作簿等源文件只包含需要按契约校验的行数据。
支持的文件格式以及等价的 TOML/YAML/JSON/Lua 写法见 Schema 格式。
Enums
[[enums]]
name = "ItemType"
values = ["Weapon", "Armor", "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]]
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
package = "game_config"
includes = ["schema/items.toml"]
[[enums]]
name = "ItemType"
values = ["Weapon", "Armor"]
[[tables]]
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
YAML
package: game_config
includes:
- schema/items.yaml
enums:
- name: ItemType
values: [Weapon, Armor]
tables:
- name: Item
mode: map
key: id
fields:
- name: id
type: i32
JSON
{
"package": "game_config",
"includes": ["schema/items.json"],
"enums": [
{ "name": "ItemType", "values": ["Weapon", "Armor"] }
],
"tables": [
{
"name": "Item",
"mode": "map",
"key": "id",
"fields": [
{ "name": "id", "type": "i32" }
]
}
]
}
Lua
Lua schema 文件必须 return 一个 table。这个 table 使用和 TOML/YAML/JSON 形状一致的字段名。Lua schema loader 面向数据配置;package、io、os 和 debug 不可用。
return {
package = "game_config",
includes = { "schema/items.lua" },
enums = {
{ name = "ItemType", values = { "Weapon", "Armor" } },
},
tables = {
{
name = "Item",
mode = "map",
key = "id",
fields = {
{ name = "id", type = "i32" },
},
},
},
}
项目 Build 配置
项目文件里的 build 也可以使用 YAML、JSON 或 Lua:
package: game_config
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
{
"package": "game_config",
"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 {
package = "game_config",
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]]
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
对于 map 表,key 指定表的主键字段。Sora 会用它做行唯一性校验、生成 lookup API、生成 Excel 模板提示,并校验 ref<Table.key>。
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]]
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。 |
scope | 所有字段 | 仅在选定 generation/export scope 下包含该字段。默认是 all。 |
from | 仅表字段 | 可选的子表来源,用来声明派生字段。 |
default 写成字符串,因为它会走和源数据相同的类型感知转换路径。
from 用来描述从另一张表匹配行得到的派生字段,详见引用和派生字段。派生字段可以是 list<T>、T 或 optional<T>,且不能声明 default。
枚举、结构体和联合
这些定义让 schema 可以表达超过扁平表格的数据结构。
Enums
[[enums]]
name = "Rarity"
values = ["Common", "Uncommon", "Rare", "Epic", "Legendary"]
枚举让源数据保持可读,同时让生成代码获得受约束的类型。
alias 可以保留导入数据或旧数据里的名称:
[[enums.aliases]]
name = "Purple"
alias = "Epic"
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 和 scope。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]]
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]]
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]]
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]]
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]]
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]]
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。
package = "game_config"
includes = ["schema/items.toml"]
[parsers]
scripts = ["tools/parsers.lua"]
[type_mappings]
scripts = ["tools/type_mappings.lua"]
[build]
default_source_format = "xlsx"
data_root = "data"
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"
运行所有配置好的输出:
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>] 下:
[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。 |
csharp | runtime_format 默认 sora。 |
java | runtime_format 默认 sora;nullable_annotation 默认 SoraNullable,也可以设置成 org.jetbrains.annotations.Nullable 这类 annotation class,或设置为 "" 禁用 annotation。 |
scala | runtime_format 默认 sora;scala_version = "2.12"、"2.13" 或 "3",默认 3。 |
go | runtime_format 默认 sora。 |
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;prefix 是可选 symbol prefix。 |
cpp | runtime_format = "sora";cpp_standard = "c++11"、"c++14"、"c++17"、"c++20" 或 "c++23",默认 c++17;namespace 是可选 C++ 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 | 没有 target options。它生成 .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"
namespace = "game::config"
[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。
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。 |
-s | --scope | 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。 |
-s, --scope <NAME> | 只构建包含在某个 scope 中的 schema item。 |
-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 |
-s, --scope <NAME> | 只生成包含在某个 scope 中的 schema item。 |
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> | 输出文件或目录,取决于导出格式。 |
-s, --scope <NAME> | 只导出包含在某个 scope 中的 schema item。 |
| `–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 输出路径。 |
-s, --scope <NAME> | 只比较包含在某个 scope 中的 schema item。 |
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 的输出目录。 |
-s, --scope <NAME> | 只为包含在某个 scope 中的 schema item 生成模板。 |
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 根目录。 |
-s, --scope <NAME> | 只同步包含在某个 scope 中的 schema item。 |
-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 输出路径。 |
-s, --scope <NAME> | 只锁定包含在某个 scope 中的 schema item。 |
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、scope 和 schema hash;
- 稳定字段名;
- 类型提示;
- scope 提示;
- 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;
- scope。
不属于 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。