Files
gplan-2/game/core/config_db.gd
T
DKChan 63a08d1e01 feat(t1-t3): 魔法道具数据面+库存管线+授予路由生命周期(三卡一轮交付——文件交叉合并提交)
T1 数据面:AuxItemTable schema(七字段+retired/deferred 两 additive 标记+真视窄域/board_wide 双承重护栏)+六行 tres(三有效+罗盘 retired 留痕+显影尘/全图卷 deferred)+ConfigDB 加载/访问器+RewardTable 十型扩值(aux_item)+三投放行 5/4/1+aux_none 负占位行(雷格极性字典兜底零兑付)+双表悬空校验(reward_dangling_aux,负占位豁免)+F2 投放维度覆盖(豁免面 plan/info/counter 登记)+两 fatal 夹具(expect=1 复制真表注入违规)+run_all manifest+2
T2 core 骨架:AuxItems 类(api 8 入口+probe 探针;零随机零特判;Used 不可逆;真视恒可用/预读点击级零信息拒绝;3x3 边缘窗口∩板)+action_economy.grant_ap 第 9 API(当轮加量,区别 queue_ap_delta 排队)
T3 授予路由+生命周期:_grant_routed aux 分支(正=入库存发「获得」行;负=零兑付 effect 型「道具随雷沉没」行)+use_aux 四步受理(相位 gate→validate→逐 kind 空间先 AP 后→consume;结算明细无道具行)+叠加层三清(装板点/战斗结束)+LevelRunner 关卡域库存持有注入+关卡末 reset+aux_cleared 快照计数扩签
冒烟锚随词表扩值更新:九型→十型封闭/字典 18→22 行;负向字典扫描三处稳健化(aux 不入五型轴+break 改 percent_map 三型齐备+rng 每板新实例——原第一板 break 掩盖 set_master_seed _counter 守卫违例)
闸:冒烟 577/0(净增 1)+单测 11 套全过(含两新夹具)+四探针 PASS
2026-09-29 07:07:38 +08:00

