跳至主要内容
SheetForge

数据源、导出与推送

存在三条写入路径,各自的目标不同:

  • **反映(reflect)**把创作暂存的内容写入来源。
  • **导出(Export)**把烘焙的 SO 取值写回工作表文件。
  • **推送(Push)**把烘焙的 SO 取值逐个单元格写入实时的 Google表格。

导入来源

导入来源是设置资源中的一项一等公民式选择。每种来源都会通过 CanAuthor 声明自己的创作能力:

来源读取内容创作(写回)
LocalFile一个包含 .tsv / .csv / .xlsx 文件的文件夹(仅直接子项;一个文件 = 一个标签页,xlsx 工作簿会贡献其内部的各个工作表)完整——反映、结构编辑、键/标签页重命名
GoogleSheet · SheetsApi通过服务账号 JWT 认证访问的私有/共享表格(设置指南完整——外科手术式的精准单元格写入、结构重写、推送
GoogleSheet · ExportUrl通过导出 URL 链接共享的工作表——无需认证只读CanAuthor = false)——推送/反映/结构编辑/删除均被禁用,并附带说明
自定义提供方插件所注册的任何来源(ISheetSourceProvider——数据库、REST、内部专有格式)由提供方通过其 CanAuthor 标志自行决定

说明:

  • ExportUrl 需要 gid 映射表(标签页名称 → #gid= 值)。没有 gid 的导出 URL 会静默地只返回第一个标签页,因此该映射是强制要求的(GoogleSheetGidMapMissing,重复的 gid 会被拒绝)。SheetsApi 模式会自动发现标签页,不需要映射表。
  • 内置的 xlsx 读写器是手写的 OOXML 实现(仅使用 System.IO.Compression + System.Xml——不使用 NPOI/ClosedXML,零第三方代码),因此不会引入任何可能与你项目中其他资源发生冲突的 DLL。它是同一套编解码器,两端共用:同一个读取器既运行在 Unity 编辑器中,也会——编译成 WebAssembly 后——运行在网页应用中,因此两个宿主环境不可能对同一个单元格产生分歧。它刻意保持精简,并且对此保持诚实——只处理取值,不做重新计算:
    • 公式单元格给出的是文件中缓存的取值。 没有缓存取值的公式,以及错误单元格(#REF!#DIV/0!),都会被拒绝(UnsupportedXlsxCell)——请在 Excel 中保存一次工作簿以缓存取值,或者将公式具化为值。
    • 日期格式的单元格会被当作日期读取,并渲染为 yyyy-MM-dd——无论是 ISO 日期单元格类型,还是样式为日期格式的普通数字都是如此,1900 和 1904 两种日期系统都会被遵循——而不是文件存储的原始序列号。其他数字格式、合并单元格和图表都不会被导入。
    • 这些解读方式——缓存的公式取值、把日期显示为文本、忽略格式——是读取器在两个宿主环境下共同遵循的固定策略,网页应用的导入对话框还会额外在一条**"How this workbook was read"**说明中点名实际发生过的那几种。
    • 单元格内的制表符或换行符会被拒绝(UnsupportedCellCharacter)——请用 ; 表示列表。
    • 读取器无法识别的单元格类型代码,会按其原始存储文本读入,而不会被拒绝。
  • 本地文件必须是 Unicode 编码。 UTF-8 BOM 或 UTF-16 BOM(小端或大端)都会被识别;没有 BOM 时,文件会按严格的 UTF-8 解码。像 CP949 或 Shift-JIS 这样的旧式单字节编码会被拒绝UnsupportedEncoding),而不会被猜测——猜测会在不同机器上解码出不同结果,从而静默地破坏数据。请把文件重新保存为 UTF-8。
  • 一个来源可以返回部分输出——一个损坏的文件不会连累其余可读的标签页;相关问题会以诊断信息的形式出现。
  • 自定义来源提供方会被自动发现,并出现在同一个设置下拉菜单中——见插件开发
  • 如果来源在你不知情的情况下发生了变化,Data Studio 会告诉你。 在窗口获得焦点时——或者从 ⋯ 菜单主动请求时——Data Studio 会重新读取来源,并将其与你上一次导入的快照进行比较,只有在数据确实不同时才会显示一个徽章:仅仅重新保存过、或只是重新格式化过的工作表会保持安静,因为比较依据的是内容,而不是时间戳。点击这个徽章会提议执行一次导入;没有任何东西会按定时器轮询,也没有任何东西会自行导入,处于离线状态或未获授权只意味着不会出现徽章。它对每一种来源类型都以同样的方式工作——本地文件、导出 URL 工作表和 Sheets API 皆是如此。

导出——往返的返程一半

Data Studio 工具栏中的 ⋯ ▸ 执行导出 会把烘焙出的 SO 取值写回工作表文件。

  • 结构来自 baseline,取值来自 SO。 导出会把当前取值换入你工作表结构的 baseline 快照中——标记行、列顺序、注释和人工书写的文本会被 100% 保留。
  • 语义化的取值往返:允许 1.01 归一化(数值相同);浮点数使用最短的可往返格式;小数点始终为 .
  • 格式Tsv / Csv / Xlsx / Json / MatchSource(每个标签页都会回到其被导入时的格式;来自 Google 或格式未知的会回退为 Tsv)。Json 是一种只出不进、面向机器而非电子表格的格式:每个标签页一个文件,记录以对象形式表示,int / float / bool 是真正的 JSON 数字与布尔值,而其余每一种取值——引用、列表、颜色、曲线、自定义类型——都以工作表所持有的那份精确规范单元格文本原样呈现,因此服务器或外部工具无需解析工作表文本即可消费游戏数据。JSON 不是一种导入来源,一个 JSON 文件也不携带任何可供往返的工作表结构——工作表依然是唯一权威。TSV 和 CSV 会为每个标签页各写出一个文件;Xlsx 会把每一个被导出的标签页都写入同一个工作簿SheetForge.xlsx),按标签页顺序各自成为其中一张工作表——工作簿正是为容纳多张工作表而生的格式,把它们放在一起,也正是让引用下拉列表得以跨工作表指向的原因(见下文)。在 MatchSource 下,xlsx 来源的标签页会汇入那一个工作簿,其余标签页则各自返回自己的文件。一个工作簿规则无法承载的工作表名称(过长,或含有禁止字符)会被调整,并在报告中点名——绝不会被静默改名。
  • 强制执行新鲜度:在架构变更之后用过期的烘焙结果导出,会因 ExportSchemaMismatch 而失败(烘焙的 SchemaFingerprint 必须与 baseline 的一致)——请先运行一次导入。
  • 资源引用会以工作表使用的地址文本导出回去——即键,或者对子资源而言是 parent[sub];组则采用该列自己的组——绝不会是 GUID。带类型的列(AssetRef@Group<Type>)也以同样的方式往返。ColorAnimationCurveGradient 的取值会以它们的规范文本形式返回(见表格语法);没有关键帧的曲线会导出为一个空单元格,颜色则会被钳制到 0…1 区间(不支持 HDR)。

