跳转到内容

表格映射机制

配表系统支持复杂的数据结构,包括嵌套、多态和递归嵌套。然而,Excel 是基于二维表格的结构。

核心问题:如何将灵活的树状甚至递归结构映射到 Excel 的平坦表格结构中?

本文档详细介绍了五种映射机制:autopacksepfixblock,它们提供了从复杂数据结构到表格结构的转换方案。

  • 适用类型:基本类型(primitive)、结构体(struct)、接口(interface)

  • 占格规则

    • 基本类型:占用 1 列
    • 结构体/接口:自动计算所需列数
  • 特点:默认映射方式,适用于大多数简单场景

  • 示例

    struct Range {
    rmin:int; // 最小值
    rmax:int; // 最大值
    }

    Range 结构体占用 2 列

  • 适用类型:结构体(struct)、接口(interface)、列表(list)、映射(map)

  • 占格规则:将整个数据结构压缩到 1 列

  • 分隔符

    • 字段间:逗号或分号分隔
    • 嵌套结构:使用 () 包裹
  • 应用场景

    • 减少表格列数
    • 处理递归结构
    • 简化配置
  • 示例

    struct Position (pack) {
    x:int;
    y:int;
    z:int;
    }

    数据格式:"1,2,3"

    list<Position> (pack)

    数据格式:"(1,2,3);(4,5,6)"

    interface Attr (pack) {
    struct Damage {
    value:int;
    }
    struct Range {
    value:int;
    }
    }

    数据格式:"Damage(12)"

    list<Attr> (pack)

    数据格式:"Damage(12);Range(42)"

  • 重要说明:当数据结构形成循环引用(递归)时,必须至少在一处使用 pack 来打破循环,否则无法计算所需列数

  • 适用类型:结构体(struct)、列表(list)

  • 占格规则:将整个数据结构压缩到 1 列

  • 分隔符:支持自定义分隔符(:=$ 等)

  • 示例

    struct Time (sep=':'){
    hour:int;
    minute:int;
    second:int;
    }

    数据格式:"12:10:00"

  • 使用限制

    • 结构体中的所有字段必须是基本类型(primitive)
    • 不支持在类型为结构体的字段上设置 sep(应在结构体定义上设置)
    • 如果字段是 list<struct> 结构,分隔符需要与内部结构的分隔符区分
  • 建议:除非有特定分隔符需求,否则推荐使用功能更强大的 pack 映射

  • 适用类型:列表(list)、映射(map)

  • 占格规则:固定列数 = 元素类型占用列数 × count

  • 参数:count - 固定长度

  • 应用场景:已知确切长度的列表或映射

  • 示例

    list<int> (fix=2) // 占用 2 列
  • 适用类型:列表(list)、映射(map)

  • 占格规则:横向固定列数,纵向可扩展

  • 参数:fix - 横向块数

  • 应用场景:变长列表,需要在表格中垂直排列

  • 示例

    list<RewardItem> (block=1) // 横向占用 1 × RewardItem 列数,纵向任意行数

Block 的读取算法基于”祖先 block 首列”判断嵌套边界。解析器按 schema 预计算每个 block 字段首列对应的祖先 block 首列集合(即词法上包围它的所有外层 block 的首列),扫描后续行时检查这些祖先首列:

// ancestors:本 block 的所有祖先 block 首列(按 schema 预计算)
if (isPkCellAllEmpty(line)) { // 主键全为空 => 仍属于本 record
// 任一祖先 block 首列非空 => 外层 block 起了新项 => 结束本 block
boolean newOuterItem = false;
for (int bc : ancestors) {
if (!line.get(bc).isCellEmpty()) {
newOuterItem = true;
break;
}
}
if (newOuterItem) break;
DCell thisCell = line.get(firstColIndex);
if (thisCell.isCellEmpty()) {
// 本格为空:更深层嵌套 block 的行,忽略,继续
} else {
// 本格不为空:属于当前 block
res.add(new CellsWithRowIndex(line.subList(firstColIndex, firstColIndex + colSize), row));
}
} else {
break; // 主键非空 => 下一个 record,结束
}

Block 支持多层嵌套。判断一行属于”当前 block 的延续”还是”外层 block 起了新项”,看的是外层 block 的首列(每一项的标识列)是否非空,而不是相邻的前一列:

