diff --git a/tools/generate_asset_enums.py b/tools/generate_asset_enums.py new file mode 100644 index 00000000..06f130ab --- /dev/null +++ b/tools/generate_asset_enums.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +import argparse +import yaml +import re +import os +from typing import Dict, List +from pathlib import Path + + +def build_asset_map(assets_file: str) -> Dict[str, str]: + file_types_to_skip = ( + 'LevelSetup', # 0x071D - 0x07B5 + 'DemoInput', # 0x09A2 - 0x0A0A + 'QuizQuestion', # 0x1213 - 0x13D5 + 'GruntyQuestion', # 0x1407 - 0x1424 + 'Midi' # 0x1516 - 0x15C2 + ) + + file_type_to_asset_type = { + 'Animation': 'ANIM', + 'Model': 'MODEL', + 'Sprite_I4': 'SPRITE', + 'Sprite_I8': 'SPRITE', + 'Sprite_CI4': 'SPRITE', + 'Sprite_CI8': 'SPRITE', + 'Sprite_RGBA16': 'SPRITE', + 'Sprite_RGBA32': 'SPRITE', + 'Sprite_UNKNOWN(256)': 'SPRITE', + 'Dialog': 'DIALOG', + } + + asset_map = {} + with open(assets_file, 'r') as stream: + try: + data = yaml.safe_load(stream) + if "files" not in data: + raise Exception("Expected to find 'files' key in assets file") + files = data["files"] + for file in files: + file_type = file["type"] + if file_type in file_types_to_skip: + continue + + if file_type not in file_type_to_asset_type: + continue + + asset_type = file_type_to_asset_type[file_type] + address = file["uid"] + asset_map[f"{address:X}"] = f"ASSET_{address:X}_{asset_type}_UNKNOWN" + except yaml.YAMLError as exc: + print(exc) + + return asset_map + + +def get_existing_asset_enums(assets_enum_file: str) -> Dict[str, str]: + with open(assets_enum_file, "r") as enums_stream: + existing_asset_match = re.compile(r"^\s*(ASSET_([A-F0-9]+)_\w+)", re.IGNORECASE) + existing_asset_mappings = {} + for line in enums_stream: + asset_line = existing_asset_match.match(line) + if asset_line is not None: + asset_address = asset_line.group(2).upper() + existing_asset_mappings[asset_address] = asset_line.group(1) + + return existing_asset_mappings + + +def build_asset_enum(asset_map: Dict[str, str], existing_assets: Dict[str, str]) -> List[str]: + asset_enum_lines = [] + address_already_defined = False + for i in range(1, int(0x1516)): + address = f"{i:X}" + if address in existing_assets: + if address_already_defined is False: + asset_enum_lines.append( + f"{existing_assets[address]} = 0x{address},") + address_already_defined = True + else: + asset_enum_lines.append(f"{existing_assets[address]},") + elif address in asset_map: + if address_already_defined is False: + asset_enum_lines.append(f"{asset_map[address]} = 0x{address},") + address_already_defined = True + else: + asset_enum_lines.append(f"{asset_map[address]},") + else: + asset_enum_lines.append(f"// {address} unused") + address_already_defined = False + + asset_enum_lines = remove_unnecessary_unused_asset_lines(asset_enum_lines) + + # Remove trailing comma in last asset line + last_line: str = asset_enum_lines[-1] + if last_line.endswith(","): + last_line = last_line.replace(",", "") + asset_enum_lines[-1] = last_line + + return map(lambda x: f" {x}", asset_enum_lines) + + +def remove_unnecessary_unused_asset_lines(lines: List[str]) -> List[str]: + prev_unused = False + unused_asset_line_indexes = [] + # Create list of lines that contain "// xx is unused" + for idx, line in enumerate(lines): + if line.endswith('unused') is False: + continue + + if idx + 1 >= len(lines): + continue + + prev: str | None = lines[idx - 1] + prev_unused = prev is not None and prev.endswith('unused') + + next: str | None = lines[idx + 1] + next_unused = next is not None and next.endswith('unused') + + if prev_unused and next_unused: + unused_asset_line_indexes.append(idx) + + # "Group" lines where multiple "// xx is unused" lines occur in a row + groups = [] + group = [] + target = None + for idx, line_index in enumerate(unused_asset_line_indexes): + if target is not None and target > idx: + continue + else: + target = None + + for next in range(0, len(unused_asset_line_indexes) - idx): + if unused_asset_line_indexes[next + idx] == line_index + next: + group.append(next + idx) + else: + break + + if len(group) == 0: + continue + + combine_from_index = group[0] + combine_from = unused_asset_line_indexes[combine_from_index] + combine_to_index = group[len(group)-1] + combine_to = unused_asset_line_indexes[combine_to_index] + groups.append((combine_from - 1, combine_to + 1)) + group = [] + target = next + idx + + # Replace the "unnecessary" lines with a single line that lists the range that is unused + offset = 0 + address_regex = re.compile(r"^// (\w+) unused$") + for group in groups: + (start, end) = group + start -= offset + start_line = lines[start] + start_match = address_regex.match(start_line) + + end -= offset + end_line = lines[end] + end_match = address_regex.match(end_line) + + if start_match is not None and end_match is not None: + lines[start:end+1] = [ + f"// 0x{start_match.group(1)} - 0x{end_match.group(1)} unused" + ] + + offset += end - start + + return lines + + +def write_enum_file(asset_lines: List[str], out_file: str): + with open(out_file, "w") as enums_out: + lines = [ + "enum asset_e", + "{", + *asset_lines, + "};" + ] + enums_out.write("\n".join(lines)) + + +def main(): + parser = argparse.ArgumentParser( + description='Generate asset_e enum from assets/assets.yaml and include/enums.h files', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + 'basedir', + type=str, + help="base directory (containing src/)" + ) + args = parser.parse_args() + + asset_map = build_asset_map( + assets_file=f"{args.basedir}{os.sep}assets{os.sep}assets.yaml" + ) + existing_assets = get_existing_asset_enums( + assets_enum_file=f"{args.basedir}{os.sep}include{os.sep}enums.h" + ) + + asset_enum_lines = build_asset_enum(asset_map, existing_assets) + write_enum_file( + asset_enum_lines, + out_file=f"{args.basedir}{os.sep}assets.h" + ) + + +if __name__ == '__main__': + main()