From ac010263462f4db269fe1ce71a42949446933956 Mon Sep 17 00:00:00 2001 From: jdflyer Date: Thu, 18 Jul 2024 08:04:43 -0700 Subject: [PATCH] Tools: add libbti and assets_config (#2182) * Start d_menu_fmap2d * checkpoint * checkpoint * Add libbti and bti packaging, start libjaudio * Add asset config, move libast, formatting --- .gitignore | 4 + Makefile | 14 +- tools/assets_config.py | 119 ++++ tools/extract_game_assets.py | 28 +- tools/libarc/arc.py | 12 +- tools/libast/__init__.py | 1 - tools/libbti/__init__.py | 1 + tools/libbti/libbti.py | 929 ++++++++++++++++++++++++++ tools/libjaudio/__init__.py | 0 tools/libjaudio/baa.py | 208 ++++++ tools/libjaudio/create_id_enums.py | 33 + tools/{libast => libjaudio}/libast.py | 0 tools/package_game_assets.py | 35 +- tools/requirements.txt | 1 + tools/tp.py | 37 +- 15 files changed, 1398 insertions(+), 24 deletions(-) create mode 100644 tools/assets_config.py delete mode 100644 tools/libast/__init__.py create mode 100644 tools/libbti/__init__.py create mode 100644 tools/libbti/libbti.py create mode 100644 tools/libjaudio/__init__.py create mode 100644 tools/libjaudio/baa.py create mode 100644 tools/libjaudio/create_id_enums.py rename tools/{libast => libjaudio}/libast.py (100%) diff --git a/.gitignore b/.gitignore index b601a1e93d..8314bb885e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,9 @@ docs/doxygen/ __pycache__/ venv/ +# m2ctx files m2ctx.py ctx.c + +# Asset Config +asset_config.json diff --git a/Makefile b/Makefile index 34634df8a2..86106a8809 100644 --- a/Makefile +++ b/Makefile @@ -156,7 +156,11 @@ tools: dirs $(ELF2DOL) $(YAZ0) assets: @mkdir -p game - $(PYTHON) tools/extract_game_assets.py $(IMAGENAME) game + $(PYTHON) tools/extract_game_assets.py $(IMAGENAME) game native asset_config.json + +assets-fast: + @mkdir -p game + $(PYTHON) tools/extract_game_assets.py $(IMAGENAME) game oead asset_config.json docs: $(DOXYGEN) Doxyfile @@ -189,19 +193,19 @@ shiftedrels: shift $(RELS) game: shiftedrels @mkdir -p game - @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) copyCode native + @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) copyCode native asset_config.json game-fast: shiftedrels @mkdir -p game - @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) copyCode oead + @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) copyCode oead asset_config.json game-nocompile: @mkdir -p game - @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) noCopyCode native + @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) noCopyCode native asset_config.json game-nocompile-fast: @mkdir -p game - @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) noCopyCode oead + @$(PYTHON) tools/package_game_assets.py ./game $(BUILD_PATH) noCopyCode oead asset_config.json rungame-nocompile: game-nocompile @echo If you are playing on a shifted game make sure Hyrule Field Speed hack is disabled in dolphin! diff --git a/tools/assets_config.py b/tools/assets_config.py new file mode 100644 index 0000000000..67826018eb --- /dev/null +++ b/tools/assets_config.py @@ -0,0 +1,119 @@ +import curses +from sys import argv +import json +from os import path + +CONFIG_DESCRIPTIONS = { + "decompress_assets": "Decompress Yaz0 compressed assets (appends .c to the filename)", + "extract_arc": "Extract archive (.arc) files", + "convert_stages": "Convert Stage Files (.dzs and .dzr) to json", + "convert_textures": "Convert Textures (.bti) to png", + "update_copydate": "Update COPYDATE on build", + "package_maps": "Package Symbol Map (.map) files on build", +} + +CONFIG_DEFAULTS = { + "decompress_assets": True, + "extract_arc": True, + "convert_stages": True, + "convert_textures": False, + "update_copydate": True, + "package_maps": True, +} + +CONFIGFILE_DEFAULT = "asset_config.json" + + +def printMenu(stdscr, selected_idx, options): + stdscr.clear() + height, width = stdscr.getmaxyx() + + left = min([(width // 2) - (len(option) // 2) for option in options]) + top = (height // 2) - (len(options) // 2) + + stdscr.addstr(top - 2, left, "Configure Asset Extraction/Packaging (q to save):") + + for idx, option in enumerate(options): + x = left + y = top + idx + + if idx == selected_idx: + stdscr.attron(curses.color_pair(1)) + stdscr.addstr(y, x, option) + stdscr.attroff(curses.color_pair(1)) + else: + stdscr.addstr(y, x, option) + + stdscr.refresh() + + +def cursesMenu(stdscr, values): + curses.curs_set(0) + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) + + keys = list(values.keys()) + options = [ + f"{CONFIG_DESCRIPTIONS[key]}: ({"X" if values[key] else " "})" for key in keys + ] + bool_values = [values[key] for key in keys] + + selected_idx = 0 + + while True: + printMenu(stdscr, selected_idx, options) + + key = stdscr.getch() + + if key == curses.KEY_UP and selected_idx > 0: + selected_idx -= 1 + elif key == curses.KEY_DOWN and selected_idx < len(options) - 1: + selected_idx += 1 + elif key in (curses.KEY_ENTER, ord("\n"), 13): + bool_values[selected_idx] = not bool_values[selected_idx] + options[selected_idx] = ( + f"{CONFIG_DESCRIPTIONS[keys[selected_idx]]}: ({"X" if bool_values[selected_idx] else " "})" + ) + elif key in (27, ord("q")): # Escape, q + break + + return {key: value for key, value in zip(keys, bool_values)} + + +def saveConfig(values, configfile=CONFIGFILE_DEFAULT): + with open(configfile, "w") as f: + json.dump(values, f) + + +def updateConfig(configfile=CONFIGFILE_DEFAULT): + values = getConfig(configfile) + + values = curses.wrapper(cursesMenu, values) + saveConfig(values, configfile) + return values + + +def getConfig(configfile=CONFIGFILE_DEFAULT, update=False): + values = CONFIG_DEFAULTS + + if path.exists(configfile): + with open(configfile, "r") as f: + config = json.load(f) + for key, value in config.items(): + if key in CONFIG_DEFAULTS: + values[key] = value + elif update == True: + values = updateConfig(configfile) + + return values + + +def main(): + if len(argv) < 2: + print(f"{argv[0]} Usage: python3 {argv[0]} asset_config.json") + exit(1) + + updateConfig(argv[1]) + + +if __name__ == "__main__": + main() diff --git a/tools/extract_game_assets.py b/tools/extract_game_assets.py index b23ebf0cd2..4c607f4a83 100644 --- a/tools/extract_game_assets.py +++ b/tools/extract_game_assets.py @@ -4,6 +4,8 @@ import libarc from pathlib import Path import libyaz0 import libstage +import libbti +import assets_config """ Extracts the game assets and stores them in the game folder @@ -138,17 +140,24 @@ convertDefinitions = { ".arc": { "function": libarc.extract_to_directory, "exceptions": ["archive/dat/speakerse.arc"], + "config_key": "extract_arc" }, ".dzs": { - "function": libstage.extract_to_json + "function": libstage.extract_to_json, + "config_key": "convert_stages" }, ".dzr": { - "function": libstage.extract_to_json + "function": libstage.extract_to_json, + "config_key": "convert_stages" + }, + ".bti": { + "function": libbti.bti_to_png, + "config_key": "convert_textures" } } def writeFile(name, data): - if data[0:4] == bytes("Yaz0", "ascii"): + if data[0:4] == bytes("Yaz0", "ascii") and config["decompress_assets"]: splitName = os.path.splitext(name) name = splitName[0] + ".c" + splitName[1] data = libyaz0.decompress(data) @@ -158,15 +167,19 @@ def writeFile(name, data): ext = splitName[1] if ext in convertDefinitions: extractDef = convertDefinitions[ext] - if "exceptions" in extractDef and str(name) in extractDef["exceptions"]: + if ("exceptions" in extractDef and str(name) in extractDef["exceptions"]) or ("config_key" in extractDef and config[extractDef["config_key"]] == False): extractDef = None + if config["decompress_assets"] == None: + extractDef = None # If assets aren't being decompressed, just write the raw file + if extractDef == None: file = open(name, "wb") file.write(data) file.close() else: name = extractDef["function"](name, data, writeFile) + # print(name) return name @@ -232,8 +245,7 @@ def getDolInfo(disc): return dolOffset, dolSize - -def extract(isoPath: Path, gamePath: Path, yaz0Encoder): +def extract(isoPath: Path, gamePath: Path, yaz0Encoder: str, config_file: str): if yaz0Encoder == "oead": try: from oead import yaz0 @@ -241,6 +253,8 @@ def extract(isoPath: Path, gamePath: Path, yaz0Encoder): yaz0DecompressFunction = yaz0.decompress except: print("Extract: oead isn't installed, falling back to native yaz0") + global config + config = assets_config.getConfig(config_file,update=True) isoPath = isoPath.absolute() cwd = os.getcwd() os.chdir(gamePath) @@ -288,7 +302,7 @@ def extract(isoPath: Path, gamePath: Path, yaz0Encoder): def main(): - extract(Path(sys.argv[1]), Path(sys.argv[2]), "native") + extract(Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3], sys.argv[4]) if __name__ == "__main__": diff --git a/tools/libarc/arc.py b/tools/libarc/arc.py index 61970f7801..8d625c661d 100644 --- a/tools/libarc/arc.py +++ b/tools/libarc/arc.py @@ -283,6 +283,7 @@ def getNodeIdent(fullName): def parseDirForPack( fileDataLines, path, convertFunction, nodes, dirs, currentNode, stringTable, data ): + # print(currentNode.directory_index) for i in range( currentNode.directory_index, currentNode.directory_count + currentNode.directory_index, @@ -400,12 +401,21 @@ def convert_dir_to_arc(sourceDir, convertFunction): nodes = [] dirs = [None] * len(fileDataLines) stringTable = ".\0..\0" + + numRootEntries = 0 + for line in fileDataLines: + entry = line.split(":")[1] + # Only count files and directories under the root + # print(entry) + if entry.startswith(rootName + "/") and entry[:len(entry)-1].count("/") == 1: + numRootEntries += 1 + nodes.append( Node( getNodeIdent("ROOT"), len(stringTable), computeHash(rootName), - len(os.listdir(sourceDir / rootName)) + 2, + numRootEntries, 0, rootName, ) diff --git a/tools/libast/__init__.py b/tools/libast/__init__.py deleted file mode 100644 index 5f47cb0b32..0000000000 --- a/tools/libast/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .libast import * \ No newline at end of file diff --git a/tools/libbti/__init__.py b/tools/libbti/__init__.py new file mode 100644 index 0000000000..22989cce61 --- /dev/null +++ b/tools/libbti/__init__.py @@ -0,0 +1 @@ +from .libbti import * diff --git a/tools/libbti/libbti.py b/tools/libbti/libbti.py new file mode 100644 index 0000000000..6a712b49a7 --- /dev/null +++ b/tools/libbti/libbti.py @@ -0,0 +1,929 @@ +import struct +import sys +import os +from PIL import Image +from io import BytesIO +from math import ceil +from dataclasses import asdict, dataclass +from enum import Enum +import json + +imageFormatToInt = { + "I4": 0, + "I8": 1, + "IA4": 2, + "IA8": 3, + "RGB565": 4, + "RGB5A3": 5, + "RGBA32": 6, + "C4": 8, + "C8": 9, + "C14X2": 0xA, + "CMPR": 0xE, +} + +colorFormatToInt = {"IA8": 0, "RGB565": 1, "RGB5A3": 2} + +wrapModeToInt = {"CLAMP": 0, "REPEAT": 1, "MIRROR": 2, "MAX_TEXWRAP_MODE": 3} + +texFilterToInt = { + "NEAR": 0, + "LINEAR": 1, + "NEAR_MIP_NEAR": 2, + "LINEAR_MIP_NEAR": 3, + "NEAR_MIP_LINEAR": 4, + "LINEAR_MIP_LINEAR": 5, +} + + +def invertDict(dict): + return {v: i for i, v in dict.items()} + + +@dataclass +class BTI_Header: + format: int + alphaEnabled: int + width: int + height: int + wrapS: int + wrapT: int + indexTexture: int + colorFormat: int + numColors: int + paletteOffset: int + mipmapEnabled: int + doEdgeLOD: int + biasClamp: int + maxAnisotropy: int + minFilter: int + magFilter: int + minLOD: int + maxLOD: int + mipmapCount: int + unk0: int + LODBias: int + imageOffset: int + + @classmethod + def fromBytes(cls, header_bytes): + assert len(header_bytes) == 0x20 + unpacked = struct.unpack(">BBHHBBBBHIBBBBBBbbBBhI", header_bytes) + return cls(*unpacked) + + def getBytes(self): + return struct.pack( + ">BBHHBBBBHIBBBBBBbbBBhI", + self.format, + self.alphaEnabled, + self.width, + self.height, + self.wrapS, + self.wrapT, + self.indexTexture, + self.colorFormat, + self.numColors, + self.paletteOffset, + self.mipmapEnabled, + self.doEdgeLOD, + self.biasClamp, + self.maxAnisotropy, + self.minFilter, + self.magFilter, + self.minLOD, + self.maxLOD, + self.mipmapCount, + self.unk0, + self.LODBias, + self.imageOffset, + ) + + +def decodeIA8(bytes): + intensity = bytes[1] + return intensity, intensity, intensity, bytes[0] + + +def encodeIA8(color): + return ((color[3] << 8) & 0xFF00) | (((color[0] + color[1] + color[2]) // 3) & 0xFF) + + +def decodeRGB565(bytes): + full = (bytes[0] << 8) | bytes[1] + return (full >> 11) * 8, ((full >> 5) & 0b111111) * 4, (full & 0b11111) * 8, 0xFF + + +def encodeRGB565(color): + return ( + (((color[0] // 8) & 0b11111) << 11) + | (((color[1] // 4) & 0b111111) << 5) + | ((color[2] // 8) & 0b11111) + ) + + +def decodeRGB5A3(bytes): + full = (bytes[0] << 8) | bytes[1] + isAlpha = not (full & 0x8000) > 0 + if isAlpha: + return ( + ((full >> 8) & 0xF) * 0x11, + ((full >> 4) & 0xF) * 0x11, + (full & 0xF) * 0x11, + ((full >> 12) & 0b111) * 0x20, + ) + else: + return ( + ((full >> 10) & 0b11111) * 8, + ((full >> 5) & 0b11111) * 8, + (full & 0b11111) * 8, + 0xFF, + ) + + +def encodeRGB5A3(color): + isAlpha = color[3] < 0xFF + if isAlpha: + return ( + (((color[3] // 0x20) & 0b111) << 12) + | (((color[0] // 0x11) & 0xF) << 8) + | (((color[1] // 0x11) & 0xF) << 4) + | ((color[2] // 0x11) & 0xF) + ) + else: + return ( + (1 << 15) + | (((color[0] // 8) & 0b11111) << 10) + | (((color[1] // 8) & 0b11111) << 5) + | ((color[2] // 8) & 0b11111) + ) + + +class ImageBase: + def __init__(self, header, data=None, imageBuffer=None): + self.header = header + self.data = data + self.paletteColors = [] + if self.data == None: + self.data = bytearray() + self.imageBuffer = ( + imageBuffer + if imageBuffer != None + else [(0, 0, 0, 0xFF)] * (header.width * header.height) + ) + + def decodePaletteColors(self): + decodeFunc = None + match self.header.colorFormat: + case 0: + decodeFunc = decodeIA8 + case 1: + decodeFunc = decodeRGB565 + case 2: + decodeFunc = decodeRGB5A3 + + colors = [] + + offset = self.header.paletteOffset + for i in range(self.header.numColors): + colors.append(decodeFunc(self.data[offset : offset + 2])) + offset += 2 + + self.paletteColors = colors + + def encodePaletteColors(self): + encodeFunc = None + match self.header.colorFormat: + case 0: + encodeFunc = encodeIA8 + case 1: + encodeFunc = encodeRGB565 + case 2: + encodeFunc = encodeRGB5A3 + + for color in self.paletteColors: + self.data += struct.pack(">H", encodeFunc(color)) + + def writePixel(self, x, y, pixelValue): + if x >= self.header.width or y >= self.header.height: + return + self.imageBuffer[(y * self.header.width) + x] = pixelValue + + def readPixel(self, x, y): + if x >= self.header.width or y >= self.header.height: + return 0xFF, 0xFF, 0xFF, 0xFF + return self.imageBuffer[(y * self.header.width) + x] + + def decodeLoop(self, blockWidth, blockHeight, blockStride): + widthInBlocks = ceil(self.header.width / blockWidth) + heightInBlocks = ceil(self.header.height / blockHeight) + + offset = self.header.imageOffset + for blockY in range(heightInBlocks): + for blockX in range(widthInBlocks): + self.decodeBlock( + blockX * blockWidth, + blockY * blockHeight, + self.data[offset : offset + blockStride], + ) + offset += blockStride + + def decode(self): + pass + + def decodeBlock(self, x, y, blockBytes): + pass + + def encodeLoop(self, blockWidth, blockHeight): + widthInBlocks = ceil(self.header.width / blockWidth) + heightInBlocks = ceil(self.header.height / blockHeight) + + for blockY in range(heightInBlocks): + for blockX in range(widthInBlocks): + self.encodeBlock(blockX * blockWidth, blockY * blockHeight) + + def encode(self): + pass + + def encodeBlock(self, x, y): + pass + + +class I4Image(ImageBase): + def decode(self): + self.decodeLoop(8, 8, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(8): + for inX in range(4): + val = ((blockBytes[offset] & 0xF0) >> 4) * 0x11 + self.writePixel((x + (inX * 2)), y + inY, (val, val, val, 0xFF)) + val = (blockBytes[offset] & 0xF) * 0x11 + self.writePixel((x + (inX * 2)) + 1, y + inY, (val, val, val, 0xFF)) + offset += 1 + + def encode(self): + self.encodeLoop(8, 8) + self.header.numColors = len(self.paletteColors) + self.header.paletteOffset = len(self.data) + 0x20 + self.encodePaletteColors() + + def encodeBlock(self, x, y): + for inY in range(8): + for inX in range(4): + color1 = self.readPixel(x + (inX * 2), y + inY) + intensity1 = ((color1[0] + color1[1] + color1[2]) // 3) // 0x11 + color2 = self.readPixel(x + (inX * 2) + 1, y + inY) + intensity2 = ((color2[0] + color2[1] + color2[2]) // 3) // 0x11 + self.data.append((intensity1 << 4) | intensity2) + + +class I8Image(ImageBase): + def decode(self): + self.decodeLoop(8, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(8): + intensity = blockBytes[offset] + self.writePixel( + x + inX, y + inY, (intensity, intensity, intensity, 0xFF) + ) + offset += 1 + + def encode(self): + self.encodeLoop(8, 4) + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(8): + color = self.readPixel(x + inX, y + inY) + intensity = (color[0] + color[1] + color[2]) // 3 + self.data.append(intensity) + + +class IA4Image(ImageBase): + def decode(self): + self.decodeLoop(8, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(8): + byte = blockBytes[offset] + intensity = (byte & 0b1111) * 0x11 + self.writePixel( + x + inX, + y + inY, + (intensity, intensity, intensity, (byte >> 4) * 0x11), + ) + offset += 1 + + def encode(self): + self.encodeLoop(8, 4) + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(8): + color = self.readPixel(x + inX, y + inY) + intensity = (color[0] + color[1] + color[2]) // 3 + self.data.append(((color[3] // 0x11) << 4) | (intensity // 0x11)) + + +class IA8Image(ImageBase): + def decode(self): + self.decodeLoop(4, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(4): + self.writePixel( + x + inX, y + inY, decodeIA8(blockBytes[offset : offset + 2]) + ) + offset += 2 + + def encode(self): + self.encodeLoop(4, 4) + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(4): + color = self.readPixel(x + inX, y + inY) + self.data += struct.pack(">H", encodeIA8(color)) + + +class RGB565Image(ImageBase): + def decode(self): + self.decodeLoop(4, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(4): + self.writePixel( + x + inX, y + inY, decodeRGB565(blockBytes[offset : offset + 2]) + ) + offset += 2 + + def encode(self): + self.encodeLoop(4, 4) + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(4): + color = self.readPixel(x + inX, y + inY) + self.data += struct.pack(">H", encodeRGB565(color)) + + +class RGB5A3Image(ImageBase): + def decode(self): + self.decodeLoop(4, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(4): + self.writePixel( + x + inX, y + inY, decodeRGB5A3(blockBytes[offset : offset + 2]) + ) + offset += 2 + + def encode(self): + self.encodeLoop(4, 4) + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(4): + color = self.readPixel(x + inX, y + inY) + self.data += struct.pack(">H", encodeRGB5A3(color)) + + +class RGBA32Image(ImageBase): + def decode(self): + self.decodeLoop(4, 4, 64) + + def decodeBlock(self, x, y, blockBytes): + aOffset = 0 + rOffset = 1 + gOffset = 32 + bOffset = 33 + for inY in range(4): + for inX in range(4): + self.writePixel( + x + inX, + y + inY, + ( + blockBytes[rOffset], + blockBytes[gOffset], + blockBytes[bOffset], + blockBytes[aOffset], + ), + ) + aOffset += 2 + rOffset += 2 + gOffset += 2 + bOffset += 2 + + def encode(self): + self.encodeLoop(4, 4) + + def encodeBlock(self, x, y): + aOffset = 0 + rOffset = 1 + gOffset = 32 + bOffset = 33 + buffer = bytearray(64) + for inY in range(4): + for inX in range(4): + color = self.readPixel(x + inX, y + inY) + buffer[rOffset] = color[0] + buffer[gOffset] = color[1] + buffer[bOffset] = color[2] + buffer[aOffset] = color[3] + + aOffset += 2 + rOffset += 2 + gOffset += 2 + bOffset += 2 + + +class C4Image(ImageBase): + def decode(self): + self.decodePaletteColors() + self.decodeLoop(8, 8, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(8): + for inX in range(4): + index = (blockBytes[offset] & 0xF0) >> 4 + if index >= self.header.numColors: + offset += 1 + continue # Out of bounds, likely due to the block going out of the image + self.writePixel((x + (inX * 2)), y + inY, self.paletteColors[index]) + index = blockBytes[offset] & 0xF + if index >= self.header.numColors: + offset += 1 + continue # Out of bounds, likely due to the block going out of the image + self.writePixel((x + (inX * 2)) + 1, y + inY, self.paletteColors[index]) + offset += 1 + + def encode(self): + # Mipmaps are currently not allowed for paletted textures + assert ( + self.header.mipmapEnabled == 0 and self.header.mipmapCount == 1 + ), "Mipmaps are not supported on Paletted textures!" + self.encodeLoop(8, 8) + self.header.numColors = len(self.paletteColors) + self.header.paletteOffset = len(self.data) + 0x20 + self.encodePaletteColors() + + def encodeBlock(self, x, y): + for inY in range(8): + for inX in range(4): + pixelValue = self.readPixel(x + (inX * 2), y + inY) + if not pixelValue in self.paletteColors: + self.paletteColors.append(pixelValue) + index = self.paletteColors.index(pixelValue) + pixelValue = self.readPixel(x + (inX * 2) + 1, y + inY) + if not pixelValue in self.paletteColors: + self.paletteColors.append(pixelValue) + index2 = self.paletteColors.index(pixelValue) + self.data.append((index << 4) | index2) + + +class C8Image(ImageBase): + def decode(self): + self.decodePaletteColors() + self.decodeLoop(8, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(8): + # print(self.header.numColors) + if blockBytes[offset] >= self.header.numColors: + offset += 1 + continue # Out of bounds, likely due to the block going out of the image + self.writePixel( + x + inX, y + inY, self.paletteColors[blockBytes[offset]] + ) + offset += 1 + + def encode(self): + # Mipmaps are currently not allowed for paletted textures + assert ( + self.header.mipmapEnabled == 0 and self.header.mipmapCount == 1 + ), "Mipmaps are not supported on Paletted textures!" + self.encodeLoop(8, 4) + self.header.numColors = len(self.paletteColors) + self.header.paletteOffset = len(self.data) + 0x20 + self.encodePaletteColors() + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(8): + pixelValue = self.readPixel(x + inX, y + inY) + if not pixelValue in self.paletteColors: + self.paletteColors.append(pixelValue) + index = self.paletteColors.index(pixelValue) + self.data.append(index) + + +class C14X2Image(ImageBase): + def decode(self): + self.decodePaletteColors() + self.decodeLoop(4, 4, 32) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for inY in range(4): + for inX in range(8): + index = struct.unpack(">H", blockBytes[offset : offset + 2])[0] & 0x3FFF + if index < self.header.numColors: + offset += 2 + continue # Out of bounds, likely due to the block going out of the image + self.writePixel(x + inX, y + inY, self.paletteColors[index]) + offset += 2 + + def encode(self): + # Mipmaps are currently not allowed for paletted textures + assert ( + self.header.mipmapEnabled == 0 and self.header.mipmapCount == 1 + ), "Mipmaps are not supported on Paletted textures!" + self.encodeLoop(4, 4) + self.header.numColors = len(self.paletteColors) + self.header.paletteOffset = len(self.data) + 0x20 + self.encodePaletteColors() + + def encodeBlock(self, x, y): + for inY in range(4): + for inX in range(4): + pixelValue = self.readPixel(x + inX, y + inY) + if not pixelValue in self.paletteColors: + self.paletteColors.append(pixelValue) + index = self.paletteColors.index(pixelValue) + self.data += struct.pack(">H", index) + + +class CMPRImage(ImageBase): + def decode(self): + self.decodeLoop(8, 8, 32) + + def interpolateColor(self, color1, color2, factor): + return tuple(int(c1 + ((c2 - c1) * factor)) for c1, c2 in zip(color1, color2)) + + def decodeBlock(self, x, y, blockBytes): + offset = 0 + for subY in range(2): + for subX in range(2): + colors = [ + decodeRGB565(blockBytes[offset + 0 : offset + 2]), + decodeRGB565(blockBytes[offset + 2 : offset + 4]), + ] + realVal1, realVal2 = struct.unpack( + ">HH", blockBytes[offset + 0 : offset + 4] + ) + if realVal1 > realVal2: + colors.append(self.interpolateColor(colors[0], colors[1], (1 / 3))) + colors.append(self.interpolateColor(colors[0], colors[1], (2 / 3))) + else: + colors.append(self.interpolateColor(colors[0], colors[1], (1 / 2))) + colors.append((0, 0, 0, 0)) # transparent + + # print(colors) + + val = struct.unpack(">I", blockBytes[offset + 4 : offset + 8])[0] + + for inY in range(4): + for inX in range(4): + self.writePixel( + x + (subX * 4) + inX, + y + (subY * 4) + inY, + colors[val >> 30], + ) + val = (val << 2) & 0xFFFFFFFF + + offset += 8 + + def encode(self): + self.encodeLoop(8, 8) + + def color_distance(self, color1, color2): + return ( + ((color1[0] - color2[0]) ** 2) + + ((color1[1] - color2[1]) ** 2) + + ((color1[2] - color2[2]) ** 2) + ) + + def encodeBlock(self, x, y): + for subY in range(2): + for subX in range(2): + block = [ + self.readPixel(x + (subX * 4) + inX, y + (subY * 4) + inY) + for inY in range(4) + for inX in range(4) + ] + # print(f"{x} {y}") + # print(block) + + isTransparent = False + for color in block: + if color[3] < 200: + isTransparent = True + break + + colors = [min(block), max(block)] + + realColor1 = encodeRGB565(colors[0]) + realColor2 = encodeRGB565(colors[1]) + + if realColor1 < realColor2: + colors[0], colors[1] = colors[1], colors[0] + realColor1, realColor2 = realColor2, realColor1 + + if isTransparent and realColor1 > realColor2: + colors[0], colors[1] = colors[1], colors[0] + realColor1, realColor2 = realColor2, realColor1 + + # print(colors[0]) + + if not isTransparent: + colors.append(self.interpolateColor(colors[0], colors[1], (1 / 3))) + colors.append(self.interpolateColor(colors[0], colors[1], (2 / 3))) + else: + colors.append(self.interpolateColor(colors[0], colors[1], (1 / 2))) + # colors.append((0,0,0,0)) + + indices = [] + for color in block: + if color[3] < 200: + indices.append(3) + continue + + distances = [ + self.color_distance(color, testColor) for testColor in colors + ] + indices.append(distances.index(min(distances))) + + indicesInt = 0 + for i in range(16): + indicesInt = (indicesInt << 2) | ( + indices[i] & 0b11 + ) # (indicesInt >> 2) | ((indices[i]&0b11)<<30) + + # print(realColor1) + # print(realColor2) + self.data += struct.pack(">HHI", realColor1, realColor2, indicesInt) + + +def bti_to_png_cli(inFile, outFile, data, writeFunc): + bti_to_png(os.path.splitext(outFile)[0] + ".bti", data, writeFunc) + + +def bti_to_png(name, data, writefunc): + outName = os.path.splitext(name)[0] + ".png" + print(f"Converting {name} to {outName}") + + header = BTI_Header.fromBytes(data[0:0x20]) + # print(header.getBytes()) + + image = None + + match header.format: + case 0: + image = I4Image(header, data) + case 1: + image = I8Image(header, data) + case 2: + image = IA4Image(header, data) + case 3: + image = IA8Image(header, data) + case 4: + image = RGB565Image(header, data) + case 5: + image = RGB5A3Image(header, data) + case 6: + image = RGBA32Image(header, data) + case 8: + image = C4Image(header, data) + case 9: + image = C8Image(header, data) + case 10: + image = C14X2Image(header, data) + case 0xE: + image = CMPRImage(header, data) + case _: + print("Invalid format!") + + image.decode() + + pilImage = Image.new("RGBA", (header.width, header.height)) + pilImage.putdata(image.imageBuffer) + + with BytesIO() as output: + pilImage.save(output, format="PNG") + writefunc(outName, output.getvalue()) + + header_dict = asdict(header) + # print(header_dict) + + header_dict["format"] = invertDict(imageFormatToInt)[header.format] + + if header.format >= 8 and header.format <= 10: + header_dict["colorFormat"] = invertDict(colorFormatToInt)[header.colorFormat] + else: + del header_dict["colorFormat"] + + header_dict["wrapS"] = invertDict(wrapModeToInt)[header.wrapS] + header_dict["wrapT"] = invertDict(wrapModeToInt)[header.wrapT] + + header_dict["minFilter"] = invertDict(texFilterToInt)[header.minFilter] + header_dict["magFilter"] = invertDict(texFilterToInt)[header.magFilter] + + del header_dict["width"] + del header_dict["height"] + del header_dict["numColors"] + del header_dict["paletteOffset"] + del header_dict["unk0"] + del header_dict["imageOffset"] + + # print(header_dict) + + splitext = os.path.splitext(name) + writefunc( + splitext[0] + ".bti.json", bytes(json.dumps(header_dict, indent=4), "ascii") + ) + + return outName + + +def namedValueFromDict(value, dict): + if not value in dict: + print( + f"Format {value} is not a valid type. Valid types are {", ".join(dict.keys())}" + ) + return None + return dict[value] + + +def png_to_bti(name, convertFunc): + # print(name) + splitext = os.path.splitext(name) + header_dict = json.load(open(splitext[0] + ".bti.json", "r")) + + format = namedValueFromDict(header_dict["format"], imageFormatToInt) + + colorFormat = 0 + + if format >= 8 and format <= 10: + colorFormat = namedValueFromDict(header_dict["colorFormat"], colorFormatToInt) + + wrapS = namedValueFromDict(header_dict["wrapS"], wrapModeToInt) + wrapT = namedValueFromDict(header_dict["wrapT"], wrapModeToInt) + + minFilter = namedValueFromDict(header_dict["minFilter"], texFilterToInt) + magFilter = namedValueFromDict(header_dict["magFilter"], texFilterToInt) + + pilImage = Image.open(name) + + # Reduce the amount of colors to go within the palette bounds, if needed + + if format >= 8 and format <= 10: + unique_pixels = len(set(list(pilImage.getdata()))) + + match format: + case 8: # C4 + if unique_pixels > 16: + pilImage = pilImage.quantize(15) + case 9: # C8 + if unique_pixels > 256: + pilImage = pilImage.quantize(255) + case 10: # C14X2 + if unique_pixels > 16384: + pilImage = pilImage.quantize(16383) + + pilImage = pilImage.convert("RGBA") + + width = pilImage.width + height = pilImage.height + imageBuffer = list(pilImage.getdata()) + pilImage.close() + + # Fill header with default values + header = BTI_Header( + format, + header_dict["alphaEnabled"], + width, + height, + wrapS, + wrapT, + header_dict["indexTexture"], + colorFormat, + 0, + 0, + header_dict["mipmapEnabled"], + header_dict["doEdgeLOD"], + header_dict["biasClamp"], + header_dict["maxAnisotropy"], + minFilter, + magFilter, + header_dict["minLOD"], + header_dict["maxLOD"], + header_dict["mipmapCount"], + 0, + header_dict["LODBias"], + 32, + ) + match header.format: + case 0: + image = I4Image(header, None, imageBuffer) + case 1: + image = I8Image(header, None, imageBuffer) + case 2: + image = IA4Image(header, None, imageBuffer) + case 3: + image = IA8Image(header, None, imageBuffer) + case 4: + image = RGB565Image(header, None, imageBuffer) + case 5: + image = RGB5A3Image(header, None, imageBuffer) + case 6: + image = RGBA32Image(header, None, imageBuffer) + case 8: + image = C4Image(header, None, imageBuffer) + case 9: + image = C8Image(header, None, imageBuffer) + case 10: + image = C14X2Image(header, None, imageBuffer) + case 0xE: + image = CMPRImage(header, None, imageBuffer) + + if header.mipmapEnabled == 0 or header.mipmapCount == 1: + image.encode() + else: + # Handle mipmaps + for i in range(header.mipmapCount): + image.encode() + header.width = header.width // 2 + header.height = header.height // 2 + newPixels = [(0, 0, 0, 0xFF)] * (header.width * header.height) + for y in range(header.height): + for x in range(header.width): + p1 = image.imageBuffer[(y * header.width * 4) + (x * 2)] + p2 = image.imageBuffer[(y * header.width * 4) + (x * 2) + 1] + p3 = image.imageBuffer[ + (header.width * 2) + (y * header.width * 4) + (x * 2) + ] + p4 = image.imageBuffer[ + (header.width * 2) + (y * header.width * 4) + (x * 2) + 1 + ] + newPixels[(y * header.width) + x] = ( + (p1[0] + p2[0] + p3[0] + p4[0]) // 4, + (p1[1] + p2[1] + p3[1] + p4[1]) // 4, + (p1[2] + p2[2] + p3[2] + p4[2]) // 4, + (p1[3] + p2[3] + p3[3] + p4[3]) // 4, + ) + image.imageBuffer = newPixels + # reset image info + header.width = width + header.height = height + + # The encoding function should've modified the header for any changes + + return header.getBytes() + image.data + + +def testWriteFunc(name, data): + with open(name, "wb") as f: + f.write(data) + + +def main(): + if len(sys.argv) != 3: + print( + f"Usage: {sys.argv[0]} input.bti output.png OR {sys.argv[0]} input.png output.bti" + ) + exit(1) + + inputString = sys.argv[1] + outputString = sys.argv[2] + + if inputString.rfind(".png") != -1 and outputString.rfind(".bti") != -1: + bti_bytes = png_to_bti(inputString, None) + testWriteFunc(outputString, bti_bytes) + elif inputString.rfind(".bti") != -1 and outputString.rfind(".png") != -1: + bti_file = open(inputString, "rb") + bti_bytes = bti_file.read() + bti_file.close() + + name = bti_to_png_cli(inputString, outputString, bti_bytes, testWriteFunc) + else: + print("Error: One of the provided arguments is not valid!") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/libjaudio/__init__.py b/tools/libjaudio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/libjaudio/baa.py b/tools/libjaudio/baa.py new file mode 100644 index 0000000000..b1bfe6efe3 --- /dev/null +++ b/tools/libjaudio/baa.py @@ -0,0 +1,208 @@ +import struct + +class JAUAudioArcInterpreter: + def __init__(self,file): + self.offset = 0 + self.file = file + self.nameTable = {"Sections": []} + + def readU32_string(self): + val = struct.unpack(">4s",self.file[self.offset:self.offset+4])[0] + self.offset += 4 + return val + + def readU32_(self): + val = struct.unpack(">I",self.file[self.offset:self.offset+4])[0] + self.offset += 4 + return val + + def readU8_(self): + val = struct.unpack(">B",self.file[self.offset:self.offset+1])[0] + self.offset += 1 + return val + + def getString(self,bytes,offset): + string = bytes[offset:] + string = string[:string.index(0)] + return string + + # More JAudio Decomp is required for this + def readWS(self,bank_no,offset,waveArc): + # print(bank_no) + # print(offset) + # print(waveArc) + + size = struct.unpack(">I",self.file[offset+4:offset+8])[0] + # print(size) + wsys = self.file[offset:offset+size] + someNumber,archiveBankOffset,ctrlGroupOffset = struct.unpack(">III",wsys[12:24]) + # print(ctrlGroupOffset) + + def readBNK(self,param_1,param_2): + pass + + def beginBNKList(self,param_1,param_2): + pass + + def endBNKList(self): + pass + + def readBSC(self,param_1,param_2): + pass + + def readBST(self,offset,size): + # print(offset) + bst = self.file[offset:offset+size] + numSections = struct.unpack(">I",bst[32:36])[0] + + sectionOffsets = [] + for i in range(numSections): + sectionOffsets.append(struct.unpack(">I",bst[36+(i*4):40+(i*4)])[0]) + for sectionOffset in sectionOffsets: + numGroups = struct.unpack(">I",bst[sectionOffset:sectionOffset+4])[0] + groupOffsets = [] + for i in range(numGroups): + groupOffsets.append(struct.unpack(">I",bst[sectionOffset+4+(i*4):sectionOffset+8+(i*4)])[0]) + # print(groupOffsets) + for groupOffset in groupOffsets: + numEntries,unk = struct.unpack(">II",bst[groupOffset:groupOffset+8]) + entries = [] + for i in range(numEntries): + val = struct.unpack(">I",bst[groupOffset+8+(i*4):groupOffset+12+(i*4)])[0] + entries.append({"Type":val>>24,"Offset":val&0xFFFFFF}) + for entry in entries: + + # print(f"{entry['Type']} {entry['Offset']}") + match entry["Type"] & 0xf0: + case 0x50: #SFX + priority,unk,pad,swBit,volume = struct.unpack(">BBHIf",bst[entry["Offset"]:entry["Offset"]+12]) + # print(f"{priority:01x} {unk:01x} {pad:02x} {swBit:04x} {volume}") + case 0x60: #Arc BGM + priority,unk,arcIndex = struct.unpack(">BBH",bst[entry["Offset"]:entry["Offset"]+4]) + # print(f"{priority:01x} {unk:01x} Arc Index: {arcIndex}") + case 0x70: #Streams + priority,unk,resourceID,stringOffset = struct.unpack(">BBHI",bst[entry["Offset"]:entry["Offset"]+8]) + # print(f"{priority:01x} {unk:01x} {resourceID:02x} {stringOffset:04x} ") + streamPath = self.getString(bst,stringOffset) + # print(f"{streamPath}") + + + def readBSTN(self,offset,size): + # print(param_1) + bstn = self.file[offset:offset+size] + numSections = struct.unpack(">I",bstn[32:36])[0] + # print(numSections) + sectionOffsets = [] + for i in range(numSections): + sectionOffsets.append(struct.unpack(">I",bstn[36+(i*4):40+(i*4)])[0]) + # print(sectionOffsets) + sections = [] + for offset in sectionOffsets: + numGroups,sectionNameOffset = struct.unpack(">II",bstn[offset:offset+8]) + sectionName = self.getString(bstn,sectionNameOffset) + # print(f"Section {sectionName} has {numGroups} groups") + section = {"Section Name":sectionName.decode('ascii'), + "Groups":[]} + groupOffsets = [] + for i in range(numGroups): + groupOffsets.append(struct.unpack(">I",bstn[offset+8+(i*4):offset+12+(i*4)])[0]) + groups = [] + for groupOffset in groupOffsets: + groupSize,groupNameOffset = struct.unpack(">II",bstn[groupOffset:groupOffset+8]) + groupName = self.getString(bstn,groupNameOffset) + group = {"Group Name":groupName.decode('ascii'), + "Names": []} + # print(f"Group {groupName} has {groupSize} entries") + nameOffsets = [] + for i in range(groupSize): + nameOffsets.append(struct.unpack(">I",bstn[groupOffset+8+(i*4):groupOffset+12+(i*4)])[0]) + names = [] + for nameOffset in nameOffsets: + names.append(self.getString(bstn,nameOffset).decode("ascii")) + # print(names) + group["Names"] = names + groups.append(group) + section["Groups"] = groups + sections.append(section) + self.nameTable["Sections"] = sections + + + def readBMS(self,param_1,param_2,param_3): + pass + + def readBMS_fromArchive(self): + pass + + def newVoiceBank(self,param_1,param_2): + pass + + def newDynamicSeqBlock(self,param_1): + pass + + def readBSFT(self,param_1): + pass + + def readMaxSeCategory(self,param_1,param_2,param_3): + pass + + def parse(self): + if self.readU32_string() != b'AA_<': + return False + + while self.readCommand_() == True: + pass + + return True + + def readCommand_(self): + command = self.readU32_string() + print(command) + match command: + case b'>_AA': + return False + case b'ws ': + self.readWS(self.readU32_(),self.readU32_(),self.readU32_()) + case b'bnk ': + self.readBNK(self.readU32_(),self.readU32_()) + case b'bl_<': + self.beginBNKList(self.readU32_(),self.readU32_()) + case b'>_bl': + self.endBNKList() + case b'bsc ': + start = self.readU32_() + end = self.readU32_() + self.readBSC(start,end-start) + case b'bst ': + start = self.readU32_() + end = self.readU32_() + self.readBST(start,end-start) + case b'bstn': + start = self.readU32_() + end = self.readU32_() + self.readBSTN(start,end-start) + case b'bms ': + param_1 = self.readU32_() + start = self.readU32_() + end = self.readU32_() + self.readBMS(param_1,start,end-start) + case b'bmsa': + self.readBMS_fromArchive(self.readU32_()) + case b'vbnk': + self.newVoiceBank(self.readU32_(),self.readU32_()) + case b'dsqb': + self.newDynamicSeqBlock(self.readU32_()) + case b'bsft': + self.readBSFT(self.readU32_()) + case b'sect': + self.readU8_() + self.readMaxSeCategory(self.readU8_(),self.readU8_(),self.readU8_()) + return True + + + +def main(): + baa = JAUAudioArcInterpreter(open("Z2Sound.baa","rb").read()) + baa.parse() + +if __name__ == "__main__": + main() diff --git a/tools/libjaudio/create_id_enums.py b/tools/libjaudio/create_id_enums.py new file mode 100644 index 0000000000..01e4376fa4 --- /dev/null +++ b/tools/libjaudio/create_id_enums.py @@ -0,0 +1,33 @@ +from baa import JAUAudioArcInterpreter +from sys import argv +import re + +def main(): + if len(argv) != 3: + print(f"{argv[0]} Usage: python3 {argv[0]} input.baa output.h") + baa = JAUAudioArcInterpreter(open(argv[1],"rb").read()) + if not baa.parse(): + return print("BAA Failed to parse!") + + outfile = open(argv[2],"w") + + header_guard_name = argv[2].upper() + header_guard_name = re.sub(r'[^A-Z0-9]', '_', header_guard_name) + + outfile.write(f"#ifndef {header_guard_name}\n#define {header_guard_name}\n\n") + + for sectionNum,section in enumerate(baa.nameTable["Sections"]): + for groupNum,group in enumerate(section["Groups"]): + groupname = group["Group Name"] + outfile.write(f"enum {groupname} {{\n") + for nameId,name in enumerate(group["Names"]): + if name == '': + continue + comma = ',' if nameId != len(group["Names"])-1 else '' + outfile.write(f" {name} = 0x{sectionNum:02x}{groupNum:02x}{nameId:04x}{comma}\n") + outfile.write("};\n\n") + + outfile.write(f"#endif /* {header_guard_name} */\n") + +if __name__ == "__main__": + main() diff --git a/tools/libast/libast.py b/tools/libjaudio/libast.py similarity index 100% rename from tools/libast/libast.py rename to tools/libjaudio/libast.py diff --git a/tools/package_game_assets.py b/tools/package_game_assets.py index 58fb190232..daa7b753a5 100644 --- a/tools/package_game_assets.py +++ b/tools/package_game_assets.py @@ -7,6 +7,8 @@ import libyaz0 import libarc import libstage from datetime import datetime +import libbti +import assets_config def getMaxDateFromDir(path): @@ -35,6 +37,16 @@ convertDefinitions = [ "sourceExtension": "dzr.json", "destExtension": ".dzr", "convertFunction": libstage.package_from_json, + }, + { + "sourceExtension": "png", + "destExtension": ".bti", + "convertFunction": libbti.png_to_bti + }, + { + "sourceExtension": "bti.json", + "destExtension": None, + "convertFunction": None } ] @@ -54,6 +66,8 @@ def convertEntry(file, path, destPath, returnData): extractDef = None for extractData in convertDefinitions: if sourceExtension == extractData["sourceExtension"]: + if extractData["destExtension"] == None and extractData["convertFunction"] == None: + return extractDef = extractData if "exceptions" in extractData: for exception in extractData["exceptions"]: @@ -334,7 +348,7 @@ def copyMapFiles(buildPath): for map in (buildPath/"dolzel2/rel/").rglob("*.map"): open(buildPath/"dolzel2/game/files/map/Final/Release/"/map.name,"w").write(postprocessMapFile(open(map,"r").read())) -def main(gamePath, buildPath, copyCode, yaz0Encoding): +def main(gamePath, buildPath, copyCode, yaz0Encoding, config_file): if yaz0Encoding == "oead": try: from oead import yaz0 @@ -354,7 +368,10 @@ def main(gamePath, buildPath, copyCode, yaz0Encoding): if not (gamePath / "files").exists() or not (gamePath / "sys").exists(): print("ISO is not extracted; extracting...") - extract_game_assets.extract(iso.absolute(),gamePath.absolute(),yaz0Encoding) + extract_game_assets.extract(iso.absolute(),gamePath.absolute(),yaz0Encoding,config_file) + + global config + config = assets_config.getConfig(config_file, update = True) print("Copying game files...") if os.path.exists(buildPath / "dolzel2") == False: @@ -376,13 +393,15 @@ def main(gamePath, buildPath, copyCode, yaz0Encoding): copyRelFiles(gamePath, buildPath, aMemRels.splitlines(), mMemRels.splitlines()) - shutil.copy(buildPath/"dolzel2/frameworkF.str",buildPath/"dolzel2/game/files/str/Final/Release/frameworkF.str") - copyMapFiles(buildPath) + if config["package_maps"]: + shutil.copy(buildPath/"dolzel2/frameworkF.str",buildPath/"dolzel2/game/files/str/Final/Release/frameworkF.str") + copyMapFiles(buildPath) - now = datetime.now() - copydate = str(now.year)+"/"+str(now.month).zfill(2)+"/"+str(now.day).zfill(2)+" "+str(now.hour).zfill(2)+":"+str(now.minute).zfill(2)+"\n" - open(buildPath/"dolzel2/game/files/str/Final/Release/COPYDATE","w").write(copydate) + if config["update_copydate"]: + now = datetime.now() + copydate = str(now.year)+"/"+str(now.month).zfill(2)+"/"+str(now.day).zfill(2)+" "+str(now.hour).zfill(2)+":"+str(now.minute).zfill(2)+"\n" + open(buildPath/"dolzel2/game/files/str/Final/Release/COPYDATE","w").write(copydate) if __name__ == "__main__": - main(Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3], sys.argv[4]) + main(Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3], sys.argv[4], sys.argv[5]) diff --git a/tools/requirements.txt b/tools/requirements.txt index fc6011a52a..227d450e12 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -12,3 +12,4 @@ requests GitPython clang pyyaml +pillow diff --git a/tools/tp.py b/tools/tp.py index 1792404830..80b686bfe6 100644 --- a/tools/tp.py +++ b/tools/tp.py @@ -153,7 +153,8 @@ elif platform.system() == "Darwin": ) @click.option("--force-download/--no-force-download") @click.option("--skip-iso/--no-skip-iso", default=False) -def setup(debug: bool, game_path: Path, tools_path: Path, yaz0_encoder: str, force_download: bool, skip_iso: bool): +@click.option("--use-default-config/--no-use-default-config", default=False) +def setup(debug: bool, game_path: Path, tools_path: Path, yaz0_encoder: str, force_download: bool, skip_iso: bool, use_default_config: bool): """Setup project""" if debug: @@ -362,10 +363,29 @@ def setup(debug: bool, game_path: Path, tools_path: Path, yaz0_encoder: str, for ) ) sys.exit(1) + + configfile_name = "" + try: + import assets_config + configfile_name = assets_config.CONFIGFILE_DEFAULT + if not use_default_config: + text = Text("--- Prompting for Asset Configuration") + text.stylize("bold magenta") + CONSOLE.print(text) + assets_config.updateConfig(configfile_name) + else: + assets_config.saveConfig(assets_config.CONFIG_DEFAULTS, configfile_name) + except ImportError as ex: + _handle_import_error(ex) + except Exception as e: + LOG.error(f"failure:") + LOG.error(e) + sys.exit(1) + try: import extract_game_assets - extract_game_assets.extract(iso, game_path, yaz0_encoder) + extract_game_assets.extract(iso, game_path, yaz0_encoder, configfile_name) except ImportError as ex: _handle_import_error(ex) except Exception as e: @@ -1712,6 +1732,19 @@ def upload_progress(debug: bool, base_url: str, api_key: str, project: str, vers LOG.error(f"HTTP request failed: {err}") exit(1) +@tp.command(name="assets-config") +@click.argument('path', type=click.Path(), default="asset_config.json") +def assets_config(path: str): + """Update the config for asset extraction/packaging""" + try: + import assets_config + assets_config.updateConfig(path) + except ImportError as ex: + _handle_import_error(ex) + except Exception as e: + LOG.error(f"failure:") + LOG.error(e) + sys.exit(1) if __name__ == "__main__": tp()