Decode battle animation tables
This commit is contained in:
@@ -42,6 +42,11 @@ CDINIT2 is a special name-mode registry: 81 cards occupy one contiguous
|
||||
100-row definition block with story gates, item/event/point rewards, ranged
|
||||
HP/SP/FS and spirit effects, conditions, warp behavior, and visual assets.
|
||||
|
||||
BTANINIT2 is a special numeric-mode registry: 122 sparse battle animations
|
||||
occupy three reserved 1,000-row arrays for six effect ids, six start delays,
|
||||
and one total duration. BTANINIT dispatches 202 effect ids into BTL's six-slot
|
||||
visual/audio/hit-pulse work record.
|
||||
|
||||
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.
|
||||
@@ -70,6 +75,7 @@ T_GLOBAL_INT = 3
|
||||
T_GLOBAL_STRING = 5
|
||||
T_IMM = 0
|
||||
T_LOCAL_INT = 9
|
||||
T_LOCAL_PTR = 12
|
||||
|
||||
CURRENT_UNIT_ID = 0x66715
|
||||
CURRENT_UNIT_LEVELS = 0x6930
|
||||
@@ -755,6 +761,53 @@ CARD_TYPE_NAMES = {
|
||||
}
|
||||
CARD_RESOURCE_COLUMNS = ("hp", "sp", "fs")
|
||||
|
||||
BATTLE_ANIMATION_SELECTOR = 0x15288C
|
||||
BATTLE_ANIMATION_EFFECT_ID_BASE = 0x15288E
|
||||
BATTLE_ANIMATION_EFFECT_DELAY_BASE = 0x153FFE
|
||||
BATTLE_ANIMATION_DURATION_BASE = 0x15576E
|
||||
BATTLE_ANIMATION_CAPACITY = 1000
|
||||
BATTLE_ANIMATION_SLOT_COUNT = 6
|
||||
BATTLE_EFFECT_HIT_PULSE_COUNT = 3
|
||||
BATTLE_EFFECT_WORK_ARRAYS = {
|
||||
"visual_asset_id": (0x155B56, 1),
|
||||
"visual_mode_id": (0x155B5C, 1),
|
||||
"additive_blend_flag": (0x155B62, 1),
|
||||
"width": (0x155B68, 1),
|
||||
"height": (0x155B6E, 1),
|
||||
"atlas_column_count": (0x155B74, 1),
|
||||
"atlas_row_count": (0x155B7A, 1),
|
||||
"atlas_frame_count": (0x155B80, 1),
|
||||
"duration_ms": (0x155B86, 1),
|
||||
"combatant_anchor_flag": (0x155B8C, 1),
|
||||
"offset_x": (0x155B92, 1),
|
||||
"offset_y": (0x155B98, 1),
|
||||
"sound_asset_id": (0x155B9E, 1),
|
||||
"sound_delay_ms": (0x155BA4, 1),
|
||||
"hit_pulse_offsets_ms": (0x155BAA, BATTLE_EFFECT_HIT_PULSE_COUNT),
|
||||
}
|
||||
BATTLE_EFFECT_VISUAL_MODES = {
|
||||
0: "movie",
|
||||
1: "green_colorkey_sprite_sheet",
|
||||
2: "opaque_sprite_sheet",
|
||||
}
|
||||
BATTLE_EFFECT_SEMANTIC_NAMES = {
|
||||
"visual_asset_id": "battle_effect_visual_asset_ids",
|
||||
"visual_mode_id": "battle_effect_visual_mode_ids",
|
||||
"additive_blend_flag": "battle_effect_additive_blend_flags",
|
||||
"width": "battle_effect_widths",
|
||||
"height": "battle_effect_heights",
|
||||
"atlas_column_count": "battle_effect_atlas_column_counts",
|
||||
"atlas_row_count": "battle_effect_atlas_row_counts",
|
||||
"atlas_frame_count": "battle_effect_atlas_frame_counts",
|
||||
"duration_ms": "battle_effect_durations_ms",
|
||||
"combatant_anchor_flag": "battle_effect_combatant_anchor_flags",
|
||||
"offset_x": "battle_effect_offset_x_pixels",
|
||||
"offset_y": "battle_effect_offset_y_pixels",
|
||||
"sound_asset_id": "battle_effect_sound_asset_ids",
|
||||
"sound_delay_ms": "battle_effect_sound_delays_ms",
|
||||
"hit_pulse_offsets_ms": "battle_effect_hit_pulse_offsets_ms",
|
||||
}
|
||||
|
||||
|
||||
def resolve(name: str) -> Path:
|
||||
for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"):
|
||||
@@ -4664,6 +4717,682 @@ def extract_card_definitions(scr):
|
||||
}
|
||||
|
||||
|
||||
def extract_battle_effect_definitions(scr):
|
||||
"""Extract BTANINIT's effect-id dispatch into six-slot BTL work fields."""
|
||||
instructions = scr.instructions
|
||||
labels = [
|
||||
sys4load.display_label(ins.opcode) for ins in instructions
|
||||
]
|
||||
classified_offsets = set()
|
||||
|
||||
clear_layout = [
|
||||
(base, (
|
||||
BATTLE_ANIMATION_SLOT_COUNT * stride
|
||||
if stride > 1 else BATTLE_ANIMATION_SLOT_COUNT
|
||||
))
|
||||
for base, stride in BATTLE_EFFECT_WORK_ARRAYS.values()
|
||||
]
|
||||
if len(clear_layout) != 15:
|
||||
raise AssertionError("battle-effect work layout must have 15 arrays")
|
||||
for index, (base, count) in enumerate(clear_layout):
|
||||
ins = instructions[index]
|
||||
expected_args = [
|
||||
(T_GLOBAL_INT, base),
|
||||
(T_IMM, count),
|
||||
]
|
||||
if labels[index] != "copy-to-global" or ins.args != expected_args:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unexpected work clear at "
|
||||
f"0x{ins.offset:x}: {labels[index]} {ins.args!r}"
|
||||
)
|
||||
classified_offsets.add(ins.offset)
|
||||
|
||||
control_labels = (
|
||||
"mov",
|
||||
"jmp",
|
||||
"add",
|
||||
"lt",
|
||||
"jcc",
|
||||
"call",
|
||||
"jmp",
|
||||
"jmp",
|
||||
"lookup-array-2d",
|
||||
"mov",
|
||||
)
|
||||
control_start = len(clear_layout)
|
||||
for relative, expected_label in enumerate(control_labels):
|
||||
ins = instructions[control_start + relative]
|
||||
if labels[control_start + relative] != expected_label:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unexpected dispatch control at "
|
||||
f"0x{ins.offset:x}: expected {expected_label}, got "
|
||||
f"{labels[control_start + relative]}"
|
||||
)
|
||||
classified_offsets.add(ins.offset)
|
||||
|
||||
input_lookup = instructions[control_start + 8]
|
||||
expected_lookup_args = [
|
||||
(T_LOCAL_PTR, 0),
|
||||
(T_GLOBAL_INT, BATTLE_ANIMATION_EFFECT_ID_BASE),
|
||||
(T_GLOBAL_INT, BATTLE_ANIMATION_SELECTOR),
|
||||
(T_IMM, BATTLE_ANIMATION_SLOT_COUNT),
|
||||
(T_LOCAL_INT, 1),
|
||||
]
|
||||
if input_lookup.args != expected_lookup_args:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unexpected effect-id input lookup "
|
||||
f"{input_lookup.args!r}"
|
||||
)
|
||||
|
||||
cases = {}
|
||||
cursor = control_start + len(control_labels)
|
||||
while cursor < len(instructions) and labels[cursor] == "eq":
|
||||
compare = instructions[cursor]
|
||||
if (
|
||||
len(compare.args) != 3
|
||||
or compare.args[0] != (T_LOCAL_INT, 2)
|
||||
or compare.args[1] != (T_LOCAL_INT, 0)
|
||||
or compare.args[2][0] != T_IMM
|
||||
):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: malformed effect comparison at "
|
||||
f"0x{compare.offset:x}"
|
||||
)
|
||||
effect_id = compare.args[2][1]
|
||||
if effect_id in cases:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: duplicate effect id {effect_id}"
|
||||
)
|
||||
classified_offsets.add(compare.offset)
|
||||
cursor += 1
|
||||
|
||||
branch = instructions[cursor]
|
||||
if labels[cursor] != "jcc":
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: missing branch after effect "
|
||||
f"{effect_id}"
|
||||
)
|
||||
classified_offsets.add(branch.offset)
|
||||
cursor += 1
|
||||
|
||||
values = {}
|
||||
raw_fields = {}
|
||||
raw_record_fields = {}
|
||||
while labels[cursor] != "ret":
|
||||
lookup = instructions[cursor]
|
||||
if labels[cursor] not in {
|
||||
"lookup-array", "lookup-array-2d"
|
||||
}:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unexpected effect {effect_id} "
|
||||
f"instruction at 0x{lookup.offset:x}: "
|
||||
f"{labels[cursor]}"
|
||||
)
|
||||
cursor += 1
|
||||
write = instructions[cursor]
|
||||
if (
|
||||
labels[cursor] != "mov"
|
||||
or write.args[0] != (T_LOCAL_PTR, 0)
|
||||
or write.args[1][0] != T_IMM
|
||||
):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: malformed effect {effect_id} "
|
||||
f"write at 0x{write.offset:x}"
|
||||
)
|
||||
|
||||
base = lookup.args[1][1]
|
||||
field_name = next((
|
||||
name
|
||||
for name, (candidate_base, _) in (
|
||||
BATTLE_EFFECT_WORK_ARRAYS.items()
|
||||
)
|
||||
if candidate_base == base
|
||||
), None)
|
||||
if field_name is None:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: effect {effect_id} writes "
|
||||
f"unknown work base 0x{base:x}"
|
||||
)
|
||||
_, stride = BATTLE_EFFECT_WORK_ARRAYS[field_name]
|
||||
if stride == 1:
|
||||
if (
|
||||
labels[cursor - 1] != "lookup-array"
|
||||
or lookup.args != [
|
||||
(T_LOCAL_PTR, 0),
|
||||
(T_GLOBAL_INT, base),
|
||||
(T_LOCAL_INT, 1),
|
||||
]
|
||||
):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: malformed {field_name} lookup "
|
||||
f"for effect {effect_id}"
|
||||
)
|
||||
key = field_name
|
||||
raw_fields[f"0x{base:x}"] = write.args[1][1]
|
||||
else:
|
||||
if (
|
||||
labels[cursor - 1] != "lookup-array-2d"
|
||||
or lookup.args[:4] != [
|
||||
(T_LOCAL_PTR, 0),
|
||||
(T_GLOBAL_INT, base),
|
||||
(T_LOCAL_INT, 1),
|
||||
(T_IMM, stride),
|
||||
]
|
||||
or lookup.args[4][0] != T_IMM
|
||||
):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: malformed {field_name} lookup "
|
||||
f"for effect {effect_id}"
|
||||
)
|
||||
column = lookup.args[4][1]
|
||||
if not 0 <= column < stride:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: effect {effect_id} "
|
||||
f"{field_name} column {column} out of range"
|
||||
)
|
||||
key = f"{field_name}.{column}"
|
||||
raw_record_fields[
|
||||
f"0x{base:x}/{stride}/{column}"
|
||||
] = write.args[1][1]
|
||||
|
||||
_store_unique(
|
||||
values, key, write.args[1][1], effect_id
|
||||
)
|
||||
classified_offsets.update((lookup.offset, write.offset))
|
||||
cursor += 1
|
||||
|
||||
classified_offsets.add(instructions[cursor].offset)
|
||||
cursor += 1
|
||||
cases[effect_id] = {
|
||||
"values": values,
|
||||
"fields": raw_fields,
|
||||
"record_fields": raw_record_fields,
|
||||
}
|
||||
|
||||
expected_tail = (
|
||||
"comment",
|
||||
"instruction-marker-noop",
|
||||
"ret",
|
||||
"exit",
|
||||
)
|
||||
if tuple(labels[cursor:]) != expected_tail:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unexpected dispatch tail "
|
||||
f"{labels[cursor:]!r}"
|
||||
)
|
||||
classified_offsets.update(
|
||||
ins.offset for ins in instructions[cursor:]
|
||||
)
|
||||
if len(classified_offsets) != len(instructions):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: classified {len(classified_offsets)}/"
|
||||
f"{len(instructions)} instructions"
|
||||
)
|
||||
if cases.get(0, {}).get("values"):
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: effect id zero must remain the empty sentinel"
|
||||
)
|
||||
|
||||
asset_names = callscript_names()
|
||||
animation_references = collections.defaultdict(set)
|
||||
animation_scr = sys4load.load(resolve("BTANINIT2"))
|
||||
for ins in animation_scr.instructions:
|
||||
write = _static_global_write(ins)
|
||||
if write is None:
|
||||
continue
|
||||
destination, value = write
|
||||
index = destination - BATTLE_ANIMATION_EFFECT_ID_BASE
|
||||
if not (
|
||||
0 <= index
|
||||
< BATTLE_ANIMATION_CAPACITY * BATTLE_ANIMATION_SLOT_COUNT
|
||||
):
|
||||
continue
|
||||
animation_id, _ = divmod(
|
||||
index, BATTLE_ANIMATION_SLOT_COUNT
|
||||
)
|
||||
animation_references[value].add(animation_id)
|
||||
|
||||
records = []
|
||||
for effect_id, case in sorted(cases.items()):
|
||||
if effect_id == 0:
|
||||
continue
|
||||
values = case["values"]
|
||||
visual_asset_id = values["visual_asset_id"]
|
||||
visual_mode_id = values["visual_mode_id"]
|
||||
atlas = {}
|
||||
for field_name, output_name in (
|
||||
("atlas_column_count", "columns"),
|
||||
("atlas_row_count", "authored_rows"),
|
||||
("atlas_frame_count", "frame_count"),
|
||||
("duration_ms", "duration_ms"),
|
||||
):
|
||||
if field_name in values:
|
||||
atlas[output_name] = values[field_name]
|
||||
|
||||
sound_asset_id = values.get("sound_asset_id", 0)
|
||||
hit_pulses = [
|
||||
values.get(f"hit_pulse_offsets_ms.{column}", 0)
|
||||
for column in range(BATTLE_EFFECT_HIT_PULSE_COUNT)
|
||||
]
|
||||
record = {
|
||||
"id": effect_id,
|
||||
"visual_asset_id": visual_asset_id,
|
||||
"visual_asset_name": asset_names.get(
|
||||
visual_asset_id, ""
|
||||
),
|
||||
"visual_mode_id": visual_mode_id,
|
||||
"visual_mode": BATTLE_EFFECT_VISUAL_MODES.get(
|
||||
visual_mode_id, ""
|
||||
),
|
||||
"additive_blend": bool(
|
||||
values.get("additive_blend_flag", 0)
|
||||
),
|
||||
"width": values["width"],
|
||||
"height": values["height"],
|
||||
"anchor": (
|
||||
"slot_combatant"
|
||||
if values.get("combatant_anchor_flag", 0)
|
||||
else "battlefield_center"
|
||||
),
|
||||
"offset_x": values["offset_x"],
|
||||
"offset_y": values["offset_y"],
|
||||
"atlas": atlas,
|
||||
"sound_asset_id": sound_asset_id,
|
||||
"sound_asset_name": (
|
||||
asset_names.get(sound_asset_id, "")
|
||||
if sound_asset_id else ""
|
||||
),
|
||||
"sound_delay_ms": values.get("sound_delay_ms", 0),
|
||||
"hit_pulse_offsets_ms": [
|
||||
value for value in hit_pulses if value
|
||||
],
|
||||
"referenced_animation_ids": sorted(
|
||||
animation_references.get(effect_id, ())
|
||||
),
|
||||
"fields": case["fields"],
|
||||
"record_fields": case["record_fields"],
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
unreferenced = [
|
||||
record["id"]
|
||||
for record in records
|
||||
if not record["referenced_animation_ids"]
|
||||
]
|
||||
schema_field_semantics = {}
|
||||
semantic_array_names = {}
|
||||
for field_name, (base, stride) in (
|
||||
BATTLE_EFFECT_WORK_ARRAYS.items()
|
||||
):
|
||||
semantic_name = BATTLE_EFFECT_SEMANTIC_NAMES[field_name]
|
||||
semantic_array_names[f"0x{base:x}"] = semantic_name
|
||||
if stride == 1:
|
||||
schema_field_semantics[f"0x{base:x}"] = semantic_name
|
||||
else:
|
||||
for column in range(stride):
|
||||
schema_field_semantics[
|
||||
f"0x{base:x}/{stride}/{column}"
|
||||
] = f"{semantic_name}.pulse_{column + 1}"
|
||||
|
||||
return records, {
|
||||
"schema": "battle-effect-definitions",
|
||||
"effect_definition_count": len(records),
|
||||
"zero_effect_id_is_empty": True,
|
||||
"runtime_work_slot_count": BATTLE_ANIMATION_SLOT_COUNT,
|
||||
"hit_pulse_capacity_per_slot": BATTLE_EFFECT_HIT_PULSE_COUNT,
|
||||
"definition_write_count": sum(
|
||||
len(record["fields"]) + len(record["record_fields"])
|
||||
for record in records
|
||||
),
|
||||
"classified_instruction_count": len(classified_offsets),
|
||||
"visual_mode_counts": dict(sorted(collections.Counter(
|
||||
record["visual_mode"] for record in records
|
||||
).items())),
|
||||
"resolved_visual_asset_count": sum(
|
||||
bool(record["visual_asset_name"]) for record in records
|
||||
),
|
||||
"resolved_sound_asset_count": sum(
|
||||
bool(record["sound_asset_name"]) for record in records
|
||||
),
|
||||
"sound_effect_count": sum(
|
||||
bool(record["sound_asset_id"]) for record in records
|
||||
),
|
||||
"hit_pulse_effect_count": sum(
|
||||
bool(record["hit_pulse_offsets_ms"]) for record in records
|
||||
),
|
||||
"sprite_sheet_effect_count": sum(
|
||||
bool(record["atlas"]) for record in records
|
||||
),
|
||||
"engine_dead_atlas_row_count": sum(
|
||||
"authored_rows" in record["atlas"] for record in records
|
||||
),
|
||||
"referenced_effect_definition_count": (
|
||||
len(records) - len(unreferenced)
|
||||
),
|
||||
"unreferenced_effect_definition_ids": unreferenced,
|
||||
"array_layouts": {
|
||||
f"0x{base:x}": {"stride": stride}
|
||||
for base, stride in BATTLE_EFFECT_WORK_ARRAYS.values()
|
||||
if stride > 1
|
||||
},
|
||||
"schema_field_semantics": schema_field_semantics,
|
||||
"semantic_array_names": semantic_array_names,
|
||||
"consumer_contract": {
|
||||
"BTANINIT2.BIN": (
|
||||
"selects up to six effect ids and their start delays for "
|
||||
"each sparse battle-animation row"
|
||||
),
|
||||
"BTL.BIN": (
|
||||
"draws a movie or sprite-sheet surface, applies optional "
|
||||
"additive blending, anchors slots 0/1 to the actor and "
|
||||
"slots 2..5 to the target when requested, schedules WAV "
|
||||
"start, and consumes up to three hit-pulse offsets"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def extract_battle_animations(scr):
|
||||
"""Extract BTANINIT2's sparse 1000-row battle-animation timeline."""
|
||||
cells = {
|
||||
"effect_ids": {},
|
||||
"effect_start_delays_ms": {},
|
||||
"duration_ms": {},
|
||||
}
|
||||
classified_offsets = set()
|
||||
static_write_count = 0
|
||||
layouts = {
|
||||
"effect_ids": (
|
||||
BATTLE_ANIMATION_EFFECT_ID_BASE,
|
||||
BATTLE_ANIMATION_SLOT_COUNT,
|
||||
),
|
||||
"effect_start_delays_ms": (
|
||||
BATTLE_ANIMATION_EFFECT_DELAY_BASE,
|
||||
BATTLE_ANIMATION_SLOT_COUNT,
|
||||
),
|
||||
"duration_ms": (BATTLE_ANIMATION_DURATION_BASE, 1),
|
||||
}
|
||||
|
||||
for ins in scr.instructions:
|
||||
if sys4load.display_label(ins.opcode) == "exit":
|
||||
classified_offsets.add(ins.offset)
|
||||
continue
|
||||
write = _static_global_write(ins)
|
||||
if write is None:
|
||||
continue
|
||||
static_write_count += 1
|
||||
destination, value = write
|
||||
for field_name, (base, stride) in layouts.items():
|
||||
index = destination - base
|
||||
if not (
|
||||
0 <= index < BATTLE_ANIMATION_CAPACITY * stride
|
||||
):
|
||||
continue
|
||||
animation_id, column = divmod(index, stride)
|
||||
if animation_id == 0:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: writes reserved animation row zero "
|
||||
f"at 0x{ins.offset:x}"
|
||||
)
|
||||
_store_unique(
|
||||
cells[field_name],
|
||||
(animation_id, column),
|
||||
value,
|
||||
animation_id,
|
||||
)
|
||||
classified_offsets.add(ins.offset)
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unclassified animation write "
|
||||
f"0x{destination:x} at 0x{ins.offset:x}"
|
||||
)
|
||||
|
||||
unclassified = [
|
||||
f"0x{ins.offset:x}"
|
||||
for ins in scr.instructions
|
||||
if ins.offset not in classified_offsets
|
||||
]
|
||||
if unclassified:
|
||||
raise ValueError(
|
||||
f"{scr.path.name}: unclassified instructions "
|
||||
+ ", ".join(unclassified)
|
||||
)
|
||||
|
||||
effect_records, effect_meta = extract_battle_effect_definitions(
|
||||
sys4load.load(resolve("BTANINIT"))
|
||||
)
|
||||
effects_by_id = {
|
||||
record["id"]: record for record in effect_records
|
||||
}
|
||||
|
||||
skill_records, _ = extract_name(
|
||||
sys4load.load(resolve("SKINIT"))
|
||||
)
|
||||
skill_uses = collections.defaultdict(list)
|
||||
for record in skill_records:
|
||||
animation_id = record.get("fields", {}).get("0xaaa1e", 0)
|
||||
if animation_id:
|
||||
skill_uses[animation_id].append({
|
||||
"skill_id": record["id"],
|
||||
"skill_name": record["name"],
|
||||
})
|
||||
|
||||
item_records, _ = extract_name(
|
||||
sys4load.load(resolve("ITINIT"))
|
||||
)
|
||||
weapon_class_items = collections.defaultdict(list)
|
||||
for record in item_records:
|
||||
weapon_class = record.get("fields", {}).get("0xa6689", 0)
|
||||
if weapon_class:
|
||||
weapon_class_items[weapon_class].append({
|
||||
"item_id": record["id"],
|
||||
"item_name": record["name"],
|
||||
})
|
||||
|
||||
animation_ids = sorted({
|
||||
animation_id
|
||||
for field_cells in cells.values()
|
||||
for animation_id, _ in field_cells
|
||||
})
|
||||
records = []
|
||||
resolved_effect_reference_count = 0
|
||||
for animation_id in animation_ids:
|
||||
raw_record_fields = {}
|
||||
slots = []
|
||||
for slot in range(BATTLE_ANIMATION_SLOT_COUNT):
|
||||
effect_key = (animation_id, slot)
|
||||
if effect_key not in cells["effect_ids"]:
|
||||
continue
|
||||
effect_id = cells["effect_ids"][effect_key]
|
||||
delay_key = (animation_id, slot)
|
||||
start_delay_ms = cells[
|
||||
"effect_start_delays_ms"
|
||||
].get(delay_key, 0)
|
||||
effect = effects_by_id.get(effect_id, {})
|
||||
if effect:
|
||||
resolved_effect_reference_count += 1
|
||||
raw_record_fields[
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}/"
|
||||
f"{BATTLE_ANIMATION_SLOT_COUNT}/{slot}"
|
||||
] = effect_id
|
||||
if delay_key in cells["effect_start_delays_ms"]:
|
||||
raw_record_fields[
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}/"
|
||||
f"{BATTLE_ANIMATION_SLOT_COUNT}/{slot}"
|
||||
] = start_delay_ms
|
||||
slots.append({
|
||||
"slot": slot,
|
||||
"anchor_side": (
|
||||
"actor" if slot < 2 else "target"
|
||||
),
|
||||
"effect_id": effect_id,
|
||||
"start_delay_ms": start_delay_ms,
|
||||
"effect": {
|
||||
key: value
|
||||
for key, value in effect.items()
|
||||
if key not in {
|
||||
"fields",
|
||||
"record_fields",
|
||||
"referenced_animation_ids",
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
duration_key = (animation_id, 0)
|
||||
duration_ms = cells["duration_ms"].get(duration_key, 0)
|
||||
raw_fields = {}
|
||||
if duration_key in cells["duration_ms"]:
|
||||
raw_fields[
|
||||
f"0x{BATTLE_ANIMATION_DURATION_BASE:x}"
|
||||
] = duration_ms
|
||||
uses = []
|
||||
if 1 <= animation_id <= 21:
|
||||
uses.append("normal_attack_weapon_class")
|
||||
if skill_uses.get(animation_id):
|
||||
uses.append("skill")
|
||||
if animation_id == 809:
|
||||
uses.append("defeat")
|
||||
if not uses:
|
||||
uses.append("unjoined_authored")
|
||||
records.append({
|
||||
"id": animation_id,
|
||||
"duration_ms": duration_ms,
|
||||
"effect_slots": slots,
|
||||
"uses": uses,
|
||||
"skill_uses": skill_uses.get(animation_id, []),
|
||||
"weapon_class_items": weapon_class_items.get(
|
||||
animation_id, []
|
||||
) if 1 <= animation_id <= 21 else [],
|
||||
"fields": raw_fields,
|
||||
"record_fields": raw_record_fields,
|
||||
})
|
||||
|
||||
effect_reference_values = list(cells["effect_ids"].values())
|
||||
delay_columns = collections.Counter(
|
||||
column
|
||||
for _, column in cells["effect_start_delays_ms"]
|
||||
)
|
||||
effect_columns = collections.Counter(
|
||||
column for _, column in cells["effect_ids"]
|
||||
)
|
||||
unjoined_animation_ids = [
|
||||
record["id"] for record in records
|
||||
if record["uses"] == ["unjoined_authored"]
|
||||
]
|
||||
return records, {
|
||||
"schema": "battle-animation-timelines",
|
||||
"reserved_record_count": BATTLE_ANIMATION_CAPACITY,
|
||||
"authored_record_count": len(records),
|
||||
"effect_slots_per_record": BATTLE_ANIMATION_SLOT_COUNT,
|
||||
"effect_id_array_base": (
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}"
|
||||
),
|
||||
"effect_start_delay_array_base": (
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}"
|
||||
),
|
||||
"duration_array_base": (
|
||||
f"0x{BATTLE_ANIMATION_DURATION_BASE:x}"
|
||||
),
|
||||
"static_write_count": static_write_count,
|
||||
"classified_instruction_count": len(classified_offsets),
|
||||
"effect_reference_count": len(effect_reference_values),
|
||||
"distinct_effect_id_count": len(set(effect_reference_values)),
|
||||
"resolved_effect_reference_count": (
|
||||
resolved_effect_reference_count
|
||||
),
|
||||
"effect_slot_populations": {
|
||||
str(column): effect_columns.get(column, 0)
|
||||
for column in range(BATTLE_ANIMATION_SLOT_COUNT)
|
||||
},
|
||||
"delay_slot_populations": {
|
||||
str(column): delay_columns.get(column, 0)
|
||||
for column in range(BATTLE_ANIMATION_SLOT_COUNT)
|
||||
},
|
||||
"duration_cell_count": len(cells["duration_ms"]),
|
||||
"complete_timeline_count": sum(
|
||||
bool(record["duration_ms"]) for record in records
|
||||
),
|
||||
"auxiliary_timeline_count": sum(
|
||||
not record["duration_ms"] for record in records
|
||||
),
|
||||
"skill_reference_count": sum(
|
||||
len(record["skill_uses"]) for record in records
|
||||
),
|
||||
"skill_animation_count": sum(
|
||||
bool(record["skill_uses"]) for record in records
|
||||
),
|
||||
"weapon_class_animation_count": 21,
|
||||
"unjoined_authored_animation_ids": unjoined_animation_ids,
|
||||
"effect_definition_count": effect_meta[
|
||||
"effect_definition_count"
|
||||
],
|
||||
"unreferenced_effect_definition_ids": effect_meta[
|
||||
"unreferenced_effect_definition_ids"
|
||||
],
|
||||
"array_layouts": {
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}": {
|
||||
"stride": BATTLE_ANIMATION_SLOT_COUNT
|
||||
},
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}": {
|
||||
"stride": BATTLE_ANIMATION_SLOT_COUNT
|
||||
},
|
||||
},
|
||||
"schema_field_semantics": {
|
||||
**{
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}/"
|
||||
f"{BATTLE_ANIMATION_SLOT_COUNT}/{column}": (
|
||||
"battle_animation_effect_ids."
|
||||
f"effect_slot_{column}"
|
||||
)
|
||||
for column in range(BATTLE_ANIMATION_SLOT_COUNT)
|
||||
},
|
||||
**{
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}/"
|
||||
f"{BATTLE_ANIMATION_SLOT_COUNT}/{column}": (
|
||||
"battle_animation_effect_start_delays_ms."
|
||||
f"effect_slot_{column}"
|
||||
)
|
||||
for column in range(BATTLE_ANIMATION_SLOT_COUNT)
|
||||
},
|
||||
f"0x{BATTLE_ANIMATION_DURATION_BASE:x}": (
|
||||
"battle_animation_duration_ms"
|
||||
),
|
||||
},
|
||||
"semantic_array_names": {
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_ID_BASE:x}": (
|
||||
"battle_animation_effect_ids"
|
||||
),
|
||||
f"0x{BATTLE_ANIMATION_EFFECT_DELAY_BASE:x}": (
|
||||
"battle_animation_effect_start_delays_ms"
|
||||
),
|
||||
f"0x{BATTLE_ANIMATION_DURATION_BASE:x}": (
|
||||
"battle_animation_duration_ms"
|
||||
),
|
||||
},
|
||||
"consumer_contract": {
|
||||
"CALCDMG.BIN": (
|
||||
"selects a skill's animation id or the equipped item's "
|
||||
"weapon-class id for an ordinary attack"
|
||||
),
|
||||
"BTL.BIN": (
|
||||
"calls BTANINIT for the selected row, starts each populated "
|
||||
"effect at its six-slot delay, schedules audio and hit "
|
||||
"pulses, and uses the row duration to stage voice and HP "
|
||||
"interpolation; animation 809 is the hardcoded defeat row"
|
||||
),
|
||||
"SKINIT.BIN": (
|
||||
"provides 101 skill references across 98 distinct animation "
|
||||
"rows, including passive reaction rows 801 through 808"
|
||||
),
|
||||
"ITINIT.BIN": (
|
||||
"provides the weapon-class ids that select normal-attack "
|
||||
"animation rows 1 through 21"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def extract_card_generation_lists(scr):
|
||||
"""Extract CDINIT's selector-dispatched weighted card candidate lists."""
|
||||
instructions = scr.instructions
|
||||
@@ -5705,6 +6434,11 @@ def write_data_index(data_dir: Path) -> None:
|
||||
"six effect types, and raw array coordinates remain together; item, event, condition,",
|
||||
"and visual ids join through ITINIT, SCINIT, ILINIT, and SYS4INI resources.",
|
||||
"",
|
||||
"BTANINIT2's dedicated battle-animation schema exposes 122 sparse timelines in",
|
||||
"three reserved 1,000-row arrays: six effect ids, six start delays, and total",
|
||||
"duration. BTANINIT's paired schema decodes 202 effect ids into BTL's six-slot",
|
||||
"movie/sprite, blend, geometry, audio, and hit-pulse work record.",
|
||||
"",
|
||||
"MPINIT's dedicated terrain-atlas schema exposes 1,472 authored rows of a sparse",
|
||||
"53-column half-tile grid. It joins STINIT2's doubled tile-bound rectangles to 66",
|
||||
"stage definitions, preserves implicit-zero rows and raw footer provenance, and",
|
||||
@@ -5814,6 +6548,10 @@ def main() -> int:
|
||||
extractor = extract_card_definitions
|
||||
elif mode == "numeric" and name == "CDINIT":
|
||||
extractor = extract_card_generation_lists
|
||||
elif mode == "numeric" and name == "BTANINIT":
|
||||
extractor = extract_battle_effect_definitions
|
||||
elif mode == "numeric" and name == "BTANINIT2":
|
||||
extractor = extract_battle_animations
|
||||
elif mode == "numeric" and name == "SPINIT":
|
||||
extractor = extract_h_scene_gallery
|
||||
elif mode == "footer" and name == "MPINIT":
|
||||
|
||||
@@ -494,6 +494,90 @@ def profile_card_definitions(data: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def profile_battle_effect_definitions(data: dict) -> dict:
|
||||
"""Summarize BTANINIT's visual/audio/hit-pulse effect catalog."""
|
||||
if data.get("schema") != "battle-effect-definitions":
|
||||
return {}
|
||||
return {
|
||||
"effect_definition_count": data.get(
|
||||
"effect_definition_count", 0
|
||||
),
|
||||
"runtime_work_slot_count": data.get(
|
||||
"runtime_work_slot_count", 0
|
||||
),
|
||||
"visual_mode_counts": data.get("visual_mode_counts", {}),
|
||||
"resolved_visual_asset_count": data.get(
|
||||
"resolved_visual_asset_count", 0
|
||||
),
|
||||
"sound_effect_count": data.get("sound_effect_count", 0),
|
||||
"resolved_sound_asset_count": data.get(
|
||||
"resolved_sound_asset_count", 0
|
||||
),
|
||||
"sprite_sheet_effect_count": data.get(
|
||||
"sprite_sheet_effect_count", 0
|
||||
),
|
||||
"hit_pulse_effect_count": data.get(
|
||||
"hit_pulse_effect_count", 0
|
||||
),
|
||||
"referenced_effect_definition_count": data.get(
|
||||
"referenced_effect_definition_count", 0
|
||||
),
|
||||
"unreferenced_effect_definition_ids": data.get(
|
||||
"unreferenced_effect_definition_ids", []
|
||||
),
|
||||
"engine_dead_atlas_row_count": data.get(
|
||||
"engine_dead_atlas_row_count", 0
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def profile_battle_animations(data: dict) -> dict:
|
||||
"""Summarize BTANINIT2's sparse six-slot animation timelines."""
|
||||
if data.get("schema") != "battle-animation-timelines":
|
||||
return {}
|
||||
return {
|
||||
"authored_record_count": data.get(
|
||||
"authored_record_count", 0
|
||||
),
|
||||
"reserved_record_count": data.get(
|
||||
"reserved_record_count", 0
|
||||
),
|
||||
"effect_reference_count": data.get(
|
||||
"effect_reference_count", 0
|
||||
),
|
||||
"distinct_effect_id_count": data.get(
|
||||
"distinct_effect_id_count", 0
|
||||
),
|
||||
"resolved_effect_reference_count": data.get(
|
||||
"resolved_effect_reference_count", 0
|
||||
),
|
||||
"effect_slot_populations": data.get(
|
||||
"effect_slot_populations", {}
|
||||
),
|
||||
"delay_slot_populations": data.get(
|
||||
"delay_slot_populations", {}
|
||||
),
|
||||
"complete_timeline_count": data.get(
|
||||
"complete_timeline_count", 0
|
||||
),
|
||||
"auxiliary_timeline_count": data.get(
|
||||
"auxiliary_timeline_count", 0
|
||||
),
|
||||
"skill_reference_count": data.get(
|
||||
"skill_reference_count", 0
|
||||
),
|
||||
"skill_animation_count": data.get(
|
||||
"skill_animation_count", 0
|
||||
),
|
||||
"weapon_class_animation_count": data.get(
|
||||
"weapon_class_animation_count", 0
|
||||
),
|
||||
"unjoined_authored_animation_ids": data.get(
|
||||
"unjoined_authored_animation_ids", []
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def profile_messages(data: dict) -> dict:
|
||||
"""Summarize the joined player-facing message evidence."""
|
||||
records = data["records"]
|
||||
@@ -621,7 +705,54 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
|
||||
f"- records: {data['record_count']}",
|
||||
f"- populated fields: {len(rows)}",
|
||||
]
|
||||
if definition_profile := profile_card_definitions(data):
|
||||
if effect_profile := profile_battle_effect_definitions(data):
|
||||
lines.extend([
|
||||
f"- effect definitions: "
|
||||
f"{effect_profile['effect_definition_count']} for "
|
||||
f"{effect_profile['runtime_work_slot_count']} runtime slots",
|
||||
f"- visual modes: {effect_profile['visual_mode_counts']}",
|
||||
f"- visual assets resolved: "
|
||||
f"{effect_profile['resolved_visual_asset_count']}/"
|
||||
f"{effect_profile['effect_definition_count']}",
|
||||
f"- sound assets resolved: "
|
||||
f"{effect_profile['resolved_sound_asset_count']}/"
|
||||
f"{effect_profile['sound_effect_count']}",
|
||||
f"- sprite-sheet/hit-pulse effects: "
|
||||
f"{effect_profile['sprite_sheet_effect_count']}/"
|
||||
f"{effect_profile['hit_pulse_effect_count']}",
|
||||
f"- referenced definitions: "
|
||||
f"{effect_profile['referenced_effect_definition_count']}/"
|
||||
f"{effect_profile['effect_definition_count']}; unreferenced "
|
||||
f"{effect_profile['unreferenced_effect_definition_ids']}",
|
||||
f"- engine-dead authored atlas-row cells: "
|
||||
f"{effect_profile['engine_dead_atlas_row_count']}",
|
||||
])
|
||||
elif animation_profile := profile_battle_animations(data):
|
||||
lines.extend([
|
||||
f"- battle animations: "
|
||||
f"{animation_profile['authored_record_count']}/"
|
||||
f"{animation_profile['reserved_record_count']} rows",
|
||||
f"- effect references: "
|
||||
f"{animation_profile['effect_reference_count']} across "
|
||||
f"{animation_profile['distinct_effect_id_count']} definitions "
|
||||
f"({animation_profile['resolved_effect_reference_count']} "
|
||||
f"resolved)",
|
||||
f"- populated effect slots: "
|
||||
f"{animation_profile['effect_slot_populations']}",
|
||||
f"- populated delay slots: "
|
||||
f"{animation_profile['delay_slot_populations']}",
|
||||
f"- full/auxiliary timelines: "
|
||||
f"{animation_profile['complete_timeline_count']}/"
|
||||
f"{animation_profile['auxiliary_timeline_count']}",
|
||||
f"- SKINIT joins: "
|
||||
f"{animation_profile['skill_reference_count']} skills across "
|
||||
f"{animation_profile['skill_animation_count']} animations",
|
||||
f"- weapon-class rows: "
|
||||
f"{animation_profile['weapon_class_animation_count']}",
|
||||
f"- unjoined authored rows: "
|
||||
f"{animation_profile['unjoined_authored_animation_ids']}",
|
||||
])
|
||||
elif definition_profile := profile_card_definitions(data):
|
||||
lines.extend([
|
||||
f"- card definitions: {definition_profile['card_count']}/"
|
||||
f"{definition_profile['reserved_record_count']} rows",
|
||||
|
||||
@@ -1810,6 +1810,169 @@ def test_card_definitions() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_battle_effect_definitions() -> None:
|
||||
scripts = paths.scripts()
|
||||
script = sys4load.load(scripts["BTANINIT.BIN"])
|
||||
check(
|
||||
extract_init.detect_mode(script) == "numeric",
|
||||
"BTANINIT remains compatible with numeric-mode auto-detection",
|
||||
)
|
||||
records, meta = extract_init.extract_battle_effect_definitions(
|
||||
script
|
||||
)
|
||||
by_id = {record["id"]: record for record in records}
|
||||
check(
|
||||
len(records) == 202
|
||||
and meta["runtime_work_slot_count"] == 6
|
||||
and meta["hit_pulse_capacity_per_slot"] == 3
|
||||
and meta["zero_effect_id_is_empty"],
|
||||
"BTANINIT exposes 202 sparse effect ids and the six-slot work ABI",
|
||||
)
|
||||
check(
|
||||
meta["definition_write_count"] == 1917
|
||||
and meta["classified_instruction_count"] == 4472,
|
||||
"BTANINIT classifies every clear, branch, work write, and exit",
|
||||
)
|
||||
check(
|
||||
meta["visual_mode_counts"]
|
||||
== {
|
||||
"green_colorkey_sprite_sheet": 1,
|
||||
"movie": 186,
|
||||
"opaque_sprite_sheet": 15,
|
||||
}
|
||||
and meta["sprite_sheet_effect_count"] == 16,
|
||||
"BTANINIT separates movie and sprite-sheet effect paths",
|
||||
)
|
||||
check(
|
||||
by_id[1]["visual_asset_name"] == "MVB001.AGF"
|
||||
and by_id[1]["sound_asset_name"] == "A0124.WAV"
|
||||
and by_id[1]["additive_blend"]
|
||||
and by_id[1]["anchor"] == "slot_combatant"
|
||||
and by_id[1]["width"] == 280
|
||||
and by_id[1]["height"] == 352,
|
||||
"BTANINIT effect 1 joins its movie, sound, blend, and geometry",
|
||||
)
|
||||
check(
|
||||
by_id[24]["visual_asset_name"] == "AM994.AGF"
|
||||
and by_id[24]["visual_mode"]
|
||||
== "green_colorkey_sprite_sheet"
|
||||
and by_id[24]["atlas"]
|
||||
== {
|
||||
"columns": 4,
|
||||
"authored_rows": 4,
|
||||
"frame_count": 16,
|
||||
"duration_ms": 800,
|
||||
}
|
||||
and by_id[131]["visual_mode"] == "opaque_sprite_sheet"
|
||||
and by_id[131]["atlas"]["frame_count"] == 28,
|
||||
"BTANINIT preserves sprite-atlas geometry and duration",
|
||||
)
|
||||
check(
|
||||
by_id[915]["hit_pulse_offsets_ms"] == [100]
|
||||
and by_id[915]["record_fields"]["0x155baa/3/0"] == 100
|
||||
and meta["hit_pulse_effect_count"] == 26,
|
||||
"BTANINIT exposes BTL's hit-pulse timing rows",
|
||||
)
|
||||
check(
|
||||
meta["resolved_visual_asset_count"] == 202
|
||||
and meta["sound_effect_count"] == 199
|
||||
and meta["resolved_sound_asset_count"] == 199,
|
||||
"BTANINIT resolves every populated AGF and WAV resource",
|
||||
)
|
||||
check(
|
||||
meta["referenced_effect_definition_count"] == 186
|
||||
and meta["unreferenced_effect_definition_ids"]
|
||||
== [
|
||||
24, 806, 807, 901, 902, 903, 904, 905,
|
||||
906, 907, 910, 995, 996, 997, 998, 999,
|
||||
],
|
||||
"BTANINIT retains all sixteen unreferenced authored effects",
|
||||
)
|
||||
|
||||
|
||||
def test_battle_animations() -> None:
|
||||
scripts = paths.scripts()
|
||||
script = sys4load.load(scripts["BTANINIT2.BIN"])
|
||||
check(
|
||||
extract_init.detect_mode(script) == "numeric",
|
||||
"BTANINIT2 remains compatible with numeric-mode auto-detection",
|
||||
)
|
||||
records, meta = extract_init.extract_battle_animations(script)
|
||||
by_id = {record["id"]: record for record in records}
|
||||
check(
|
||||
len(records) == 122
|
||||
and meta["reserved_record_count"] == 1000
|
||||
and meta["effect_slots_per_record"] == 6,
|
||||
"BTANINIT2 exposes 122 sparse rows in its reserved timeline registry",
|
||||
)
|
||||
check(
|
||||
meta["static_write_count"] == 1017
|
||||
and meta["classified_instruction_count"] == 1018
|
||||
and meta["effect_reference_count"] == 573
|
||||
and meta["distinct_effect_id_count"] == 186
|
||||
and meta["resolved_effect_reference_count"] == 573,
|
||||
"BTANINIT2 classifies every write and resolves every effect id",
|
||||
)
|
||||
check(
|
||||
meta["effect_slot_populations"]
|
||||
== {
|
||||
"0": 114, "1": 114, "2": 113,
|
||||
"3": 113, "4": 0, "5": 119,
|
||||
}
|
||||
and meta["delay_slot_populations"]
|
||||
== {
|
||||
"0": 0, "1": 0, "2": 111,
|
||||
"3": 111, "4": 0, "5": 111,
|
||||
},
|
||||
"BTANINIT2 preserves its actor/target slot and delay geometry",
|
||||
)
|
||||
check(
|
||||
by_id[1]["duration_ms"] == 670
|
||||
and [
|
||||
(slot["slot"], slot["effect_id"], slot["start_delay_ms"])
|
||||
for slot in by_id[1]["effect_slots"]
|
||||
]
|
||||
== [
|
||||
(0, 958, 0),
|
||||
(1, 958, 0),
|
||||
(2, 1, 200),
|
||||
(3, 1, 200),
|
||||
(5, 921, 470),
|
||||
]
|
||||
and by_id[1]["weapon_class_items"]
|
||||
== [{"item_id": 901, "item_name": "素手"}],
|
||||
"BTANINIT2 row 1 joins the unarmed attack timeline and item class",
|
||||
)
|
||||
check(
|
||||
by_id[101]["skill_uses"]
|
||||
== [{"skill_id": 101, "skill_name": "急所射撃"}]
|
||||
and by_id[101]["effect_slots"][2]["effect"][
|
||||
"visual_asset_name"
|
||||
] == "MVB101.AGF"
|
||||
and meta["skill_reference_count"] == 101
|
||||
and meta["skill_animation_count"] == 98,
|
||||
"BTANINIT2 joins all SKINIT animation references",
|
||||
)
|
||||
check(
|
||||
by_id[801]["skill_uses"]
|
||||
== [{"skill_id": 33, "skill_name": "見切り"}]
|
||||
and by_id[801]["duration_ms"] == 0
|
||||
and by_id[809]["uses"] == ["defeat"]
|
||||
and by_id[809]["effect_slots"][0]["effect"][
|
||||
"visual_asset_name"
|
||||
] == "AM050.AGF"
|
||||
and meta["complete_timeline_count"] == 111
|
||||
and meta["auxiliary_timeline_count"] == 11,
|
||||
"BTANINIT2 distinguishes full timelines and passive/defeat auxiliaries",
|
||||
)
|
||||
check(
|
||||
meta["unjoined_authored_animation_ids"]
|
||||
== [205, 221, 222, 224]
|
||||
and by_id[205]["uses"] == ["unjoined_authored"],
|
||||
"BTANINIT2 preserves four authored rows without shipped consumers",
|
||||
)
|
||||
|
||||
|
||||
def test_condition_definitions() -> None:
|
||||
scripts = paths.scripts()
|
||||
script = sys4load.load(scripts["ILINIT.BIN"])
|
||||
@@ -2029,6 +2192,8 @@ if __name__ == "__main__":
|
||||
test_training_actions()
|
||||
test_card_generation_lists()
|
||||
test_card_definitions()
|
||||
test_battle_effect_definitions()
|
||||
test_battle_animations()
|
||||
test_map_terrain_atlas()
|
||||
test_condition_definitions()
|
||||
test_field_semantics()
|
||||
|
||||
@@ -103,6 +103,16 @@ def test_load_and_lint():
|
||||
and entries[0x4dfbb]["name"]
|
||||
== "stage_card_spendable_point_bonus",
|
||||
"CDINIT2/FIELD card-effect state is curated")
|
||||
check(entries[0x15288c]["name"] == "selected_battle_animation_id"
|
||||
and entries[0x15288e]["columns"]["4"]
|
||||
== "reserved_effect_slot_4"
|
||||
and entries[0x153ffe]["name"]
|
||||
== "battle_animation_effect_start_delays_ms"
|
||||
and entries[0x15576e]["name"] == "battle_animation_duration_ms"
|
||||
and entries[0x155b5c]["name"] == "battle_effect_visual_mode_ids"
|
||||
and entries[0x155b7a]["name"] == "battle_effect_atlas_row_counts"
|
||||
and entries[0x155baa]["columns"]["2"] == "pulse_3",
|
||||
"BTANINIT/BTANINIT2 battle-animation state is curated")
|
||||
check(entries[0x15a095]["name"] == "information_tab_index"
|
||||
and entries[0x15a096]["name"] == "information_message_handled"
|
||||
and entries[0x15a097]["name"]
|
||||
|
||||
@@ -350,6 +350,70 @@ def main() -> int:
|
||||
assert "- card definitions: 3/100 rows" in rendered_definitions
|
||||
assert "- engine-dead third required flags: 1" in rendered_definitions
|
||||
|
||||
effect_fixture = {
|
||||
"table": "EFFECTS",
|
||||
"mode": "numeric",
|
||||
"schema": "battle-effect-definitions",
|
||||
"record_count": 4,
|
||||
"effect_definition_count": 4,
|
||||
"runtime_work_slot_count": 6,
|
||||
"visual_mode_counts": {"movie": 3, "opaque_sprite_sheet": 1},
|
||||
"resolved_visual_asset_count": 4,
|
||||
"sound_effect_count": 3,
|
||||
"resolved_sound_asset_count": 3,
|
||||
"sprite_sheet_effect_count": 1,
|
||||
"hit_pulse_effect_count": 2,
|
||||
"referenced_effect_definition_count": 3,
|
||||
"unreferenced_effect_definition_ids": [999],
|
||||
"engine_dead_atlas_row_count": 1,
|
||||
"records": [{}, {}, {}, {}],
|
||||
}
|
||||
effect_summary = profile.profile_battle_effect_definitions(
|
||||
effect_fixture
|
||||
)
|
||||
assert effect_summary["effect_definition_count"] == 4
|
||||
assert effect_summary["visual_mode_counts"]["movie"] == 3
|
||||
rendered_effects = profile.render_markdown(
|
||||
effect_fixture, [], 40
|
||||
)
|
||||
assert "- effect definitions: 4 for 6 runtime slots" in rendered_effects
|
||||
assert "- sound assets resolved: 3/3" in rendered_effects
|
||||
|
||||
animation_fixture = {
|
||||
"table": "ANIMATIONS",
|
||||
"mode": "numeric",
|
||||
"schema": "battle-animation-timelines",
|
||||
"record_count": 3,
|
||||
"authored_record_count": 3,
|
||||
"reserved_record_count": 1000,
|
||||
"effect_reference_count": 12,
|
||||
"distinct_effect_id_count": 8,
|
||||
"resolved_effect_reference_count": 12,
|
||||
"effect_slot_populations": {
|
||||
"0": 2, "1": 2, "2": 2, "3": 2, "4": 0, "5": 4,
|
||||
},
|
||||
"delay_slot_populations": {
|
||||
"0": 0, "1": 0, "2": 2, "3": 2, "4": 0, "5": 2,
|
||||
},
|
||||
"complete_timeline_count": 2,
|
||||
"auxiliary_timeline_count": 1,
|
||||
"skill_reference_count": 2,
|
||||
"skill_animation_count": 2,
|
||||
"weapon_class_animation_count": 21,
|
||||
"unjoined_authored_animation_ids": [205],
|
||||
"records": [{}, {}, {}],
|
||||
}
|
||||
animation_summary = profile.profile_battle_animations(
|
||||
animation_fixture
|
||||
)
|
||||
assert animation_summary["authored_record_count"] == 3
|
||||
assert animation_summary["auxiliary_timeline_count"] == 1
|
||||
rendered_animations = profile.render_markdown(
|
||||
animation_fixture, [], 40
|
||||
)
|
||||
assert "- battle animations: 3/1000 rows" in rendered_animations
|
||||
assert "- full/auxiliary timelines: 2/1" in rendered_animations
|
||||
|
||||
training_fixture = {
|
||||
"table": "TRAINING",
|
||||
"mode": "name",
|
||||
|
||||
Reference in New Issue
Block a user