From 4cbff2b996493343e76819cf76350ffb1df202c9 Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Wed, 7 Feb 2024 04:02:22 +0100 Subject: [PATCH 01/10] refactored spec (minor issue with removing, entry.to_c improvement needed) --- fast64_internal/oot/oot_level_writer.py | 4 +- .../oot/scene/exporter/to_c/spec.py | 333 ++++++++++++------ fast64_internal/oot/scene/operators.py | 2 +- 3 files changed, 220 insertions(+), 119 deletions(-) diff --git a/fast64_internal/oot/oot_level_writer.py b/fast64_internal/oot/oot_level_writer.py index 973af5700..ac2e223fe 100644 --- a/fast64_internal/oot/oot_level_writer.py +++ b/fast64_internal/oot/oot_level_writer.py @@ -230,7 +230,9 @@ def writeTextureArraysExistingScene(fModel: OOTModel, exportPath: str, sceneIncl def writeOtherSceneProperties(scene, exportInfo, levelC): modifySceneTable(scene, exportInfo) - editSpecFile(scene, exportInfo, levelC) + editSpecFile( + scene, exportInfo, levelC.sceneTexturesIsUsed(), levelC.sceneCutscenesIsUsed(), len(levelC.sceneCutscenesC) + ) modifySceneFiles(scene, exportInfo) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index 32ba71e08..680db54c0 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -1,132 +1,231 @@ -import os, re, bpy -from .....utility import readFile, writeFile, indent +import os +import bpy + +from dataclasses import dataclass, field +from typing import Optional +from .....utility import PluginError, writeFile, indent from ....oot_utility import ExportInfo, getSceneDirFromLevelName from ....oot_level_classes import OOTScene -from .scene import OOTSceneC - - -def getSceneSpecEntries(segmentDefinition: list[str], sceneName: str): - """Returns the existing spec entries for the selected scene""" - entries = [] - matchText = rf'\s*name\s*"{sceneName}\_' - - for entry in segmentDefinition: - if re.match(matchText + 'scene"', entry) or re.match(matchText + 'room\_\d+"', entry): - entries.append(entry) - - return entries - - -def getSpecEntries(fileData: str): - """Returns the existing spec entries for the whole file""" - entries = [] - compressFlag = "" - for match in re.finditer("beginseg(((?!endseg).)*)endseg", fileData, re.DOTALL): - segData = match.group(1) - entries.append(segData) - - # avoid deleting compress flag if the user is using it - # (defined by whether it is present at least once in spec or not) - if "compress" in segData: - compressFlag = indent + "compress\n" - - includes = [] - for match in re.finditer("(#include.*)", fileData): - includes.append(match.group(0)) - - return entries, compressFlag, includes +@dataclass +class SpecEntry: + """Defines an entry of ``spec``""" + + original: Optional[list[str]] # the original lines from the parsed file + segmentName: str = "" + isCompressed: bool = False + romalign: Optional[str] = None + number: Optional[int] = None + files: list[tuple[str, str, str]] = field(default_factory=list) # (prefix, command, file) + address: Optional[str] = None + after: Optional[str] = None + align: Optional[str] = None + flags: Optional[str] = None + pad_text: Optional[str] = None + prefix: str = "" + suffix: Optional[str] = None + contentSuffix: Optional[str] = None + + def __post_init__(self): + if self.prefix == "\n": + self.prefix = "" + + if self.original is not None: + prefix = "" + content = None + for line in self.original: + line = line.strip() + if not line.startswith("#") and not "pad_text" in line: + split = line.split(" ") + command = split[0] + if len(split) > 2: + content = " ".join(elem for i, elem in enumerate(split) if i > 0) + elif len(split) > 1: + content = split[1] + match command: + case "compress": + self.isCompressed = True + case "name" | "after" | "flags" | "align" | "address" | "romalign": + if content is not None: + setattr(self, "segmentName" if command == "name" else command, content) + case "include" | "include_data_with_rodata": + if content is not None: + self.files.append( + ( + (prefix + ("\n" if len(prefix) > 0 else "")) if prefix != "\n" else "", + command, + content, + ) + ) + case "number": + if content is not None: + self.number = int(content) + case _: + raise PluginError(f"ERROR: Unknown spec command: `{command}`") + prefix = "" + else: + prefix += line + if len(prefix) > 0: + self.contentSuffix = f"{prefix}\n" + + def to_c(self): + return ( + self.prefix + + "beginseg\n" + + f"".join( + ( + (indent + f"name {self.segmentName}\n"), + (indent + "compress\n" if self.isCompressed else ""), + (indent + f"after {self.after}\n" if self.after is not None else ""), + (indent + f"flags {self.flags}\n" if self.flags is not None else ""), + (indent + f"align {self.align}\n" if self.align is not None else ""), + (indent + f"address {self.address}\n" if self.address is not None else ""), + (indent + f"romalign {self.romalign}\n" if self.romalign is not None else ""), + "".join(prefix + indent + f"{command} {file}\n" for prefix, command, file in self.files), + (indent + f"number {self.number}\n" if self.number is not None else ""), + ) + ) + + (self.contentSuffix if self.contentSuffix is not None else "") + + "endseg" + + ("\n" + self.suffix if self.suffix is not None else "") + ) + + +@dataclass +class SpecFile: + exportPath: str + # exportName: Optional[str] + exportInfo: ExportInfo + scene: OOTScene + hasSceneTextures: bool + hasSceneCutscenes: bool + cutsceneTotal: int + isSingleFile: bool + entries: list[SpecEntry] = field(default_factory=list) + + def __post_init__(self): + # read the file's data + try: + with open(os.path.join(self.exportPath, "spec"), "r") as fileData: + # data = fileData.read() + # fileData.seek(0) + lines = fileData.readlines() + except FileNotFoundError: + raise PluginError("ERROR: Can't find spec!") + + # parse the entries and populate the list of entries (``self.entries``) + prefix = "" + parsedLines = [] + 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(" *") # multi-line comments + and "/*\n" not in line # multi-line comments + and line != "\n" + and line != "" + ): + if len(parsedLines) > 0 or not line.startswith("#"): + if "beginseg" not in line and "endseg" not in line: + # if inside a segment, between beginseg and endseg + parsedLines.append(line) + elif "endseg" in line: + # else, if the line has endseg in it (> if we reached the end of the current segment) + entry = SpecEntry(parsedLines, prefix=prefix) + self.entries.append(entry) + prefix = "" + parsedLines = [] + elif line.startswith("#") and len(parsedLines) == 0: + # else, if between 2 segments and the line is a preprocessor command + prefix += line + else: + prefix += line + self.entries[-1].suffix = prefix.removesuffix("\n") + + def find(self, segmentName: str): + for i, entry in enumerate(self.entries): + if entry.segmentName == segmentName: + return self.entries[i] + return None + + def append(self, entry: SpecEntry): + self.entries.append(entry) + + def remove(self, segmentName: str): + entry = self.find(segmentName) + if entry is not None: + if not "_room_" in segmentName: + self.entries[-2].suffix = entry.prefix + self.entries.remove(entry) + + def to_c(self): + return ( + "".join( + (("\n" * (1 if len(entry.prefix) > 0 else 2)) if i > 0 else "") + entry.to_c() + for i, entry in enumerate(self.entries) + ) + + "\n" + ) + + +def editSpecFile( + scene: Optional[OOTScene], exportInfo: ExportInfo, hasSceneTextures: bool, hasSceneCutscenes: bool, csTotal: int +): + specFile = SpecFile( + exportInfo.exportPath, + exportInfo, + scene, + hasSceneTextures, + hasSceneCutscenes, + csTotal, + bpy.context.scene.ootSceneExportSettings.singleFile, + ) -def editSpecFile(scene: OOTScene, exportInfo: ExportInfo, sceneC: OOTSceneC): - """Adds or removes entries for the selected scene""" - exportPath = exportInfo.exportPath sceneName = scene.name if scene is not None else exportInfo.name - fileData = readFile(os.path.join(exportPath, "spec")) + segmentName = f"{sceneName}_scene" + specFile.remove(segmentName) + for entry in specFile.entries: + if entry.segmentName.startswith(sceneName): + specFile.remove(entry.segmentName) - specEntries, compressFlag, includes = getSpecEntries(fileData) - sceneSpecEntries = getSceneSpecEntries(specEntries, sceneName) - - if exportInfo.customSubPath is not None: - includeDir = f"build/{exportInfo.customSubPath + sceneName}/{sceneName}" - else: - includeDir = f"build/{getSceneDirFromLevelName(sceneName)}/{sceneName}" - - if len(sceneSpecEntries) > 0: - firstIndex = specEntries.index(sceneSpecEntries[0]) - - # remove the entries of the selected scene - for entry in sceneSpecEntries: - specEntries.remove(entry) - else: - firstIndex = len(specEntries) - - # Add the spec data for the exported scene if scene is not None: - if bpy.context.scene.ootSceneExportSettings.singleFile: - specEntries.insert( - firstIndex, - ("\n" + indent + f'name "{scene.name}_scene"\n') - + compressFlag - + (indent + "romalign 0x1000\n") - + (indent + f'include "{includeDir}_scene.o"\n') - + (indent + "number 2\n"), - ) + includeDir = "$(BUILD_DIR)/" + if exportInfo.customSubPath is not None: + includeDir += f"{exportInfo.customSubPath + sceneName}" + else: + includeDir += f"{getSceneDirFromLevelName(sceneName)}" - firstIndex += 1 + if specFile.isSingleFile: + files = [("", "include", f'"{includeDir}/{segmentName}.o"')] + else: + files = [ + ("", "include", f'"{includeDir}/{segmentName}_main.o"'), + ("", "include", f'"{includeDir}/{segmentName}_col.o"'), + ] - for i in range(len(scene.rooms)): - specEntries.insert( - firstIndex, - ("\n" + indent + f'name "{scene.name}_room_{i}"\n') - + compressFlag - + (indent + "romalign 0x1000\n") - + (indent + f'include "{includeDir}_room_{i}.o"\n') - + (indent + "number 3\n"), - ) + if specFile.hasSceneTextures: + files.append(("", "include", f'"{includeDir}/{segmentName}_tex.o"')) - firstIndex += 1 - else: - sceneSegInclude = ( - ("\n" + indent + f'name "{scene.name}_scene"\n') - + compressFlag - + (indent + "romalign 0x1000\n") - + (indent + f'include "{includeDir}_scene_main.o"\n') - + (indent + f'include "{includeDir}_scene_col.o"\n') - ) + if specFile.hasSceneCutscenes: + for i in range(specFile.cutsceneTotal): + files.append(("", "include", f'"{includeDir}/{segmentName}_cs_{i}.o"')) - if sceneC is not None: - if sceneC.sceneTexturesIsUsed(): - sceneSegInclude += indent + f'include "{includeDir}_scene_tex.o"\n' - - if sceneC.sceneCutscenesIsUsed(): - for i in range(len(sceneC.sceneCutscenesC)): - sceneSegInclude += indent + f'include "{includeDir}_scene_cs_{i}.o"\n' - - sceneSegInclude += indent + "number 2\n" - specEntries.insert(firstIndex, sceneSegInclude) - firstIndex += 1 - - for i in range(len(scene.rooms)): - specEntries.insert( - firstIndex, - ("\n" + indent + f'name "{scene.name}_room_{i}"\n') - + compressFlag - + (indent + "romalign 0x1000\n") - + (indent + f'include "{includeDir}_room_{i}_main.o"\n') - + (indent + f'include "{includeDir}_room_{i}_model_info.o"\n') - + (indent + f'include "{includeDir}_room_{i}_model.o"\n') - + (indent + "number 3\n"), - ) + specFile.append(SpecEntry(None, segmentName, True, "0x1000", 2, files)) - firstIndex += 1 + for i in range(len(scene.rooms)): + segmentName = f"{sceneName}_room_{i}" - # Write the file data - newFileData = ( - "/*\n * ROM spec file\n */\n\n" - + ("\n".join(includes) + "\n\n" if len(includes) > 0 else "") - + "\n".join("beginseg" + entry + "endseg\n" for entry in specEntries) - ) + if specFile.isSingleFile: + files = [("", '"include", f"{includeDir}/{segmentName}.o"')] + else: + files = [ + ("", "include", f'"{includeDir}/{segmentName}_main.o"'), + ("", "include", f'"{includeDir}/{segmentName}_model_info.o"'), + ("", "include", f'"{includeDir}/{segmentName}_model.o"'), + ] + + specFile.append(SpecEntry(None, segmentName, True, "0x1000", 3, files)) - if newFileData != fileData: - writeFile(os.path.join(exportPath, "spec"), newFileData) + writeFile(os.path.join(exportInfo.exportPath, "spec"), specFile.to_c()) diff --git a/fast64_internal/oot/scene/operators.py b/fast64_internal/oot/scene/operators.py index 44f1da117..9cb9b8e9a 100644 --- a/fast64_internal/oot/scene/operators.py +++ b/fast64_internal/oot/scene/operators.py @@ -18,7 +18,7 @@ def ootRemoveSceneC(exportInfo): modifySceneTable(None, exportInfo) - editSpecFile(None, exportInfo, None) + editSpecFile(None, exportInfo, False, False, 0) deleteSceneFiles(exportInfo) From 9ce85273668c9ba4fe3da2252f7372606b3e3d0c Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:38:36 +0100 Subject: [PATCH 02/10] fix remove issues + minor cleanup --- .../oot/scene/exporter/to_c/spec.py | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index 680db54c0..4bdd0beaf 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -36,7 +36,7 @@ def __post_init__(self): content = None for line in self.original: line = line.strip() - if not line.startswith("#") and not "pad_text" in line: + if not line.startswith("#") and not "pad_text" in line and line != "\n": split = line.split(" ") command = split[0] if len(split) > 2: @@ -65,7 +65,7 @@ def __post_init__(self): raise PluginError(f"ERROR: Unknown spec command: `{command}`") prefix = "" else: - prefix += line + prefix += (indent if "pad_text" in line else "") + line if len(prefix) > 0: self.contentSuffix = f"{prefix}\n" @@ -98,8 +98,6 @@ class SpecFile: # exportName: Optional[str] exportInfo: ExportInfo scene: OOTScene - hasSceneTextures: bool - hasSceneCutscenes: bool cutsceneTotal: int isSingleFile: bool entries: list[SpecEntry] = field(default_factory=list) @@ -121,26 +119,25 @@ def __post_init__(self): 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 + isNotEmptyOrNewline = len(line) > 0 and line != "\n" if ( - not line.startswith(" *") # multi-line comments - and "/*\n" not in line # multi-line comments - and line != "\n" - and line != "" + len(parsedLines) > 0 + or not line.startswith(" *") + and "/*\n" not in line + and not line.startswith("#") + and isNotEmptyOrNewline ): - if len(parsedLines) > 0 or not line.startswith("#"): - if "beginseg" not in line and "endseg" not in line: - # if inside a segment, between beginseg and endseg - parsedLines.append(line) - elif "endseg" in line: - # else, if the line has endseg in it (> if we reached the end of the current segment) - entry = SpecEntry(parsedLines, prefix=prefix) - self.entries.append(entry) - prefix = "" - parsedLines = [] - elif line.startswith("#") and len(parsedLines) == 0: - # else, if between 2 segments and the line is a preprocessor command - prefix += line + if "beginseg" not in line and "endseg" not in line: + # if inside a segment, between beginseg and endseg + parsedLines.append(line) + elif "endseg" in line: + # else, if the line has endseg in it (> if we reached the end of the current segment) + entry = SpecEntry(parsedLines, prefix=prefix) + self.entries.append(entry) + prefix = "" + parsedLines = [] else: + # else, if between 2 segments and the line is a preprocessor command prefix += line self.entries[-1].suffix = prefix.removesuffix("\n") @@ -156,8 +153,9 @@ def append(self, entry: SpecEntry): def remove(self, segmentName: str): entry = self.find(segmentName) if entry is not None: - if not "_room_" in segmentName: - self.entries[-2].suffix = entry.prefix + lastEntry = self.entries[self.entries.index(entry) - 1] + if len(entry.prefix) > 0 and entry.prefix != "\n": + lastEntry.suffix = (lastEntry.suffix if lastEntry.suffix is not None else "") + entry.prefix[:-2] self.entries.remove(entry) def to_c(self): @@ -177,17 +175,15 @@ def editSpecFile( exportInfo.exportPath, exportInfo, scene, - hasSceneTextures, - hasSceneCutscenes, csTotal, bpy.context.scene.ootSceneExportSettings.singleFile, ) sceneName = scene.name if scene is not None else exportInfo.name segmentName = f"{sceneName}_scene" - specFile.remove(segmentName) + specFile.remove(f'"{segmentName}"') for entry in specFile.entries: - if entry.segmentName.startswith(sceneName): + if entry.segmentName.startswith(f'"{sceneName}'): specFile.remove(entry.segmentName) if scene is not None: @@ -205,20 +201,20 @@ def editSpecFile( ("", "include", f'"{includeDir}/{segmentName}_col.o"'), ] - if specFile.hasSceneTextures: + if hasSceneTextures: files.append(("", "include", f'"{includeDir}/{segmentName}_tex.o"')) - if specFile.hasSceneCutscenes: + if hasSceneCutscenes: for i in range(specFile.cutsceneTotal): files.append(("", "include", f'"{includeDir}/{segmentName}_cs_{i}.o"')) - specFile.append(SpecEntry(None, segmentName, True, "0x1000", 2, files)) + specFile.append(SpecEntry(None, f'"{segmentName}"', True, "0x1000", 2, files)) for i in range(len(scene.rooms)): segmentName = f"{sceneName}_room_{i}" if specFile.isSingleFile: - files = [("", '"include", f"{includeDir}/{segmentName}.o"')] + files = [("", "include", f'"{includeDir}/{segmentName}.o"')] else: files = [ ("", "include", f'"{includeDir}/{segmentName}_main.o"'), @@ -226,6 +222,6 @@ def editSpecFile( ("", "include", f'"{includeDir}/{segmentName}_model.o"'), ] - specFile.append(SpecEntry(None, segmentName, True, "0x1000", 3, files)) + specFile.append(SpecEntry(None, f'"{segmentName}"', True, "0x1000", 3, files)) writeFile(os.path.join(exportInfo.exportPath, "spec"), specFile.to_c()) From cf762f8c287d46186dc0bda8bf55761541eac3ba Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:02:18 +0100 Subject: [PATCH 03/10] improvements & documentation (newline issues with several exports) --- fast64_internal/oot/oot_level_writer.py | 7 +- .../oot/scene/exporter/to_c/spec.py | 276 ++++++++++-------- fast64_internal/oot/scene/operators.py | 2 +- 3 files changed, 161 insertions(+), 124 deletions(-) diff --git a/fast64_internal/oot/oot_level_writer.py b/fast64_internal/oot/oot_level_writer.py index ac2e223fe..77c91e195 100644 --- a/fast64_internal/oot/oot_level_writer.py +++ b/fast64_internal/oot/oot_level_writer.py @@ -231,7 +231,12 @@ def writeTextureArraysExistingScene(fModel: OOTModel, exportPath: str, sceneIncl def writeOtherSceneProperties(scene, exportInfo, levelC): modifySceneTable(scene, exportInfo) editSpecFile( - scene, exportInfo, levelC.sceneTexturesIsUsed(), levelC.sceneCutscenesIsUsed(), len(levelC.sceneCutscenesC) + True, + exportInfo, + levelC.sceneTexturesIsUsed(), + levelC.sceneCutscenesIsUsed(), + len(scene.rooms), + len(levelC.sceneCutscenesC), ) modifySceneFiles(scene, exportInfo) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index 4bdd0beaf..506dadb0f 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -1,124 +1,130 @@ import os import bpy +import enum from dataclasses import dataclass, field from typing import Optional from .....utility import PluginError, writeFile, indent from ....oot_utility import ExportInfo, getSceneDirFromLevelName -from ....oot_level_classes import OOTScene + + +class CommandType(enum.Enum): + """This class defines the different spec command types""" + + NAME = 0 + COMPRESS = 1 + AFTER = 2 + FLAGS = 3 + ALIGN = 4 + ADDRESS = 5 + ROMALIGN = 6 + INCLUDE = 7 + INCLUDE_DATA_WITH_RODATA = 8 + NUMBER = 9 + PAD_TEXT = 10 + + @staticmethod + def from_string(value: str): + """Returns one of the enum values from a string""" + + return getattr(CommandType, value.upper()) + + +@dataclass +class SpecEntryCommand: + """This class defines a single spec command""" + + type: CommandType + content: str = "" + prefix: str = "" + suffix: str = "" + + def to_c(self): + return self.prefix + indent + f"{self.type.name.lower()} {self.content}".strip() + self.suffix + "\n" @dataclass class SpecEntry: """Defines an entry of ``spec``""" - original: Optional[list[str]] # the original lines from the parsed file - segmentName: str = "" - isCompressed: bool = False - romalign: Optional[str] = None - number: Optional[int] = None - files: list[tuple[str, str, str]] = field(default_factory=list) # (prefix, command, file) - address: Optional[str] = None - after: Optional[str] = None - align: Optional[str] = None - flags: Optional[str] = None - pad_text: Optional[str] = None - prefix: str = "" - suffix: Optional[str] = None - contentSuffix: Optional[str] = None + original: Optional[list[str]] = field(default_factory=list) # the original lines from the parsed file + commands: list[SpecEntryCommand] = field(default_factory=list) # list of the different spec commands + segmentName: str = "" # the name of the current segment + prefix: str = "" # data between two commands + suffix: str = "" # remaining data after the entry (used for the last entry) + contentSuffix: str = "" # remaining data after the last command in the current entry def __post_init__(self): - if self.prefix == "\n": - self.prefix = "" - if self.original is not None: + # parse the commands from the existing data prefix = "" - content = None for line in self.original: line = line.strip() - if not line.startswith("#") and not "pad_text" in line and line != "\n": - split = line.split(" ") - command = split[0] - if len(split) > 2: - content = " ".join(elem for i, elem in enumerate(split) if i > 0) - elif len(split) > 1: - content = split[1] - match command: - case "compress": - self.isCompressed = True - case "name" | "after" | "flags" | "align" | "address" | "romalign": - if content is not None: - setattr(self, "segmentName" if command == "name" else command, content) - case "include" | "include_data_with_rodata": - if content is not None: - self.files.append( - ( - (prefix + ("\n" if len(prefix) > 0 else "")) if prefix != "\n" else "", - command, - content, - ) - ) - case "number": - if content is not None: - self.number = int(content) - case _: - raise PluginError(f"ERROR: Unknown spec command: `{command}`") - prefix = "" - else: - prefix += (indent if "pad_text" in line else "") + line + if line != "\n": + if not line.startswith("#"): + split = line.split(" ") + command = split[0] + if len(split) > 2: + content = " ".join(elem for i, elem in enumerate(split) if i > 0) + elif len(split) > 1: + content = split[1] + elif command == "name": + content = self.segmentName + else: + content = "" + + self.commands.append( + SpecEntryCommand( + CommandType.from_string(command), + content, + (prefix + ("\n" if len(prefix) > 0 else "")) if prefix != "\n" else "", + ) + ) + prefix = "" + else: + prefix += line + # if there's a prefix it's the remaining data after the last entry if len(prefix) > 0: - self.contentSuffix = f"{prefix}\n" + self.contentSuffix = prefix + + if len(self.segmentName) == 0 and len(self.commands[0].content) > 0: + self.segmentName = self.commands[0].content + else: + raise PluginError("ERROR: The segment name can't be set!") def to_c(self): return ( - self.prefix + (self.prefix if len(self.prefix) > 0 else "\n") + "beginseg\n" - + f"".join( - ( - (indent + f"name {self.segmentName}\n"), - (indent + "compress\n" if self.isCompressed else ""), - (indent + f"after {self.after}\n" if self.after is not None else ""), - (indent + f"flags {self.flags}\n" if self.flags is not None else ""), - (indent + f"align {self.align}\n" if self.align is not None else ""), - (indent + f"address {self.address}\n" if self.address is not None else ""), - (indent + f"romalign {self.romalign}\n" if self.romalign is not None else ""), - "".join(prefix + indent + f"{command} {file}\n" for prefix, command, file in self.files), - (indent + f"number {self.number}\n" if self.number is not None else ""), - ) - ) - + (self.contentSuffix if self.contentSuffix is not None else "") + + "".join(cmd.to_c() for cmd in self.commands) + + (f"{self.contentSuffix}\n" if len(self.contentSuffix) > 0 else "") + "endseg" - + ("\n" + self.suffix if self.suffix is not None else "") + + (self.suffix if self.suffix == "\n" else f"\n{self.suffix}\n" if len(self.suffix) > 0 else "") ) @dataclass class SpecFile: - exportPath: str - # exportName: Optional[str] - exportInfo: ExportInfo - scene: OOTScene - cutsceneTotal: int - isSingleFile: bool - entries: list[SpecEntry] = field(default_factory=list) + """This class defines the spec's file data""" + + exportPath: str # path to the spec file + entries: list[SpecEntry] = field(default_factory=list) # list of the different spec entries def __post_init__(self): # read the file's data try: - with open(os.path.join(self.exportPath, "spec"), "r") as fileData: - # data = fileData.read() - # fileData.seek(0) + with open(self.exportPath, "r") as fileData: lines = fileData.readlines() except FileNotFoundError: raise PluginError("ERROR: Can't find spec!") - # parse the entries and populate the list of entries (``self.entries``) prefix = "" parsedLines = [] 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 we're inside a spec entry or if the lines between two entries do not contains these characters + # fill the ``parsedLine`` list if it's inside a segment + # when we reach the end of the current segment add a new ``SpecEntry`` to ``self.entries`` isNotEmptyOrNewline = len(line) > 0 and line != "\n" if ( len(parsedLines) > 0 @@ -137,91 +143,117 @@ def __post_init__(self): prefix = "" parsedLines = [] else: - # else, if between 2 segments and the line is a preprocessor command + # else, if between 2 segments and the line is something we don't need prefix += line + # set the last's entry's suffix to the remaining prefix self.entries[-1].suffix = prefix.removesuffix("\n") def find(self, segmentName: str): + """Returns an entry from a segment name, returns ``None`` if nothing was found""" + for i, entry in enumerate(self.entries): if entry.segmentName == segmentName: return self.entries[i] return None def append(self, entry: SpecEntry): + """Appends an entry to the list""" + + # prefix/suffix shenanigans + lastEntry = self.entries[-1] + if len(lastEntry.suffix) > 0: + entry.prefix = f"{lastEntry.suffix}\n\n" + lastEntry.suffix = "" self.entries.append(entry) def remove(self, segmentName: str): + """Removes an entry from a segment name""" + + # prefix/suffix shenanigans entry = self.find(segmentName) if entry is not None: - lastEntry = self.entries[self.entries.index(entry) - 1] if len(entry.prefix) > 0 and entry.prefix != "\n": + lastEntry = self.entries[self.entries.index(entry) - 1] lastEntry.suffix = (lastEntry.suffix if lastEntry.suffix is not None else "") + entry.prefix[:-2] self.entries.remove(entry) def to_c(self): - return ( - "".join( - (("\n" * (1 if len(entry.prefix) > 0 else 2)) if i > 0 else "") + entry.to_c() - for i, entry in enumerate(self.entries) - ) - + "\n" - ) + return "\n".join(entry.to_c() for entry in self.entries) def editSpecFile( - scene: Optional[OOTScene], exportInfo: ExportInfo, hasSceneTextures: bool, hasSceneCutscenes: bool, csTotal: int + isScene: bool, exportInfo: ExportInfo, hasSceneTex: bool, hasSceneCS: bool, roomTotal: int, csTotal: int ): - specFile = SpecFile( - exportInfo.exportPath, - exportInfo, - scene, - csTotal, - bpy.context.scene.ootSceneExportSettings.singleFile, - ) - - sceneName = scene.name if scene is not None else exportInfo.name + # get the spec's data + specFile = SpecFile(os.path.join(exportInfo.exportPath, "spec")) + + # get the scene and current segment name and remove the scene + sceneName = exportInfo.name segmentName = f"{sceneName}_scene" specFile.remove(f'"{segmentName}"') for entry in specFile.entries: - if entry.segmentName.startswith(f'"{sceneName}'): + if entry.segmentName.startswith(f'"{sceneName}_'): specFile.remove(entry.segmentName) - if scene is not None: + if isScene: + isSingleFile = bpy.context.scene.ootSceneExportSettings.singleFile includeDir = "$(BUILD_DIR)/" if exportInfo.customSubPath is not None: includeDir += f"{exportInfo.customSubPath + sceneName}" else: includeDir += f"{getSceneDirFromLevelName(sceneName)}" - if specFile.isSingleFile: - files = [("", "include", f'"{includeDir}/{segmentName}.o"')] + sceneCmds = [ + SpecEntryCommand(CommandType.NAME, f'"{segmentName}"'), + SpecEntryCommand(CommandType.COMPRESS), + SpecEntryCommand(CommandType.ROMALIGN, "0x1000"), + ] + + # scene + if isSingleFile: + sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}.o"')) else: - files = [ - ("", "include", f'"{includeDir}/{segmentName}_main.o"'), - ("", "include", f'"{includeDir}/{segmentName}_col.o"'), - ] + sceneCmds.extend( + [ + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_main.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_col.o"'), + ] + ) - if hasSceneTextures: - files.append(("", "include", f'"{includeDir}/{segmentName}_tex.o"')) + if hasSceneTex: + sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_tex.o"')) - if hasSceneCutscenes: - for i in range(specFile.cutsceneTotal): - files.append(("", "include", f'"{includeDir}/{segmentName}_cs_{i}.o"')) + if hasSceneCS: + for i in range(csTotal): + sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_cs_{i}.o"')) - specFile.append(SpecEntry(None, f'"{segmentName}"', True, "0x1000", 2, files)) + sceneCmds.append(SpecEntryCommand(CommandType.NUMBER, "2")) + specFile.append(SpecEntry(None, sceneCmds)) - for i in range(len(scene.rooms)): + # rooms + for i in range(roomTotal): segmentName = f"{sceneName}_room_{i}" - if specFile.isSingleFile: - files = [("", "include", f'"{includeDir}/{segmentName}.o"')] + roomCmds = [ + SpecEntryCommand(CommandType.NAME, f'"{segmentName}"'), + SpecEntryCommand(CommandType.COMPRESS), + SpecEntryCommand(CommandType.ROMALIGN, "0x1000"), + ] + + if isSingleFile: + roomCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}.o"')) else: - files = [ - ("", "include", f'"{includeDir}/{segmentName}_main.o"'), - ("", "include", f'"{includeDir}/{segmentName}_model_info.o"'), - ("", "include", f'"{includeDir}/{segmentName}_model.o"'), - ] + roomCmds.extend( + [ + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_main.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_model_info.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_model.o"'), + ] + ) - specFile.append(SpecEntry(None, f'"{segmentName}"', True, "0x1000", 3, files)) + roomCmds.append(SpecEntryCommand(CommandType.NUMBER, "3")) + specFile.append(SpecEntry(None, roomCmds)) + specFile.entries[-1].suffix = "\n" - writeFile(os.path.join(exportInfo.exportPath, "spec"), specFile.to_c()) + # finally, write the spec file + writeFile(specFile.exportPath, specFile.to_c()) diff --git a/fast64_internal/oot/scene/operators.py b/fast64_internal/oot/scene/operators.py index 9cb9b8e9a..c3b888096 100644 --- a/fast64_internal/oot/scene/operators.py +++ b/fast64_internal/oot/scene/operators.py @@ -18,7 +18,7 @@ def ootRemoveSceneC(exportInfo): modifySceneTable(None, exportInfo) - editSpecFile(None, exportInfo, False, False, 0) + editSpecFile(False, exportInfo, False, False, 0, 0) deleteSceneFiles(exportInfo) From abd9c5eb897ba9570aedd38c03c9340b35edcaa8 Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Fri, 19 Apr 2024 00:25:31 +0200 Subject: [PATCH 04/10] fix some issues --- fast64_internal/oot/scene/exporter/to_c/spec.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index 506dadb0f..a3aa21a8c 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -27,7 +27,10 @@ class CommandType(enum.Enum): def from_string(value: str): """Returns one of the enum values from a string""" - return getattr(CommandType, value.upper()) + cmdType = CommandType._member_map_.get(value.upper()) + if cmdType is None: + raise PluginError(f"ERROR: Can't find value: ``{value}`` in the enum!") + return cmdType @dataclass @@ -60,8 +63,12 @@ def __post_init__(self): prefix = "" for line in self.original: line = line.strip() + dontHaveComments = ( + not line.startswith("// ") and not line.startswith("/* ") and not line.startswith(" */") + ) + if line != "\n": - if not line.startswith("#"): + if not line.startswith("#") and dontHaveComments: split = line.split(" ") command = split[0] if len(split) > 2: @@ -82,7 +89,7 @@ def __post_init__(self): ) prefix = "" else: - prefix += line + prefix += (f"\n{indent}" if not dontHaveComments else "") + line # if there's a prefix it's the remaining data after the last entry if len(prefix) > 0: self.contentSuffix = prefix From 2323d71567dad40281621fdd23f7f1512275ece6 Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Sun, 21 Apr 2024 03:01:55 +0200 Subject: [PATCH 05/10] fixed consecutive directives newline issue and build dirname --- fast64_internal/oot/scene/exporter/to_c/spec.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index a3aa21a8c..b50b72dab 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -8,6 +8,10 @@ from ....oot_utility import ExportInfo, getSceneDirFromLevelName +# either "$(BUILD_DIR)", "$(BUILD)" or "build" +buildDirectory = None + + class CommandType(enum.Enum): """This class defines the different spec command types""" @@ -59,6 +63,7 @@ class SpecEntry: def __post_init__(self): if self.original is not None: + global buildDirectory # parse the commands from the existing data prefix = "" for line in self.original: @@ -80,6 +85,9 @@ def __post_init__(self): else: content = "" + if buildDirectory is None and (content.startswith('"build') or content.startswith('"$(BUILD')): + buildDirectory = content.split("/")[0].removeprefix('"') + self.commands.append( SpecEntryCommand( CommandType.from_string(command), @@ -89,6 +97,9 @@ def __post_init__(self): ) prefix = "" else: + if prefix.startswith("#") and line.startswith("#"): + # add newline if there's two consecutive preprocessor directives + prefix += "\n" prefix += (f"\n{indent}" if not dontHaveComments else "") + line # if there's a prefix it's the remaining data after the last entry if len(prefix) > 0: @@ -203,8 +214,10 @@ def editSpecFile( specFile.remove(entry.segmentName) if isScene: + global buildDirectory + assert buildDirectory is not None isSingleFile = bpy.context.scene.ootSceneExportSettings.singleFile - includeDir = "$(BUILD_DIR)/" + includeDir = f"{buildDirectory}/" if exportInfo.customSubPath is not None: includeDir += f"{exportInfo.customSubPath + sceneName}" else: From 70599b38cf8b3b40f6c75442a3460d7d72de12a3 Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Sun, 21 Apr 2024 03:06:15 +0200 Subject: [PATCH 06/10] fixed minor issue where the build dirname wasn't updating properly in some cases --- fast64_internal/oot/scene/exporter/to_c/spec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index b50b72dab..dec947ed6 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -277,3 +277,6 @@ def editSpecFile( # finally, write the spec file writeFile(specFile.exportPath, specFile.to_c()) + + # reset build directory name so it can update properly on the next run + buildDirectory = None From d7ae94739ab2acf1f06e8b6c9efdc6ae7c8eabb0 Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Sun, 21 Apr 2024 03:07:06 +0200 Subject: [PATCH 07/10] move the global variable earlier in the function --- fast64_internal/oot/scene/exporter/to_c/spec.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index dec947ed6..c628bb7d5 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -202,6 +202,8 @@ def to_c(self): def editSpecFile( isScene: bool, exportInfo: ExportInfo, hasSceneTex: bool, hasSceneCS: bool, roomTotal: int, csTotal: int ): + global buildDirectory + # get the spec's data specFile = SpecFile(os.path.join(exportInfo.exportPath, "spec")) @@ -214,7 +216,6 @@ def editSpecFile( specFile.remove(entry.segmentName) if isScene: - global buildDirectory assert buildDirectory is not None isSingleFile = bpy.context.scene.ootSceneExportSettings.singleFile includeDir = f"{buildDirectory}/" From 95bf72e931d7ec399693d963b9ed03b59bf177a5 Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Sun, 21 Apr 2024 03:30:35 +0200 Subject: [PATCH 08/10] fixed rooms not being deleted properly --- fast64_internal/oot/scene/exporter/to_c/spec.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index c628bb7d5..6eb628603 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -211,9 +211,16 @@ def editSpecFile( sceneName = exportInfo.name segmentName = f"{sceneName}_scene" specFile.remove(f'"{segmentName}"') + + # mark the other scene elements to remove (like rooms) + segmentsToRemove: list[str] = [] for entry in specFile.entries: if entry.segmentName.startswith(f'"{sceneName}_'): - specFile.remove(entry.segmentName) + segmentsToRemove.append(entry.segmentName) + + # remove the segments + for segmentName in segmentsToRemove: + specFile.remove(segmentName) if isScene: assert buildDirectory is not None From 5d6a56935f864b23022983132613b268a2dfe5fd Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:12:20 +0200 Subject: [PATCH 09/10] fixed issue where it was using the wrong segment name for declaring scenes --- .../oot/scene/exporter/to_c/spec.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index 6eb628603..1ed42401d 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -209,8 +209,8 @@ def editSpecFile( # get the scene and current segment name and remove the scene sceneName = exportInfo.name - segmentName = f"{sceneName}_scene" - specFile.remove(f'"{segmentName}"') + sceneSegmentName = f"{sceneName}_scene" + specFile.remove(f'"{sceneSegmentName}"') # mark the other scene elements to remove (like rooms) segmentsToRemove: list[str] = [] @@ -232,50 +232,50 @@ def editSpecFile( includeDir += f"{getSceneDirFromLevelName(sceneName)}" sceneCmds = [ - SpecEntryCommand(CommandType.NAME, f'"{segmentName}"'), + SpecEntryCommand(CommandType.NAME, f'"{sceneSegmentName}"'), SpecEntryCommand(CommandType.COMPRESS), SpecEntryCommand(CommandType.ROMALIGN, "0x1000"), ] # scene if isSingleFile: - sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}.o"')) + sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}.o"')) else: sceneCmds.extend( [ - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_main.o"'), - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_col.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_main.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_col.o"'), ] ) if hasSceneTex: - sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_tex.o"')) + sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_tex.o"')) if hasSceneCS: for i in range(csTotal): - sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_cs_{i}.o"')) + sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_cs_{i}.o"')) sceneCmds.append(SpecEntryCommand(CommandType.NUMBER, "2")) specFile.append(SpecEntry(None, sceneCmds)) # rooms for i in range(roomTotal): - segmentName = f"{sceneName}_room_{i}" + roomSegmentName = f"{sceneName}_room_{i}" roomCmds = [ - SpecEntryCommand(CommandType.NAME, f'"{segmentName}"'), + SpecEntryCommand(CommandType.NAME, f'"{roomSegmentName}"'), SpecEntryCommand(CommandType.COMPRESS), SpecEntryCommand(CommandType.ROMALIGN, "0x1000"), ] if isSingleFile: - roomCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}.o"')) + roomCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}.o"')) else: roomCmds.extend( [ - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_main.o"'), - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_model_info.o"'), - SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{segmentName}_model.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_main.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_model_info.o"'), + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{roomSegmentName}_model.o"'), ] ) From a2abbc7bc0e0873ccf7bc2cc27c9c66f5c018d0b Mon Sep 17 00:00:00 2001 From: Yanis42 <35189056+Yanis42@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:12:50 +0200 Subject: [PATCH 10/10] format --- fast64_internal/oot/scene/exporter/to_c/spec.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fast64_internal/oot/scene/exporter/to_c/spec.py b/fast64_internal/oot/scene/exporter/to_c/spec.py index 1ed42401d..d522f073a 100644 --- a/fast64_internal/oot/scene/exporter/to_c/spec.py +++ b/fast64_internal/oot/scene/exporter/to_c/spec.py @@ -253,7 +253,9 @@ def editSpecFile( if hasSceneCS: for i in range(csTotal): - sceneCmds.append(SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_cs_{i}.o"')) + sceneCmds.append( + SpecEntryCommand(CommandType.INCLUDE, f'"{includeDir}/{sceneSegmentName}_cs_{i}.o"') + ) sceneCmds.append(SpecEntryCommand(CommandType.NUMBER, "2")) specFile.append(SpecEntry(None, sceneCmds))