Decode ILINIT condition and RECOVER schemas
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a *INIT data table to JSON. Auto-detects the table's shape.
|
||||
|
||||
*INIT scripts populate global arrays and work buffers with static game data. Seven shapes seen:
|
||||
*INIT scripts populate global arrays and work buffers with static game data. Eight shapes seen:
|
||||
|
||||
name — records keyed by a name string. Each record: set-string(name), static field writes,
|
||||
set-string(desc). Arrays indexed by record id in lockstep (+1/record).
|
||||
@@ -19,6 +19,9 @@
|
||||
banked —twenty parallel 1000-by-20 banks define sparse movement and battle routine
|
||||
step records, including provider joins and source overwrites. (RTINIT routines)
|
||||
|
||||
ILINIT is a special name-mode matrix: 30 reserved condition ids by five authored
|
||||
levels, joined to the runtime condition-state ABI and RECOVER policy.
|
||||
|
||||
Records are {id, name?, desc?, fields:{"0x<col_base>": value}} or, for footer tables,
|
||||
{id, global_addr, footer_off, values:[...]}. Column addresses are raw engine globals;
|
||||
confirmed names come from the generated engine global registry while raw keys remain provenance.
|
||||
@@ -529,6 +532,57 @@ VOCABULARY_RECORD_TABLE_BASE = 0x15A2A9
|
||||
VOCABULARY_RECORD_STRIDE = 3
|
||||
VOCABULARY_RECORD_SPAN = 200
|
||||
|
||||
CONDITION_RECORD_SPAN = 30
|
||||
CONDITION_LEVEL_COUNT = 5
|
||||
CONDITION_LEVEL_NAME_BASE = 0x25FA
|
||||
CONDITION_COLUMNS = {
|
||||
1: "instant_death",
|
||||
2: "hp_drain",
|
||||
3: "sp_drain",
|
||||
4: "fs_drain",
|
||||
5: "curse",
|
||||
6: "charm",
|
||||
7: "confusion",
|
||||
8: "paralysis",
|
||||
9: "poison",
|
||||
10: "water_flow",
|
||||
11: "fear",
|
||||
12: "reserved",
|
||||
13: "regeneration",
|
||||
14: "exaltation",
|
||||
}
|
||||
CONDITION_SCALAR_ARRAYS = {
|
||||
0xAAC78: "effectiveness_element_id",
|
||||
0xAAC96: "can_affect_bosses",
|
||||
0xAACB4: "cleared_by_recover",
|
||||
0xAACD2: "icon_id",
|
||||
}
|
||||
CONDITION_DURATION_BASE = 0xAACF0
|
||||
CONDITION_STAT_DELTA_BASE = 0xAAD86
|
||||
CONDITION_RESOURCE_DELTA_BASE = 0xAB3F8
|
||||
CONDITION_STAT_COLUMNS = (
|
||||
"accuracy",
|
||||
"evasion",
|
||||
"physical_attack",
|
||||
"physical_defense",
|
||||
"magic_attack",
|
||||
"magic_defense",
|
||||
"speed",
|
||||
"luck",
|
||||
"critical_chance",
|
||||
"capture_power",
|
||||
"movement",
|
||||
)
|
||||
CONDITION_RESOURCE_COLUMNS = ("hp", "sp", "fs")
|
||||
|
||||
RECOVER_CURRENT_ENTITY = 0x152616
|
||||
RECOVER_EFFECTIVE_STATS = 0x4E11B
|
||||
RECOVER_CURRENT_RESOURCES = 0x4E085
|
||||
RECOVER_CURRENT_LEVELS = 0x52383
|
||||
RECOVER_REMAINING_TURNS = 0x5295F
|
||||
RECOVER_BASELINE_LEVELS = 0x52F3B
|
||||
RECOVER_POLICY = 0xAACB4
|
||||
|
||||
|
||||
def resolve(name: str) -> Path:
|
||||
for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"):
|
||||
@@ -1199,6 +1253,280 @@ def extract_vocabulary(scr):
|
||||
}
|
||||
|
||||
|
||||
def extract_recovery_protocol(scr) -> dict:
|
||||
"""Validate and describe RECOVER's resource/condition reset ABI."""
|
||||
lookups_2d = {
|
||||
(ins.args[1][1], ins.args[3][1])
|
||||
for ins in scr.instructions
|
||||
if (
|
||||
sys4load.display_label(ins.opcode) == "lookup-array-2d"
|
||||
and len(ins.args) >= 5
|
||||
and ins.args[1][0] == T_GLOBAL_INT
|
||||
and ins.args[3][0] == T_IMM
|
||||
)
|
||||
}
|
||||
lookups_1d = {
|
||||
ins.args[1][1]
|
||||
for ins in scr.instructions
|
||||
if (
|
||||
sys4load.display_label(ins.opcode) == "lookup-array"
|
||||
and len(ins.args) >= 3
|
||||
and ins.args[1][0] == T_GLOBAL_INT
|
||||
)
|
||||
}
|
||||
loop_bounds = {
|
||||
ins.args[2][1]
|
||||
for ins in scr.instructions
|
||||
if (
|
||||
sys4load.display_label(ins.opcode) == "lt"
|
||||
and len(ins.args) >= 3
|
||||
and ins.args[2][0] == T_IMM
|
||||
)
|
||||
}
|
||||
calls = {
|
||||
ins.args[0][1]
|
||||
for ins in scr.instructions
|
||||
if sys4load.display_label(ins.opcode) == "call-script" and ins.args
|
||||
}
|
||||
required_2d = {
|
||||
(RECOVER_EFFECTIVE_STATS, 14),
|
||||
(RECOVER_CURRENT_RESOURCES, 3),
|
||||
(RECOVER_CURRENT_LEVELS, CONDITION_RECORD_SPAN),
|
||||
(RECOVER_REMAINING_TURNS, CONDITION_RECORD_SPAN),
|
||||
(RECOVER_BASELINE_LEVELS, CONDITION_RECORD_SPAN),
|
||||
}
|
||||
failures = []
|
||||
if not required_2d <= lookups_2d:
|
||||
failures.append(f"missing 2d lookups {sorted(required_2d - lookups_2d)}")
|
||||
if RECOVER_POLICY not in lookups_1d:
|
||||
failures.append("missing recovery-policy lookup")
|
||||
if not {3, CONDITION_RECORD_SPAN} <= loop_bounds:
|
||||
failures.append("missing resource or condition loop bound")
|
||||
if not {0x329D, 0x2ADE} <= calls:
|
||||
failures.append("missing CALCREVISE or DRAWCHP post-call")
|
||||
if failures:
|
||||
raise ValueError(f"{scr.path.name}: " + "; ".join(failures))
|
||||
|
||||
return {
|
||||
"source": scr.path.name,
|
||||
"current_entity_selector": f"0x{RECOVER_CURRENT_ENTITY:x}",
|
||||
"resource_restore": {
|
||||
"source_table": f"0x{RECOVER_EFFECTIVE_STATS:x}",
|
||||
"source_columns": ["max_hp", "max_sp", "max_fs"],
|
||||
"destination_table": f"0x{RECOVER_CURRENT_RESOURCES:x}",
|
||||
"destination_columns": ["current_hp", "current_sp", "current_fs"],
|
||||
},
|
||||
"condition_reset": {
|
||||
"column_count": CONDITION_RECORD_SPAN,
|
||||
"current_level_table": f"0x{RECOVER_CURRENT_LEVELS:x}",
|
||||
"baseline_level_table": f"0x{RECOVER_BASELINE_LEVELS:x}",
|
||||
"remaining_turns_table": f"0x{RECOVER_REMAINING_TURNS:x}",
|
||||
"recovery_policy_table": f"0x{RECOVER_POLICY:x}",
|
||||
"policy": (
|
||||
"For each active condition whose policy cell is nonzero, copy "
|
||||
"the equipment/passive baseline into the current level and set "
|
||||
"remaining turns to -1 when that baseline is nonzero, otherwise 0."
|
||||
),
|
||||
},
|
||||
"post_recovery_scripts": ["CALCREVISE.BIN", "DRAWCHP.BIN"],
|
||||
}
|
||||
|
||||
|
||||
def _condition_family_name(level_names: dict[int, str]) -> str | None:
|
||||
"""Collapse authored `name1`..`name5` strings to their shared family."""
|
||||
if not level_names:
|
||||
return None
|
||||
ordered = [level_names[level] for level in sorted(level_names)]
|
||||
prefixes = [
|
||||
text[:-1]
|
||||
for level, text in sorted(level_names.items())
|
||||
if text.endswith(str(level))
|
||||
]
|
||||
if len(prefixes) == len(ordered) and len(set(prefixes)) == 1:
|
||||
return prefixes[0]
|
||||
return ordered[0]
|
||||
|
||||
|
||||
def extract_condition_definitions(scr):
|
||||
"""Extract ILINIT's sparse 30-condition, five-level definition matrix."""
|
||||
level_names: dict[int, dict[int, str]] = collections.defaultdict(dict)
|
||||
records_by_id: dict[int, dict] = {}
|
||||
classified_writes = 0
|
||||
static_write_count = 0
|
||||
|
||||
def record_for(condition_id: int) -> dict:
|
||||
if not (1 <= condition_id < CONDITION_RECORD_SPAN):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: condition id outside reserved span: {condition_id}"
|
||||
)
|
||||
return records_by_id.setdefault(condition_id, {
|
||||
"id": condition_id,
|
||||
"condition": CONDITION_COLUMNS.get(condition_id, f"reserved_{condition_id}"),
|
||||
"string_fields": {},
|
||||
"fields": {},
|
||||
"record_fields": {},
|
||||
})
|
||||
|
||||
for ins in scr.instructions:
|
||||
if (
|
||||
ins.opcode == SET_STRING
|
||||
and len(ins.args) >= 2
|
||||
and ins.args[0][0] == T_GLOBAL_STRING
|
||||
):
|
||||
relative = ins.args[0][1] - CONDITION_LEVEL_NAME_BASE
|
||||
condition_id, level_index = divmod(relative, CONDITION_LEVEL_COUNT)
|
||||
if not (
|
||||
1 <= condition_id < CONDITION_RECORD_SPAN
|
||||
and 0 <= level_index < CONDITION_LEVEL_COUNT
|
||||
):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unexpected condition name destination "
|
||||
f"0x{ins.args[0][1]:x}"
|
||||
)
|
||||
text = scr.strings.get(ins.args[1][1], (None,))[0]
|
||||
level_names[condition_id][level_index + 1] = text
|
||||
_store_unique(
|
||||
record_for(condition_id)["string_fields"],
|
||||
(
|
||||
f"0x{CONDITION_LEVEL_NAME_BASE:x}/"
|
||||
f"{CONDITION_LEVEL_COUNT}/{level_index}"
|
||||
),
|
||||
text,
|
||||
condition_id,
|
||||
)
|
||||
continue
|
||||
|
||||
write = _static_global_write(ins)
|
||||
if write is None:
|
||||
continue
|
||||
static_write_count += 1
|
||||
destination, value = write
|
||||
matched = False
|
||||
for base in CONDITION_SCALAR_ARRAYS:
|
||||
condition_id = destination - base
|
||||
if 1 <= condition_id < CONDITION_RECORD_SPAN:
|
||||
_store_unique(
|
||||
record_for(condition_id)["fields"],
|
||||
f"0x{base:x}",
|
||||
value,
|
||||
condition_id,
|
||||
)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
layouts = (
|
||||
(CONDITION_DURATION_BASE, CONDITION_LEVEL_COUNT),
|
||||
(
|
||||
CONDITION_STAT_DELTA_BASE,
|
||||
CONDITION_LEVEL_COUNT * len(CONDITION_STAT_COLUMNS),
|
||||
),
|
||||
(
|
||||
CONDITION_RESOURCE_DELTA_BASE,
|
||||
CONDITION_LEVEL_COUNT * len(CONDITION_RESOURCE_COLUMNS),
|
||||
),
|
||||
)
|
||||
for base, stride in layouts:
|
||||
relative = destination - base
|
||||
condition_id, column = divmod(relative, stride)
|
||||
if 1 <= condition_id < CONDITION_RECORD_SPAN:
|
||||
_store_unique(
|
||||
record_for(condition_id)["record_fields"],
|
||||
f"0x{base:x}/{stride}/{column}",
|
||||
value,
|
||||
condition_id,
|
||||
)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unclassified static write 0x{destination:x}"
|
||||
)
|
||||
classified_writes += 1
|
||||
|
||||
records = []
|
||||
for condition_id in sorted(records_by_id):
|
||||
record = records_by_id[condition_id]
|
||||
names = level_names.get(condition_id, {})
|
||||
record["name"] = _condition_family_name(names)
|
||||
record["level_names"] = [
|
||||
names.get(level) for level in range(1, CONDITION_LEVEL_COUNT + 1)
|
||||
]
|
||||
levels = []
|
||||
for level in range(1, CONDITION_LEVEL_COUNT + 1):
|
||||
level_record = {"level": level}
|
||||
if level in names:
|
||||
level_record["name"] = names[level]
|
||||
|
||||
duration_key = (
|
||||
f"0x{CONDITION_DURATION_BASE:x}/{CONDITION_LEVEL_COUNT}/{level - 1}"
|
||||
)
|
||||
if duration_key in record["record_fields"]:
|
||||
level_record["duration_turns"] = record["record_fields"][duration_key]
|
||||
|
||||
stat_deltas = {}
|
||||
for stat_index, stat_name in enumerate(CONDITION_STAT_COLUMNS):
|
||||
column = (level - 1) * len(CONDITION_STAT_COLUMNS) + stat_index
|
||||
key = (
|
||||
f"0x{CONDITION_STAT_DELTA_BASE:x}/"
|
||||
f"{CONDITION_LEVEL_COUNT * len(CONDITION_STAT_COLUMNS)}/{column}"
|
||||
)
|
||||
if key in record["record_fields"]:
|
||||
stat_deltas[stat_name] = record["record_fields"][key]
|
||||
if stat_deltas:
|
||||
level_record["stat_deltas"] = stat_deltas
|
||||
|
||||
resource_deltas = {}
|
||||
for resource_index, resource_name in enumerate(CONDITION_RESOURCE_COLUMNS):
|
||||
column = (level - 1) * len(CONDITION_RESOURCE_COLUMNS) + resource_index
|
||||
key = (
|
||||
f"0x{CONDITION_RESOURCE_DELTA_BASE:x}/"
|
||||
f"{CONDITION_LEVEL_COUNT * len(CONDITION_RESOURCE_COLUMNS)}/{column}"
|
||||
)
|
||||
if key in record["record_fields"]:
|
||||
resource_deltas[resource_name] = record["record_fields"][key]
|
||||
if resource_deltas:
|
||||
level_record["resource_deltas"] = resource_deltas
|
||||
|
||||
if len(level_record) > 1:
|
||||
levels.append(level_record)
|
||||
record["levels"] = levels
|
||||
records.append(record)
|
||||
|
||||
defined_ids = [record["id"] for record in records]
|
||||
record_columns = sorted(
|
||||
{
|
||||
key
|
||||
for record in records
|
||||
for key in record.get("record_fields", {})
|
||||
},
|
||||
key=lambda key: tuple(int(part, 0) for part in key.split("/")),
|
||||
)
|
||||
return records, {
|
||||
"schema": "condition-definitions",
|
||||
"record_span": CONDITION_RECORD_SPAN,
|
||||
"level_count": CONDITION_LEVEL_COUNT,
|
||||
"condition_columns": {
|
||||
str(index): name for index, name in CONDITION_COLUMNS.items()
|
||||
},
|
||||
"defined_condition_ids": defined_ids,
|
||||
"reserved_condition_ids": [
|
||||
condition_id
|
||||
for condition_id in range(1, CONDITION_RECORD_SPAN)
|
||||
if condition_id not in defined_ids
|
||||
],
|
||||
"level_name_table": {
|
||||
"base": f"0x{CONDITION_LEVEL_NAME_BASE:x}",
|
||||
"stride": CONDITION_LEVEL_COUNT,
|
||||
},
|
||||
"record_field_columns": record_columns,
|
||||
"static_write_count": static_write_count,
|
||||
"classified_static_write_count": classified_writes,
|
||||
"recovery_protocol": extract_recovery_protocol(
|
||||
sys4load.load(resolve("RECOVER"))
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def extract_character_profiles(scr):
|
||||
"""Extract CIINIT's profile-id keyed character-information registry."""
|
||||
records = []
|
||||
@@ -2094,6 +2422,12 @@ def write_data_index(data_dir: Path) -> None:
|
||||
"name-keyed convenience view. Complete footer copies expose their row values there while",
|
||||
"raw keys and footer metadata remain intact as bytecode provenance.",
|
||||
"",
|
||||
"ILINIT's dedicated condition schema exposes thirteen authored condition ids in the",
|
||||
"reserved 30-by-5 layout. Each record keeps its raw scalar and row-table cells while",
|
||||
"joining level names, durations, eleven-stat deltas, three-resource deltas, boss/recovery",
|
||||
"policies, and icon ids. Top-level `recovery_protocol` validates RECOVER.BIN and links",
|
||||
"current levels, equipment/passive baselines, remaining turns, and full resource restore.",
|
||||
"",
|
||||
"Mixed-mode tables preserve the sparse selector id, branch offset, condition strings,",
|
||||
"scalar fields, cells within preallocated buffers, and length-prefixed footer arrays.",
|
||||
"STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`",
|
||||
@@ -2165,6 +2499,8 @@ def main() -> int:
|
||||
extractor = extract_character_profiles
|
||||
elif mode == "name" and name == "MAINIT":
|
||||
extractor = extract_magic_actions
|
||||
elif mode == "name" and name == "ILINIT":
|
||||
extractor = extract_condition_definitions
|
||||
recs, meta = extractor(scr)
|
||||
if mode == "name" and name in MESSAGE_TABLES:
|
||||
message_name = MESSAGE_TABLES[name]
|
||||
|
||||
@@ -107,11 +107,22 @@ def profile_columns(data: dict) -> list[dict]:
|
||||
"semantic_name": field_semantics.get(key),
|
||||
}, value, record)
|
||||
for address, value in record.get("string_fields", {}).items():
|
||||
base = int(address, 16)
|
||||
key = f"0x{base:x}"
|
||||
parts = address.split("/")
|
||||
base = int(parts[0], 16)
|
||||
if len(parts) == 1:
|
||||
stride = column = None
|
||||
key = f"0x{base:x}"
|
||||
kind = "string-field"
|
||||
elif len(parts) == 3:
|
||||
stride = int(parts[1])
|
||||
column = int(parts[2])
|
||||
key = f"0x{base:x}/{stride}/{column}"
|
||||
kind = "string-record-column"
|
||||
else:
|
||||
raise ValueError(f"bad string-field key: {address}")
|
||||
add(key, {
|
||||
"key": key, "kind": "string-field", "base": key,
|
||||
"stride": None, "column": None,
|
||||
"key": key, "kind": kind, "base": f"0x{base:x}",
|
||||
"stride": stride, "column": column,
|
||||
"semantic_name": field_semantics.get(key),
|
||||
}, value, record)
|
||||
for key, value in record.get("record_fields", {}).items():
|
||||
|
||||
@@ -911,6 +911,97 @@ def test_message_join() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_condition_definitions() -> None:
|
||||
scripts = paths.scripts()
|
||||
script = sys4load.load(scripts["ILINIT.BIN"])
|
||||
check(
|
||||
extract_init.detect_mode(script) == "name",
|
||||
"ILINIT remains compatible with name-mode auto-detection",
|
||||
)
|
||||
records, meta = extract_init.extract_condition_definitions(script)
|
||||
by_id = {record["id"]: record for record in records}
|
||||
check(
|
||||
list(by_id) == [*range(1, 12), 13, 14],
|
||||
"ILINIT extracts all thirteen authored condition ids",
|
||||
)
|
||||
check(
|
||||
meta["record_span"] == 30
|
||||
and meta["level_count"] == 5
|
||||
and meta["reserved_condition_ids"] == [12, *range(15, 30)],
|
||||
"ILINIT exposes its complete 30-by-5 reserved layout",
|
||||
)
|
||||
check(
|
||||
meta["static_write_count"] == 228
|
||||
and meta["classified_static_write_count"] == 228,
|
||||
"ILINIT classifies every static integer write",
|
||||
)
|
||||
check(
|
||||
by_id[1]["name"] == "即死"
|
||||
and by_id[2]["name"] == "HP吸"
|
||||
and by_id[14]["condition"] == "exaltation",
|
||||
"condition records preserve authored names and canonical ids",
|
||||
)
|
||||
check(
|
||||
by_id[2]["string_fields"]["0x25fa/5/0"] == "HP吸1"
|
||||
and by_id[2]["string_fields"]["0x25fa/5/4"] == "HP吸5",
|
||||
"condition level names retain raw base/stride/column provenance",
|
||||
)
|
||||
check(
|
||||
[level["duration_turns"] for level in by_id[6]["levels"]]
|
||||
== [2, 3, 4, 5, 6]
|
||||
and by_id[6]["fields"]["0xaacb4"] == 1
|
||||
and by_id[6]["fields"]["0xaacd2"] == 10,
|
||||
"charm exposes level durations, RECOVER policy, and icon id",
|
||||
)
|
||||
check(
|
||||
by_id[5]["levels"][4]["stat_deltas"]["accuracy"] == -25
|
||||
and by_id[14]["levels"][4]["stat_deltas"]["physical_attack"] == 10
|
||||
and by_id[14]["levels"][4]["stat_deltas"]["physical_defense"] == -10,
|
||||
"curse and exaltation expose their five-level stat matrices",
|
||||
)
|
||||
check(
|
||||
by_id[9]["levels"][4]["resource_deltas"]["hp"] == -5
|
||||
and by_id[13]["levels"][4]["resource_deltas"]["hp"] == 5,
|
||||
"poison and regeneration expose opposing periodic HP deltas",
|
||||
)
|
||||
|
||||
recovery = meta["recovery_protocol"]
|
||||
check(
|
||||
recovery["resource_restore"]["source_columns"]
|
||||
== ["max_hp", "max_sp", "max_fs"]
|
||||
and recovery["resource_restore"]["destination_columns"]
|
||||
== ["current_hp", "current_sp", "current_fs"],
|
||||
"RECOVER joins max-stat columns to current HP/SP/FS",
|
||||
)
|
||||
check(
|
||||
recovery["condition_reset"]["current_level_table"] == "0x52383"
|
||||
and recovery["condition_reset"]["baseline_level_table"] == "0x52f3b"
|
||||
and recovery["condition_reset"]["remaining_turns_table"] == "0x5295f"
|
||||
and recovery["condition_reset"]["recovery_policy_table"] == "0xaacb4",
|
||||
"RECOVER publishes the four-table condition reset protocol",
|
||||
)
|
||||
|
||||
semantics = extract_init.field_semantics(records)
|
||||
check(
|
||||
semantics["0xaacf0/5/4"]
|
||||
== "condition_duration_turns_by_level.level_5_turns"
|
||||
and semantics["0x25fa/5/4"] == "condition_level_names.level_5"
|
||||
and semantics["0xaad86/55/46"]
|
||||
== "condition_stat_deltas.level_5_physical_attack"
|
||||
and semantics["0xab3f8/15/12"]
|
||||
== "condition_resource_deltas.level_5_hp",
|
||||
"ILINIT raw cells join to level-aware condition semantics",
|
||||
)
|
||||
extract_init.attach_semantic_fields(records, semantics)
|
||||
check(
|
||||
by_id[6]["semantic_fields"]["condition_cleared_by_recover"] == 1
|
||||
and by_id[14]["semantic_fields"][
|
||||
"condition_stat_deltas.level_5_physical_attack"
|
||||
] == 10,
|
||||
"condition definitions retain raw addresses beside the semantic view",
|
||||
)
|
||||
|
||||
|
||||
def test_field_semantics() -> None:
|
||||
scripts = paths.scripts()
|
||||
items, _ = extract_init.extract_name(sys4load.load(scripts["ITINIT.BIN"]))
|
||||
@@ -1028,6 +1119,7 @@ if __name__ == "__main__":
|
||||
test_real_message_tables()
|
||||
test_message_infrastructure()
|
||||
test_message_join()
|
||||
test_condition_definitions()
|
||||
test_field_semantics()
|
||||
if FAILS:
|
||||
raise SystemExit(f"{len(FAILS)} failed checks")
|
||||
|
||||
@@ -15,9 +15,11 @@ def main() -> int:
|
||||
"field_semantics": {
|
||||
"0x10": "test_parallel",
|
||||
"0x30/3/0": "test_record.zero",
|
||||
"0x40/5/4": "test_names.level_5",
|
||||
},
|
||||
"records": [
|
||||
{"id": 1, "name": "one", "fields": {"0x10": 2, "0x20": 0},
|
||||
"string_fields": {"0x40/5/4": "One5"},
|
||||
"record_fields": {"0x30/3/0": 9},
|
||||
"message": {"title": "One", "description": "First",
|
||||
"furigana": [{"line": 0, "text": "One", "reading": "one"}]}},
|
||||
@@ -41,6 +43,10 @@ def main() -> int:
|
||||
assert rows["0x30/3/0"]["stride"] == 3
|
||||
assert rows["0x30/3/0"]["semantic_name"] == "test_record.zero"
|
||||
assert rows["0x30/3/2"]["column"] == 2
|
||||
assert rows["0x40/5/4"]["kind"] == "string-record-column"
|
||||
assert rows["0x40/5/4"]["stride"] == 5
|
||||
assert rows["0x40/5/4"]["column"] == 4
|
||||
assert rows["0x40/5/4"]["semantic_name"] == "test_names.level_5"
|
||||
|
||||
mixed_fixture = {
|
||||
"table": "MIXED",
|
||||
|
||||
Reference in New Issue
Block a user