|
| 1 | +import re |
| 2 | +from typing import List, Tuple, Any, Protocol |
| 3 | +from jinja2 import Environment, FileSystemLoader |
| 4 | + |
| 5 | + |
| 6 | +class ParsedStruct(Protocol): |
| 7 | + name: str |
| 8 | + is_class: bool |
| 9 | + methods: List[Tuple[str, Any]] |
| 10 | + properties: List[Any] |
| 11 | + |
| 12 | + |
| 13 | +class ParsedTypeDecl(Protocol): |
| 14 | + c_type: str |
| 15 | + type: int |
| 16 | + |
| 17 | + |
| 18 | +class ParsedMethod(Protocol): |
| 19 | + name: str |
| 20 | + args: List[Any] |
| 21 | + generated: bool |
| 22 | + |
| 23 | + |
| 24 | +TYPE_BASIC = 1 |
| 25 | +TYPE_STRUCT = 2 |
| 26 | +TYPE_CLASS = 3 |
| 27 | +TYPE_POINTER = 4 |
| 28 | + |
| 29 | +INPUT_USAGES = ("input", "input_ptr", "input_deref") |
| 30 | +OUTPUT_USAGES = ("output", "output_ptr") |
| 31 | + |
| 32 | +BASIC_NUMERIC_TYPES = {"float", "double"} |
| 33 | +INTEGER_TYPES = {"int", "short", "long", "char", "FMOD_BOOL"} |
| 34 | + |
| 35 | +TYPE_HANDLERS = { |
| 36 | + TYPE_BASIC: lambda c_type: _convert_basic_type(c_type), |
| 37 | + TYPE_POINTER: lambda c_type: _convert_pointer_type(c_type), |
| 38 | + TYPE_STRUCT: lambda c_type: _convert_struct_type(c_type), |
| 39 | + TYPE_CLASS: lambda c_type: _convert_struct_type(c_type), |
| 40 | +} |
| 41 | + |
| 42 | +PARAM_DESCRIPTIONS = { |
| 43 | + "system": "FMOD system handle", |
| 44 | + "sound": "Sound handle", |
| 45 | + "channel": "Channel handle", |
| 46 | + "name": "Name or path", |
| 47 | + "filename": "Name or path", |
| 48 | + "length": "Length or size value", |
| 49 | + "size": "Length or size value", |
| 50 | + "mode": "Mode flags", |
| 51 | + "volume": "Volume level (0.0 to 1.0)", |
| 52 | + "position": "Position value", |
| 53 | + "paused": "Paused state", |
| 54 | + "index": "Index value", |
| 55 | +} |
| 56 | + |
| 57 | + |
| 58 | +def _convert_basic_type(c_type: str) -> str: |
| 59 | + if "FMOD_VECTOR" in c_type: |
| 60 | + return "vector3" |
| 61 | + if c_type in BASIC_NUMERIC_TYPES: |
| 62 | + return "number" |
| 63 | + if any(base_type in c_type for base_type in INTEGER_TYPES): |
| 64 | + return "boolean" if "FMOD_BOOL" in c_type else "number" |
| 65 | + if c_type.startswith("FMOD_"): |
| 66 | + return "number" |
| 67 | + return "number" |
| 68 | + |
| 69 | + |
| 70 | +def _convert_pointer_type(c_type: str) -> str: |
| 71 | + if "FMOD_VECTOR" in c_type: |
| 72 | + return "vector3" |
| 73 | + if "char" in c_type: |
| 74 | + return "string" |
| 75 | + return "userdata" |
| 76 | + |
| 77 | + |
| 78 | +def _convert_struct_type(c_type: str) -> str: |
| 79 | + if "FMOD_VECTOR" in c_type: |
| 80 | + return "vector3" |
| 81 | + type_lower = c_type.lower() |
| 82 | + if type_lower.startswith("fmod_studio_"): |
| 83 | + return type_lower.replace("fmod_studio_", "fmod.studio.") |
| 84 | + if type_lower.startswith("fmod_"): |
| 85 | + return type_lower.replace("fmod_", "fmod.") |
| 86 | + return "userdata" |
| 87 | + |
| 88 | + |
| 89 | +def convert_to_snake_case(text: str) -> str: |
| 90 | + valid_pattern = re.compile(r"^_*(IDs|[A-Z][a-z]+|[A-Z0-9]+(?![a-z]))") |
| 91 | + components = [] |
| 92 | + remaining = text |
| 93 | + while True: |
| 94 | + match = valid_pattern.match(remaining) |
| 95 | + if match is None: |
| 96 | + break |
| 97 | + components.append(match.group(1).lower()) |
| 98 | + remaining = remaining[match.end():] |
| 99 | + return "_".join(components) |
| 100 | + |
| 101 | + |
| 102 | +def convert_c_type_to_lua_type(c_type: str, type_enum: int) -> str: |
| 103 | + handler = TYPE_HANDLERS.get(type_enum) |
| 104 | + if handler: |
| 105 | + return handler(c_type) |
| 106 | + return "any" |
| 107 | + |
| 108 | + |
| 109 | +def generate_parameter_description(param_name: str, function_name: str) -> str: |
| 110 | + param_lower = param_name.lower() |
| 111 | + |
| 112 | + if param_lower in PARAM_DESCRIPTIONS: |
| 113 | + return PARAM_DESCRIPTIONS[param_lower] |
| 114 | + |
| 115 | + for key, description in PARAM_DESCRIPTIONS.items(): |
| 116 | + if key in param_lower: |
| 117 | + return description |
| 118 | + |
| 119 | + return param_name |
| 120 | + |
| 121 | + |
| 122 | +def get_input_args(method: ParsedMethod, skip_self: bool = False) -> List[Any]: |
| 123 | + args = method.args[1:] if skip_self else method.args |
| 124 | + return [arg for arg in args if arg.usage in INPUT_USAGES] |
| 125 | + |
| 126 | + |
| 127 | +def get_output_args(method: ParsedMethod) -> List[Any]: |
| 128 | + return [arg for arg in method.args if arg.usage in OUTPUT_USAGES] |
| 129 | + |
| 130 | + |
| 131 | +def get_arg_type_info(arg: Any) -> Tuple[str, int]: |
| 132 | + if arg.usage == "output_ptr": |
| 133 | + child = getattr(arg.type, "child", None) |
| 134 | + if child is not None: |
| 135 | + return child.c_type, child.type |
| 136 | + return arg.type.c_type, arg.type.type |
| 137 | + |
| 138 | + |
| 139 | +def write_script_api_file( |
| 140 | + output_path: str, |
| 141 | + enums: List[str], |
| 142 | + structs: List[ParsedStruct], |
| 143 | + global_functions: List[Tuple[int, str, ParsedMethod]] |
| 144 | +) -> None: |
| 145 | + env = Environment( |
| 146 | + loader=FileSystemLoader('.'), |
| 147 | + autoescape=False, |
| 148 | + trim_blocks=True, |
| 149 | + lstrip_blocks=True, |
| 150 | + ) |
| 151 | + |
| 152 | + env.globals['c_type_to_lua_type'] = convert_c_type_to_lua_type |
| 153 | + env.globals['get_param_description'] = generate_parameter_description |
| 154 | + env.globals['get_input_args'] = get_input_args |
| 155 | + env.globals['get_output_args'] = get_output_args |
| 156 | + env.globals['get_arg_type_info'] = get_arg_type_info |
| 157 | + |
| 158 | + template = env.get_template('fmod_script_api_template.yaml') |
| 159 | + |
| 160 | + rendered_output = template.render( |
| 161 | + enums=enums, |
| 162 | + structs=structs, |
| 163 | + global_functions=global_functions, |
| 164 | + ) |
| 165 | + |
| 166 | + with open(output_path, 'w') as output_file: |
| 167 | + output_file.write(rendered_output) |
| 168 | + |
| 169 | + print(f"Generated {output_path}") |
0 commit comments