推送——向 Google表格 的单元格级写回

Data Studio 工具栏中的 ⋯ ▸ 推送到 Google表格 会把烘焙出的 SO 取值逐个单元格发送到实时工作表。除非当前活动来源是 API 模式下的 Google表格,否则这一项会被禁用,并明确说明原因。它的设计确保它绝不会破坏别人正在编辑的实时工作表。

由这条安全链得出三项保证:

  • 未经你对单元格级计划的批准,不会发送任何内容。
  • 在你导入之后于实时工作表上发生更改的单元格会被跳过,绝不会被覆盖。
  • 只有当实时工作表仍在那一确切的行上显示那个键时,行删除才会被发送出去——任何发生漂移的情况都会附带通知被跳过,绝不会靠猜测处理。

安全链,按顺序:

  1. 需要 SheetsApi 凭据——在 ExportUrl 模式下,推送会在发起任何网络请求之前就被拒绝(GooglePushRequiresSheetsApi)。
  2. 每个被推送的标签页都需要一个键列——推送会在实时工作表中按键重新定位每一行,因此能够检测到已经移动的行,并安全地跳过那一次写入(绝不会发送到错误的行)。带有更改但没有键的标签页会被拒绝(PushKeylessTabUnsupported)。
  3. 计划 + 批准:会先计算出一个单元格级别的差异(baseline 与当前 SO 对比)作为计划——写入、追加、行删除——并在发送任何内容之前展示出来,等待明确批准;删除会单独列在自己的区块中,每一项都标明将要消失的那个键。拒绝 = 不发送任何单元格。
  4. 发送前的实时重新获取:在发送之前,会立即重新获取实时工作表并进行比较。发生冲突的单元格会被跳过,而不是覆盖(以警告形式报告):
    • PushConflictCellChanged——该单元格被第三方编辑过。
    • PushConflictRowMoved——该键被发现出现在了与你导入时不同的行,因此写入会被跳过(绝不会发送到错误的行)。重新导入以重新同步,然后再次推送。
    • PushConflictRowMissing——该行已在外部被删除。
    • PushConflictDuplicateLiveKey / PushConflictAppendKeyExists——目标存在歧义。
  5. 行删除会先按键匹配,然后才会被发送。 你删除的一条记录,只有在发送前的重新获取确认它的键仍然位于你导入时所见的那一确切行上之后,才会从实时工作表中移除:已经不存在的行算作已完成(再次推送不会把任何东西删除两次);如果在别的行上发现了这个键——说明工作表发生了漂移——就会附带通知被跳过,绝不会按位置删除。删除操作最后发送,在每个标签页内部由下往上进行,这样先删除的行就不会挪动之后要删除的那些行的坐标。无法删除行的来源(没有这项能力的自定义提供方)会诚实地回退到旧的行为:删除会被报告,实时工作表中的那一行留给你自己处理。

