From 23b787def4f658a6528f2d785adbfdf1b0b36a63 Mon Sep 17 00:00:00 2001 From: LagoLunatic Date: Fri, 5 Jul 2024 13:13:12 -0400 Subject: [PATCH] Type hints, cleanup, error handling, minor bug fixes --- asm/disassemble.py | 22 ++++++++++++-------- logic/logic.py | 13 +++++++----- randomizers/base_randomizer.py | 12 ++++++++--- randomizers/boss_reqs.py | 2 +- randomizers/entrances.py | 18 ++++++++-------- randomizers/hints.py | 32 +++++++++++++++++++--------- test/test_dry.py | 2 +- tweaks.py | 38 +++++++++++++++++----------------- wwlib/dzb.py | 15 ++++++++------ wwlib/dzx.py | 4 ++-- 10 files changed, 93 insertions(+), 65 deletions(-) diff --git a/asm/disassemble.py b/asm/disassemble.py index c9839dec0..689075492 100644 --- a/asm/disassemble.py +++ b/asm/disassemble.py @@ -1,3 +1,7 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from randomizer import WWRandomizer from subprocess import call from subprocess import DEVNULL @@ -30,7 +34,7 @@ def get_bin(name): return os.path.join(devkitpath(), name + ".exe") -def disassemble_all_code(self): +def disassemble_all_code(self: WWRandomizer): if not os.path.isfile(get_bin("powerpc-eabi-objdump")): raise Exception(r"Failed to disassemble code: Could not find devkitPPC. devkitPPC should be installed to: C:\devkitPro\devkitPPC") @@ -75,7 +79,7 @@ def disassemble_all_code(self): data.seek(0) f.write(data.read()) - all_rels_by_path = {} + all_rels_by_path: dict[str, REL] = {} all_rel_symbols_by_path = {} for file_path_in_gcm in all_rel_paths: basename_with_ext = os.path.basename(file_path_in_gcm) @@ -141,7 +145,7 @@ def disassemble_file(bin_path, asm_path): if result != 0: raise Exception("Disassembler call failed") -def add_relocations_and_symbols_to_rel(asm_path, rel_path, file_path_in_gcm, main_symbols, all_rel_symbols_by_path, all_rels_by_path): +def add_relocations_and_symbols_to_rel(asm_path, rel_path, file_path_in_gcm: str, main_symbols, all_rel_symbols_by_path, all_rels_by_path: dict[str, REL]): rel = all_rels_by_path[file_path_in_gcm] rel_symbol_names = all_rel_symbols_by_path[file_path_in_gcm] @@ -303,7 +307,7 @@ def add_relocations_and_symbols_to_rel(asm_path, rel_path, file_path_in_gcm, mai "stfdux", ] -def add_symbols_to_main(self, asm_path, main_symbols): +def add_symbols_to_main(self: WWRandomizer, asm_path, main_symbols): out_str = "" with open(asm_path) as f: last_lis_match = None @@ -436,8 +440,8 @@ def add_symbols_to_main(self, asm_path, main_symbols): f.write(out_str) -def get_list_of_all_rels(self): - all_rel_paths = [] +def get_list_of_all_rels(self: WWRandomizer): + all_rel_paths: list[str] = [] for file_path in self.gcm.files_by_path: if file_path.startswith("files/rels/"): @@ -450,7 +454,7 @@ def get_list_of_all_rels(self): return all_rel_paths -def find_rel_by_module_num(all_rels_by_path, module_num): +def find_rel_by_module_num(all_rels_by_path: dict[str, REL], module_num): for rel_path, rel in all_rels_by_path.items(): if rel.id == module_num: return (rel, rel_path) @@ -465,7 +469,7 @@ def get_main_symbols(framework_map_contents): main_symbols[address] = name return main_symbols -def get_rel_symbols(rel, rel_map_data): +def get_rel_symbols(rel: REL, rel_map_data: str): rel_map_lines = rel_map_data.splitlines() found_memory_map = False next_section_index = 0 @@ -526,7 +530,7 @@ def get_padded_comment_string_for_line(line): return (" "*spaces_needed) + "; " -def check_offset_in_executable_dol_section(self, offset): +def check_offset_in_executable_dol_section(self: WWRandomizer, offset): section_index = self.dol.convert_offset_to_section_index(offset) if section_index is None: return False diff --git a/logic/logic.py b/logic/logic.py index 6993bdb20..c1a3120a5 100644 --- a/logic/logic.py +++ b/logic/logic.py @@ -207,7 +207,7 @@ def get_num_progression_locations(self): @staticmethod def get_num_progression_locations_static(item_locations: dict[str, dict], options: Options): progress_locations = Logic.filter_locations_for_progression_static( - item_locations.keys(), + list(item_locations.keys()), item_locations, options, filter_sunken_treasure=True @@ -224,7 +224,7 @@ def get_max_required_bosses_banned_locations(self): if not self.options.required_bosses: return 0 - all_locations = self.item_locations.keys() + all_locations = list(self.item_locations.keys()) progress_locations = self.filter_locations_for_progression(all_locations) location_counts_by_dungeon = {} @@ -259,7 +259,7 @@ def get_max_required_bosses_banned_locations(self): return max_banned_locations def get_progress_and_non_progress_locations(self): - all_locations = self.item_locations.keys() + all_locations = list(self.item_locations.keys()) progress_locations = self.filter_locations_for_progression(all_locations, filter_sunken_treasure=True) nonprogress_locations = [] for location_name in all_locations: @@ -481,7 +481,7 @@ def check_item_is_useful(self, item_name, inaccessible_undone_item_locations): self.cached_items_are_useful[item_name] = False return False - def filter_locations_for_progression(self, locations_to_filter, filter_sunken_treasure=False): + def filter_locations_for_progression(self, locations_to_filter: list[str], filter_sunken_treasure=False): return Logic.filter_locations_for_progression_static( locations_to_filter, self.item_locations, @@ -765,7 +765,7 @@ def make_useless_progress_items_nonprogress(self): if self.options.progression_triforce_charts or self.options.progression_treasure_charts: filter_sunken_treasure = False progress_locations = Logic.filter_locations_for_progression_static( - self.item_locations.keys(), + list(self.item_locations.keys()), self.item_locations, self.options, filter_sunken_treasure=filter_sunken_treasure @@ -1069,6 +1069,7 @@ def get_items_needed_from_logical_expression_req(self, logical_expression, reqs_ def check_progressive_item_req(self, req_name: str): match = re.search(r"^(Progressive .+) x(\d+)$", req_name) + assert match item_name = match.group(1) num_required = int(match.group(2)) @@ -1077,6 +1078,7 @@ def check_progressive_item_req(self, req_name: str): def check_small_key_req(self, req_name: str): match = re.search(r"^(.+ Small Key) x(\d+)$", req_name) + assert match small_key_name = match.group(1) num_keys_required = int(match.group(2)) @@ -1085,6 +1087,7 @@ def check_small_key_req(self, req_name: str): def check_item_location_requirement(self, req_name: str): match = re.search(r"^Can Access Item Location \"([^\"]+)\"$", req_name) + assert match item_location_name = match.group(1) return self.check_location_accessible(item_location_name) diff --git a/randomizers/base_randomizer.py b/randomizers/base_randomizer.py index 4a09f1828..895b5c951 100644 --- a/randomizers/base_randomizer.py +++ b/randomizers/base_randomizer.py @@ -18,7 +18,7 @@ def __init__(self, rando: WWRandomizer): self.rando = rando self.logic = rando.logic self.options = rando.options - self.rng = None + self._rng = None self.made_any_changes = False def is_enabled(self) -> bool: @@ -48,13 +48,19 @@ def progress_save_text(self) -> str: """The message displayed to the user during the save step.""" return "Applying changes..." + @property + def rng(self): + if self._rng is None: + raise Exception("Attempted to use the RNG outside of the randomization step.") + return self._rng + def reset_rng(self): - self.rng = self.rando.get_new_rng() + self._rng = self.rando.get_new_rng() def randomize(self): self.reset_rng() self._randomize() - self.rng = None + self._rng = None self.made_any_changes = True def _randomize(self): diff --git a/randomizers/boss_reqs.py b/randomizers/boss_reqs.py index 40ffdbe87..9cc313421 100644 --- a/randomizers/boss_reqs.py +++ b/randomizers/boss_reqs.py @@ -85,7 +85,7 @@ def randomize_required_bosses(self): if len(possible_boss_item_locations) != 6: raise Exception("Number of boss item locations is incorrect: " + ", ".join(possible_boss_item_locations)) if num_required_bosses > 6 or num_required_bosses < 1: - raise Exception(f"Number of required bosses is invalid: {len(num_required_bosses)}") + raise Exception(f"Number of required bosses is invalid: {num_required_bosses}") self.required_boss_item_locations = self.rng.sample(possible_boss_item_locations, num_required_bosses) diff --git a/randomizers/entrances.py b/randomizers/entrances.py index 509184d14..ae0542e63 100644 --- a/randomizers/entrances.py +++ b/randomizers/entrances.py @@ -15,11 +15,11 @@ class ZoneEntrance: scls_exit_index: int spawn_id: int entrance_name: str - island_name: str = None - warp_out_stage_name: str = None - warp_out_room_num: int = None - warp_out_spawn_id: int = None - nested_in: 'ZoneExit' = None + island_name: str | None = None + warp_out_stage_name: str | None = None + warp_out_room_num: int | None = None + warp_out_spawn_id: int | None = None + nested_in: 'ZoneExit | None' = None @property def is_nested(self): @@ -41,12 +41,12 @@ def __post_init__(self): class ZoneExit: stage_name: str room_num: int - scls_exit_index: int + scls_exit_index: int | None spawn_id: int unique_name: str _: KW_ONLY - boss_stage_name: str = None - zone_name: str = None + boss_stage_name: str | None = None + zone_name: str | None = None # If zone_name is specified, this exit will assume by default that it owns all item locations in # that zone which are behind randomizable entrances. If a single zone has multiple randomizable # entrances, only one of them at most can use zone_name. The rest must have their item locations @@ -1047,7 +1047,7 @@ def get_entrance_zone_for_item_location(self, location_name: str) -> str: outermost_entrance = self.get_outermost_entrance_for_exit(zone_exit) return outermost_entrance.island_name - def get_all_zones_for_item_location(self, location_name: str) -> list[str]: + def get_all_zones_for_item_location(self, location_name: str) -> set[str]: # Helper function to return a set of zone names that include the location. # # All returned zones are either an island name or a dungeon name - that is, if the entrance to diff --git a/randomizers/hints.py b/randomizers/hints.py index 1ce10e463..eed84c05e 100644 --- a/randomizers/hints.py +++ b/randomizers/hints.py @@ -30,7 +30,12 @@ class ItemImportance(Enum): class Hint: - def __init__(self, type: HintType, place, reward=None, importance=None): + type: HintType + place: str + reward: str | None + importance: ItemImportance | None + + def __init__(self, type: HintType, place: str, reward: str | None = None, importance: ItemImportance | None = None): assert place is not None if type == HintType.BARREN: assert reward is None if type != HintType.BARREN: assert reward is not None @@ -148,13 +153,13 @@ class HintsRandomizer(BaseRandomizer): #endregion - cryptic_item_hints = None - cryptic_zone_hints = None - location_hints = None + cryptic_item_hints: dict[str, str] = None + cryptic_zone_hints: dict[str, str] = None + location_hints: dict[str, dict] = None def __init__(self, rando): super().__init__(rando) - self.path_logic = None + self._path_logic = None self.path_logic_initial_state = None # Define instance variable shortcuts for hint distribution options. @@ -213,8 +218,14 @@ def progress_randomize_text(self) -> str: def progress_save_text(self) -> str: return "Saving hints..." + @property + def path_logic(self): + if self._path_logic is None: + raise Exception("Hints randomizer attempted to use uninitialized path logic.") + return self._path_logic + def _randomize(self): - self.path_logic = Logic(self.rando) + self._path_logic = Logic(self.rando) self.path_logic_initial_state = self.path_logic.save_simulated_playthrough_state() # Generate the hints that will be distributed over the hint placement options @@ -871,9 +882,9 @@ def get_barren_hint(self, unhinted_zones, zone_weights): return barren_hint - def filter_out_hinted_barren_locations(self, hintable_locations, hinted_barren_zones): + def filter_out_hinted_barren_locations(self, hintable_locations: list[str], hinted_barren_zones: list[Hint]): # Remove locations in hinted barren areas. - new_hintable_locations = [] + new_hintable_locations: list[str] = [] barrens = [hint.place for hint in hinted_barren_zones] for location_name in hintable_locations: entrance_zones = self.rando.entrances.get_all_zones_for_item_location(location_name) @@ -916,6 +927,7 @@ def get_importance_for_location(self, location_name): def check_is_legal_item_hint(self, location_name, progress_locations, previously_hinted_locations): item_name = self.logic.done_item_locations[location_name] + assert item_name is not None if not self.check_item_can_be_hinted_at(item_name): return False @@ -934,7 +946,7 @@ def check_is_legal_item_hint(self, location_name, progress_locations, previously return True - def get_legal_item_hints(self, progress_locations, hinted_barren_zones, previously_hinted_locations): + def get_legal_item_hints(self, progress_locations, hinted_barren_zones: list[Hint], previously_hinted_locations): # Helper function to build a list of locations which may be hinted as item hints in this seed. # Filter out locations which are invalid to be hinted at for item hints. @@ -1157,7 +1169,7 @@ def generate_hints(self): # We select at most `self.max_barren_hints` zones at random to hint as barren. Barren zones are weighted by the # square root of the number of locations at that zone. unhinted_barren_zones = self.get_barren_zones(progress_locations, [hint.place for hint in hinted_remote_locations]) - hinted_barren_zones = [] + hinted_barren_zones: list[Hint] = [] while len(unhinted_barren_zones) > 0 and len(hinted_barren_zones) < self.max_barren_hints: # Weight each barren zone by the square root of the number of locations there. zone_weights = [math.sqrt(location_counter[zone]) for zone in unhinted_barren_zones] diff --git a/test/test_dry.py b/test/test_dry.py index 5ab5e8c6d..4aa1366f5 100644 --- a/test/test_dry.py +++ b/test/test_dry.py @@ -80,7 +80,7 @@ def test_trick_logic_checks(): def test_parse_string_option_to_enum(): options = Options() - options.logic_precision = "Normal" + options.logic_precision = "Normal" # pyright: ignore [reportAttributeAccessIssue] rando = dry_rando_with_options(options) assert isinstance(rando.options.logic_precision, StrEnum) diff --git a/tweaks.py b/tweaks.py index e238d279e..522f4f4f7 100644 --- a/tweaks.py +++ b/tweaks.py @@ -25,7 +25,7 @@ from logic.item_types import PROGRESS_ITEMS, NONPROGRESS_ITEMS, CONSUMABLE_ITEMS, DUPLICATABLE_CONSUMABLE_ITEMS from data_tables import DataTables from wwlib.events import EventList -from wwlib.dzx import DZx, ACTR, EVNT, FILI, PLYR, SCLS, SCOB, SHIP, TGDR, TRES, Pale +from wwlib.dzx import DZx, DZxLayer, ACTR, EVNT, FILI, PLYR, SCLS, SCOB, SHIP, TGDR, TRES, Pale from options.wwrando_options import SwordMode try: @@ -372,10 +372,10 @@ def add_ganons_tower_warp_to_ff2(self: WWRandomizer): dzx = self.get_arc("files/res/Stage/sea/Room1.arc").get_file("room.dzr", DZx) - layer_2_actors = dzx.entries_by_type_and_layer(ACTR, layer=2) + layer_2_actors = dzx.entries_by_type_and_layer(ACTR, layer=DZxLayer.Layer2) layer_2_warp = next(x for x in layer_2_actors if x.name == "Warpmj") - layer_1_warp = dzx.add_entity(ACTR, layer=1) + layer_1_warp = dzx.add_entity(ACTR, layer=DZxLayer.Layer1) layer_1_warp.name = layer_2_warp.name layer_1_warp.params = layer_2_warp.params layer_1_warp.x_pos = layer_2_warp.x_pos @@ -907,7 +907,7 @@ def add_pirate_ship_to_windfall(self: WWRandomizer): ship_dzs = self.get_arc("files/res/Stage/Asoko/Stage.arc").get_file("stage.dzs", DZx) event_list = self.get_arc("files/res/Stage/Asoko/Stage.arc").get_file("event_list.dat", EventList) - windfall_layer_2_actors = windfall_dzr.entries_by_type_and_layer(ACTR, layer=2) + windfall_layer_2_actors = windfall_dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Layer2) layer_2_pirate_ship = next(x for x in windfall_layer_2_actors if x.name == "Pirates") default_layer_pirate_ship = windfall_dzr.add_entity(ACTR) @@ -1247,11 +1247,11 @@ def add_inter_dungeon_warp_pots(self: WWRandomizer): def remove_makar_kidnapping_event(self: WWRandomizer): dzx = self.get_arc("files/res/Stage/kaze/Room3.arc").get_file("room.dzr", DZx) - actors = dzx.entries_by_type_and_layer(ACTR, layer=None) + actors = dzx.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default) # Remove the AND switch actor that makes the Floormasters appear after unlocking the door. and_switch_actor = next(x for x in actors if x.name == "AND_SW2") - dzx.remove_entity(and_switch_actor, ACTR, layer=None) + dzx.remove_entity(and_switch_actor, ACTR, layer=DZxLayer.Default) # Remove the enable spawn switch from the Wizzrobe so it's just always there. wizzrobe = next(x for x in actors if x.name == "wiz_r") @@ -1452,7 +1452,7 @@ def add_hint_signs(self: WWRandomizer): msg.text_alignment = 3 # Centered text alignment dzx = self.get_arc("files/res/Stage/M_NewD2/Room2.arc").get_file("room.dzr", DZx) - bomb_flowers = [actor for actor in dzx.entries_by_type_and_layer(ACTR, layer=None) if actor.name == "BFlower"] + bomb_flowers = [actor for actor in dzx.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default) if actor.name == "BFlower"] bomb_flowers[1].name = "Kanban" bomb_flowers[1].params = new_message_id bomb_flowers[1].y_rot = 0x2000 @@ -1621,7 +1621,7 @@ def add_chest_in_place_of_jabun_cutscene(self: WWRandomizer): # This is so they appear during the day too, not just at night. outset_dzr = self.get_arc("files/res/Stage/sea/Room44.arc").get_file("room.dzr", DZx) - layer_5_actors = outset_dzr.entries_by_type_and_layer(ACTR, layer=5) + layer_5_actors = outset_dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Layer5) layer_5_door = next(x for x in layer_5_actors if x.name == "Ajav") layer_5_whirlpool = next(x for x in layer_5_actors if x.name == "Auzu") @@ -1647,8 +1647,8 @@ def add_chest_in_place_of_jabun_cutscene(self: WWRandomizer): layer_none_whirlpool.z_rot = layer_5_whirlpool.z_rot layer_none_whirlpool.enemy_number = layer_5_whirlpool.enemy_number - outset_dzr.remove_entity(layer_5_door, ACTR, layer=5) - outset_dzr.remove_entity(layer_5_whirlpool, ACTR, layer=5) + outset_dzr.remove_entity(layer_5_door, ACTR, layer=DZxLayer.Layer5) + outset_dzr.remove_entity(layer_5_whirlpool, ACTR, layer=DZxLayer.Layer5) outset_dzr.save_changes() @@ -1675,12 +1675,12 @@ def add_chest_in_place_of_master_sword(self: WWRandomizer): ms_chamber_dzr = self.get_arc("files/res/Stage/kenroom/Room0.arc").get_file("room.dzr", DZx) # Remove the Master Sword entities. - ms_actors = [x for x in ms_chamber_dzr.entries_by_type_and_layer(ACTR, layer=None) if x.name in ["VmsMS", "VmsDZ"]] + ms_actors = [x for x in ms_chamber_dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default) if x.name in ["VmsMS", "VmsDZ"]] for actor in ms_actors: - ms_chamber_dzr.remove_entity(actor, ACTR, layer=None) + ms_chamber_dzr.remove_entity(actor, ACTR, layer=DZxLayer.Default) # Copy the entities necessary for the Mighty Darknuts fight from layer 5 to the default layer. - layer_5_actors = ms_chamber_dzr.entries_by_type_and_layer(ACTR, layer=5) + layer_5_actors = ms_chamber_dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Layer5) layer_5_actors_to_copy = [x for x in layer_5_actors if x.name in ["Tn", "ALLdie", "Yswdr00"]] for orig_actor in layer_5_actors_to_copy: @@ -1697,7 +1697,7 @@ def add_chest_in_place_of_master_sword(self: WWRandomizer): # Remove the entities on layer 5 that are no longer necessary. for orig_actor in layer_5_actors: - ms_chamber_dzr.remove_entity(orig_actor, ACTR, layer=5) + ms_chamber_dzr.remove_entity(orig_actor, ACTR, layer=DZxLayer.Layer5) # Add the chest. @@ -2566,7 +2566,7 @@ def fix_helmaroc_king_table_softlock(self: WWRandomizer): # Simply remove this table, as this should presumably fix the bug, and the table seems to serve no # purpose anyway (the two stools beside it don't appear during the fight to begin with). fftower_dzr = self.get_arc("files/res/Stage/M2tower/Room0.arc").get_file("room.dzr", DZx) - actors = fftower_dzr.entries_by_type_and_layer(ACTR, layer=None) + actors = fftower_dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default) table = next(x for x in actors if x.name == "Otble") fftower_dzr.remove_entity(table, ACTR) @@ -2578,7 +2578,7 @@ def make_dungeon_joy_pendant_locations_flexible(self: WWRandomizer): # an unbeatable seed if the player gets the purple rupee first. # To fix this, we give the purple rupee a different item pickup flag that was originally unused. dzr = self.get_arc("files/res/Stage/M_NewD2/Room0.arc").get_file("room.dzr", DZx) - tingle_item = dzr.entries_by_type_and_layer(ACTR, layer=None)[0x1C] + tingle_item = dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default)[0x1C] tingle_item.item_pickup_flag = 0x16 # Unused item pickup flag for Dragon Roost Cavern tingle_item.save_changes() @@ -2586,7 +2586,7 @@ def make_dungeon_joy_pendant_locations_flexible(self: WWRandomizer): # item placed here can be obtained multiple times. This was presumably unintentional as all the # other dungeon joy pendants have pickup flags, so we give it a new flag. dzr = self.get_arc("files/res/Stage/kindan/Room7.arc").get_file("room.dzr", DZx) - pot = dzr.entries_by_type_and_layer(ACTR, layer=None)[0xE0] + pot = dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default)[0xE0] pot.item_pickup_flag = 0x16 # Unused item pickup flag for Forbidden Woods pot.save_changes() @@ -2596,11 +2596,11 @@ def make_dungeon_joy_pendant_locations_flexible(self: WWRandomizer): # after destroying the stone head without picking up the item and then come back. # To avoid this, we remove the destroyed switch from the stone heads so that they always reappear. dzr = self.get_arc("files/res/Stage/kaze/Room1.arc").get_file("room.dzr", DZx) - stone_head = dzr.entries_by_type_and_layer(ACTR, layer=None)[0x10] + stone_head = dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default)[0x10] stone_head.destroyed_switch = 0xFF stone_head.save_changes() dzr = self.get_arc("files/res/Stage/kaze/Room7.arc").get_file("room.dzr", DZx) - stone_head = dzr.entries_by_type_and_layer(ACTR, layer=None)[0x2E] + stone_head = dzr.entries_by_type_and_layer(ACTR, layer=DZxLayer.Default)[0x2E] stone_head.destroyed_switch = 0xFF stone_head.save_changes() diff --git a/wwlib/dzb.py b/wwlib/dzb.py index 28109b687..53683c87f 100644 --- a/wwlib/dzb.py +++ b/wwlib/dzb.py @@ -28,12 +28,12 @@ def __init__(self): self.unknown_1 = 0 - self.vertices = [] - self.faces = [] - self.octree_blocks = [] - self.octree_nodes = [] - self.groups = [] - self.properties = [] + self.vertices: list[Vertex] = [] + self.faces: list[Face] = [] + self.octree_blocks: list[OctreeBlock] = [] + self.octree_nodes: list[OctreeNode] = [] + self.groups: list[Group] = [] + self.properties: list[Property] = [] def read(self, data): self.data = data @@ -505,6 +505,9 @@ def save_changes(self): class OctreeNode: DATA_SIZE = 0x14 + block: OctreeBlock | None + child_nodes: list['OctreeNode'] + def __init__(self, dzb_data): self.dzb_data = dzb_data diff --git a/wwlib/dzx.py b/wwlib/dzx.py index a74fa038c..aa29763c3 100644 --- a/wwlib/dzx.py +++ b/wwlib/dzx.py @@ -239,7 +239,7 @@ def entries_by_type_and_layer(self, chunk_type: Type[ChunkEntryT], layer: DZxLay entries += chunk.entries return entries - def add_entity(self, chunk_type: Type[ChunkEntryT], layer: Optional[DZxLayer] = None): + def add_entity(self, chunk_type: Type[ChunkEntryT], layer: DZxLayer = DZxLayer.Default): # If no layer is passed, it will default to DZxLayer.Default. layer = DZxLayer(layer) @@ -260,7 +260,7 @@ def add_entity(self, chunk_type: Type[ChunkEntryT], layer: Optional[DZxLayer] = return entity - def remove_entity(self, entity, chunk_type: Type[ChunkEntryT], layer: DZxLayer = None): + def remove_entity(self, entity, chunk_type: Type[ChunkEntryT], layer: DZxLayer = DZxLayer.Default): assert hasattr(entity, "name") # If no layer is passed, it will default to DZxLayer.Default.