// 示例布局(外层 aebb 块,内嵌 bb 块):
// aebb <- 外层第1项 header(aebb 首列有值)
// bb <- 内层 bb 的行(aebb 首列为空,故仍属第1项)
// bb
// aebb <- 外层第2项 header(aebb 首列又有值 => 新的一项)
// bb

嵌套规则

规则一:外层 block 的首列(项标识)在内层 block 行必须为空,否则会被判定为外层起了新项而结束当前 block。

level.waves(外层)嵌套 wave.spawns(内层)为例,结构定义如下:

struct Wave {
waveIndex:int; // 外层 waves 的首列(项标识)
spawns:list<Spawn> (block=1); // 内层 block
}
struct Spawn {
pathId:int; // 内层 spawns 的首列(项标识)
monsterId:int;
}
table level[id] {
id:int;
waves:list<Wave> (block=1);
}

列布局为 id, waveIndex, pathId, monsterIdwaveIndex 是外层 waves 的首列,pathId 是内层 spawns 的首列):

idwaves.waveIndexpathIdmonsterId说明
1110012001wave1 首行,waveIndex 有值 → 新 wave
10022002waveIndex 空 → 仍属 wave1
210032003waveIndex 又有值 → 新 wave2

规则二:外层 block 的兄弟字段(非首列)在内层行可为空、也可冗余填值,不影响边界判断(边界只看首列)。

Wave 多一个 startTime 字段(列布局 id, waveIndex, startTime, pathId, monsterId),策划在 spawn 行冗余填了 startTime,解析仍按 waveIndex 判边界,spawn 不会丢失:

idwaves.waveIndexstartTimepathIdmonsterId说明
110.010012001wave1 spawn1
1.010022002startTime 冗余填了,仍属 wave1
22.010032003wave2

规则三:支持多层嵌套——外层 block 可继续嵌套内层 block,规则一/二逐层适用。完整两层数据示例见下方 Block 嵌套示例rewardGroups → items)。

规则四:内层 block 不能作为外层 block 元素的首字段(schema 校验直接拒绝,报 BlockFirstColOverlap)。外层 block 的首列就是其元素 struct 的起始列,若首字段本身又是 block,外层与内层首列重合,该列无法同时承担两层“项标识”,强行解析会把内层每个新项误判为“外层起新项”而 break,导致内层只能读出首个元素。

反例(被拒绝):

struct Inner {
innerList:list<X> (block=1); // 首字段就是 block
name:text;
}
table t[id] {
id:int;
outer:list<Inner> (block=1); // outer 首列 = innerList 首列 → 重合
}

正例:在 innerList 前放一个非 block 兄弟字段(如 name),让内层 block 首列错开外层首列即可。

规则五:block 项的首列(项标识)为空时,该行不创建新项——嵌套场景下数据并入上一层当前项;单层场景下无上层可并入,数据被丢弃(mustFill 即为防此)。

单层 task.items 为例,结构定义如下:

struct RewardItem {
itemId:int; // 外层 items 的首列(项标识)
count:int;
}
table task[id] {
id:int;
items:list<RewardItem> (block=1);
}

列布局为 id, itemId, count,第 3 行 itemId 刻意留空:

iditems.itemIdcount说明
110015item1
100210item2
20itemId 空 → 该行被丢弃,不创建 item
10033item3

结果:items = [(1001,5), (1002,10), (1003,3)],count=20 那行丢失。

mustFill 用于强制字段必须包含有效值:

  • 列表/映射类型:元素个数必须大于 0
  • 其他类型:单元格不能为空
exp:taskexp (mustFill); // 经验奖励(必须配置)
rewardItems:list<RewardItem> (block=1); // 物品奖励
  • exp 字段设置了 mustFill,表示必须配置,不能省略
  • 如果忘记填写对应的 Excel 单元格,系统会报错

数据结构定义

table weapon[id] {
id:int;
weaponAttrs:weaponAttr; // 武器属性
}
interface weaponAttr{
struct Damage {
value:int;
}
}

Excel 表格结构

idweaponAttrsp1
1Damage2
2Damage2

