|
| 1 | +from typing import List, Dict, Tuple, Optional |
| 2 | +from collections import OrderedDict |
| 3 | +from dataclasses import dataclass, replace |
| 4 | +from pathlib import Path |
| 5 | +import json |
| 6 | +from enum import Enum |
| 7 | + |
| 8 | +from .type_spec import * |
| 9 | +from .builtins import * |
| 10 | +from .classes import * |
| 11 | +from .utils import * |
| 12 | + |
| 13 | + |
| 14 | +class BuildConfig(Enum): |
| 15 | + FLOAT_32 = "float_32" |
| 16 | + DOUBLE_32 = "double_32" |
| 17 | + FLOAT_64 = "float_64" |
| 18 | + DOUBLE_64 = "double_64" |
| 19 | + |
| 20 | + |
| 21 | +@dataclass |
| 22 | +class GlobalConstantSpec: |
| 23 | + @classmethod |
| 24 | + def parse(cls, item: dict) -> "GlobalConstantSpec": |
| 25 | + # Don't known what it is supposed to contain given the list is so far empty in the JSON |
| 26 | + raise NotImplementedError |
| 27 | + |
| 28 | + |
| 29 | +def parse_global_enum(spec: dict) -> EnumTypeSpec: |
| 30 | + assert spec.keys() == {"name", "values"}, spec.keys() |
| 31 | + return EnumTypeSpec( |
| 32 | + original_name=spec["name"], |
| 33 | + py_type=spec["name"], |
| 34 | + cy_type=spec["name"], |
| 35 | + is_bitfield=False, |
| 36 | + values={x["name"]: x["value"] for x in spec["values"]}, |
| 37 | + ) |
| 38 | + |
| 39 | + |
| 40 | +@dataclass |
| 41 | +class UtilityFunctionArgumentSpec: |
| 42 | + name: str |
| 43 | + original_name: str |
| 44 | + type: TypeInUse |
| 45 | + default_value: Optional[ValueInUse] |
| 46 | + |
| 47 | + @classmethod |
| 48 | + def parse(cls, item: dict) -> "UtilityFunctionArgumentSpec": |
| 49 | + item.setdefault("original_name", item["name"]) |
| 50 | + item.setdefault("default_value", None) |
| 51 | + assert_api_consistency(cls, item) |
| 52 | + arg_type = TypeInUse(item["type"]) |
| 53 | + return cls( |
| 54 | + name=correct_name(item["name"]), |
| 55 | + original_name=item["original_name"], |
| 56 | + type=arg_type, |
| 57 | + default_value=ValueInUse.parse(arg_type, item["default_value"]) |
| 58 | + if item["default_value"] |
| 59 | + else None, |
| 60 | + ) |
| 61 | + |
| 62 | + |
| 63 | +@dataclass |
| 64 | +class UtilityFunctionSpec: |
| 65 | + original_name: str |
| 66 | + name: str |
| 67 | + return_type: TypeInUse |
| 68 | + category: str |
| 69 | + is_vararg: bool |
| 70 | + hash: int |
| 71 | + arguments: List[Tuple[str, TypeInUse]] |
| 72 | + |
| 73 | + @classmethod |
| 74 | + def parse(cls, item: dict) -> "UtilityFunctionSpec": |
| 75 | + item.setdefault("original_name", item["name"]) |
| 76 | + item.setdefault("arguments", []) |
| 77 | + item.setdefault("return_type", "Nil") |
| 78 | + assert_api_consistency(cls, item) |
| 79 | + return cls( |
| 80 | + name=correct_name(item["name"]), |
| 81 | + original_name=item["original_name"], |
| 82 | + return_type=TypeInUse(item["return_type"]), |
| 83 | + category=item["category"], |
| 84 | + is_vararg=item["is_vararg"], |
| 85 | + hash=item["hash"], |
| 86 | + arguments=[UtilityFunctionArgumentSpec.parse(x) for x in item["arguments"]], |
| 87 | + ) |
| 88 | + |
| 89 | + |
| 90 | +@dataclass |
| 91 | +class SingletonSpec: |
| 92 | + original_name: str |
| 93 | + name: str |
| 94 | + type: TypeInUse |
| 95 | + |
| 96 | + @classmethod |
| 97 | + def parse(cls, item: dict) -> "SingletonSpec": |
| 98 | + item.setdefault("original_name", item["name"]) |
| 99 | + assert_api_consistency(cls, item) |
| 100 | + return cls( |
| 101 | + name=correct_name(item["name"]), |
| 102 | + original_name=item["original_name"], |
| 103 | + type=TypeInUse(item["type"]), |
| 104 | + ) |
| 105 | + |
| 106 | + |
| 107 | +@dataclass |
| 108 | +class NativeStructureSpec: |
| 109 | + original_name: str |
| 110 | + name: str |
| 111 | + # Format is basically a dump of the C struct content, so don't try to be clever by parsing it |
| 112 | + format: str |
| 113 | + |
| 114 | + @classmethod |
| 115 | + def parse(cls, item: dict) -> "NativeStructureSpec": |
| 116 | + item.setdefault("original_name", item["name"]) |
| 117 | + assert_api_consistency(cls, item) |
| 118 | + return cls( |
| 119 | + name=correct_name(item["name"]), |
| 120 | + original_name=item["original_name"], |
| 121 | + format=item["format"], |
| 122 | + ) |
| 123 | + |
| 124 | + |
| 125 | +@dataclass |
| 126 | +class ExtensionApi: |
| 127 | + version_major: int # e.g. 4 |
| 128 | + version_minor: int # e.g. 0 |
| 129 | + version_patch: int # e.g. 0 |
| 130 | + version_status: str # e.g. "alpha13" |
| 131 | + version_build: str # e.g. "official" |
| 132 | + version_full_name: str # e.g. "Godot Engine v4.0.alpha13.official" |
| 133 | + |
| 134 | + classes: List[ClassTypeSpec] |
| 135 | + builtins: List[BuiltinTypeSpec] |
| 136 | + global_constants: List[GlobalConstantSpec] |
| 137 | + global_enums: List[EnumTypeSpec] |
| 138 | + utility_functions: List[UtilityFunctionSpec] |
| 139 | + singletons: List[SingletonSpec] |
| 140 | + native_structures: List[NativeStructureSpec] |
| 141 | + |
| 142 | + # Expose scalars, nil and variant |
| 143 | + |
| 144 | + @property |
| 145 | + def variant_type(self): |
| 146 | + return TYPES_DB["Variant"] |
| 147 | + |
| 148 | + @property |
| 149 | + def nil_type(self): |
| 150 | + return TYPES_DB["Nil"] |
| 151 | + |
| 152 | + @property |
| 153 | + def bool_type(self): |
| 154 | + return TYPES_DB["bool"] |
| 155 | + |
| 156 | + @property |
| 157 | + def int_type(self): |
| 158 | + return TYPES_DB["int"] |
| 159 | + |
| 160 | + @property |
| 161 | + def float_type(self): |
| 162 | + return TYPES_DB["float"] |
| 163 | + |
| 164 | + def get_class_meth_hash(self, classname: str, methname: str) -> int: |
| 165 | + klass = next(c for c in self.classes if c.original_name == classname) |
| 166 | + meth = next(m for m in klass.methods if m.original_name == methname) |
| 167 | + return meth.hash |
| 168 | + |
| 169 | + |
| 170 | +def merge_builtins_size_info(api_json: dict, build_config: BuildConfig) -> None: |
| 171 | + # Builtins size depend of the build config, hence it is stored separatly from |
| 172 | + # the rest of the built class definition. |
| 173 | + # Here we retreive the correct config and merge it back in the builtins classes |
| 174 | + # definition to simplify the rest of the parsing. |
| 175 | + |
| 176 | + builtin_class_sizes = next( |
| 177 | + x["sizes"] |
| 178 | + for x in api_json["builtin_class_sizes"] |
| 179 | + if x["build_configuration"] == build_config.value |
| 180 | + ) |
| 181 | + builtin_class_sizes = {x["name"]: x["size"] for x in builtin_class_sizes} |
| 182 | + builtin_class_member_offsets = next( |
| 183 | + x["classes"] |
| 184 | + for x in api_json["builtin_class_member_offsets"] |
| 185 | + if x["build_configuration"] == build_config.value |
| 186 | + ) |
| 187 | + builtin_class_member_offsets = {x["name"]: x["members"] for x in builtin_class_member_offsets} |
| 188 | + |
| 189 | + # TODO: remove me once https://github.com/godotengine/godot/pull/64690 is merged |
| 190 | + if "Projection" not in builtin_class_member_offsets: |
| 191 | + builtin_class_member_offsets["Projection"] = [ |
| 192 | + {"member": "x", "offset": 0}, |
| 193 | + {"member": "y", "offset": builtin_class_sizes["Vector4"]}, |
| 194 | + {"member": "z", "offset": 2 * builtin_class_sizes["Vector4"]}, |
| 195 | + {"member": "w", "offset": 3 * builtin_class_sizes["Vector4"]}, |
| 196 | + ] |
| 197 | + |
| 198 | + for item in api_json["builtin_classes"]: |
| 199 | + name = item["name"] |
| 200 | + item["size"] = builtin_class_sizes[name] |
| 201 | + # TODO: correct me once https://github.com/godotengine/godot/pull/64365 is merged |
| 202 | + for member in builtin_class_member_offsets.get(name, ()): |
| 203 | + for item_member in item["members"]: |
| 204 | + if item_member["name"] == member["member"]: |
| 205 | + item_member["offset"] = member["offset"] |
| 206 | + # Float builtin in extension_api.json is always 64bits long, |
| 207 | + # however builtins made of floating point number can be made of |
| 208 | + # 32bits (C float) or 64bits (C double) |
| 209 | + # But Color is a special case: it is always made of 32bits floats ! |
| 210 | + if name == "Color": |
| 211 | + item_member["type"] = "meta:float" |
| 212 | + elif item_member["type"] == "float": |
| 213 | + if build_config in (BuildConfig.FLOAT_32, BuildConfig.FLOAT_64): |
| 214 | + item_member["type"] = "meta:float" |
| 215 | + else: |
| 216 | + assert build_config in (BuildConfig.DOUBLE_32, BuildConfig.DOUBLE_64) |
| 217 | + item_member["type"] = "meta:double" |
| 218 | + elif item_member["type"] == "int": |
| 219 | + # Builtins containing int is always made of int32 |
| 220 | + item_member["type"] = "meta:int32" |
| 221 | + break |
| 222 | + else: |
| 223 | + raise RuntimeError(f"Member `{member}` doesn't seem to be part of `{name}` !") |
| 224 | + |
| 225 | + # Variant&Object are not present among the `builtin_classes`, only their size is provided. |
| 226 | + # So we have to create our own custom entry for them. |
| 227 | + api_json["variant_size"] = builtin_class_sizes["Variant"] |
| 228 | + api_json["object_size"] = builtin_class_sizes["Object"] |
| 229 | + |
| 230 | + |
| 231 | +def order_classes(classes: List[ClassTypeSpec]) -> List[ClassTypeSpec]: |
| 232 | + # Order classes by inheritance dependency needs |
| 233 | + ordered_classes = OrderedDict() # Makes it explicit we need ordering here ! |
| 234 | + ordered_count = 0 |
| 235 | + |
| 236 | + while len(classes) != len(ordered_classes): |
| 237 | + for klass in classes: |
| 238 | + if klass.inherits is None or klass.inherits.type_name in ordered_classes: |
| 239 | + ordered_classes[klass.original_name] = klass |
| 240 | + |
| 241 | + # Sanity check to avoid infinite loop in case of error in `extension_api.json` |
| 242 | + if ordered_count == len(ordered_classes): |
| 243 | + bad_class = next( |
| 244 | + klass |
| 245 | + for klass in classes |
| 246 | + if klass.inherits is not None and klass.inherits.type_name not in ordered_classes |
| 247 | + ) |
| 248 | + raise RuntimeError( |
| 249 | + f"Class `{bad_class.original_name}` inherits of unknown class `{bad_class.inherits.type_name}`" |
| 250 | + ) |
| 251 | + ordered_count = len(ordered_classes) |
| 252 | + |
| 253 | + return list(ordered_classes.values()) |
| 254 | + |
| 255 | + |
| 256 | +def parse_extension_api_json( |
| 257 | + path: Path, build_config: BuildConfig, skip_classes: bool = False |
| 258 | +) -> ExtensionApi: |
| 259 | + api_json = json.loads(path.read_text(encoding="utf8")) |
| 260 | + assert isinstance(api_json, dict) |
| 261 | + |
| 262 | + merge_builtins_size_info(api_json, build_config) |
| 263 | + |
| 264 | + # Not much info about variant |
| 265 | + variant_type = VariantTypeSpec(size=api_json["variant_size"]) |
| 266 | + TYPES_DB_REGISTER_TYPE("Variant", variant_type) |
| 267 | + |
| 268 | + # Unlike int type that is always 8 bytes long, float depends on config |
| 269 | + if build_config in (BuildConfig.DOUBLE_32, BuildConfig.DOUBLE_64): |
| 270 | + real_type = replace(TYPES_DB[f"meta:float"], original_name="float") |
| 271 | + else: |
| 272 | + real_type = replace(TYPES_DB[f"meta:double"], original_name="float") |
| 273 | + TYPES_DB_REGISTER_TYPE("float", real_type) |
| 274 | + |
| 275 | + def _register_enums(enums, parent_id=None): |
| 276 | + for enum_type in enums: |
| 277 | + classifier = "bitfield" if enum_type.is_bitfield else "enum" |
| 278 | + if parent_id: |
| 279 | + type_id = f"{classifier}::{parent_id}.{enum_type.original_name}" |
| 280 | + else: |
| 281 | + type_id = f"{classifier}::{enum_type.original_name}" |
| 282 | + TYPES_DB_REGISTER_TYPE(type_id, enum_type) |
| 283 | + |
| 284 | + builtins = parse_builtins_ignore_scalars_and_nil(api_json["builtin_classes"]) |
| 285 | + for builtin_type in builtins: |
| 286 | + TYPES_DB_REGISTER_TYPE(builtin_type.original_name, builtin_type) |
| 287 | + _register_enums(builtin_type.enums, parent_id=builtin_type.original_name) |
| 288 | + |
| 289 | + # Parsing classes takes ~75% of the time while not being needed to render builtins stuff |
| 290 | + if skip_classes: |
| 291 | + classes = [] |
| 292 | + |
| 293 | + else: |
| 294 | + classes = order_classes( |
| 295 | + [parse_class(x, object_size=api_json["object_size"]) for x in api_json["classes"]] |
| 296 | + ) |
| 297 | + for class_type in classes: |
| 298 | + TYPES_DB_REGISTER_TYPE(class_type.original_name, class_type) |
| 299 | + _register_enums(class_type.enums, parent_id=class_type.original_name) |
| 300 | + |
| 301 | + global_enums = [parse_global_enum(x) for x in api_json["global_enums"]] |
| 302 | + _register_enums(global_enums) |
| 303 | + |
| 304 | + ensure_types_db_consistency() |
| 305 | + |
| 306 | + api = ExtensionApi( |
| 307 | + version_major=api_json["header"]["version_major"], |
| 308 | + version_minor=api_json["header"]["version_minor"], |
| 309 | + version_patch=api_json["header"]["version_patch"], |
| 310 | + version_status=api_json["header"]["version_status"], |
| 311 | + version_build=api_json["header"]["version_build"], |
| 312 | + version_full_name=api_json["header"]["version_full_name"], |
| 313 | + classes=classes, |
| 314 | + builtins=builtins, |
| 315 | + global_constants=[GlobalConstantSpec.parse(x) for x in api_json["global_constants"]], |
| 316 | + global_enums=global_enums, |
| 317 | + utility_functions=[UtilityFunctionSpec.parse(x) for x in api_json["utility_functions"]], |
| 318 | + singletons=[SingletonSpec.parse(x) for x in api_json["singletons"]], |
| 319 | + native_structures=[NativeStructureSpec.parse(x) for x in api_json["native_structures"]], |
| 320 | + ) |
| 321 | + |
| 322 | + return api |
0 commit comments