推送之后,请检查报告中"已应用/已跳过"的计数;如果有单元格被跳过,先重新导入以进行协调,然后再次推送。

对 Google 的结构更改

对 Google 来源进行的结构编辑(列、标记、重排序、重命名)会重写整个目标标签页——会先进行一次实时差异检查,并在覆盖你上次导入之后在工作表上发生的任何更改之前,要求明确批准。取值编辑仍然是精准的(按单元格);只有结构更改才会使用重写路径。

反映所进行的 Addressables 注册

把一个资源拖放到 Data Studio 中的一个 AssetRef@Group 单元格上,或是从项目中选取一个,除了工作表之外,可能还会为项目暂存一项更改:把该资源加入这个组、把它从另一个组移动过来,或是创建这个组。这些注册属于反映的一部分,并且会在这条链路中的一个固定位置运行——无论对本地文件夹、Google 表格,还是自定义来源提供方,都是同一个位置:

  1. 预检会验证整个投影后的状态,并把暂存的注册算作已经存在,因此一个指向尚未注册的资源的单元格不算错误。
  2. 工作表被写入。 如果写入被取消或失败,下面的步骤都不会运行:Addressables 设置不会被触及,注册会继续保持暂存状态,等待下一次尝试。一次因为所有被触及的标签页都被跳过而无法写入任何标签页的反映(例如只涉及工作簿承载的标签页时)同样不会运行它们。而一次根本没有任何内容需要写入工作表的反映——唯一的暂存更改就是一项注册——运行它们并重新导入;那一趟不会提交其他任何暂存编辑,因此它仍然可以撤销。
  3. 注册开始运行,按以下顺序:先创建组(带上默认的 BundledAssetGroupSchemaContentUpdateGroupSchema),然后添加或移动条目并赋予它们地址,最后统一保存一次设置。每一项都会在运行前被立即重新检查,遇到问题就跳过,而不是强行执行——资源自那以后已被删除时、地址已被该组中另一个资源占用时、组无法被创建或找到时,或者已经没有任何单元格引用该地址时(一项注册绝不会创建一个没有任何东西指向它的条目,一个所有条目都被跳过的组也同样不会被创建)。如果项目还没有 Addressables 设置资源,会为此专门创建一个。
  4. 暂存列表会被清空——无论是已应用的还是被跳过的——随后是自动的重新导入,这样烘焙才能看到新的条目。因此,一项被跳过的注册会在那次重新导入时被如实报告为它所属单元格上的 UnknownAssetKey

控制台会为每一个结果输出一行——每一项被应用的会是 Addressables: 'address' → group 'Group',每一项被跳过的会以警告形式输出 Addressables: skipped 'address' (reason)——并附带一行汇总 Addressables: N registered, M skipped。对本地文件夹来源而言,反映的完成对话框会以同一行汇总结尾。

写入工作表的下拉列表

选项数量有限的列会在工作表上附带一条数据校验规则,这样在 Google表格 或 Excel 中编辑的人就可以从一份列表中选择,而不必去记忆具体的拼写。这项功能不需要任何手动开启:每一次导出、推送和创作写回都会计算这些规则,并应用到目标格式能够承载它们的任何地方。

