summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjdflyer <jdflyer10@gmail.com>2024-07-18 08:04:43 -0700
committerGitHub <noreply@github.com>2024-07-18 09:04:43 -0600
commitab7d0b88c301a42679396a15d35b71343820e82d (patch)
treee884a0256dce661a40c9d3e052d4a65ad0794370
parent5e283fd567b03c3ce8d1c2fc127fca505da1388f (diff)
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
-rw-r--r--.gitignore4
-rw-r--r--Makefile14
-rw-r--r--tools/assets_config.py119
-rw-r--r--tools/extract_game_assets.py28
-rw-r--r--tools/libarc/arc.py12
-rw-r--r--tools/libast/__init__.py1
-rw-r--r--tools/libbti/__init__.py1
-rw-r--r--tools/libbti/libbti.py929
-rw-r--r--tools/libjaudio/__init__.py0
-rw-r--r--tools/libjaudio/baa.py208
-rw-r--r--tools/libjaudio/create_id_enums.py33
-rw-r--r--tools/libjaudio/libast.py (renamed from tools/libast/libast.py)0
-rw-r--r--tools/package_game_assets.py35
-rw-r--r--tools/requirements.txt1
-rw-r--r--tools/tp.py37
15 files changed, 1398 insertions, 24 deletions
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
--- /dev/null
+++ b/tools/libjaudio/__init__.py
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
index 955f0dfa69..955f0dfa69 100644
--- a/tools/libast/libast.py
+++ b/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()