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
This commit is contained in:
jdflyer
2024-07-18 08:04:43 -07:00
committed by GitHub
parent 763d8d41d2
commit ac01026346
15 changed files with 1398 additions and 24 deletions
+4
View File
@@ -32,5 +32,9 @@ docs/doxygen/
__pycache__/
venv/
# m2ctx files
m2ctx.py
ctx.c
# Asset Config
asset_config.json
+9 -5
View File
@@ -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!
+119
View File
@@ -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()
+21 -7
View File
@@ -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__":
+11 -1
View File
@@ -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,
)
-1
View File
@@ -1 +0,0 @@
from .libast import *
+1
View File
@@ -0,0 +1 @@
from .libbti import *
+929
View File
@@ -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()
View File
+208
View File
@@ -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()
+33
View File
@@ -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()
+27 -8
View File
@@ -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])
+1
View File
@@ -12,3 +12,4 @@ requests
GitPython
clang
pyyaml
pillow
+35 -2
View File
@@ -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()