508 lines
21 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# ConfigDB autoload(数据配置)——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
# 切片注:headless --script 模式下由冒烟脚本显式实例化(数据配置 AC 前言);玩法代码禁止绕过本单点自建 load()。
extends Node
const SCHEMA_VERSION := 1
const TABLE_MANIFEST := {
"action_economy": "action_economy.tres",
"variation": "variation.tres",
"status_effects": "status_effects.tres",
"board_gen": "board_gen.tres",
"enemy": "enemy.tres",
"skill": "skills.tres",
"hero": "hero.tres",
"reward": "reward.tres",
"settlement": "settlement.tres",
"level": "level.tres",
"difficulty": "difficulty.tres",
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
"aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注)
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
}
var action_economy: ActionEconomyTable
var variation: VariationTable
var status_effects: StatusEffectTable
var board_gen: BoardGenTable
var enemy: EnemyTable
var skill: SkillTable
var hero: HeroTable
var reward: RewardTable
var settlement: SettlementTable
var level: LevelTable
var difficulty: DifficultyTable
# GP+商店房实施批(2026-09-21):ShopTable 暂持无类型注记——schema class_name 落籍归并行数据面,
# 落籍后可收紧为 `var shop: ShopTable`(消费面一律走下方 shop_* 访问器,禁直读行集)
var shop = null
# 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题)
var meta: MetaTable
var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器)
var adventure: AdventureTable
var is_ready := false
var _diagnostics: Array = [] # {severity, category, message}
var _warn_dedup: Dictionary = {} # R1 四元组去重注册表
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
func push_diag(severity: String, category: String, message: String) -> void:
_diagnostics.append({"severity": severity, "category": category, "message": message})
if severity == "warning":
push_warning("[ConfigDB/%s] %s" % [category, message])
else:
push_error("[ConfigDB/%s] %s" % [category, message])
# 运行期数据致命(2026-09-18 用户拍板:越域记 Fatal 级日志后立即退出——数据配置通道 fail-fast 纪律补齐到查询期)
func _fatal(category: String, message: String) -> void:
push_diag("fatal", category, message)
push_error("[ConfigDB/%s] FATAL: %s —— 数据配置致命,进程退出" % [category, message])
var main_loop := Engine.get_main_loop()
if main_loop is SceneTree:
main_loop.quit(1) # 帧末退出,退出码 1——调用方须立即 return,勿再消费返回值
else:
OS.crash("ConfigDB/%s fatal: %s" % [category, message])
func reset_diagnostics() -> void:
_diagnostics.clear()
_warn_dedup.clear()
func diagnostics() -> Array:
return _diagnostics.duplicate()
func diagnostics_count(severity: String) -> int:
var n := 0
for d in _diagnostics:
if d.severity == severity:
n += 1
return n
func _ready() -> void:
load_tables("res://data/tables", false)
func load_tables(root: String, collect_only: bool) -> bool:
is_ready = false
_diagnostics.clear()
_warn_dedup.clear()
var loaded := {}
for key in TABLE_MANIFEST.keys():
var path := root.path_join(TABLE_MANIFEST[key])
if not ResourceLoader.exists(path):
push_diag("error", "missing_file", "表文件缺失: %s" % path)
continue
var res: Resource = load(path)
if res == null:
push_diag("error", "load_failed", "表加载失败: %s" % path)
continue
loaded[key] = res
for e in res.validate():
push_diag("error", e.category, "%s → %s" % [key, e.message])
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)
_cross_validate(loaded)
var has_errors := diagnostics_count("error") > 0
if has_errors and not collect_only:
# 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建
return false
action_economy = loaded.get("action_economy")
variation = loaded.get("variation")
status_effects = loaded.get("status_effects")
board_gen = loaded.get("board_gen")
enemy = loaded.get("enemy")
skill = loaded.get("skill")
hero = loaded.get("hero")
reward = loaded.get("reward")
settlement = loaded.get("settlement")
level = loaded.get("level")
difficulty = loaded.get("difficulty")
shop = loaded.get("shop")
meta = loaded.get("meta")
aux_items = loaded.get("aux_items")
adventure = loaded.get("adventure")
is_ready = not has_errors
return not has_errors
func _cross_validate(loaded: Dictionary) -> void:
var st: StatusEffectTable = loaded.get("status_effects")
var rt: RewardTable = loaded.get("reward")
var st_tab: SettlementTable = loaded.get("settlement")
if st == null or rt == null or st_tab == null:
return # 单表校验已报错,不连锁
var effect_ids := {}
for row in st.rows:
effect_ids[row.get("effect_id", "")] = true
# ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
# GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
# (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
for row in rt.rows:
if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面
var eff: String = row.get("effect_id", "")
if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff):
push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
# ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id,
# 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
# 后仍投放(连带错误),同拦。
var aux_live := {}
var aux_tab = loaded.get("aux_items")
if aux_tab != null:
for arow in aux_tab.active_rows():
aux_live[String(arow.get("item_id", ""))] = true
for row in rt.rows:
if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE:
continue
var aux_eff: String = String(row.get("effect_id", ""))
if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验
continue
if not aux_live.has(aux_eff):
push_diag("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"])
# ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter}
# = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减
if aux_tab != null:
var covered := {}
for row in rt.rows:
if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE or int(row.get("weight", 0)) <= 0:
continue
var arow: Dictionary = aux_tab.row_by_id(String(row.get("effect_id", "")))
for tag in arow.get("dimension_tags", []):
covered[String(tag)] = true
var f2_exempt := {"plan": true, "info": true, "counter": true} # 豁免面(随 Alpha 重定/天赋批消减)
for d in AuxItemTable.DIMENSIONS:
if f2_exempt.has(d):
continue
if not covered.has(d):
push_diag("error", "dimension_coverage", "F2 投放维度覆盖缺 %s(豁免面外零活行投放)" % d)
# ② SettlementTable.effect_pool_map 悬空
for eff in st_tab.effect_pool_map.keys():
if not effect_ids.has(eff):
push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
# ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键)
var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"]
for s in required_sources:
if not st_tab.source_label_map.has(s):
push_diag("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
# ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行——
# 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦)
var lv: LevelTable = loaded.get("level")
var bg: BoardGenTable = loaded.get("board_gen")
var en: EnemyTable = loaded.get("enemy")
if lv != null and bg != null and en != null:
var bg_keys := {}
for row in bg.rows:
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
var en_keys := {}
for row in en.rows:
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
for lrow in lv.rows:
var li: int = int(lrow.level_index)
# 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes)
var node_lists: Array = []
var tpls: Array = lrow.get("templates", [])
if tpls.is_empty():
node_lists.append(lrow.get("nodes", []))
else:
for tpl in tpls:
node_lists.append(tpl.get("nodes", []))
for nodes_arr in node_lists:
for node in nodes_arr:
var t: String = String(node.get("type", ""))
if t != "battle" and t != "boss":
continue
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
if not bg_keys.has(key):
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
if not en_keys.has(key):
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
var skt = loaded.get("skill")
if skt != null and en != null:
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
push_diag("error", e.category, e.message)
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
func validate_skill_pool_links(skill_table, enemy_table, status_table = null) -> Array:
var errs: Array = []
var skill_types := {}
for row in skill_table.rows:
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
# 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接
for row in skill_table.rows:
var st: String = String(row.get("type", ""))
if st != "buff" and st != "debuff":
continue
var want_side: String = "enemy" if st == "buff" else "hero"
var eff: String = String(row.get("buff_ref", ""))
var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {}
if not srow.is_empty() and String(srow.get("target_side", "")) != want_side:
errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]})
for row in enemy_table.rows:
var eid: String = String(row.get("enemy_id", ""))
var basic_n := 0
var buff_n := 0
var debuff_n := 0
for entry in row.get("pool", []):
var sid: String = String(entry.get("skill_id", ""))
if not skill_types.has(sid):
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
continue
match skill_types[sid]:
"basic":
basic_n += 1
"buff":
buff_n += 1
"debuff":
debuff_n += 1
if basic_n != 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
if buff_n > 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
if debuff_n > 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]})
return errs
# ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)----
func resolve_board_gen(L: int, D: int) -> Dictionary:
return _resolve_progress(board_gen.rows, L, D, "board_gen")
func resolve_enemy_rowset(L: int, D: int) -> Dictionary:
return _resolve_progress(enemy.rows, L, D, "enemy")
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
if difficulty == null:
return {}
for row in difficulty.rows:
if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D:
return {
"hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0),
"atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0),
}
return {}
func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary:
if L < 1 or D < 0:
_fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
return {}
var level_keys: Array = []
for row in rows:
var lv := int(row.get("level_index", 0))
if not level_keys.has(lv):
level_keys.append(lv)
if level_keys.is_empty():
_fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id)
return {}
level_keys.sort()
var l_star := _ceil_key(level_keys, L)
var l_clamped := l_star < 0
if l_clamped:
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值)
var depth_keys: Array = []
for row in rows:
if int(row.get("level_index", 0)) == l_star:
var dv := int(row.get("node_depth", 0))
if not depth_keys.has(dv):
depth_keys.append(dv)
depth_keys.sort()
var d_star := _ceil_key(depth_keys, D)
var d_clamped := d_star < 0
if d_clamped:
d_star = int(depth_keys[-1])
var missing := ""
if l_clamped and d_clamped:
missing = "both_clamp"
elif l_clamped:
missing = "level_clamp"
elif d_clamped:
missing = "depth_clamp"
elif l_star != L and d_star != D:
missing = "both_ceil"
elif l_star != L:
missing = "level_ceil"
elif d_star != D:
missing = "depth_ceil"
if missing != "":
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star]
if not _warn_dedup.has(dedup_key):
_warn_dedup[dedup_key] = true
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing])
for row in rows:
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
return row
return {}
# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal
func _ceil_key(sorted_keys: Array, request: int) -> int:
for k in sorted_keys:
if int(k) >= request:
return int(k)
return -1
# ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐
# board_type 词表——biome_tags 系旧观念残留退役)----
func variant_pool(level_index: int, board_type: String) -> Array:
var pool: Array = []
for row in variation.rows:
if int(row.get("unlock_level", 999)) > level_index:
continue
var tags: Array = row.get("board_types", [])
if tags.is_empty():
pool.append(row)
continue
if tags.has(board_type):
pool.append(row)
return pool
func status_effect_row(effect_id: String) -> Dictionary:
return status_effects.row_by_id(effect_id)
# ---- GP+商店房实施批(2026-09-21):ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)----
# ── 魔法道具访问器(迭代 7 T1——消费面一律走访问器,禁直读行集)──
# 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径
func aux_active_rows() -> Array:
return aux_items.active_rows() if aux_items != null else []
func aux_item_row(item_id: String) -> Dictionary:
return aux_items.row_by_id(item_id) if aux_items != null else {}
# 活行词表(掉落投放 effect_id 白名单)
func aux_live_ids() -> Dictionary:
var out := {}
for row in aux_active_rows():
out[String(row.get("item_id", ""))] = true
return out
# 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} ·
# level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value,
# base_price, price_increment} · price_level_bonus {level_index, bonus}。
# 行族经 ShopTable 同名数组属性读取(.get 容缺——表落籍前查询恒空,掉落按 0 兑付的兜底在调用方)。
func shop_gp_drop_row(tier: String) -> Dictionary:
for row in _shop_rows("gp_drops"):
if String(row.get("tier", "")) == tier:
return row
return {}
func shop_level_gp_bonus(level_index: int) -> int:
for row in _shop_rows("level_gp_bonus"):
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
func shop_price_level_bonus(level_index: int) -> int:
for row in _shop_rows("price_level_bonus"):
if int(row.get("level_index", -1)) == level_index:
return int(row.get("bonus", 0))
return 0
func shop_products() -> Array:
return _shop_rows("products")
func shop_product_row(product_id: String) -> Dictionary:
for row in _shop_rows("products"):
if String(row.get("product_id", "")) == product_id:
return row
return {}
# 行族数组读取(ShopTable 属性名 = 行族名;表未落籍/属性缺失 = 空集非错误态)
func _shop_rows(family: String) -> Array:
if shop == null:
return []
var arr = shop.get(family)
return arr if arr is Array else []
# ---- 元进度实现批(2026-09-21):MetaTable 消费面(行集解析单点——MetaProgression/呈现层禁自扫表)----
# 行族四轴(数据契约,行集归 meta.tres,数值全 provisional 归元进度设计批配读数):
# forge_products {attr, display_name, flat_value, base_price, price_ratio_pct} ·
# boss_keystone {level_index, first_kill, repeat_kill} ·
# workshop_items {item_id, display_name, clear_threshold} ·
# conversion {attr, per_level_shift_pct}。
func meta_forge_row(attr: String) -> Dictionary:
for row in _meta_rows("forge_products"):
if String(row.get("attr", "")) == attr:
return row
return {}
func meta_boss_keystone_row(level_index: int) -> Dictionary:
for row in _meta_rows("boss_keystone"):
if int(row.get("level_index", -1)) == level_index:
return row
return {}
func meta_workshop_items() -> Array:
return _meta_rows("workshop_items")
func meta_conversion_row(attr: String) -> Dictionary:
for row in _meta_rows("conversion"):
if String(row.get("attr", "")) == attr:
return row
return {}
# 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态)
func _meta_rows(family: String) -> Array:
if meta == null:
return []
var arr = meta.get(family)
return arr if arr is Array else []
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
func skill_row(skill_id: String) -> Dictionary:
return skill.row_by_id(skill_id)
# ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
func set_reload_gate(gated: bool) -> void:
_reload_gate = gated
func reload() -> bool:
if _reload_gate:
push_diag("warning", "reload_gated", "战斗中热重载被拒绝(数据配置 EC8)")
return false
var ok := load_tables("res://data/tables", false)
if ok:
push_diag("debug", "config_reloaded", "表热重载完成")
return ok