diff --git a/src/mercury_engine_data_structures/common_types.py b/src/mercury_engine_data_structures/common_types.py index 7257e9ca..014dd259 100644 --- a/src/mercury_engine_data_structures/common_types.py +++ b/src/mercury_engine_data_structures/common_types.py @@ -1,4 +1,5 @@ import copy +import functools import typing import construct @@ -16,6 +17,21 @@ CVector4D = construct.Array(4, Float) +def _vector_emitparse(length: int, code: construct.CodeGen): + code.append(f"CVector{length}D_Format = struct.Struct('<{length}f')") + return f"ListContainer(CVector{length}D_Format.unpack(io.read({length * 4})))" + + +def _vector_emitbuild(length: int, code: construct.CodeGen): + code.append(f"CVector{length}D_Format = struct.Struct('<{length}f')") + return f"(io.write(CVector{length}D_Format.pack(*obj)), obj)" + + +for i, vec in enumerate([CVector2D, CVector3D, CVector4D]): + vec._emitparse = functools.partial(_vector_emitparse, i + 2) + vec._emitbuild = functools.partial(_vector_emitbuild, i + 2) + + class ListContainerWithKeyAccess(construct.ListContainer): def __init__(self, item_key_field: str, item_value_field: str = "value"): super().__init__() @@ -52,7 +68,8 @@ def __init__(self, subcon, *, allow_duplicates: bool = False): super().__init__(subcon) self.allow_duplicates = allow_duplicates - def _decode(self, obj: construct.ListContainer, context, path): + def _decode(self, obj: construct.ListContainer, context: construct.Container, path: str, + ) -> construct.ListContainer | construct.Container: result = construct.Container() for item in obj: key = item.key @@ -63,7 +80,8 @@ def _decode(self, obj: construct.ListContainer, context, path): result[key] = item.value return result - def _encode(self, obj: construct.Container, context, path): + def _encode(self, obj: construct.ListContainer | construct.Container, context: construct.Container, path: str, + ) -> list: if self.allow_duplicates and isinstance(obj, list): return obj return construct.ListContainer( @@ -73,6 +91,11 @@ def _encode(self, obj: construct.Container, context, path): def _emitparse(self, code): fname = f"parse_dict_adapter_{code.allocateId()}" + if self.allow_duplicates: + on_duplicate = "return obj" + else: + on_duplicate = 'raise ConstructError("Duplicated keys found in object")' + block = f""" def {fname}(io, this): obj = {self.subcon._compileparse(code)} @@ -80,7 +103,7 @@ def {fname}(io, this): for item in obj: result[item.key] = item.value if len(result) != len(obj): - raise ConstructError("Duplicated keys found in object") + {on_duplicate} return result """ code.append(block) @@ -88,15 +111,19 @@ def {fname}(io, this): def _emitbuild(self, code): fname = f"build_dict_adapter_{code.allocateId()}" - block = f""" + wrap = "obj = ListContainer(Container(key=type_, value=item) for type_, item in original_obj.items())" + if self.allow_duplicates: + wrap = f""" + if isinstance(original_obj, list): + obj = original_obj + else: + {wrap} + """ + code.append(f""" def {fname}(original_obj, io, this): - obj = ListContainer( - Container(key=type_, value=item) - for type_, item in original_obj.items() - ) + {wrap} return {self.subcon._compilebuild(code)} - """ - code.append(block) + """) return f"{fname}(obj, io, this)" @@ -265,11 +292,13 @@ def make_vector(value: construct.Construct): def _emitparse(code): return f"ListContainer(({value._compileparse(code)}) for i in range({construct.Int32ul._compileparse(code)}))" + result._emitparse = _emitparse def _emitbuild(code): return (f"(reuse(len(obj), lambda obj: {construct.Int32ul._compilebuild(code)})," f" list({value._compilebuild(code)} for obj in obj), obj)[2]") + result._emitbuild = _emitbuild return result diff --git a/src/mercury_engine_data_structures/construct_extensions/enum.py b/src/mercury_engine_data_structures/construct_extensions/enum.py index 2eb15ff8..61328ecb 100644 --- a/src/mercury_engine_data_structures/construct_extensions/enum.py +++ b/src/mercury_engine_data_structures/construct_extensions/enum.py @@ -21,6 +21,26 @@ def _encode(self, obj: typing.Union[str, enum.IntEnum, int], context, path) -> i return obj + def _emitbuild(self, code: construct.CodeGen): + i = code.allocateId() + + mapping = ", ".join( + f"{repr(enum_entry.name)}: {enum_entry.value}" + for enum_entry in self.enum_class + ) + + code.append(f""" + _enum_name_to_value_{i} = {{{mapping}}} + def _encode_enum_{i}(io, obj): + # {self.name} + try: + obj = obj.value + except AttributeError: + obj = _enum_name_to_value_{i}.get(obj, obj) + return {construct.Int32ul._compilebuild(code)} + """) + return f"_encode_enum_{i}(io, obj)" + def BitMaskEnum(enum_type: typing.Type[enum.IntEnum]): flags = {} diff --git a/src/mercury_engine_data_structures/construct_extensions/function_complex.py b/src/mercury_engine_data_structures/construct_extensions/function_complex.py new file mode 100644 index 00000000..783e58ab --- /dev/null +++ b/src/mercury_engine_data_structures/construct_extensions/function_complex.py @@ -0,0 +1,46 @@ +import construct + + +class SwitchComplexKey(construct.Switch): + def _insert_keyfunc(self, code: construct.CodeGen): + if id(self.keyfunc) not in code.linkedinstances: + code.linkedinstances[id(self.keyfunc)] = self.keyfunc + return f"linkedinstances[{id(self.keyfunc)}](this)" + + def _emitparse(self, code: construct.CodeGen): + fname = f"switch_cases_{code.allocateId()}" + code.append(f"{fname} = {{}}") + for key, sc in self.cases.items(): + code.append(f"{fname}[{repr(key)}] = lambda io,this: {sc._compileparse(code)}") + defaultfname = f"switch_defaultcase_{code.allocateId()}" + code.append(f"{defaultfname} = lambda io,this: {self.default._compileparse(code)}") + return f"{fname}.get({self._insert_keyfunc(code)}, {defaultfname})(io, this)" + + def _emitbuild(self, code: construct.CodeGen): + fname = f"switch_cases_{code.allocateId()}" + code.append(f"{fname} = {{}}") + for key, sc in self.cases.items(): + code.append(f"{fname}[{repr(key)}] = lambda obj,io,this: {sc._compilebuild(code)}") + defaultfname = f"switch_defaultcase_{code.allocateId()}" + code.append(f"{defaultfname} = lambda obj,io,this: {self.default._compilebuild(code)}") + return f"{fname}.get({self._insert_keyfunc(code)}, {defaultfname})(obj, io, this)" + + +class ComplexIfThenElse(construct.IfThenElse): + def _insert_cond(self, code: construct.CodeGen): + if id(self.condfunc) not in code.linkedinstances: + code.linkedinstances[id(self.condfunc)] = self.condfunc + return f"linkedinstances[{id(self.condfunc)}](this)" + + def _emitparse(self, code): + return "(({}) if ({}) else ({}))".format(self.thensubcon._compileparse(code), + self._insert_cond(code), + self.elsesubcon._compileparse(code),) + + def _emitbuild(self, code): + return (f"(({self.thensubcon._compilebuild(code)}) if (" + f"{self._insert_cond(code)}) else ({self.elsesubcon._compilebuild(code)}))") + + +def ComplexIf(condfunc, subcon): + return ComplexIfThenElse(condfunc, subcon, construct.Pass) diff --git a/src/mercury_engine_data_structures/construct_extensions/strings.py b/src/mercury_engine_data_structures/construct_extensions/strings.py index 5cbf9335..3aa7a95d 100644 --- a/src/mercury_engine_data_structures/construct_extensions/strings.py +++ b/src/mercury_engine_data_structures/construct_extensions/strings.py @@ -48,8 +48,10 @@ def PaddedStringRobust(length, encoding): u'Афон' """ macro = StringEncodedRobust(FixedSized(length, NullStripped(GreedyBytes, pad=encodingunit(encoding))), encoding) + def _emitfulltype(ksy, bitwise): return dict(size=length, type="strz", encoding=encoding) + macro._emitfulltype = _emitfulltype return macro @@ -79,6 +81,7 @@ def PascalStringRobust(lengthfield: construct.Construct, encoding): def _emitparse(code): return f"io.read({lengthfield._compileparse(code)}).decode({repr(encoding)})" + macro._emitparse = _emitparse def _emitseq(ksy, bitwise): @@ -86,6 +89,7 @@ def _emitseq(ksy, bitwise): dict(id="lengthfield", type=lengthfield._compileprimitivetype(ksy, bitwise)), dict(id="data", size="lengthfield", type="str", encoding=encoding), ] + macro._emitseq = _emitseq def _emitbuild(code: construct.CodeGen): @@ -97,9 +101,9 @@ def _emitbuild(code: construct.CodeGen): macro._emitbuild = _emitbuild - return macro + def CStringRobust(encoding): r""" String ending in a terminating null byte (or null bytes in case of UTF16 UTF32). @@ -121,12 +125,66 @@ def CStringRobust(encoding): >>> d.parse(_) u'Афон' """ - macro = StringEncodedRobust(NullTerminated(GreedyBytes, term=encodingunit(encoding)), encoding) + term = encodingunit(encoding) + macro = StringEncodedRobust(NullTerminated(GreedyBytes, term=term), encoding) + + expected_size = 16 + + def _emitparse(code: construct.CodeGen): + i = code.allocateId() + code.append(f""" + def read_util_term_{i}(io): + try: + # Assume it's a BytesIO. Then use bytes.find to do hard work on C + b = io.getvalue() + end = b.find({repr(term)}, io.tell()) + if end == -1: + raise StreamError + data = io.read(end - io.tell()) + io.read({len(term)}) + return data + except AttributeError: + # not a BytesIO + pass + + data = bytearray() + while True: + before = io.tell() + b = io.read({len(term) * expected_size}) + pos = b.find({repr(term)}) + if pos != -1: + io.seek(before + pos + {len(term)}) + data += b[:pos] + break + + if len(b) < {len(term) * expected_size}: + io.seek(before) + b = io.read({len(term)}) + if b == {repr(term)}: + break + elif len(b) < {len(term)}: + raise StreamError + data += b + return data + """) + + return f"read_util_term_{i}(io).decode({repr(encoding)})" + + macro._emitparse = _emitparse + def _emitfulltype(ksy, bitwise): return dict(type="strz", encoding=encoding) + macro._emitfulltype = _emitfulltype + + def _emitbuild(code: construct.CodeGen): + return f"(io.write(obj.encode({repr(encoding)})), io.write({repr(term)}), obj)[-1]" + + macro._emitbuild = _emitbuild + return macro + def GreedyStringRobust(encoding): r""" String that reads entire stream until EOF, and writes a given string as-is. @@ -147,7 +205,9 @@ def GreedyStringRobust(encoding): u'Афон' """ macro = StringEncodedRobust(GreedyBytes, encoding) + def _emitfulltype(ksy, bitwise): return dict(size_eos=True, type="str", encoding=encoding) + macro._emitfulltype = _emitfulltype return macro diff --git a/src/mercury_engine_data_structures/file_tree_editor.py b/src/mercury_engine_data_structures/file_tree_editor.py index 5c839ae6..c7cc358b 100644 --- a/src/mercury_engine_data_structures/file_tree_editor.py +++ b/src/mercury_engine_data_structures/file_tree_editor.py @@ -31,12 +31,6 @@ class OutputFormat(enum.Enum): ROMFS = enum.auto() -def _find_entry_for_asset_id(asset_id: AssetId, pkg_header): - for entry in pkg_header.file_entries: - if entry.asset_id == asset_id: - return entry - - def _read_file_with_entry(path: Path, entry): with path.open("rb") as f: f.seek(entry.start_offset) @@ -125,11 +119,13 @@ def _update_headers(self): self._ensured_asset_ids[name] = set() + self.headers[name].entries_by_id = {} for entry in self.headers[name].file_entries: if self._toc.get_size_for(entry.asset_id) is None: logger.warning("File with asset id 0x%016x in pkg %s does not have an entry in the TOC", entry.asset_id, name) self._add_pkg_name_for_asset_id(entry.asset_id, name) + self.headers[name].entries_by_id[entry.asset_id] = entry def all_asset_ids(self) -> Iterator[AssetId]: """ @@ -184,7 +180,7 @@ def get_raw_asset(self, asset_id: NameOrAssetId, *, in_pkg: Optional[str] = None if in_pkg is not None and name != in_pkg: continue - entry = _find_entry_for_asset_id(asset_id, header) + entry = header.entries_by_id.get(asset_id) if entry is not None: logger.info("Reading asset %s from pkg %s", str(original_name), name) return _read_file_with_entry(self.path_for_pkg(name), entry) @@ -305,7 +301,12 @@ def get_pkg(self, pkg_name: str) -> Pkg: return self._in_memory_pkgs[pkg_name] - def save_modifications(self, output_path: Path, output_format: OutputFormat): + def save_modifications(self, output_path: Path, output_format: OutputFormat, *, finalize_editor: bool = True): + """Creates a mod file in the given output format with all the modifications requested. + :param output_path: Where to write the mod files. + :param output_format: If we should create PKG files or not. + :param finalize_editor: If set, this editor will no longer be usable after this function, but is faster. + """ replacements = [] modified_pkgs = set() asset_ids_to_copy = {} @@ -316,6 +317,11 @@ def save_modifications(self, output_path: Path, output_format: OutputFormat): if None in modified_pkgs: modified_pkgs.remove(None) + if output_format == OutputFormat.ROMFS: + # Clear modified_pkgs, so we don't read/write any new pkg + # We keep system.pkg because .bmmaps don't read properly with exlaunch and it's only 4MB + modified_pkgs = list(filter(lambda pkg: pkg == "packs/system/system.pkg", modified_pkgs)) + # Ensure all pkgs we'll modify is in memory already. # We'll need to read these files anyway to modify, so do it early to speedup # the get_raw_assets for _ensured_asset_ids. @@ -367,10 +373,6 @@ def save_modifications(self, output_path: Path, output_format: OutputFormat): }, indent=4) output_path.joinpath("replacements.json").write_text(replacement_json, "utf-8") - # Clear modified_pkgs so we don't write any new pkg - # We keep system.pkg because .bmmaps don't read properly with exlaunch and it's only 4MB - modified_pkgs = list(filter(lambda pkg: pkg == "packs/system/system.pkg", modified_pkgs)) - # Update the PKGs for pkg_name in modified_pkgs: logger.info("Updating %s", pkg_name) @@ -407,4 +409,14 @@ def save_modifications(self, output_path: Path, output_format: OutputFormat): ) self._modified_resources = {} - self._update_headers() + if finalize_editor: + # _update_headers has significant runtime costs, so avoid it. + # But lets delete these attributes so further use of this object fails explicitly + del self.all_pkgs + del self.headers + del self._ensured_asset_ids + del self._files_for_asset_id + del self._name_for_asset_id + del self._toc + else: + self._update_headers() diff --git a/src/mercury_engine_data_structures/formats/bmmap.py b/src/mercury_engine_data_structures/formats/bmmap.py index 0f3e643c..53e9b8bc 100644 --- a/src/mercury_engine_data_structures/formats/bmmap.py +++ b/src/mercury_engine_data_structures/formats/bmmap.py @@ -1,3 +1,5 @@ +import functools + from construct import Construct, Container from mercury_engine_data_structures.formats import BaseResource, standard_format @@ -8,8 +10,9 @@ class Bmmap(BaseResource): @classmethod + @functools.lru_cache def construct_class(cls, target_game: Game) -> Construct: - return BMMAP + return BMMAP.compile() @property def items(self) -> Container: diff --git a/src/mercury_engine_data_structures/formats/bmsad.py b/src/mercury_engine_data_structures/formats/bmsad.py index 273fd966..74b368f7 100644 --- a/src/mercury_engine_data_structures/formats/bmsad.py +++ b/src/mercury_engine_data_structures/formats/bmsad.py @@ -25,11 +25,12 @@ from mercury_engine_data_structures import common_types, game_check, type_lib from mercury_engine_data_structures.common_types import Char, CVector3D, Float, StrId, make_dict, make_vector from mercury_engine_data_structures.construct_extensions.alignment import PrefixedAllowZeroLen +from mercury_engine_data_structures.construct_extensions.function_complex import ComplexIf, SwitchComplexKey from mercury_engine_data_structures.construct_extensions.misc import ErrorWithMessage from mercury_engine_data_structures.formats import BaseResource, dread_types from mercury_engine_data_structures.formats.bmsas import BMSAS_SR, Bmsas from mercury_engine_data_structures.formats.property_enum import PropertyEnum -from mercury_engine_data_structures.game_check import Game +from mercury_engine_data_structures.game_check import Game, GameSpecificStruct from mercury_engine_data_structures.type_lib import get_type_lib_dread, get_type_lib_for_game @@ -67,7 +68,6 @@ def SR_or_Dread(sr, dread): )), )) - # Fields ExtraFields = common_types.DictAdapter(make_vector( common_types.DictElement(Struct( @@ -164,7 +164,7 @@ def component_type(this): return component_type return None - return Switch(component_type, component_dependencies) + return SwitchComplexKey(component_type, component_dependencies) def SRDependencies(): @@ -232,6 +232,20 @@ def SRDependencies(): return Switch(construct.this.type, component_dependencies) +FieldsSwitch = construct.Switch( + lambda ctx: find_charclass_for_type(ctx._._.type), + fieldtypes(Game.DREAD), + ErrorWithMessage(lambda ctx: f"Unknown component type: {ctx._._.type}", construct.SwitchError) +) + + +def _not_implemented(code): + raise NotImplementedError + + +FieldsSwitch._emitparse = _not_implemented +FieldsSwitch._emitbuild = _not_implemented + # Components DreadComponent = Struct( type=StrId, @@ -242,14 +256,10 @@ def SRDependencies(): Struct( empty_string=PropertyEnum, root=PropertyEnum, - fields=Switch( - lambda ctx: find_charclass_for_type(ctx._._.type), - fieldtypes(Game.DREAD), - ErrorWithMessage(lambda ctx: f"Unknown component type: {ctx._._.type}", construct.SwitchError) - ) + fields=FieldsSwitch, ) ), - extra_fields=construct.If( + extra_fields=ComplexIf( lambda this: get_type_lib_dread().is_child_of(this.type, "CComponent"), ExtraFields, ), @@ -266,7 +276,6 @@ def SRDependencies(): dependencies=SRDependencies(), ) - # Header _CActorDefFields = { "unk_1": Flag, @@ -316,7 +325,6 @@ def SRDependencies(): unk_8=Int32ul, ) - # BMSAD BMSAD_SR = Struct( "_magic" / Const(b"MSAD"), @@ -329,17 +337,17 @@ def SRDependencies(): "components" / make_dict(SRComponent), "unk_1" / Int32ul, - "unk_1a" / construct.If(lambda this: this.unk_1 == 2, construct.Bytes(9)), - "unk_1b" / construct.If(lambda this: this.unk_1 == 0, construct.Bytes(15)), + "unk_1a" / construct.If(construct.this.unk_1 == 2, construct.Bytes(9)), + "unk_1b" / construct.If(construct.this.unk_1 == 0, construct.Bytes(15)), "unk_2" / StrId, "unk_3" / Int32ul, "action_sets" / make_vector(BMSAS_SR), "_remaining" / construct.Peek(construct.GreedyBytes), - "sound_fx" / construct.If( + "sound_fx" / ComplexIf( lambda this: ( - (this._parsing and this._remaining) - or (this._building and (this.sound_fx is not None)) + (this._parsing and this._remaining) + or (this._building and (this.sound_fx is not None)) ), make_vector(StrId >> Byte) ), @@ -369,8 +377,9 @@ def SRDependencies(): construct.Terminated, ) - ArgAnyType = str | float | bool | int + + class ActorDefFunc: def __init__(self, raw: dict) -> None: self._raw = raw @@ -380,7 +389,7 @@ def new(cls, name: str, unk1: bool = True, unk2: bool = False, - ): + ): return cls(Container( name=name, unk1=unk1, @@ -454,6 +463,8 @@ def set_param(self, param_name: int | str, value: ArgAnyType): T = typing.TypeVar('T', bound=ArgAnyType) + + class ActorDefFuncParam(typing.Generic[T]): def __init__(self, index: int) -> None: self.index = index @@ -467,6 +478,8 @@ def __set__(self, inst: ActorDefFunc, value: T): Vec3 = list FieldType = typing.Union[bool, str, float, int, Vec3] + + class ComponentFields: def __init__(self, parent: "Component") -> None: self.parent = parent @@ -505,8 +518,8 @@ def __getattr__(self, __name: str) -> typing.Any: return self._get_extra_field(self.parent.raw.extra_fields, __name) if ( - self.parent.raw.fields is not None - and __name in self.parent.raw.fields.fields + self.parent.raw.fields is not None + and __name in self.parent.raw.fields.fields ): return self.parent.raw.fields.fields[__name] @@ -620,11 +633,12 @@ def dependencies(self, value: Container | ListContainer | None): class Bmsad(BaseResource): @classmethod + @functools.lru_cache def construct_class(cls, target_game: Game) -> Construct: - return { - Game.SAMUS_RETURNS: BMSAD_SR, - Game.DREAD: BMSAD_Dread, - }[target_game] + return GameSpecificStruct({ + Game.SAMUS_RETURNS: BMSAD_SR, + Game.DREAD: BMSAD_Dread, + }[target_game], target_game).compile() @property def name(self) -> str: @@ -718,4 +732,3 @@ def sound_fx(self, value: typing.Iterable[tuple[str, int]]): ) if self.target_game == Game.SAMUS_RETURNS and not self.raw.sound_fx: self.raw.sound_fx = None - diff --git a/src/mercury_engine_data_structures/formats/bmsas.py b/src/mercury_engine_data_structures/formats/bmsas.py index e4ede816..7a560ba0 100644 --- a/src/mercury_engine_data_structures/formats/bmsas.py +++ b/src/mercury_engine_data_structures/formats/bmsas.py @@ -226,7 +226,7 @@ def _emitbuild(self, code: construct.CodeGen): unk=Hex(Int32ul), animations=make_vector(AnimationDread), _end=construct.Terminated, -).compile() +) BMSAS_SR = Struct( diff --git a/src/mercury_engine_data_structures/formats/brem.py b/src/mercury_engine_data_structures/formats/brem.py index 8e0f3a3e..6209c6c0 100644 --- a/src/mercury_engine_data_structures/formats/brem.py +++ b/src/mercury_engine_data_structures/formats/brem.py @@ -3,10 +3,8 @@ from mercury_engine_data_structures.formats import BaseResource, standard_format from mercury_engine_data_structures.game_check import Game -BREM = standard_format.game_model('CEnvironmentMusicPresets', 0x02000004) - class Brem(BaseResource): @classmethod def construct_class(cls, target_game: Game) -> construct.Construct: - return BREM + return standard_format.game_model('CEnvironmentMusicPresets', 0x02000004) diff --git a/src/mercury_engine_data_structures/formats/bres.py b/src/mercury_engine_data_structures/formats/bres.py index 3c27237e..7dfc58aa 100644 --- a/src/mercury_engine_data_structures/formats/bres.py +++ b/src/mercury_engine_data_structures/formats/bres.py @@ -3,10 +3,8 @@ from mercury_engine_data_structures.formats import BaseResource, standard_format from mercury_engine_data_structures.game_check import Game -BRES = standard_format.game_model('CEnvironmentSoundPresets', 0x02020001) - class Bres(BaseResource): @classmethod def construct_class(cls, target_game: Game) -> construct.Construct: - return BRES + return standard_format.game_model('CEnvironmentSoundPresets', 0x02020001) diff --git a/src/mercury_engine_data_structures/formats/brev.py b/src/mercury_engine_data_structures/formats/brev.py index 97e5e059..dc6690ad 100644 --- a/src/mercury_engine_data_structures/formats/brev.py +++ b/src/mercury_engine_data_structures/formats/brev.py @@ -3,10 +3,8 @@ from mercury_engine_data_structures.formats import BaseResource, standard_format from mercury_engine_data_structures.game_check import Game -BREV = standard_format.game_model('CEnvironmentVisualPresets', 0x02020004) - class Brev(BaseResource): @classmethod def construct_class(cls, target_game: Game) -> construct.Construct: - return BREV + return standard_format.game_model('CEnvironmentVisualPresets', 0x02020004) diff --git a/src/mercury_engine_data_structures/formats/brfld.py b/src/mercury_engine_data_structures/formats/brfld.py index 88a8ee5e..8b97d8eb 100644 --- a/src/mercury_engine_data_structures/formats/brfld.py +++ b/src/mercury_engine_data_structures/formats/brfld.py @@ -8,14 +8,13 @@ from mercury_engine_data_structures.game_check import Game logger = logging.getLogger(__name__) -_BRFLD = standard_format.game_model('CScenario', 0x02000031) class Brfld(BaseResource): @classmethod @functools.lru_cache def construct_class(cls, target_game: Game) -> construct.Construct: - return _BRFLD.compile() + return standard_format.game_model('CScenario', 0x02000031) def actors_for_layer(self, name: str) -> dict: return self.raw.Root.pScenario.rEntitiesLayer.dctSublayers[name].dctActors diff --git a/src/mercury_engine_data_structures/formats/brsa.py b/src/mercury_engine_data_structures/formats/brsa.py index 13675da9..b1c2f342 100644 --- a/src/mercury_engine_data_structures/formats/brsa.py +++ b/src/mercury_engine_data_structures/formats/brsa.py @@ -6,14 +6,12 @@ from mercury_engine_data_structures.formats import BaseResource, standard_format from mercury_engine_data_structures.game_check import Game -_BRSA = standard_format.game_model('CSubAreaManager', 0x02010002) - class Brsa(BaseResource): @classmethod @functools.lru_cache def construct_class(cls, target_game: Game) -> Construct: - return _BRSA.compile() + return standard_format.game_model('CSubAreaManager', 0x02010002) @property def subarea_setups(self) -> Iterator[Container]: diff --git a/src/mercury_engine_data_structures/formats/pkg.py b/src/mercury_engine_data_structures/formats/pkg.py index 55da98ed..01dee7a6 100644 --- a/src/mercury_engine_data_structures/formats/pkg.py +++ b/src/mercury_engine_data_structures/formats/pkg.py @@ -17,25 +17,36 @@ from mercury_engine_data_structures import dread_data, samus_returns_data from mercury_engine_data_structures.construct_extensions.alignment import AlignTo from mercury_engine_data_structures.formats.base_resource import AssetId, BaseResource, NameOrAssetId, resolve_asset_id -from mercury_engine_data_structures.game_check import Game, get_current_game, is_sr_or_else - -Construct_AssetId = Hex(is_sr_or_else(Int32ul, Int64ul)) - - -FileEntry = Struct( - asset_id=Construct_AssetId, - start_offset=Int32ul, - end_offset=Int32ul, -) +from mercury_engine_data_structures.game_check import Game, get_current_game def _file_entry(target_game: Game): - return Struct( + result = Struct( asset_id=Hex(Int32ul if target_game == Game.SAMUS_RETURNS else Int64ul), start_offset=Int32ul, end_offset=Int32ul, ) + def _emitparse(code: construct.CodeGen) -> str: + fname = f"pkg_file_entry_{target_game.name}" + + if target_game == Game.SAMUS_RETURNS: + id_fmt = "L" + byte_count = 12 + else: + id_fmt = "Q" + byte_count = 16 + + code.append("import collections") + code.append("FileEntry = collections.namedtuple('FileEntry', ['asset_id', 'start_offset', 'end_offset'])") + code.append(f"format_{fname} = struct.Struct('<{id_fmt}LL')") + + return f"FileEntry(*format_{fname}.unpack(io.read({byte_count})))" + + result._emitparse = _emitparse + + return result + def _pkg_header(target_game: Game): return Struct( @@ -49,10 +60,11 @@ class PkgConstruct(construct.Construct): int_size: construct.FormatField file_headers_type: construct.Construct - def __init__(self): + def __init__(self, target_game: Game): super().__init__() self.int_size = typing.cast(construct.FormatField, Int32ul) - self.file_headers_type = PrefixedArray(self.int_size, FileEntry).compile() + self._file_entry = _file_entry(target_game) + self.file_headers_type = PrefixedArray(self.int_size, self._file_entry).compile() def _parse(self, stream, context, path) -> construct.Container: # Skip over header size and data section size @@ -77,7 +89,7 @@ def _parse(self, stream, context, path) -> construct.Container: return construct.Container(files=files) def _build(self, obj: construct.Container, stream, context, path): - file_entry_size = FileEntry.sizeof(target_game=get_current_game(context)) + file_entry_size = self._file_entry.sizeof() header_start = construct.stream_tell(stream, path) @@ -124,9 +136,6 @@ def _build(self, obj: construct.Container, stream, context, path): construct.stream_seek(stream, files_end, 0, path) -PKG = PkgConstruct() - - @dataclasses.dataclass(frozen=True) class PkgFile: game: Game @@ -146,7 +155,7 @@ def asset_name(self) -> str | None: class Pkg(BaseResource): @classmethod def construct_class(cls, target_game: Game) -> Construct: - return PKG + return PkgConstruct(target_game) @classmethod @functools.lru_cache diff --git a/src/mercury_engine_data_structures/formats/property_enum.py b/src/mercury_engine_data_structures/formats/property_enum.py index 90b18aae..6725389e 100644 --- a/src/mercury_engine_data_structures/formats/property_enum.py +++ b/src/mercury_engine_data_structures/formats/property_enum.py @@ -92,11 +92,20 @@ def _emitparse(self, code: construct.CodeGen): n = self.hash_set.name code.append("from mercury_engine_data_structures.formats.property_enum import HashSet") + code.append(f""" + _inverted_hashes_{n} = HashSet.{n}.inverted_hashes(Container(_params=Container(target_game=TARGET_GAME))) + if TARGET_GAME == Game.DREAD: + def _parse_hashset_{n}(io, this): + return {construct.Int64ul._compileparse(code)} + elif TARGET_GAME == Game.SAMUS_RETURNS: + def _parse_hashset_{n}(io, this): + return {construct.Int32ul._compileparse(code)} + """) + if self.allow_unknowns: - return (f"reuse({self.subcon._compileparse(code)}, " - f"lambda key: HashSet.{n}.inverted_hashes(this).get(key, key))") + return f"reuse(_parse_hashset_{n}(io, this), lambda key: _inverted_hashes_{n}.get(key, key))" else: - return f"HashSet.{n}.inverted_hashes(this)[{self.subcon._compileparse(code)}]" + return f"_inverted_hashes_{n}[_parse_hashset_{n}(io, this)]" def _emitbuild(self, code: construct.CodeGen): if self.allow_unknowns: @@ -105,8 +114,16 @@ def _emitbuild(self, code: construct.CodeGen): n = self.hash_set.name code.append("from mercury_engine_data_structures.formats.property_enum import HashSet") - ret: str = self._raw_subcon._compilebuild(code) - return ret.replace(".pack(obj)", f".pack(HashSet.{n}.known_hashes(this)[obj])") + code.append(f""" + _known_hashes_{n} = HashSet.{n}.known_hashes(Container(_params=Container(target_game=TARGET_GAME))) + if TARGET_GAME == Game.DREAD: + def _build_hashset_{n}(obj, io, this): + return {construct.Int64ul._compilebuild(code)} + elif TARGET_GAME == Game.SAMUS_RETURNS: + def _build_hashset_{n}(obj, io, this): + return {construct.Int32ul._compilebuild(code)} + """) + return f"(_build_hashset_{n}(_known_hashes_{n}[obj], io, this), obj)[1]" PropertyEnum = CRCAdapter(HashSet.PROPERTY) diff --git a/src/mercury_engine_data_structures/formats/standard_format.py b/src/mercury_engine_data_structures/formats/standard_format.py index 380adeab..5e09242d 100644 --- a/src/mercury_engine_data_structures/formats/standard_format.py +++ b/src/mercury_engine_data_structures/formats/standard_format.py @@ -1,12 +1,19 @@ +import functools +import typing from typing import Optional import construct from mercury_engine_data_structures.formats.property_enum import PropertyEnum +from mercury_engine_data_structures.game_check import Game, GameSpecificStruct from mercury_engine_data_structures.type_lib import get_type_lib_dread -def create(name: str, version: int, root_name: Optional[str] = None, explicit_root: bool = False): +def _const_if_present(con: construct.Construct, value: typing.Any | None) -> construct.Construct: + return construct.Const(value, con) if value is not None else con + + +def create(name: Optional[str], version: Optional[int], root_name: Optional[str] = None, explicit_root: bool = False): # this maybe needs to change in the future if SR and Dread have different formats for type using this type_lib = get_type_lib_dread() if root_name is None: @@ -21,18 +28,23 @@ def create(name: str, version: int, root_name: Optional[str] = None, explicit_ro else: root = type_lib.get_type(root_name).construct - result = construct.Struct( - _class_crc=construct.Const(name, PropertyEnum), - _version=construct.Const(version, construct.Hex(construct.Int32ul)), + result = GameSpecificStruct(construct.Struct( + _class_crc=_const_if_present(PropertyEnum, name), + _version=_const_if_present(construct.Hex(construct.Int32ul), version), root_type=construct.Const('Root', PropertyEnum), Root=root, _end=construct.Terminated, - ) + ), Game.DREAD) result.name = name return result +@functools.lru_cache +def _cached_game_model(): + return create(None, None, "gameeditor::CGameModelRoot").compile() + + def game_model(name: str, version: int): - return create(name, version, "gameeditor::CGameModelRoot") + return _cached_game_model() diff --git a/src/mercury_engine_data_structures/game_check.py b/src/mercury_engine_data_structures/game_check.py index d05a0e82..89f63606 100644 --- a/src/mercury_engine_data_structures/game_check.py +++ b/src/mercury_engine_data_structures/game_check.py @@ -100,3 +100,35 @@ def result(ctx): def is_sr_or_else(subcon1, subcon2) -> IfThenElse: return IfThenElse(construct.this._params.target_game == Game.SAMUS_RETURNS.value, subcon1, subcon2) + + +class GameSpecificStruct(construct.Subconstruct): + def __init__(self, subcon, game: Game): + super().__init__(subcon) + self.target_game = game + + def _parse(self, stream, context, path): + if get_current_game(context) != self.target_game: + raise construct.ExplicitError( + f"Expected {self.target_game}, got {get_current_game(context)}", path=path + ) + + return super()._parse(stream, context, path) + + def _build(self, obj, stream, context, path): + if get_current_game(context) != self.target_game: + raise construct.ExplicitError( + f"Expected {self.target_game}, got {get_current_game(context)}", path=path + ) + + return super()._build(obj, stream, context, path) + + def _emitparse(self, code: construct.CodeGen): + code.append("from mercury_engine_data_structures.game_check import Game") + code.append(f"TARGET_GAME = Game.{self.target_game.name}") + return self.subcon._emitparse(code) + + def _emitbuild(self, code: construct.CodeGen): + code.append("from mercury_engine_data_structures.game_check import Game") + code.append(f"TARGET_GAME = Game.{self.target_game.name}") + return self.subcon._emitbuild(code) diff --git a/src/mercury_engine_data_structures/object.py b/src/mercury_engine_data_structures/object.py index 2fa053d4..2fba304d 100644 --- a/src/mercury_engine_data_structures/object.py +++ b/src/mercury_engine_data_structures/object.py @@ -55,72 +55,78 @@ def list_iter(): PropertyEnum._build(field_type, stream, context, field_path) self.fields[field_type]._build(field_value, stream, context, field_path) - def _emitparse(self, code): - fname = f"parse_object_{code.allocateId()}" - type_table = f"parse_object_types_{code.allocateId()}" + def _emitparse(self, code: construct.CodeGen) -> str: + n = code.allocateId() + fname = f"parse_object_{n}" + type_table = f"parse_object_types_{n}" + + code.append(f""" + def _parse_object(io, this, type_table): + field_count = {construct.Int32ul._compileparse(code)} + result = Container() + array_response = False + + for i in range(field_count): + field_type = {PropertyEnum._compileparse(code)} + field_value = type_table[field_type](io, this) + + if array_response or field_type in result: + if not array_response: + result = ListContainer( + Container(type=name, item=value) + for name, value in result.items() + ) + array_response = True + result.append(Container(type=field_type, item=field_value)) + else: + result[field_type] = field_value - block = f""" - {type_table} = {{}} - """ + return result + """) + block = f"{type_table} = {{\n" for type_name, type_class in self.fields.items(): - block += f""" - {type_table}[{repr(type_name)}] = lambda io, this: {type_class._compileparse(code)} - """ + block += f" {repr(type_name)}: lambda io, this: {type_class._compileparse(code)},\n" + block += "}" + code.append(block) - block += f""" + code.append(f""" def {fname}(io, this): - field_count = {construct.Int32ul._compileparse(code)} - result = Container() - array_response = False - - for i in range(field_count): - field_type = {PropertyEnum._compileparse(code)} - field_value = {type_table}[field_type](io, this) - - if array_response or field_type in result: - if not array_response: - result = ListContainer( - Container(type=name, item=value) - for name, value in result.items() - ) - array_response = True - result.append(Container(type=field_type, item=field_value)) - else: - result[field_type] = field_value - - return result - """ - code.append(block) + # {self.name} + return _parse_object(io, this, {type_table}) + """) return f"{fname}(io, this)" - def _emitbuild(self, code): - fname = f"build_object_{code.allocateId()}" - type_table = f"build_object_types_{code.allocateId()}" - - block = f""" - {type_table} = {{}} - """ + def _emitbuild(self, code: construct.CodeGen) -> str: + n = code.allocateId() + fname = f"build_object_{n}" + type_table = f"build_object_types_{n}" + + code.append(f""" + def _build_object(the_obj, io, this, type_table): + obj = len_(the_obj) + {construct.Int32ul._compilebuild(code)} + if isinstance(the_obj, list): + for it in the_obj: + obj = it["type"] + {PropertyEnum._compilebuild(code)} + type_table[it["type"]](it["item"], io, this) + else: + for field_type, field_value in the_obj.items(): + obj = field_type + {PropertyEnum._compilebuild(code)} + type_table[field_type](field_value, io, this) + """) + block = f"{type_table} = {{\n" for type_name, type_class in self.fields.items(): - block += f""" - {type_table}[{repr(type_name)}] = lambda obj, io, this: {type_class._compilebuild(code)} - """ + block += f" {repr(type_name)}: lambda obj, io, this: {type_class._compilebuild(code)},\n" + block += "}" + code.append(block) - block += f""" + code.append(f""" def {fname}(the_obj, io, this): - obj = len_(the_obj) - {construct.Int32ul._compilebuild(code)} - if isinstance(the_obj, list): - for it in the_obj: - obj = it["type"] - {PropertyEnum._compilebuild(code)} - {type_table}[it["type"]](it["item"], io, this) - else: - for field_type, field_value in the_obj.items(): - obj = field_type - {PropertyEnum._compilebuild(code)} - {type_table}[field_type](field_value, io, this) - """ - code.append(block) + # {self.name} + _build_object(the_obj, io, this, {type_table}) + """) return f"{fname}(obj, io, this)" diff --git a/src/mercury_engine_data_structures/pointer_set.py b/src/mercury_engine_data_structures/pointer_set.py index ba29e0f5..2e366d39 100644 --- a/src/mercury_engine_data_structures/pointer_set.py +++ b/src/mercury_engine_data_structures/pointer_set.py @@ -2,6 +2,7 @@ Helper class to handle objects that contain a pointer to objects of varied types, usually all with the same base type. """ import copy +import struct from typing import Dict, Type, Union import construct @@ -14,9 +15,21 @@ class PointerAdapter(Adapter): types: Dict[int, Union[Construct, Type[Construct]]] - def __init__(self, subcon, types): - super().__init__(subcon) + def __init__(self, types: Dict[int, Union[Construct, Type[Construct]]], category: str): + get_name = mercury_engine_data_structures.dread_data.all_property_id_to_name().get + self.switch_con = Switch( + construct.this.type, + types, + ErrorWithMessage(lambda ctx: ( + f"Property {ctx.type} ({get_name(ctx.type)}) without assigned type" + )), + ) + super().__init__(Struct( + type=Hex(Int64ul), + ptr=self.switch_con, + )) self.types = types + self.category = category @property def _allow_null(self): @@ -69,53 +82,71 @@ def _encode(self, obj: construct.Container, context, path): ptr=obj, ) - def _emitparse(self, code): - if self._single_type: - code.parsercache[id(self)] = f"({self.subcon._compileparse(code)}).ptr" - return code.parsercache[id(self)] + def _emitparse(self, code: construct.CodeGen) -> str: + n = code.allocateId() - fname = f"parse_object_{code.allocateId()}" - code.parsercache[id(self)] = f"{fname}(io, this)" + fname = f"parse_pointer_{n}" + case_name = f"switch_cases_{n}" - block = f""" - import mercury_engine_data_structures.dread_data - def {fname}(io, this): - obj = {self.subcon._compileparse(code)} - if obj.ptr is None: - return None - obj.ptr["@type"] = mercury_engine_data_structures.dread_data.all_property_id_to_name()[obj.type] - return obj.ptr - """ - code.append(block) + if self._single_type: + code.parsercache[id(self)] = f"{case_name}[{Int64ul._compileparse(code)}](io, this)" + else: + code.parsercache[id(self)] = f"{fname}(io, this)" + code.append("import mercury_engine_data_structures.dread_data") + block = f""" + def {fname}(io, this): + # {self.category} supports {len(self.types)} types + obj_type = {Int64ul._compileparse(code)} + ptr = {case_name}[obj_type](io, this) + if ptr is None: + return None + ptr["@type"] = mercury_engine_data_structures.dread_data.all_property_id_to_name()[obj_type] + return ptr + """ + code.append(block) + + code.append(f"{case_name} = {{}}") + for key, sc in self.types.items(): + code.append(f"{case_name}[{repr(key)}] = lambda io,this: {sc._compileparse(code)}") return code.parsercache[id(self)] - def _emitbuild(self, code): + def _emitbuild(self, code: construct.CodeGen) -> str: void_id = mercury_engine_data_structures.dread_data.all_name_to_property_id()["void"] - fname = f"build_object_{code.allocateId()}" + + n = code.allocateId() + + fname = f"build_pointer_{n}" + case_name = f"switch_cases_{n}" # PointerSet is used recursively, so break the infinite loop by prefilling the cache code.buildercache[id(self)] = f"{fname}(obj, io, this)" + # Switch cases + code.append(f"{case_name} = {{}}") + for key, sc in self.types.items(): + code.append(f"{case_name}[{repr(key)}] = lambda obj,io,this: {sc._compilebuild(code)}") + block = f""" def {fname}(the_obj, io, this): + # {self.category} supports {len(self.types)} types if the_obj is None: - obj = Container(type={void_id}, ptr=None) - return {self.subcon._compilebuild(code)} + return io.write({repr(struct.pack(Int64ul.fmtstr, void_id))}) """ if self._single_type: type_id = list(self.types.keys())[1] block += f""" - obj = Container(type={type_id}, ptr=the_obj) - return {self.subcon._compilebuild(code)} + io.write({repr(struct.pack(Int64ul.fmtstr, type_id))}) + return {case_name}[{type_id}](the_obj, io, this) """ else: + code.append("import mercury_engine_data_structures.dread_data") block += f""" - ptr = Container(**the_obj) - type_id = mercury_engine_data_structures.dread_data.all_name_to_property_id()[ptr.pop("@type")] - obj = Container(type=type_id, ptr=ptr) - return {self.subcon._compilebuild(code)} + new_obj = Container(**the_obj) + obj = mercury_engine_data_structures.dread_data.all_name_to_property_id()[new_obj.pop("@type")] + {Int64ul._compilebuild(code)} + return {case_name}[obj](new_obj, io, this) """ code.append(block) @@ -149,16 +180,4 @@ def type_names(self) -> tuple[str, ...]: return tuple(all_names[prop_id] for prop_id in self.types) def create_construct(self) -> Construct: - get_name = mercury_engine_data_structures.dread_data.all_property_id_to_name().get - - return PointerAdapter(Struct( - type=Hex(Int64ul), - ptr=Switch( - construct.this.type, - self.types, - ErrorWithMessage( - lambda - ctx: f"Property {ctx.type} ({get_name(ctx.type)}) " - "without assigned type"), - ) - ), self.types) + return PointerAdapter(self.types, self.category) diff --git a/tests/formats/test_minimap.py b/tests/formats/test_minimap.py index 00c4c8b5..65a3dbd0 100644 --- a/tests/formats/test_minimap.py +++ b/tests/formats/test_minimap.py @@ -1,16 +1,20 @@ -from tests.test_lib import parse_and_build_compare +import pytest +from tests.test_lib import parse_build_compare_editor -from mercury_engine_data_structures.formats.bmmap import BMMAP -from mercury_engine_data_structures.formats.bmmdef import BMMDEF -from mercury_engine_data_structures.game_check import Game +from mercury_engine_data_structures import dread_data +from mercury_engine_data_structures.formats.bmmap import Bmmap +from mercury_engine_data_structures.formats.bmmdef import Bmmdef +all_dread_bmmap = [name for name in dread_data.all_name_to_asset_id().keys() + if name.endswith(".bmmap")] + + +@pytest.mark.parametrize("path", all_dread_bmmap) +def test_dread_bmsnav(dread_file_tree, path): + parse_build_compare_editor(Bmmap, dread_file_tree, path) -def test_compare_bmmap_dread(dread_path): - parse_and_build_compare( - BMMAP, Game.DREAD, dread_path.joinpath("maps/levels/c10_samus/s010_cave/s010_cave.bmmap"), True - ) -def test_compare_bmmdef_dread(dread_path): - parse_and_build_compare( - BMMDEF, Game.DREAD, dread_path.joinpath("system/minimap/minimap.bmmdef"), True +def test_compare_bmmdef_dread(dread_file_tree): + parse_build_compare_editor( + Bmmdef, dread_file_tree, "system/minimap/minimap.bmmdef" ) diff --git a/tests/formats/test_pkg.py b/tests/formats/test_pkg.py index 4152e4b4..9c64521c 100644 --- a/tests/formats/test_pkg.py +++ b/tests/formats/test_pkg.py @@ -1,7 +1,7 @@ from construct import Container, ListContainer from tests.test_lib import parse_and_build_compare -from mercury_engine_data_structures.formats.pkg import PKG, Pkg +from mercury_engine_data_structures.formats.pkg import Pkg from mercury_engine_data_structures.game_check import Game _EMPTY_DREAD_PKG = (b'|\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -16,7 +16,7 @@ def test_compare_dread(dread_path): parse_and_build_compare( - PKG, Game.DREAD, dread_path.joinpath("packs/system/system.pkg") + Pkg.construct_class(Game.DREAD), Game.DREAD, dread_path.joinpath("packs/system/system.pkg") ) diff --git a/tests/test_lib.py b/tests/test_lib.py index e8e4b70f..24c70682 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -1,12 +1,13 @@ import typing from pathlib import Path +import construct import pytest from construct.lib.containers import Container from mercury_engine_data_structures.file_tree_editor import FileTreeEditor from mercury_engine_data_structures.formats import BaseResource -from mercury_engine_data_structures.game_check import Game +from mercury_engine_data_structures.game_check import Game, GameSpecificStruct def _parse_and_build_compare(module, game: Game, file_path: Path, print_data=False, save_file=None): @@ -57,3 +58,11 @@ def parse_build_compare_editor(module: typing.Type[BaseResource], encoded = construct_class.build(data, target_game=editor.target_game) assert encoded == raw + + +def game_compile_build(con: construct.Construct, data: construct.Container, target_game: Game) -> bytes: + return GameSpecificStruct(con, target_game).compile().build(data, target_game=target_game) + + +def game_compile_parse(con: construct.Construct, data: bytes, target_game: Game) -> construct.Container: + return GameSpecificStruct(con, target_game).compile().parse(data, target_game=target_game) diff --git a/tests/test_object.py b/tests/test_object.py index 56dc337e..6493d66b 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -3,6 +3,7 @@ from mercury_engine_data_structures import common_types from mercury_engine_data_structures.game_check import Game from mercury_engine_data_structures.object import Object +from tests.test_lib import game_compile_build, game_compile_parse TestClass = Object({ "fTimeToChargeDoubleGroundShock": common_types.Float, @@ -43,10 +44,10 @@ def test_parse_object(sample_object): def test_compile_build_object(sample_object): - result = TestClass.compile().build(sample_object[0], target_game=Game.DREAD) + result = game_compile_build(TestClass, sample_object[0], target_game=Game.DREAD) assert result == sample_object[1] def test_compile_parse_object(sample_object): - result = TestClass.compile().parse(sample_object[1], target_game=Game.DREAD) + result = game_compile_parse(TestClass, sample_object[1], target_game=Game.DREAD) assert result == sample_object[0] diff --git a/tests/test_pointer_set.py b/tests/test_pointer_set.py index acf1ef71..98993ee2 100644 --- a/tests/test_pointer_set.py +++ b/tests/test_pointer_set.py @@ -4,6 +4,7 @@ from mercury_engine_data_structures.game_check import Game from mercury_engine_data_structures.object import Object from mercury_engine_data_structures.pointer_set import PointerSet +from tests.test_lib import game_compile_build, game_compile_parse CEnemyPreset = Object({ "sId": common_types.StrId, @@ -78,7 +79,7 @@ def test_parse_single_object(single_type_sample): def test_compile_parse_single_object(single_type_sample): - result = SingleTypeConstruct.compile().parse(single_type_sample[1], target_game=Game.DREAD) + result = game_compile_parse(SingleTypeConstruct, single_type_sample[1], target_game=Game.DREAD) assert result == single_type_sample[0] @@ -93,10 +94,10 @@ def test_parse_two_object(two_type_sample): def test_compile_build_two_object(two_type_sample): - result = TwoTypeConstruct.compile().build(two_type_sample[0], target_game=Game.DREAD) + result = game_compile_build(TwoTypeConstruct, two_type_sample[0], target_game=Game.DREAD) assert result == two_type_sample[1] def test_compile_parse_two_object(two_type_sample): - result = TwoTypeConstruct.compile().parse(two_type_sample[1], target_game=Game.DREAD) + result = game_compile_parse(TwoTypeConstruct, two_type_sample[1], target_game=Game.DREAD) assert result == two_type_sample[0]