规则
Enum<T> 标量该 enum 全部成员组成的一份固定列表
引用标量(RecordId@Tab,以及具有引用对等性的自定义类型——见 §4.4a一个覆盖目标标签页键列的区间,结尾保持开放,因此后续添加到目标标签页的记录会自动加入这份列表。
List<>、包装类型列、键列本身没有规则——这里的一个单元格容纳的是多个值,或者根本没有可供列出的目标。
  • 只是引导,绝不强制。 每条规则都是非严格的(Google 端为 strict:false,xlsx 端为 showErrorMessage="0"):列表之外的取值会被标上一个警告标记,但依然会被接受。硬性拒绝会破坏"先写下引用、稍后再定义记录"这种寻常的工作流程,也会与导入自身的最接近匹配建议相冲突。
  • 这些规则是显示元数据,而不是取值。 它们绝不会出现在单元格里,因此往返不受影响;一次不含任何规则的导出,与这项功能出现之前产生的导出结果,在字节层面完全相同。
  • 独立于取值本身被应用。 附加规则是独立的一个步骤,而不是写入单元格的副作用——最常见的流程(新增一个 enum 成员,不改动任何数据)会发送零个单元格,因此副作用永远不会触发。这个操作是幂等的,重复执行不会带来任何变化。
  • 失败只是一条警告,而不是推送失败。 如果取值已经发送成功,只是规则未能附加上去,推送依然算作成功;再运行一次,届时只会重新应用这些规则。

各种格式各自能承载什么:

目标机制说明
Google表格(推送 / 写回)setDataValidation,被批量合并进一个请求中两种规则都支持。引用区间不设结尾行,因此会随着目标标签页的增长而跟随扩展。
xlsx(导出)工作表数据之后的 dataValidations两种规则都支持。因为导出结果是同一个工作簿,引用区间会指向同一文件内目标工作表的键列,沿着该工作表向下保持开放式结尾——这与 Google 的区间含义相同。规则仍会在三种如实说明的情况下被跳过,且会在警告中点名:某个列表成员包含逗号(内联分隔符会将其拆开)、内联列表超出该格式规定的 255 字符上限(这里指的是整份带引号的列表,也正是该格式所限制的对象),以及规则是一个区间、而其目标标签页不在该工作簿中。
TSV / CSV(导出)纯文本没有地方可以承载它们。
JSON(导出)是数据文件,不是电子表格——根本没有单元格可以附加下拉列表。

任何被省略的部分,都会在每次运行中被如实报告为一条 DropdownNotSupportedByFormat 警告——并列出每一个受影响的列,因此"为什么 Google 上有下拉列表,我的文件里却没有?"这个问题的答案就在报告里,而不是一个谜。之所以是警告而不是错误,是因为取值本身已经完整导出;缺失的只是编辑时的这份便利。

gid 映射表

仅在 ExportUrl 模式下使用。每一项都把一个标签页名称映射到该工作表的 #gid= 值(在浏览器地址栏中选中该标签页时可见)。只有在相关时,设置面板才会显示这个映射表。

你不必把这些数字一个一个地从浏览器里抄出来。设置资源的面板中有一个 Google Sheets 部分,可以替你填好这份映射表。

  • 在 ExportUrl 模式下,Autofill gid from live 会读取实时表格的标签页列表,并据此重写整份映射表,然后保存设置资源。
  • 在 SheetsApi 模式下,同一个面板则会改为提供 Fetch live tab list,它只是把该表格当前拥有的标签页展示给你看。这种模式会自行发现 gid,完全不需要映射表。

有一点需要留意:自动填充需要与 Sheets API 通信,因此即便 ExportUrl 导入本身不需要服务账号密钥,这个功能也需要配置一个。如果没有配置,它会停下并说明原因,而不会写出一份填了一半的映射表。

构建新鲜度检查钩子——过期的烘焙会导致构建失败

每次构建之前,一个构建前钩子会针对每个已提交的生成 Database 类型验证:

  • (i) 烘焙的 SO 是否存在;
  • (ii) 其架构指纹是否与 baseline 匹配;
  • (iii) 其 Addressables 注册是否存在。

任何一项失败都会中止构建,并给出一条可执行的提示语句(例如"打开 Tools/SheetForge/Data Studio,按下 ↓ Pull from source,然后再构建")。正是这一点让"烘焙的 SO 被 Git 忽略"这件事变得安全:克隆出来的或 CI 用的机器不可能交付一个空缓存的构建。

相关页面