From 60a6d88c668ad95aab8131e0d317ba33a017dcd3 Mon Sep 17 00:00:00 2001 From: Brandon Johnson Date: Sat, 31 Jan 2026 12:32:37 -0500 Subject: [PATCH] some more work on the custom pack builder --- .../dump_vanilla_conf.cpython-311.pyc | Bin 0 -> 26130 bytes .../dump_vanilla_conf.cpython-312.pyc | Bin 0 -> 22751 bytes tools/dump_vanilla_conf.py | 717 ++++++++++++++++++ tools/dump_vanilla_conf_original.py | 497 ++++++++++++ tools/smb2_pack_builder.py | 266 ++++++- 5 files changed, 1476 insertions(+), 4 deletions(-) create mode 100644 tools/__pycache__/dump_vanilla_conf.cpython-311.pyc create mode 100644 tools/__pycache__/dump_vanilla_conf.cpython-312.pyc create mode 100644 tools/dump_vanilla_conf.py create mode 100644 tools/dump_vanilla_conf_original.py diff --git a/tools/__pycache__/dump_vanilla_conf.cpython-311.pyc b/tools/__pycache__/dump_vanilla_conf.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ea81e75304bfd83382eff18133e7cccee5d6f83 GIT binary patch literal 26130 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 literal 0 HcmV?d00001 diff --git a/tools/__pycache__/dump_vanilla_conf.cpython-312.pyc b/tools/__pycache__/dump_vanilla_conf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4887aa3366a3c014b9b03521626bab2c444e3b3 GIT binary patch literal 22751 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 len(stage_id_to_theme_id): + logging.warning("Stage id %s out of range for theme map; using theme 0", stage_id) + theme_id = 0 + else: + theme_id = stage_id_to_theme_id[stage_id] + if theme_id > 42: + theme_id = 42 + if theme_id < 0 or theme_id >= len(theme_id_to_music_id): + logging.warning("Theme id %s out of range for music map; using 0", theme_id) + music_id = 0 + else: + music_id = theme_id_to_music_id[theme_id] + return (theme_id, music_id) + + +def parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + start, + count=None, + max_cmds=1024, + strict=True, +): + def raise_error(message: str): + logging.error(message) + if strict: + raise SystemExit(message) + raise ValueError(message) + + cmds: list[CourseCommand] = [] + course_cmd_size = 0x1C + + if count is None: + i = 0 + while start + (i + 1) * course_cmd_size <= len(mainloop_buffer) and i < max_cmds: + course_cmd = CourseCommand._make( + struct.unpack_from( + ">BBxxI20x", + mainloop_buffer, + start + i * course_cmd_size, + ) + ) + cmds.append(course_cmd) + if course_cmd.opcode == CMD_COURSE_END: + break + i += 1 + else: + for i in range(count): + course_cmd = CourseCommand._make( + struct.unpack_from( + ">BBxxI20x", + mainloop_buffer, + start + i * course_cmd_size, + ) + ) + cmds.append(course_cmd) + + # Course commands to stage infos + cm_stage_infos = [] + stage_id = 0 + stage_time = 60 * 60 + blue_jump = None + green_jump = None + red_jump = None + last_goal_type = None + first = True + finished = False + + for cmd in cmds: + if cmd.opcode == CMD_FLOOR: + if cmd.type == FLOOR_STAGE_ID: + if not first: + if blue_jump is None: + raise_error("Invalid blue goal jump") + + theme_id, music_id = get_theme_and_music_ids( + stage_id, stage_id_to_theme_id_map, theme_id_to_music_id_map + ) + + stage_name = ( + stgname_lines[stage_id] + if 0 <= stage_id < len(stgname_lines) + else f"Stage {stage_id}" + ) + cm_stage_infos.append( + { + "stage_id": stage_id, + "name": stage_name, + "theme_id": theme_id, + "music_id": music_id, + "time_limit": float(stage_time / 60), + "blue_goal_jump": blue_jump, + "green_goal_jump": green_jump + if green_jump is not None + else blue_jump, + "red_goal_jump": red_jump + if red_jump is not None + else blue_jump, + "is_bonus_stage": stage_id in bonus_stage_ids, + } + ) + stage_id = 0 + stage_time = 60 * 60 + blue_jump = None + green_jump = None + red_jump = None + last_goal_type = None + + stage_id = cmd.value + first = False + + elif cmd.type == FLOOR_TIME: + stage_time = cmd.value + else: + raise_error(f"Invalid CMD_FLOOR opcode type: {cmd.type}") + + elif cmd.opcode == CMD_IF: + if cmd.type == IF_FLOOR_CLEAR: + last_goal_type = None + elif cmd.type == IF_GOAL_TYPE: + last_goal_type = cmd.value + else: + raise_error(f"Invalid CMD_IF opcode type: {cmd.type}") + + elif cmd.opcode == CMD_THEN: + if cmd.type == THEN_JUMP_FLOOR: + if last_goal_type is None: + if blue_jump is None: + blue_jump = cmd.value + if green_jump is None: + green_jump = cmd.value + if red_jump is None: + red_jump = cmd.value + elif last_goal_type == 0: + blue_jump = cmd.value + elif last_goal_type == 1: + green_jump = cmd.value + elif last_goal_type == 2: + red_jump = cmd.value + else: + raise_error(f"Invalid last goal type: {last_goal_type}") + elif cmd.type == THEN_END_COURSE: + # Jumps are irrelevant, this is end of difficulty + blue_jump = 1 + green_jump = 1 + red_jump = 1 + else: + raise_error(f"Invalid CMD_THEN opcode type: {cmd.type}") + + elif cmd.opcode == CMD_COURSE_END: + if blue_jump is None: + raise_error("Invalid blue goal jump") + theme_id, music_id = get_theme_and_music_ids( + stage_id, stage_id_to_theme_id_map, theme_id_to_music_id_map + ) + stage_name = ( + stgname_lines[stage_id] + if 0 <= stage_id < len(stgname_lines) + else f"Stage {stage_id}" + ) + cm_stage_infos.append( + { + "stage_id": stage_id, + "name": stage_name, + "theme_id": theme_id, + "music_id": music_id, + "time_limit": float(stage_time / 60), + "blue_goal_jump": blue_jump, + "green_goal_jump": green_jump + if green_jump is not None + else blue_jump, + "red_goal_jump": red_jump if red_jump is not None else blue_jump, + "is_bonus_stage": stage_id in bonus_stage_ids, + } + ) + finished = True + + else: + raise_error(f"Invalid opcode: {cmd.opcode}") + + if not finished: + raise_error("Course command list ended early") + + return cm_stage_infos + + +def annotate_cm_layout_dump(dump: str) -> str: + lines = dump.split("\n") + out_lines: list[str] = [] + + last_course = None + floor_num = 1 + for line in lines: + + old_floor_num = floor_num + floor_num = 1 + if '"beginner"' in line: + last_course = "Beginner" + elif '"beginner_extra"' in line: + last_course = "Beginner Extra" + elif '"advanced"' in line: + last_course = "Advanced" + elif '"advanced_extra"' in line: + last_course = "Advanced Extra" + elif '"expert"' in line: + last_course = "Expert" + elif '"expert_extra"' in line: + last_course = "Expert Extra" + elif '"master"' in line: + last_course = "Master" + elif '"master_extra"' in line: + last_course = "Master Extra" + else: + # Don't reset floor num if new difficulty not detected + floor_num = old_floor_num + + new_line = line[:] + if "{" in new_line and last_course is not None: + new_line += f" // {last_course} {floor_num}" + floor_num += 1 + + new_line = new_line.replace("60.0", "60.00") + new_line = new_line.replace("30.0", "30.00") + + out_lines.append(new_line) + + # if '"time_limit"' in new_line: + # out_lines.append("") + + return "\n".join(out_lines) + + +def dump_storymode_world_layout( + mainloop_buffer, + stgname_lines, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + start, +): + stage_info_size = 0x4 + + stage_infos: list[SmStageInfo] = [] + for i in range(10): + offs = start + i * stage_info_size + stage_info = SmStageInfo._make(struct.unpack_from(">hh", mainloop_buffer, offs)) + stage_infos.append(stage_info) + + out_json_array = [] + for stage_info in stage_infos: + time_limit = 60 * 60 if stage_info.stage_id != 30 else 60 * 30 + theme_id, music_id = get_theme_and_music_ids( + stage_info.stage_id, stage_id_to_theme_id_map, theme_id_to_music_id_map + ) + stage_name = ( + stgname_lines[stage_info.stage_id] + if 0 <= stage_info.stage_id < len(stgname_lines) + else f"Stage {stage_info.stage_id}" + ) + out_json_array.append( + { + "stage_id": stage_info.stage_id, + "name": stage_name, + "theme_id": theme_id, + "music_id": music_id, + "time_limit": float(time_limit / 60), + "difficulty": stage_info.difficulty, + } + ) + + return out_json_array + + +def annotate_story_layout_dump(dump: str) -> str: + lines = dump.split("\n") + out_lines: list[str] = [] + + last_course = None + world = -1 + stage = 0 + for line in lines: + new_line = line[:] + + if "[" in line: + world += 1 + stage = 0 + if world >= 1: + new_line += f" // World {world}" + if "{" in line: + stage += 1 + new_line += f" // Stage {world}-{stage}" + + new_line = new_line.replace("60.0", "60.00") + new_line = new_line.replace("30.0", "30.00") + + out_lines.append(new_line) + + # if '"time_limit"' in new_line: + # out_lines.append("") + + return "\n".join(out_lines) + + +def list_stage_ids(stage_dir: Path) -> Set[int]: + ids: Set[int] = set() + if not stage_dir.exists(): + return ids + for path in stage_dir.glob("STAGE*.lz"): + name = path.name + if len(name) == 11 and name.startswith("STAGE") and name.endswith(".lz"): + try: + ids.add(int(name[5:8])) + except ValueError: + continue + return ids + + +def collect_stage_ids_from_cm(cm_layout: Dict[str, List[dict]]) -> List[int]: + ids: List[int] = [] + for entries in cm_layout.values(): + if not isinstance(entries, list): + continue + for entry in entries: + if isinstance(entry, dict) and isinstance(entry.get("stage_id"), int): + ids.append(entry["stage_id"]) + return ids + + +def collect_stage_ids_from_story(worlds: List[List[dict]]) -> List[int]: + ids: List[int] = [] + for world in worlds: + if not isinstance(world, list): + continue + for entry in world: + if isinstance(entry, dict) and isinstance(entry.get("stage_id"), int): + ids.append(entry["stage_id"]) + return ids + + +def validate_stage_ids( + stage_ids: List[int], + valid_ids: Set[int], + label: str, + named_ids: Optional[Set[int]] = None, + min_named_ratio: float = 0.0, +) -> bool: + if not stage_ids or not valid_ids: + return True + if any(stage_id < 0 for stage_id in stage_ids): + logging.warning("%s contains negative stage ids", label) + return False + invalid = [sid for sid in stage_ids if sid not in valid_ids] + if not invalid: + if named_ids and min_named_ratio > 0: + named_count = sum(1 for sid in stage_ids if sid in named_ids) + ratio = named_count / max(1, len(stage_ids)) + if ratio < min_named_ratio: + logging.warning("%s has low named stage ratio (%.1f%%)", label, ratio * 100) + return False + return True + ratio = len(invalid) / max(1, len(stage_ids)) + logging.warning("%s has %d invalid stage ids (%.1f%%)", label, len(invalid), ratio * 100) + return ratio < 0.1 + + +def find_course_offsets( + data: bytes, + stage_ids: Set[int], + named_stage_ids: Set[int], + min_stages: int = 10, + max_cmds: int = 512, +) -> List[Tuple[int, int]]: + course_cmd_size = 0x1C + candidates: List[Tuple[int, int, float]] = [] + for off in range(0, len(data) - course_cmd_size, 4): + opcode = data[off] + cmd_type = data[off + 1] + if opcode != CMD_FLOOR or cmd_type != FLOOR_STAGE_ID: + continue + stage_count = 0 + valid_stage_count = 0 + cmd_count = 0 + finished = False + for i in range(max_cmds): + cmd_off = off + i * course_cmd_size + if cmd_off + course_cmd_size > len(data): + break + opcode = data[cmd_off] + cmd_type = data[cmd_off + 1] + value = struct.unpack_from(">I", data, cmd_off + 4)[0] + cmd_count += 1 + if opcode == CMD_FLOOR: + if cmd_type == FLOOR_STAGE_ID: + stage_count += 1 + if value in stage_ids: + valid_stage_count += 1 + elif cmd_type != FLOOR_TIME: + break + elif opcode == CMD_IF: + if cmd_type not in (IF_FLOOR_CLEAR, IF_GOAL_TYPE): + break + elif opcode == CMD_THEN: + if cmd_type not in (THEN_JUMP_FLOOR, THEN_END_COURSE): + break + elif opcode == CMD_COURSE_END: + finished = True + break + else: + break + if not finished or stage_count < min_stages: + continue + ratio = valid_stage_count / max(1, stage_count) + named_count = 0 + if named_stage_ids: + for i in range(max_cmds): + cmd_off = off + i * course_cmd_size + if cmd_off + course_cmd_size > len(data): + break + opcode = data[cmd_off] + cmd_type = data[cmd_off + 1] + if opcode == CMD_FLOOR and cmd_type == FLOOR_STAGE_ID: + value = struct.unpack_from(">I", data, cmd_off + 4)[0] + if value in named_stage_ids: + named_count += 1 + named_ratio = named_count / max(1, stage_count) + else: + named_ratio = 0.0 + score = stage_count * ratio - cmd_count * 0.05 + named_ratio + candidates.append((off, cmd_count, score)) + + candidates.sort(key=lambda item: (-item[2], item[0])) + selected: List[Tuple[int, int]] = [] + used_ranges: List[Tuple[int, int]] = [] + for off, cmd_count, _ in candidates: + start = off + end = off + cmd_count * course_cmd_size + if any(start < rng_end and end > rng_start for rng_start, rng_end in used_ranges): + continue + selected.append((off, cmd_count)) + used_ranges.append((start, end)) + if len(selected) >= 8: + break + selected.sort(key=lambda item: item[0]) + return selected + + +def find_story_block_offset( + data: bytes, + stage_ids: Set[int], + named_stage_ids: Set[int], +) -> Optional[int]: + entry_size = 4 + world_count = 10 + stages_per_world = 10 + block_size = world_count * stages_per_world * entry_size + for off in range(0, len(data) - block_size, 4): + valid = True + unique_ids: Set[int] = set() + named_count = 0 + for idx in range(world_count * stages_per_world): + entry_off = off + idx * entry_size + stage_id, difficulty = struct.unpack_from(">hh", data, entry_off) + if stage_id not in stage_ids: + valid = False + break + if difficulty < 0 or difficulty > 5: + valid = False + break + unique_ids.add(stage_id) + if stage_id in named_stage_ids: + named_count += 1 + if valid: + if len(unique_ids) < 20: + continue + if named_stage_ids and named_count / max(1, world_count * stages_per_world) < 0.3: + continue + return off + return None + + +def is_story_world_valid( + data: bytes, + offset: int, + stage_ids: Set[int], + named_stage_ids: Set[int], +) -> bool: + unique_ids: Set[int] = set() + named_count = 0 + for idx in range(10): + stage_id, difficulty = struct.unpack_from(">hh", data, offset + idx * 4) + if stage_id not in stage_ids: + return False + if difficulty < 0 or difficulty > 5: + return False + unique_ids.add(stage_id) + if stage_id in named_stage_ids: + named_count += 1 + if len(unique_ids) < 3: + return False + if named_stage_ids and named_count / 10 < 0.3: + return False + return True + + +def load_vanilla_course_data( + rom_dir: Path, + *, + course_cmd_counts: Optional[Dict[str, int]] = None, + world_offsets: Optional[List[int]] = None, +) -> dict: + mainloop_path = rom_dir / "mkb2.main_loop.rel" + stgname_path = rom_dir / "stgname" / "usa.str" + if not mainloop_path.exists(): + raise FileNotFoundError(f"missing {mainloop_path}") + if not stgname_path.exists(): + raise FileNotFoundError(f"missing {stgname_path}") + + mainloop_buffer = mainloop_path.read_bytes() + stgname_lines = stgname_path.read_text(encoding="ascii", errors="ignore").splitlines() + named_stage_ids = {i for i, name in enumerate(stgname_lines) if name and name != "-"} + + bonus_stage_ids = struct.unpack_from(">9i", mainloop_buffer, 0x00176118) + stage_id_to_theme_id_map = struct.unpack_from(">428B", mainloop_buffer, 0x00204E48) + theme_id_to_music_id_map = struct.unpack_from(">43h", mainloop_buffer, 0x0016E738) + + stage_ids = list_stage_ids(rom_dir / "stage") + + # Parse challenge mode entries using default offsets first. + counts = course_cmd_counts or {} + default_course_offsets = [ + ("beginner", 0x002075B0), + ("advanced", 0x00207914), + ("expert", 0x00208634), + ("beginner_extra", 0x00209CF4), + ("advanced_extra", 0x0020A0C8), + ("expert_extra", 0x0020A448), + ("master", 0x0020A8E0), + ("master_extra", 0x0020ACB4), + ] + cm_layout: Dict[str, List[dict]] = {} + for name, offset in default_course_offsets: + try: + cm_layout[name] = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + offset, + counts.get(name), + strict=True, + ) + except Exception: + cm_layout = {} + break + + if cm_layout: + cm_ids = collect_stage_ids_from_cm(cm_layout) + if not validate_stage_ids(cm_ids, stage_ids, "challenge courses", named_ids=named_stage_ids): + cm_layout = {} + + if not cm_layout and stage_ids: + logging.warning("Default course offsets invalid; scanning for course tables.") + offsets = find_course_offsets(mainloop_buffer, stage_ids, named_stage_ids) + order = [name for name, _ in default_course_offsets] + for idx, (offset, cmd_count) in enumerate(offsets[: len(order)]): + name = order[idx] + try: + cm_layout[name] = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + offset, + cmd_count, + strict=False, + ) + except Exception: + cm_layout = {} + break + + if not cm_layout: + raise SystemExit("Failed to locate challenge course tables.") + + if world_offsets is None: + world_offsets = [ + 0x0020b448, + 0x0020b470, + 0x0020b498, + 0x0020b4c0, + 0x0020b4e8, + 0x0020b510, + 0x0020b538, + 0x0020b560, + 0x0020b588, + 0x0020b5b0, + ] + worlds = [] + for offs in world_offsets: + if stage_ids and not is_story_world_valid(mainloop_buffer, offs, stage_ids, named_stage_ids): + worlds = [] + break + world = dump_storymode_world_layout( + mainloop_buffer, + stgname_lines, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + offs, + ) + worlds.append(world) + + if worlds: + story_ids = collect_stage_ids_from_story(worlds) + if not validate_stage_ids( + story_ids, + stage_ids, + "story worlds", + named_ids=named_stage_ids, + min_named_ratio=0.3, + ): + worlds = [] + + if not worlds and stage_ids: + logging.warning("Default story offsets invalid; scanning for story table.") + base_off = find_story_block_offset(mainloop_buffer, stage_ids, named_stage_ids) + if base_off is not None: + world_offsets = [base_off + i * 0x28 for i in range(10)] + for offs in world_offsets: + world = dump_storymode_world_layout( + mainloop_buffer, + stgname_lines, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + offs, + ) + worlds.append(world) + + if not worlds: + logging.warning("Story world data not found; output will omit story worlds.") + + return { + "challenge": cm_layout, + "story": worlds, + } + + +def main() -> None: + import argparse + + parser = argparse.ArgumentParser( + description="Dump vanilla challenge/story course data from extracted SMB2 files." + ) + parser.add_argument( + "--rom", + type=Path, + default=VANILLA_ROOT_PATH, + help="Path to extracted ROM folder (containing mkb2.main_loop.rel)", + ) + args = parser.parse_args() + + data = load_vanilla_course_data(args.rom) + cm_layout_dump = json.dumps(data["challenge"], indent=4) + annotated_cm_layout_dump = annotate_cm_layout_dump(cm_layout_dump) + print(annotated_cm_layout_dump) + + story_layout_dump = json.dumps(data["story"], indent=4) + annotated_story_layout_dump = annotate_story_layout_dump(story_layout_dump) + # print(annotated_story_layout_dump) + + +if __name__ == "__main__": + main() diff --git a/tools/dump_vanilla_conf_original.py b/tools/dump_vanilla_conf_original.py new file mode 100644 index 0000000..71cb029 --- /dev/null +++ b/tools/dump_vanilla_conf_original.py @@ -0,0 +1,497 @@ +#!/usr/bin/env python3 + +""" +Script for generating default wsmod config from a vanilla game's files +warning: bad +""" + +from pathlib import Path +import struct +from collections import namedtuple +import logging +import sys +import json +import argparse + +VANILLA_ROOT_PATH = Path( + "/mnt/c/Users/ComplexPlane/Documents/projects/romhack/smb2imm/files" +) + +CourseCommand = namedtuple("CourseCommand", ["opcode", "type", "value"]) +SmStageInfo = namedtuple("SmStageInfo", ["stage_id", "difficulty"]) + +# CMD opcodes +CMD_IF = 0 +CMD_THEN = 1 +CMD_FLOOR = 2 +CMD_COURSE_END = 3 + +# CMD_IF conditions +IF_FLOOR_CLEAR = 0 +IF_GOAL_TYPE = 2 + +# CMD_THEN actions +THEN_JUMP_FLOOR = 0 +THEN_END_COURSE = 2 + +# CMD_FLOOR value types +FLOOR_STAGE_ID = 0 +FLOOR_TIME = 1 + + +def get_theme_and_music_ids(stage_id, stage_id_to_theme_id, theme_id_to_music_id): + if stage_id < 0 or stage_id >= len(stage_id_to_theme_id): + logging.warning("Stage id %s out of range for theme map; using theme 0", stage_id) + theme_id = 0 + else: + theme_id = stage_id_to_theme_id[stage_id] + if theme_id > 42: + theme_id = 42 + if theme_id < 0 or theme_id >= len(theme_id_to_music_id): + logging.warning("Theme id %s out of range for music map; using 0", theme_id) + music_id = 0 + else: + music_id = theme_id_to_music_id[theme_id] + return (theme_id, music_id) + + +def parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + start, + count, +): + cmds: list[CourseCommand] = [] + course_cmd_size = 0x1C + + for i in range(count): + course_cmd = CourseCommand._make( + struct.unpack_from( + ">BBxxI20x", + mainloop_buffer, + start + i * course_cmd_size, + ) + ) + cmds.append(course_cmd) + + # Course commands to stage infos + cm_stage_infos = [] + stage_id = 0 + stage_time = 60 * 60 + blue_jump = None + green_jump = None + red_jump = None + last_goal_type = None + first = True + finished = False + + for cmd in cmds: + if cmd.opcode == CMD_FLOOR: + if cmd.type == FLOOR_STAGE_ID: + if not first: + if blue_jump is None: + logging.error("Invalid blue goal jump") + sys.exit(1) + + theme_id, music_id = get_theme_and_music_ids( + stage_id, stage_id_to_theme_id_map, theme_id_to_music_id_map + ) + + cm_stage_infos.append( + { + "stage_id": stage_id, + "name": stgname_lines[stage_id], + "theme_id": theme_id, + "music_id": music_id, + "time_limit": float(stage_time / 60), + "blue_goal_jump": blue_jump, + "green_goal_jump": green_jump + if green_jump is not None + else blue_jump, + "red_goal_jump": red_jump + if red_jump is not None + else blue_jump, + "is_bonus_stage": stage_id in bonus_stage_ids, + } + ) + stage_id = 0 + stage_time = 60 * 60 + blue_jump = None + green_jump = None + red_jump = None + last_goal_type = None + + stage_id = cmd.value + first = False + + elif cmd.type == FLOOR_TIME: + stage_time = cmd.value + else: + logging.error(f"Invalid CMD_FLOOR opcode type: {cmd.type}") + sys.exit(1) + + elif cmd.opcode == CMD_IF: + if cmd.type == IF_FLOOR_CLEAR: + last_goal_type = None + elif cmd.type == IF_GOAL_TYPE: + last_goal_type = cmd.value + else: + logging.error(f"Invalid CMD_IF opcode type: {cmd.type}") + sys.exit(1) + + elif cmd.opcode == CMD_THEN: + if cmd.type == THEN_JUMP_FLOOR: + if last_goal_type is None: + if blue_jump is None: + blue_jump = cmd.value + if green_jump is None: + green_jump = cmd.value + if red_jump is None: + red_jump = cmd.value + elif last_goal_type == 0: + blue_jump = cmd.value + elif last_goal_type == 1: + green_jump = cmd.value + elif last_goal_type == 2: + red_jump = cmd.value + else: + logging.error(f"Invalid last goal type: {last_goal_type}") + sys.exit(1) + elif cmd.type == THEN_END_COURSE: + # Jumps are irrelevant, this is end of difficulty + blue_jump = 1 + green_jump = 1 + red_jump = 1 + else: + logging.error(f"Invalid CMD_THEN opcode type: {cmd.type}") + sys.exit(1) + + elif cmd.opcode == CMD_COURSE_END: + if blue_jump is None: + logging.error("Invalid blue goal jump") + sys.exit(1) + theme_id, music_id = get_theme_and_music_ids( + stage_id, stage_id_to_theme_id_map, theme_id_to_music_id_map + ) + cm_stage_infos.append( + { + "stage_id": stage_id, + "name": stgname_lines[stage_id], + "theme_id": theme_id, + "music_id": music_id, + "time_limit": float(stage_time / 60), + "blue_goal_jump": blue_jump, + "green_goal_jump": green_jump + if green_jump is not None + else blue_jump, + "red_goal_jump": red_jump if red_jump is not None else blue_jump, + "is_bonus_stage": stage_id in bonus_stage_ids, + } + ) + finished = True + + else: + logging.error(f"Invalid opcode: {cmd.opcode}") + sys.exit(1) + + if not finished: + logging.error("Course command list ended early") + sys.exit(1) + + return cm_stage_infos + + +def annotate_cm_layout_dump(dump: str) -> str: + lines = dump.split("\n") + out_lines: list[str] = [] + + last_course = None + floor_num = 1 + for line in lines: + + old_floor_num = floor_num + floor_num = 1 + if '"beginner"' in line: + last_course = "Beginner" + elif '"beginner_extra"' in line: + last_course = "Beginner Extra" + elif '"advanced"' in line: + last_course = "Advanced" + elif '"advanced_extra"' in line: + last_course = "Advanced Extra" + elif '"expert"' in line: + last_course = "Expert" + elif '"expert_extra"' in line: + last_course = "Expert Extra" + elif '"master"' in line: + last_course = "Master" + elif '"master_extra"' in line: + last_course = "Master Extra" + else: + # Don't reset floor num if new difficulty not detected + floor_num = old_floor_num + + new_line = line[:] + if "{" in new_line and last_course is not None: + new_line += f" // {last_course} {floor_num}" + floor_num += 1 + + new_line = new_line.replace("60.0", "60.00") + new_line = new_line.replace("30.0", "30.00") + + out_lines.append(new_line) + + # if '"time_limit"' in new_line: + # out_lines.append("") + + return "\n".join(out_lines) + + +def dump_storymode_world_layout( + mainloop_buffer, + stgname_lines, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + start, +): + stage_info_size = 0x4 + + stage_infos: list[SmStageInfo] = [] + for i in range(10): + offs = start + i * stage_info_size + stage_info = SmStageInfo._make(struct.unpack_from(">hh", mainloop_buffer, offs)) + stage_infos.append(stage_info) + + out_json_array = [] + for stage_info in stage_infos: + time_limit = 60 * 60 if stage_info.stage_id != 30 else 60 * 30 + theme_id, music_id = get_theme_and_music_ids( + stage_info.stage_id, stage_id_to_theme_id_map, theme_id_to_music_id_map + ) + out_json_array.append( + { + "stage_id": stage_info.stage_id, + "name": stgname_lines[stage_info.stage_id], + "theme_id": theme_id, + "music_id": music_id, + "time_limit": float(time_limit / 60), + "difficulty": stage_info.difficulty, + } + ) + + return out_json_array + + +def annotate_story_layout_dump(dump: str) -> str: + lines = dump.split("\n") + out_lines: list[str] = [] + + last_course = None + world = -1 + stage = 0 + for line in lines: + new_line = line[:] + + if "[" in line: + world += 1 + stage = 0 + if world >= 1: + new_line += f" // World {world}" + if "{" in line: + stage += 1 + new_line += f" // Stage {world}-{stage}" + + new_line = new_line.replace("60.0", "60.00") + new_line = new_line.replace("30.0", "30.00") + + out_lines.append(new_line) + + # if '"time_limit"' in new_line: + # out_lines.append("") + + return "\n".join(out_lines) + + +def list_stage_ids(stage_dir: Path) -> set[int]: + ids: set[int] = set() + if not stage_dir.exists(): + return ids + for path in stage_dir.glob("STAGE*.lz"): + name = path.name + if len(name) == 11 and name.startswith("STAGE") and name.endswith(".lz"): + try: + ids.add(int(name[5:8])) + except ValueError: + continue + return ids + + +def validate_story_offsets(mainloop_buffer: bytes, world_offsets: list[int], stage_ids: set[int]) -> bool: + if not stage_ids: + return True + for offs in world_offsets: + for i in range(10): + entry_offs = offs + i * 4 + if entry_offs + 4 > len(mainloop_buffer): + return False + stage_id, difficulty = struct.unpack_from(">hh", mainloop_buffer, entry_offs) + if stage_id not in stage_ids: + return False + if difficulty < 0 or difficulty > 5: + return False + return True + + +def main(): + parser = argparse.ArgumentParser( + description="Dump vanilla challenge/story course data from extracted SMB2 files." + ) + parser.add_argument( + "--rom", + type=Path, + default=VANILLA_ROOT_PATH, + help="Path to extracted ROM folder (containing mkb2.main_loop.rel)", + ) + parser.add_argument( + "--story-only", + action="store_true", + help="Skip challenge mode parsing and only dump story worlds.", + ) + args = parser.parse_args() + root_path = args.rom + + with open(root_path / "mkb2.main_loop.rel", "rb") as f: + mainloop_buffer = f.read() + with open(root_path / "stgname" / "usa.str", "r") as f: + stgname_lines = [s.strip() for s in f.readlines()] + + bonus_stage_ids = struct.unpack_from(">9i", mainloop_buffer, 0x00176118) + stage_id_to_theme_id_map = struct.unpack_from(">428B", mainloop_buffer, 0x00204E48) + theme_id_to_music_id_map = struct.unpack_from(">43h", mainloop_buffer, 0x0016E738) + + if not args.story_only: + # Parse challenge mode entries + beginner = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x002075B0, + 31, + ) + advanced = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x00207914, + 120, + ) + expert = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x00208634, + 208, + ) + beginner_extra = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x00209CF4, + 35, + ) + advanced_extra = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x0020A0C8, + 32, + ) + expert_extra = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x0020A448, + 42, + ) + master = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x0020A8E0, + 35, + ) + master_extra = parse_cm_course( + mainloop_buffer, + stgname_lines, + bonus_stage_ids, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + 0x0020ACB4, + 50, + ) + cm_layout = { + "beginner": beginner, + "beginner_extra": beginner_extra, + "advanced": advanced, + "advanced_extra": advanced_extra, + "expert": expert, + "expert_extra": expert_extra, + "master": master, + "master_extra": master_extra, + } + + cm_layout_dump = json.dumps(cm_layout, indent=4) + annotated_cm_layout_dump = annotate_cm_layout_dump(cm_layout_dump) + print(annotated_cm_layout_dump) + + world_offsets = [ + 0x0020b448, + 0x0020b470, + 0x0020b498, + 0x0020b4c0, + 0x0020b4e8, + 0x0020b510, + 0x0020b538, + 0x0020b560, + 0x0020b588, + 0x0020b5b0, + ] + stage_ids = list_stage_ids(root_path / "stage") + if not validate_story_offsets(mainloop_buffer, world_offsets, stage_ids): + logging.warning("Story mode offsets do not look valid for this ROM.") + return + worlds = [] + for offs in world_offsets: + world = dump_storymode_world_layout( + mainloop_buffer, + stgname_lines, + stage_id_to_theme_id_map, + theme_id_to_music_id_map, + offs, + ) + worlds.append(world) + + story_layout_dump = json.dumps(worlds, indent=4) + annotated_story_layout_dump = annotate_story_layout_dump(story_layout_dump) + # print(annotated_story_layout_dump) + + +if __name__ == "__main__": + main() diff --git a/tools/smb2_pack_builder.py b/tools/smb2_pack_builder.py index 2122635..d330096 100644 --- a/tools/smb2_pack_builder.py +++ b/tools/smb2_pack_builder.py @@ -682,6 +682,188 @@ def build_pack( print(f' - {warning}') +def load_vanilla_courses_from_rom( + rom_dir: Path, +) -> Tuple[Dict[str, List[Tuple[int, bool]]], List[List[int]], Dict[int, int], List[str]]: + try: + from dump_vanilla_conf import load_vanilla_course_data + except Exception as exc: + raise RuntimeError(f'failed to import dump_vanilla_conf: {exc}') from exc + + data = load_vanilla_course_data(rom_dir) + challenge = data.get('challenge') if isinstance(data, dict) else None + story = data.get('story') if isinstance(data, dict) else None + + course_name_map = { + 'beginner': 'Beginner', + 'beginner_extra': 'Beginner Extra', + 'advanced': 'Advanced', + 'advanced_extra': 'Advanced Extra', + 'expert': 'Expert', + 'expert_extra': 'Expert Extra', + 'master': 'Master', + 'master_extra': 'Master Extra', + } + + challenge_courses: Dict[str, List[Tuple[int, bool]]] = {} + story_worlds: List[List[int]] = [] + stage_time_overrides: Dict[int, int] = {} + warnings: List[str] = [] + default_time = 60.0 + + def register_time_override(stage_id: int, time_limit: Optional[float]) -> None: + if time_limit is None: + return + if abs(time_limit - default_time) < 0.01: + return + frames = int(round(time_limit * 60)) + existing = stage_time_overrides.get(stage_id) + if existing is not None and existing != frames: + warnings.append( + f'stage {stage_id} has conflicting time limits ({existing} vs {frames} frames)' + ) + return + stage_time_overrides[stage_id] = frames + + if isinstance(challenge, dict): + for key, entries in challenge.items(): + name = course_name_map.get(key, key) + course_entries: List[Tuple[int, bool]] = [] + if isinstance(entries, list): + for entry in entries: + if not isinstance(entry, dict): + continue + stage_id = entry.get('stage_id') + if not isinstance(stage_id, int): + continue + bonus = bool(entry.get('is_bonus_stage')) + course_entries.append((stage_id, bonus)) + register_time_override(stage_id, entry.get('time_limit')) + if course_entries: + challenge_courses[name] = course_entries + + if isinstance(story, list): + for world in story: + world_entries: List[int] = [] + if isinstance(world, list): + for entry in world: + if not isinstance(entry, dict): + continue + stage_id = entry.get('stage_id') + if not isinstance(stage_id, int): + continue + world_entries.append(stage_id) + register_time_override(stage_id, entry.get('time_limit')) + if world_entries: + story_worlds.append(world_entries) + + if not story_worlds: + warnings.append('Story mode data not found; leaving story worlds empty.') + + return challenge_courses, story_worlds, stage_time_overrides, warnings + + +def parse_cmmod_config(config_path: Path) -> Tuple[ + Dict[str, List[Tuple[int, bool]]], + Dict[int, int], + List[str], +]: + warnings: List[str] = [] + entry_lists: Dict[str, List[Tuple[int, Optional[int]]]] = {} + diff_map: Dict[str, str] = {} + + def parse_num(token: str) -> Optional[int]: + try: + return int(token, 0) + except ValueError: + return None + + current_list: Optional[str] = None + for raw_line in config_path.read_text(encoding='utf-8', errors='ignore').splitlines(): + line = raw_line.split('%', 1)[0].strip() + if not line: + continue + if line.startswith('#beginEntryList'): + parts = line.split() + if len(parts) >= 2: + current_list = parts[1] + entry_lists[current_list] = [] + else: + warnings.append('Malformed #beginEntryList line') + continue + if line.startswith('#endEntryList'): + current_list = None + continue + if line.startswith('#diff'): + parts = line.split() + if len(parts) >= 3: + diff_name = parts[1] + list_name = parts[2] + diff_map[diff_name] = list_name + else: + warnings.append('Malformed #diff line') + continue + if line.startswith('#'): + continue + if current_list is None: + continue + left = line.split('|', 1)[0].strip() + if not left: + continue + tokens = left.split() + stage_id = parse_num(tokens[0]) + if stage_id is None: + warnings.append(f'Invalid stage id in line: {raw_line}') + continue + time_override: Optional[int] = None + if len(tokens) >= 2: + time_val = parse_num(tokens[1]) + if time_val is None: + warnings.append(f'Invalid time in line: {raw_line}') + elif time_val != 3600: + time_override = time_val + entry_lists.setdefault(current_list, []).append((stage_id, time_override)) + + diff_name_map = { + 'Beginner': 'Beginner', + 'Advanced': 'Advanced', + 'Expert': 'Expert', + 'BeginnerExtra': 'Beginner Extra', + 'AdvancedExtra': 'Advanced Extra', + 'ExpertExtra': 'Expert Extra', + 'Master': 'Master', + 'MasterExtra': 'Master Extra', + } + + challenge_courses: Dict[str, List[Tuple[int, bool]]] = {} + stage_time_overrides: Dict[int, int] = {} + + for diff_name, list_name in diff_map.items(): + display_name = diff_name_map.get(diff_name) + if not display_name: + continue + entries = entry_lists.get(list_name) + if not entries: + warnings.append(f'Missing entry list for {diff_name}: {list_name}') + continue + challenge_courses[display_name] = [(stage_id, False) for stage_id, _ in entries] + for stage_id, time_override in entries: + if time_override is None: + continue + existing = stage_time_overrides.get(stage_id) + if existing is not None and existing != time_override: + warnings.append( + f'stage {stage_id} has conflicting time limits ({existing} vs {time_override})' + ) + continue + stage_time_overrides[stage_id] = time_override + + if not challenge_courses: + warnings.append('No challenge courses found in cmmod config.') + + return challenge_courses, stage_time_overrides, warnings + + def main() -> None: parser = argparse.ArgumentParser(description='Build SMB2 pack from extracted ROM.') parser.add_argument('--rom', type=Path, help='Path to extracted SMB2 ROM folder') @@ -727,18 +909,27 @@ def run_gui() -> None: lst_var = tk.StringVar() zip_var = tk.BooleanVar(value=True) - def browse_dir(target_var: tk.StringVar): - value = filedialog.askdirectory() + last_rom_dir = '' + last_out_dir = '' + + def browse_dir(target_var: tk.StringVar, last_dir_attr: str): + nonlocal last_rom_dir, last_out_dir + initial = last_rom_dir if last_dir_attr == 'rom' else last_out_dir + value = filedialog.askdirectory(initialdir=initial or None) if value: target_var.set(value) + if last_dir_attr == 'rom': + last_rom_dir = value + else: + last_out_dir = value ttk.Label(config_frame, text='ROM folder').grid(row=0, column=0, sticky=tk.W, padx=4, pady=4) ttk.Entry(config_frame, textvariable=rom_var, width=60).grid(row=0, column=1, sticky=tk.W, padx=4, pady=4) - ttk.Button(config_frame, text='Browse', command=lambda: browse_dir(rom_var)).grid(row=0, column=2, padx=4, pady=4) + ttk.Button(config_frame, text='Browse', command=lambda: browse_dir(rom_var, 'rom')).grid(row=0, column=2, padx=4, pady=4) ttk.Label(config_frame, text='Output folder').grid(row=1, column=0, sticky=tk.W, padx=4, pady=4) ttk.Entry(config_frame, textvariable=out_var, width=60).grid(row=1, column=1, sticky=tk.W, padx=4, pady=4) - ttk.Button(config_frame, text='Browse', command=lambda: browse_dir(out_var)).grid(row=1, column=2, padx=4, pady=4) + ttk.Button(config_frame, text='Browse', command=lambda: browse_dir(out_var, 'out')).grid(row=1, column=2, padx=4, pady=4) ttk.Label(config_frame, text='Pack ID').grid(row=2, column=0, sticky=tk.W, padx=4, pady=4) ttk.Entry(config_frame, textvariable=pack_id_var, width=30).grid(row=2, column=1, sticky=tk.W, padx=4, pady=4) @@ -759,6 +950,9 @@ def run_gui() -> None: courses_frame = ttk.Frame(frame) courses_frame.pack(fill=tk.BOTH, expand=True) + load_controls = ttk.Frame(courses_frame) + load_controls.pack(fill=tk.X, pady=(0, 8)) + challenge_frame = ttk.LabelFrame(courses_frame, text='Challenge Courses', padding=10) story_frame = ttk.LabelFrame(courses_frame, text='Story Worlds', padding=10) challenge_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10)) @@ -1023,6 +1217,69 @@ def run_gui() -> None: courses['story'] = story_worlds return courses + def load_from_rom_clicked(): + rom_path = Path(rom_var.get().strip()) + if not rom_path.exists(): + messagebox.showerror('Invalid ROM', 'ROM folder does not exist.') + return + if challenge_courses or story_worlds: + if not messagebox.askyesno( + 'Replace courses', + 'Replace current course data with values from the ROM?', + ): + return + try: + loaded_courses, loaded_worlds, loaded_overrides, load_warnings = ( + load_vanilla_courses_from_rom(rom_path) + ) + except Exception as exc: + messagebox.showerror('Load failed', str(exc)) + return + challenge_courses.clear() + challenge_courses.update(loaded_courses) + story_worlds.clear() + story_worlds.extend(loaded_worlds) + stage_time_overrides.clear() + stage_time_overrides.update(loaded_overrides) + selected_course_name.set('') + refresh_course_list() + refresh_stage_list() + refresh_world_list() + refresh_world_stage_list() + if load_warnings: + messagebox.showwarning('Loaded with warnings', '\n'.join(load_warnings)) + + def load_from_cmmod_clicked(): + config_path_str = filedialog.askopenfilename( + title='Select cmmod config', + filetypes=[('Config files', '*.txt *.cfg'), ('All files', '*.*')], + ) + if not config_path_str: + return + config_path = Path(config_path_str) + if challenge_courses or story_worlds: + if not messagebox.askyesno( + 'Replace courses', + 'Replace current challenge courses with values from the cmmod config?', + ): + return + try: + loaded_courses, loaded_overrides, load_warnings = parse_cmmod_config(config_path) + except Exception as exc: + messagebox.showerror('Load failed', str(exc)) + return + challenge_courses.clear() + challenge_courses.update(loaded_courses) + stage_time_overrides.clear() + stage_time_overrides.update(loaded_overrides) + selected_course_name.set('') + refresh_course_list() + refresh_stage_list() + if story_worlds: + load_warnings.append('Story worlds were left unchanged.') + if load_warnings: + messagebox.showwarning('Loaded with warnings', '\n'.join(load_warnings)) + def build_pack_clicked(): rom_path = Path(rom_var.get().strip()) out_path = Path(out_var.get().strip()) @@ -1061,6 +1318,7 @@ def run_gui() -> None: action_frame = ttk.Frame(frame) action_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Button(action_frame, text='Build Pack', command=build_pack_clicked).pack(side=tk.RIGHT) + ttk.Button(load_controls, text='Load courses from cmmod config', command=load_from_cmmod_clicked).pack(side=tk.LEFT) root.mainloop()