说明

  • 没有使用 pack,接口类型和数值需要拆分为 2 列
  • 第一列存储类型名称(Damage
  • 第二列存储具体数值(2

数据结构定义

interface weaponAttr (pack){
struct Damage{
value:int;
}
}
table test[id] {
id:str; // ID
weaponAttrs:list<weaponAttr>(fix=2); // 武器属性列表
}

Excel 表格结构

idweaponAttrsp1impl2p2
1Damage2
2Damage2

说明

  • fix=2 表示列表固定为 2 个元素
  • 每个 weaponAttr 元素占用 2 列(类型 + 数值)
  • 总共占用 4 列(2 个元素 × 2 列/元素)

数据结构定义

struct DmgRatio1 {
playerAttr:int;
ratio:int;
}
struct DmgRatio2 (pack) {
playerAttr:int;
ratio:int;
}
struct DmgRatio3 {
playerAttr:int;
ratio:int;
}
table test[id] {
id:int; // ID
DmgRatio1:DmgRatio1; // 伤害比例1
DmgRatio2:DmgRatio2; // 伤害比例2
DmgRatio3:DmgRatio3(pack); // 伤害比例3
}

Excel 表格结构

idDmgRatio1.playerAttrratioDmgRatio2DmgRatio3
1112,23,3

说明

  • DmgRatio1:未使用 pack,占用 2 列
  • DmgRatio2:在结构体定义上使用 pack,占用 1 列
  • DmgRatio3:在字段上使用 pack,占用 1 列
  • 两种 pack 方式效果相同:可在结构体定义或字段上设置

数据结构定义

interface IDmgRatio{
struct DmgRatio1 {
playerAttr:int;
ratio:int;
}
struct DmgRatio2 {
playerAttr:int;
ratio:int;
}
}
table test[id] {
id:int; // ID
DmgRatio1:IDmgRatio;
DmgRatio2:IDmgRatio;
}

Excel 表格结构

idDmgRatio1p1p2DmgRatio2p1p2
1DmgRatio111DmgRatio222

说明

  • 接口类型在配置中必须明确指定具体结构体类型
  • 每个接口字段占用 3 列:
    • 第 1 列:结构体类型名称
    • 第 2-3 列:结构体字段值
interface TestAttr (pack) {
struct Damage {
value:int;
}
struct Range {
value:int;
}
}
struct TestStruct (pack) {
playerAttr:int;
ratio:int;
}
table test[id] {
id:int; // 注释行
TestStruct:list<TestStruct> (pack);
attrs:list<TestAttr>(pack);
sepAttr:list<TestAttr>(sep=';');
}
idTestStructattrssepAttr
1(1,2),(2,4)Damage(50),Range(6)Damage(50);Range(6)

数据结构定义

interface CompleteCondition {
struct KillMonster {
monsterid:int;
count:int;
}
struct TalkNpc {
npcid:int;
}
struct CollectItem {
itemid:int;
count:int;
}
struct And {
cond1:CompleteCondition (pack);
cond2:CompleteCondition (pack);
}
}

任务表定义

table task...{
...
condition: CompleteCondition;
}

Excel 表格结构

conditionp1p2
KillMonster10011
AndTalkNpc(5)CollectItem(2002, 3)

关键说明

  • And 结构中的 cond1cond2 都设置了 (pack),每个字段占用 1 列
  • And 结构总共占用 2 列
  • 递归结构处理:对于形成循环引用的递归结构,必须至少在一处使用 pack 来打破循环,否则无法计算所需列数

映射方式适用类型占格规则主要用途
auto基本类型、结构体、接口自动计算默认映射,简单场景
pack结构体、接口、列表、映射压缩到 1 列减少列数,处理递归结构
sep结构体、列表压缩到 1 列自定义分隔符格式
fix列表、映射固定列数已知长度的列表
block列表、映射横向固定,纵向扩展变长列表垂直排列
  1. 优先使用 auto:对于简单结构,使用默认的自动映射
  2. 考虑使用 pack:当需要减少表格列数或处理递归结构时
  3. 谨慎使用 sep:除非有特定分隔符需求,否则推荐使用 pack
  4. 合理使用 fix:仅用于已知确切长度的列表
  5. 灵活使用 block:处理变长列表,注意嵌套规则
  6. 善用 mustFill:确保关键字段不为空,提高数据质量
  • 在设计复杂数据结构时,提前规划表格映射方式
  • 对于递归结构,确保至少有一处使用 pack 打破循环
  • 使用 mustFill 约束 block 首列(项标识)等关键字段,避免因首列为空导致意外的数据合并
  • 保持表格结构的清晰性和可维护性