From 16d9d8be860efd884959eee3e3cea0790a758c63 Mon Sep 17 00:00:00 2001 From: kurethedead Date: Mon, 22 Jul 2024 18:38:37 -0700 Subject: [PATCH] Refactor scene_table.py --- .../oot/exporter/decomp_edit/__init__.py | 9 +- .../oot/exporter/decomp_edit/scene_table.py | 389 ++++++++---------- .../oot/exporter/decomp_edit/spec.py | 6 +- 3 files changed, 193 insertions(+), 211 deletions(-) diff --git a/fast64_internal/oot/exporter/decomp_edit/__init__.py b/fast64_internal/oot/exporter/decomp_edit/__init__.py index d5b4de7c4..8de592357 100644 --- a/fast64_internal/oot/exporter/decomp_edit/__init__.py +++ b/fast64_internal/oot/exporter/decomp_edit/__init__.py @@ -33,5 +33,10 @@ def editFiles(exporter: "SceneExport"): """Edits decomp files""" Files.modifySceneFiles(exporter) - SpecUtility.editSpec(exporter) - SceneTableUtility.editSceneTable(exporter, exporter.exportInfo) + SpecUtility.edit_spec(exporter) + SceneTableUtility.edit_scene_table( + exporter.exportInfo.exportPath, + exporter.exportInfo.name, + exporter.exportInfo.option, + exporter.scene.mainHeader.infos.drawConfig, + ) diff --git a/fast64_internal/oot/exporter/decomp_edit/scene_table.py b/fast64_internal/oot/exporter/decomp_edit/scene_table.py index c4845ea44..50a8e6ae9 100644 --- a/fast64_internal/oot/exporter/decomp_edit/scene_table.py +++ b/fast64_internal/oot/exporter/decomp_edit/scene_table.py @@ -15,12 +15,16 @@ ADDED_SCENES_COMMENT = "// Added scenes" -class SceneIndexType(enum.IntEnum): - """Used to figure out the value of ``selectedSceneIndex``""" - - # this is using negative numbers since this is used as a return type if the scene index wasn't found - CUSTOM = -1 # custom scene - VANILLA_REMOVED = -2 # vanilla scene that was removed, this is to know if it should insert an entry +def get_original_index(enum_value: str) -> Optional[int]: + """ + Returns the original index of a specific scene + """ + for index, scene_enum in enumerate( + [elem[0] for elem in ootEnumSceneID[1:]] + ): # ignore first value in array ('Custom') + if scene_enum == enum_value: + return index + return None @dataclass @@ -28,41 +32,37 @@ class SceneTableEntry: """Defines an entry of ``scene_table.h``""" # macro parameters - specName: str # name of the scene segment in spec - titleCardName: str # name of the title card segment in spec, or `none` for no title card - enumValue: str # enum value for this scene - drawConfigIdx: str # scene draw config index + spec_name: str # name of the scene segment in spec + title_card_name: str # name of the title card segment in spec, or `none` for no title card + enum_value: str # enum value for this scene + draw_config: str # scene draw config index unk1: str unk2: str - prefix: str = str() # ifdefs, endifs, comments etc, everything before the current entry - suffix: str = str() # remaining data after the last entry - @staticmethod - def from_line(original_line: str, prefix: str): + def from_line(original_line: str): macro_start = "DEFINE_SCENE(" if macro_start in original_line: # remove the index and the macro's name with the parenthesis index = original_line.index(macro_start) + len(macro_start) - parsed = original_line[index:].removesuffix(")\n") + parsed = original_line[index:].removesuffix(")") params = parsed.split(", ") assert len(params) == 6 - return SceneTableEntry(*params, prefix) + return SceneTableEntry(*params) else: raise PluginError("ERROR: This line is not a scene table entry!") @staticmethod - def from_scene(exporter: "SceneExport", export_name: str, is_custom_scene: bool): + def from_scene(scene_name: str, draw_config: str): # TODO: Implement title cards - scene_name = exporter.scene.name.lower() if is_custom_scene else export_name return SceneTableEntry( scene_name if scene_name.endswith("_scene") else f"{scene_name}_scene", "none", - ootSceneNameToID.get(export_name, f"SCENE_{export_name.upper()}"), - exporter.scene.mainHeader.infos.drawConfig, + ootSceneNameToID.get(scene_name, f"SCENE_{scene_name.upper()}"), + draw_config, "0", "0", ) @@ -70,243 +70,220 @@ def from_scene(exporter: "SceneExport", export_name: str, is_custom_scene: bool) def to_c(self, index: int): """Returns the entry as C code""" return ( - self.prefix - + f"/* 0x{index:02X} */ " - + f"DEFINE_SCENE({self.specName}, {self.titleCardName}, {self.enumValue}, " - + f"{self.drawConfigIdx}, {self.unk1}, {self.unk2})\n" - + self.suffix + f"/* 0x{index:02X} */ " + f"DEFINE_SCENE({self.spec_name}, {self.title_card_name}, {self.enum_value}, " + f"{self.draw_config}, {self.unk1}, {self.unk2})" ) +@dataclass +class SceneTableSection: + """Defines a section of the scene table, with is a list of entires with an optional preprocessor directive / comment""" + + directive: Optional[str] # can also be a comment starting with // + entries: list[SceneTableEntry] = field(default_factory=list) + + def to_c(self, index: int): + directive = f"{self.directive}\n" if self.directive else "" + terminator = "\n#endif" if self.directive and self.directive.startswith("#if") else "" + entry_string = "\n".join(entry.to_c(index + i) for i, entry in enumerate(self.entries)) + return f"{directive}{entry_string}{terminator}\n\n" + + @dataclass class SceneTable: """Defines a ``scene_table.h`` file data""" - exportPath: str - exportName: Optional[str] - selectedSceneEnumValue: Optional[str] - entries: list[SceneTableEntry] = field(default_factory=list) - sceneEnumValues: list[str] = field(default_factory=list) # existing values in ``scene_table.h`` - isFirstCustom: bool = False # if true, adds the "Added Scenes" comment to the C data - selectedSceneIndex: int = 0 - customSceneIndex: Optional[int] = None # None if the selected custom scene isn't in the table yet + header: str + sections: list[SceneTableSection] = field(default_factory=list) - def __post_init__(self): + @staticmethod + def new(export_path: str): # read the file's data try: - with open(self.exportPath) as fileData: - data = fileData.read() - fileData.seek(0) - lines = fileData.readlines() + with open(export_path) as file_data: + data = file_data.read() + file_data.seek(0) + lines = file_data.readlines() except FileNotFoundError: raise PluginError("ERROR: Can't find scene_table.h!") - # parse the entries and populate the list of entries (``self.entries``) - prefix = "" - self.isFirstCustom = ADDED_SCENES_COMMENT not in data - - assert len(lines) > 0 - - for line in lines: - # skip the lines before an entry, create one from the file's data - # and add the skipped lines as a prefix of the current entry - if ( - not line.startswith("#") # ifdefs or endifs - and not line.startswith(" *") # multi-line comments - and "//" not in line # single line comments - and "/**" not in line # multi-line comments - and line != "\n" - and line.strip() != "" - ): - entry = SceneTableEntry.from_line(line, prefix) - self.entries.append(entry) - self.sceneEnumValues.append(entry.enumValue) - prefix = "" + # Find first instance of "DEFINE_SCENE(", indicating a scene define macro + first_macro_index = data.index("DEFINE_SCENE(") + if first_macro_index == -1: + return SceneTable(data, []) # No scene defines found - add to end + + # Go backwards up to previous newline + try: + header_end_index = data[:first_macro_index].rfind("\n") + except ValueError: + header_end_index = 0 + + header = data[: header_end_index + 1] + + lines = data[header_end_index + 1 :].split("\n") + lines = list(filter(None, lines)) # removes empty lines + lines = [line.strip() for line in lines] + + sections: list[SceneTableSection] = [] + current_section: Optional[SceneTableSection] = None + + while len(lines) > 0: + line = lines.pop(0) + if line.startswith("#if"): + if current_section: # handles non-directive section preceding directive section + sections.append(current_section) + current_section = SceneTableSection(line) + elif line.startswith("#endif"): + sections.append(current_section) + current_section = None # handles back-to-back directive sections + elif line.startswith("//"): + if current_section: # handles non-directive section preceding directive section + sections.append(current_section) + current_section = SceneTableSection(line) else: - prefix += line - - # add whatever's after the last entry - if len(prefix) > 0 and prefix != "\n": - self.entries[-1].suffix = prefix - - # get the scene index for the scene chosen by the user - if self.selectedSceneEnumValue is not None: - self.selectedSceneIndex = self.getIndexFromEnumValue() - - # dictionary of entries from spec names - self.entryBySpecName = {entry.specName: entry for entry in self.entries} - - # set the custom scene index - if self.selectedSceneIndex == SceneIndexType.CUSTOM: - entry = self.entryBySpecName.get(f"{self.exportName}_scene") - if entry is not None: - self.customSceneIndex = self.entries.index(entry) - - def getIndexFromEnumValue(self): - """Returns the index (int) of the chosen scene if vanilla and found, else return an enum value from ``SceneIndexType``""" - if self.selectedSceneEnumValue == "Custom": - return SceneIndexType.CUSTOM - for i in range(len(self.sceneEnumValues)): - if self.sceneEnumValues[i] == self.selectedSceneEnumValue: - return i - # if the index is not found and it's not a custom export it means it's a vanilla scene that was removed - return SceneIndexType.VANILLA_REMOVED + if not current_section: + current_section = SceneTableSection(None) + current_section.entries.append(SceneTableEntry.from_line(line)) + + if current_section: + sections.append(current_section) # add last section if non-directive + + return SceneTable(header, sections) - def getOriginalIndex(self): + def get_entries_flattened(self) -> list[SceneTableEntry]: """ - Returns the index of a specific scene defined by which one the user chose - or by the ``sceneName`` parameter if it's not set to ``None`` + Returns all entries as a single array, without sections. + This is a shallow copy of the data and adding/removing from this list not change the scene table internally. """ - i = 0 - if self.selectedSceneEnumValue != "Custom": - for elem in ootEnumSceneID: - if elem[0] == self.selectedSceneEnumValue: - # returns i - 1 because the first entry is the ``Custom`` option - return i - 1 - i += 1 - raise PluginError("ERROR: Scene Index not found!") - - def getInsertionIndex(self, index: Optional[int] = None) -> int: - """Returns the index to know where to insert data""" - # special case where the scene is "Inside the Great Deku Tree" - # since it's the first scene simply return 0 - if self.selectedSceneEnumValue == "SCENE_DEKU_TREE": - return 0 - - # if index is None this means this is looking for ``original_scene_index - 1`` - # else, this means the table is shifted - if index is None: - currentIndex = self.getOriginalIndex() - else: - currentIndex = index - for i in range(len(self.sceneEnumValues)): - if self.sceneEnumValues[i] == ootEnumSceneID[currentIndex][0]: - return i + 1 + return [entry for section in self.sections for entry in section.entries] - # if the index hasn't been found yet, throw an error - raise PluginError("ERROR: the insertion index was not found") + def get_index_from_enum(self, enum_value: str) -> Optional[int]: + """Returns the index (int) of the chosen scene if found, else return ``None``""" - def getIndex(self) -> int: - """Returns the selected scene index if it's a vanilla one, else returns the custom scene index""" - assert self.selectedSceneIndex != SceneIndexType.VANILLA_REMOVED + for i, entry in enumerate(self.get_entries_flattened()): + if entry.enum_value == enum_value: + return i - # this function's usage makes ``customSceneIndex is None`` impossible - if self.selectedSceneIndex < 0 and self.customSceneIndex is None: - raise PluginError("ERROR: Custom Scene Index is None!") + return None - return self.selectedSceneIndex if self.selectedSceneIndex >= 0 else self.customSceneIndex + def set_entry_at_enum(self, entry: SceneTableEntry, enum_value: str): + """Replaces entry in the scene table with the given enum_value""" + for section in self.sections: + for entry_index in range(len(section.entries)): + if section.entries[entry_index].enum_value == enum_value: + section.entries[entry_index] = entry - def append(self, entry: SceneTableEntry, index: int): + def append(self, entry: SceneTableEntry): """Appends an entry to the scene table, only used by custom scenes""" - # add the "added scenes" comment if it's not already there - if self.isFirstCustom: - entry.prefix = f"\n{ADDED_SCENES_COMMENT}\n" - self.isFirstCustom = False - - if entry not in self.entries: - if index >= 0: - self.customSceneIndex = index - self.entries.append(entry) - else: - raise PluginError(f"ERROR: (Append) The index is not valid! ({index})") + + # Find current added scenes comment, or add one if not found + current_section = None + for section in self.sections: + if section.directive == ADDED_SCENES_COMMENT: + current_section = section + break + if current_section is None: + current_section = SceneTableSection(ADDED_SCENES_COMMENT) + self.sections.append(current_section) + + if entry not in current_section.entries: + current_section.entries.append(entry) else: raise PluginError("ERROR: (Append) Entry already in the table!") def insert(self, entry: SceneTableEntry, index: int): """Inserts an entry in the scene table, only used by non-custom scenes""" - if not entry in self.entries: - if index >= 0: - if index < len(self.entries): - nextEntry = self.entries[index] # the next entry is at the insertion index - # move the next entry's prefix to the one we're going to insert - if len(nextEntry.prefix) > 0 and not "INCLUDE_TEST_SCENES" in nextEntry.prefix: - entry.prefix = nextEntry.prefix - nextEntry.prefix = "" + if entry in self.get_entries_flattened(): + raise PluginError("ERROR: (Insert) Entry already in the table!") + if index < 0 or index > len(self.get_entries_flattened()) - 1: + raise PluginError(f"ERROR: (Insert) The index is not valid! ({index})") + + i = 0 + for section in self.sections: + for entry_index in range(len(section.entries)): + if i == index: + section.entries.insert(entry_index, entry) + return + else: + i += 1 + + def update(self, entry: SceneTableEntry, enum_value: str): + """Updates an entry if the enum_value exists in the scene table, otherwise appends/inserts entry depending on if custom or not""" - self.entries.insert(index, entry) + original_index = get_original_index(enum_value) # index in unmodified scene table + current_index = self.get_index_from_enum(enum_value) # index in current scene table + + if current_index is None: # Not in scene table currently + if original_index is not None: + # insert mode - we want to place vanilla scenes into their original locations if previously deleted + self.insert(entry, original_index) else: - raise PluginError(f"ERROR: (Insert) The index is not valid! ({index})") + # this is a custom level, append to end + self.append(entry) else: - raise PluginError("ERROR: (Insert) Entry already in the table!") + # update mode (for both vanilla and custom scenes since they already exist in the table) + self.set_entry_at_enum(entry, enum_value) - def remove(self, index: int): + def remove(self, enum_value: str): """Removes an entry from the scene table""" - isCustom = index == SceneIndexType.CUSTOM - if index >= 0 or isCustom: - idx = self.getIndex() - entry = self.entries[idx] - - # move the prefix of the entry to remove to the next entry - # if there's no next entry this prefix becomes the suffix of the last entry - if len(entry.prefix) > 0: - nextIndex = index + 1 - if not isCustom and nextIndex < len(self.entries): - self.entries[nextIndex].prefix = entry.prefix - else: - previousIndex = idx - 1 - if idx == len(self.entries) - 1 and ADDED_SCENES_COMMENT in entry.prefix: - entry.prefix = entry.prefix.removesuffix(f"\n{ADDED_SCENES_COMMENT}\n") - self.entries[previousIndex].suffix = entry.prefix - - self.entries.remove(entry) - elif index == SceneIndexType.VANILLA_REMOVED: - raise PluginError("INFO: This scene was already removed.") - else: - raise PluginError("ERROR: Unexpected scene index value.") + + for section in self.sections: + for entry in section.entries: + if entry.enum_value == enum_value: + section.entries.remove(entry) + return def to_c(self): """Returns the scene table as C code""" - return "".join(entry.to_c(i) for i, entry in enumerate(self.entries)) + data = f"{self.header}" + index = 0 + for section in self.sections: + data += section.to_c(index) + index += len(section.entries) + + if data[-2:] == "\n\n": # For consistency with vanilla + data = data[:-1] + return data class SceneTableUtility: """This class hosts different function to edit the scene table""" @staticmethod - def getDrawConfig(sceneName: str): + def get_draw_config(scene_name: str): """Read draw config from scene table""" - sceneTable = SceneTable( - os.path.join(bpy.path.abspath(bpy.context.scene.ootDecompPath), "include/tables/scene_table.h"), None, None + scene_table = SceneTable.new( + os.path.join(bpy.path.abspath(bpy.context.scene.ootDecompPath), "include/tables/scene_table.h") ) - entry = sceneTable.entryBySpecName.get(f"{sceneName}_scene") + spec_dict = {entry.spec_name: entry for entry in scene_table.get_entries_flattened()} + entry = spec_dict.get(f"{scene_name}_scene") if entry is not None: - return entry.drawConfigIdx + return entry.draw_config - raise PluginError(f"ERROR: Scene name {sceneName} not found in scene table.") + raise PluginError(f"ERROR: Scene name {scene_name} not found in scene table.") @staticmethod - def editSceneTable(exporter: Optional["SceneExport"], exportInfo: ExportInfo): - """Remove, append, insert or update the scene table entry of the selected scene""" - sceneTable = SceneTable( - os.path.join(exportInfo.exportPath, "include/tables/scene_table.h"), - exportInfo.name if exportInfo.option == "Custom" else None, - exportInfo.option, - ) + def edit_scene_table(export_path: str, export_name: str, export_enum: str, draw_config: str): + """Update the scene table entry of the selected scene""" + path = os.path.join(export_path, "include/tables/scene_table.h") + scene_table = SceneTable.new(path) - if exporter is None: - # remove mode - sceneTable.remove(sceneTable.selectedSceneIndex) - elif sceneTable.selectedSceneIndex == SceneIndexType.CUSTOM and sceneTable.customSceneIndex is None: - # custom mode: new custom scene - sceneTable.append( - SceneTableEntry.from_scene(exporter, exporter.exportInfo.name, True), len(sceneTable.entries) - 1 - ) - elif sceneTable.selectedSceneIndex == SceneIndexType.VANILLA_REMOVED: - # insert mode - sceneTable.insert( - SceneTableEntry.from_scene(exporter, exporter.exportInfo.name, False), sceneTable.getInsertionIndex() - ) - else: - # update mode (for both vanilla and custom scenes since they already exist in the table) - index = sceneTable.getIndex() - entry = sceneTable.entries[index] - new_entry = SceneTableEntry.from_scene(exporter, exporter.scene.name, False) - new_entry.prefix = entry.prefix - new_entry.suffix = entry.suffix - sceneTable.entries[index] = new_entry + scene_table.update(SceneTableEntry.from_scene(export_name, draw_config), export_enum) + + # write the file with the final data + writeFile(path, scene_table.to_c()) + + @staticmethod + def delete_scene_table_entry(export_path: str, export_enum: str): + """Remove the scene table entry of the selected scene""" + path = os.path.join(export_path, "include/tables/scene_table.h") + scene_table = SceneTable.new(path) + + scene_table.remove(export_enum) # write the file with the final data - writeFile(sceneTable.exportPath, sceneTable.to_c()) + writeFile(path, scene_table.to_c()) diff --git a/fast64_internal/oot/exporter/decomp_edit/spec.py b/fast64_internal/oot/exporter/decomp_edit/spec.py index 5c523d0d4..ec7e4ad5c 100644 --- a/fast64_internal/oot/exporter/decomp_edit/spec.py +++ b/fast64_internal/oot/exporter/decomp_edit/spec.py @@ -149,10 +149,10 @@ def new(export_path: str): return SpecFile(header, build_directory, sections) - def get_entries_flattened(self): + def get_entries_flattened(self) -> list[SpecEntry]: """ Returns all entries as a single array, without sections. - This is a copy of the data and modifying this will not change the spec file internally. + This is a shallow copy of the data and adding/removing from this list not change the spec file internally. """ return [entry for section in self.sections for entry in section.entries] @@ -193,7 +193,7 @@ class SpecUtility: """This class hosts different functions to edit the spec file""" @staticmethod - def editSpec(exporter: "SceneExport"): + def edit_spec(exporter: "SceneExport"): isScene = True exportInfo = exporter.exportInfo hasSceneTex = exporter.hasSceneTextures