match TSA order on efxskill

This commit is contained in:
MokhaLeee 2024-09-25 19:56:59 +08:00
parent 730b44e5db
commit 63e53c5cbc
30 changed files with 1023 additions and 51 deletions

5
.gitignore vendored
View File

@ -74,6 +74,11 @@ tools/agbcc
*.fk
data/banim/*.bin
*.feimg.bin
*.fetsa1.bin
*.fetsa2.bin
*.fetsa3.bin
# =========================
# Dump Scripts Output
# =========================

View File

@ -33,6 +33,7 @@ AIF2PCM := tools/aif2pcm/aif2pcm$(EXE)
MID2AGB := tools/mid2agb/mid2agb$(EXE)
TEXTENCODE := tools/textencode/textencode$(EXE)
JSONPROC := tools/jsonproc/jsonproc$(EXE)
FETSATOOL := tools/gfxtools/tsa_generator.py
ifeq ($(UNAME),Darwin)
SED := sed -i ''
@ -110,7 +111,7 @@ clean:
# Remove converted songs
$(RM) -f $(MID_SUBDIR)/*.s
$(RM) -f $(AUTO_GEN_TARGETS)
find . \( -iname '*.o' -o -iname '*.obj' -o -iname '*.1bpp' -o -iname '*.4bpp' -o -iname '*.8bpp' -o -iname '*.gbapal' -o -iname '*.lz' -o -iname '*.fk' -o -iname '*.latfont' -o -iname '*.hwjpnfont' -o -iname '*.fwjpnfont' \) -exec rm {} +
@find . \( -iname '*.o' -o -iname '*.obj' -o -iname '*.feimg.bin' -o -iname '*.fetsa1.bin' -o -iname '*.1bpp' -o -iname '*.4bpp' -o -iname '*.8bpp' -o -iname '*.gbapal' -o -iname '*.lz' -o -iname '*.fk' -o -iname '*.latfont' -o -iname '*.hwjpnfont' -o -iname '*.fwjpnfont' \) -exec rm {} +
.PHONY: clean
@ -152,6 +153,9 @@ sound/%.bin: sound/%.aif ; $(AIF2PCM) $< $@
%.4bpp.h: %.4bpp
$(BIN2C) $< $(subst .,_,$(notdir $<)) | sed 's/^const //' > $@
%.feimg.bin %.fetsa1.bin: %.png
$(FETSATOOL) $< $*.feimg.bin $*.fetsa1.bin
# Battle Animation Recipes
$(BANIM_OBJECT): $(shell ./scripts/arm_compressing_linker.py -t linker_script_banim.txt -m)

View File

@ -1,195 +1,193 @@
.section .data
.include "animscr.inc"
.include "gba_sprites.inc"
.global Img_EfxSkill1
Img_EfxSkill1: @ 0x085C935C
.incbin "baserom.gba", 0x5C935C, 0x5C9740 - 0x5C935C
.incbin "graphics/efxskill/efxskill_1.feimg.bin.lz"
.global Img_EfxSkill2
Img_EfxSkill2:
.incbin "baserom.gba", 0x5C9740, 0x5C9B38 - 0x5C9740
.incbin "graphics/efxskill/efxskill_2.feimg.bin.lz"
.global Img_EfxSkill3
Img_EfxSkill3:
.incbin "baserom.gba", 0x5C9B38, 0x5C9F48 - 0x5C9B38
.incbin "graphics/efxskill/efxskill_3.feimg.bin.lz"
.global Img_EfxSkill4
Img_EfxSkill4:
.incbin "baserom.gba", 0x5C9F48, 0x5CA380 - 0x5C9F48
.incbin "graphics/efxskill/efxskill_4.feimg.bin.lz"
.global Img_EfxSkill5
Img_EfxSkill5:
.incbin "baserom.gba", 0x5CA380, 0x5CA7FC - 0x5CA380
.incbin "graphics/efxskill/efxskill_5.feimg.bin.lz"
.global Img_EfxSkill6
Img_EfxSkill6:
.incbin "baserom.gba", 0x5CA7FC, 0x5CACF4 - 0x5CA7FC
.incbin "graphics/efxskill/efxskill_6.feimg.bin.lz"
.global Img_EfxSkill7
Img_EfxSkill7:
.incbin "baserom.gba", 0x5CACF4, 0x5CB2CC - 0x5CACF4
.incbin "graphics/efxskill/efxskill_7.feimg.bin.lz"
.global Img_EfxSkill8
Img_EfxSkill8:
.incbin "baserom.gba", 0x5CB2CC, 0x5CB9AC - 0x5CB2CC
.incbin "graphics/efxskill/efxskill_8.feimg.bin.lz"
.global Img_EfxSkill9
Img_EfxSkill9:
.incbin "baserom.gba", 0x5CB9AC, 0x5CC0E8 - 0x5CB9AC
.incbin "graphics/efxskill/efxskill_9.feimg.bin.lz"
.global Img_EfxSkillA
Img_EfxSkillA:
.incbin "baserom.gba", 0x5CC0E8, 0x5CC820 - 0x5CC0E8
.incbin "graphics/efxskill/efxskill_10.feimg.bin.lz"
.global Img_EfxSkillB
Img_EfxSkillB:
.incbin "baserom.gba", 0x5CC820, 0x5CCF14 - 0x5CC820
.incbin "graphics/efxskill/efxskill_11.feimg.bin.lz"
.global Img_EfxSkillC
Img_EfxSkillC:
.incbin "baserom.gba", 0x5CCF14, 0x5CD5A0 - 0x5CCF14
.incbin "graphics/efxskill/efxskill_12.feimg.bin.lz"
.global Img_EfxSkillD
Img_EfxSkillD:
.incbin "baserom.gba", 0x5CD5A0, 0x5CDC00 - 0x5CD5A0
.incbin "graphics/efxskill/efxskill_13.feimg.bin.lz"
.global Img_EfxSkillE
Img_EfxSkillE:
.incbin "baserom.gba", 0x5CDC00, 0x5CE200 - 0x5CDC00
.incbin "graphics/efxskill/efxskill_14.feimg.bin.lz"
.global Img_EfxSkillF
Img_EfxSkillF:
.incbin "baserom.gba", 0x5CE200, 0x5CE7C4 - 0x5CE200
.incbin "graphics/efxskill/efxskill_15.feimg.bin.lz"
.global Img_EfxSkill10
Img_EfxSkill10:
.incbin "baserom.gba", 0x5CE7C4, 0x5CEC6C - 0x5CE7C4
.incbin "graphics/efxskill/efxskill_16.feimg.bin.lz"
.global Pal_EfxSkill1
Pal_EfxSkill1:
.incbin "baserom.gba", 0x5CEC6C, 0x5CEC8C - 0x5CEC6C
.incbin "graphics/efxskill/efxskill_1.gbapal"
.global Pal_EfxSkill2
Pal_EfxSkill2:
.incbin "baserom.gba", 0x5CEC8C, 0x5CECAC - 0x5CEC8C
.incbin "graphics/efxskill/efxskill_2.gbapal"
.global Pal_EfxSkill3
Pal_EfxSkill3:
.incbin "baserom.gba", 0x5CECAC, 0x5CECCC - 0x5CECAC
.incbin "graphics/efxskill/efxskill_3.gbapal"
.global Pal_EfxSkill4
Pal_EfxSkill4:
.incbin "baserom.gba", 0x5CECCC, 0x5CECEC - 0x5CECCC
.incbin "graphics/efxskill/efxskill_4.gbapal"
.global Pal_EfxSkill5
Pal_EfxSkill5:
.incbin "baserom.gba", 0x5CECEC, 0x5CED0C - 0x5CECEC
.incbin "graphics/efxskill/efxskill_5.gbapal"
.global Pal_EfxSkill6
Pal_EfxSkill6:
.incbin "baserom.gba", 0x5CED0C, 0x5CED2C - 0x5CED0C
.incbin "graphics/efxskill/efxskill_6.gbapal"
.global Pal_EfxSkill7
Pal_EfxSkill7:
.incbin "baserom.gba", 0x5CED2C, 0x5CED4C - 0x5CED2C
.incbin "graphics/efxskill/efxskill_7.gbapal"
.global Pal_EfxSkill8
Pal_EfxSkill8:
.incbin "baserom.gba", 0x5CED4C, 0x5CED6C - 0x5CED4C
.incbin "graphics/efxskill/efxskill_8.gbapal"
.global Pal_EfxSkill9
Pal_EfxSkill9:
.incbin "baserom.gba", 0x5CED6C, 0x5CED8C - 0x5CED6C
.incbin "graphics/efxskill/efxskill_9.gbapal"
.global Pal_EfxSkillA
Pal_EfxSkillA:
.incbin "baserom.gba", 0x5CED8C, 0x5CEDAC - 0x5CED8C
.incbin "graphics/efxskill/efxskill_10.gbapal"
.global Pal_EfxSkillB
Pal_EfxSkillB:
.incbin "baserom.gba", 0x5CEDAC, 0x5CEDCC - 0x5CEDAC
.incbin "graphics/efxskill/efxskill_11.gbapal"
.global Pal_EfxSkillC
Pal_EfxSkillC:
.incbin "baserom.gba", 0x5CEDCC, 0x5CEDEC - 0x5CEDCC
.incbin "graphics/efxskill/efxskill_12.gbapal"
.global Pal_EfxSkillD
Pal_EfxSkillD:
.incbin "baserom.gba", 0x5CEDEC, 0x5CEE0C - 0x5CEDEC
.incbin "graphics/efxskill/efxskill_13.gbapal"
.global Pal_EfxSkillE
Pal_EfxSkillE:
.incbin "baserom.gba", 0x5CEE0C, 0x5CEE2C - 0x5CEE0C
.incbin "graphics/efxskill/efxskill_14.gbapal"
.global Pal_EfxSkillF
Pal_EfxSkillF:
.incbin "baserom.gba", 0x5CEE2C, 0x5CEE4C - 0x5CEE2C
.incbin "graphics/efxskill/efxskill_15.gbapal"
.global Pal_EfxSkill10
Pal_EfxSkill10:
.incbin "baserom.gba", 0x5CEE4C, 0x5CEE6C - 0x5CEE4C
.incbin "graphics/efxskill/efxskill_16.gbapal"
.global Tsa_EfxSkill1
Tsa_EfxSkill1:
.incbin "baserom.gba", 0x5CEE6C, 0x5CEF04 - 0x5CEE6C
.incbin "graphics/efxskill/efxskill_1.fetsa1.bin.lz"
.global Tsa_EfxSkill2
Tsa_EfxSkill2:
.incbin "baserom.gba", 0x5CEF04, 0x5CEFA4 - 0x5CEF04
.incbin "graphics/efxskill/efxskill_2.fetsa1.bin.lz"
.global Tsa_EfxSkill3
Tsa_EfxSkill3:
.incbin "baserom.gba", 0x5CEFA4, 0x5CF044 - 0x5CEFA4
.incbin "graphics/efxskill/efxskill_3.fetsa1.bin.lz"
.global Tsa_EfxSkill4
Tsa_EfxSkill4:
.incbin "baserom.gba", 0x5CF044, 0x5CF0E8 - 0x5CF044
.incbin "graphics/efxskill/efxskill_4.fetsa1.bin.lz"
.global Tsa_EfxSkill5
Tsa_EfxSkill5:
.incbin "baserom.gba", 0x5CF0E8, 0x5CF1A0 - 0x5CF0E8
.incbin "graphics/efxskill/efxskill_5.fetsa1.bin.lz"
.global Tsa_EfxSkill6
Tsa_EfxSkill6:
.incbin "baserom.gba", 0x5CF1A0, 0x5CF264 - 0x5CF1A0
.incbin "graphics/efxskill/efxskill_6.fetsa1.bin.lz"
.global Tsa_EfxSkill7
Tsa_EfxSkill7:
.incbin "baserom.gba", 0x5CF264, 0x5CF33C - 0x5CF264
.incbin "graphics/efxskill/efxskill_7.fetsa1.bin.lz"
.global Tsa_EfxSkill8
Tsa_EfxSkill8:
.incbin "baserom.gba", 0x5CF33C, 0x5CF440 - 0x5CF33C
.incbin "graphics/efxskill/efxskill_8.fetsa1.bin.lz"
.global Tsa_EfxSkill9
Tsa_EfxSkill9:
.incbin "baserom.gba", 0x5CF440, 0x5CF544 - 0x5CF440
.incbin "graphics/efxskill/efxskill_9.fetsa1.bin.lz"
.global Tsa_EfxSkillA
Tsa_EfxSkillA:
.incbin "baserom.gba", 0x5CF544, 0x5CF648 - 0x5CF544
.incbin "graphics/efxskill/efxskill_10.fetsa1.bin.lz"
.global Tsa_EfxSkillB
Tsa_EfxSkillB:
.incbin "baserom.gba", 0x5CF648, 0x5CF750 - 0x5CF648
.incbin "graphics/efxskill/efxskill_11.fetsa1.bin.lz"
.global Tsa_EfxSkillC
Tsa_EfxSkillC:
.incbin "baserom.gba", 0x5CF750, 0x5CF83C - 0x5CF750
.incbin "graphics/efxskill/efxskill_12.fetsa1.bin.lz"
.global Tsa_EfxSkillD
Tsa_EfxSkillD:
.incbin "baserom.gba", 0x5CF83C, 0x5CF91C - 0x5CF83C
.incbin "graphics/efxskill/efxskill_13.fetsa1.bin.lz"
.global Tsa_EfxSkillE
Tsa_EfxSkillE:
.incbin "baserom.gba", 0x5CF91C, 0x5CF9F4 - 0x5CF91C
.incbin "graphics/efxskill/efxskill_14.fetsa1.bin.lz"
.global Tsa_EfxSkillF
Tsa_EfxSkillF:
.incbin "baserom.gba", 0x5CF9F4, 0x5CFAC0 - 0x5CF9F4
.incbin "graphics/efxskill/efxskill_15.fetsa1.bin.lz"
.global Tsa_EfxSkill10
Tsa_EfxSkill10:
.incbin "baserom.gba", 0x5CFAC0, 0x5CFB70 - 0x5CFAC0
.incbin "graphics/efxskill/efxskill_16.fetsa1.bin.lz"

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

109
scripts/dump_img.py Executable file
View File

@ -0,0 +1,109 @@
#!/bin/python3
import sys, struct, os
import array
from PIL import Image
import lzss_lib
def read_palette_from_binary(pal_bytes):
palette = []
palette_len = len(pal_bytes)
for i in range(0, palette_len, 2):
color = struct.unpack('<H', pal_bytes[i:i+2])[0]
r = (color & 0x1F) << 3
g = ((color >> 5) & 0x1F) << 3
b = ((color >> 10) & 0x1F) << 3
palette.append(r)
palette.append(g)
palette.append(b)
return palette
def create_image_from_4bpp(img_data, tsa_data, pal_bytes, ntiles_x, ntiles_y):
width = ntiles_x * 8
height = ntiles_y * 8
img = Image.new('P', (width, height))
pal_data = read_palette_from_binary(pal_bytes)
tiles_8x8 = []
pixels = [0] * (width * height)
# step1: generate tiles
for tile_idx in range(len(img_data) // 0x20):
_tile = []
for y in range(8):
for x in range(0, 8, 2):
offset = tile_idx * (8 * 8 // 2) + y * (8 // 2) + (x // 2)
byte = img_data[offset]
pixel1 = byte & 0x0F
pixel2 = (byte >> 4) & 0x0F
_tile.append(pixel1)
_tile.append(pixel2)
tiles_8x8.append(_tile)
# apply TSA
for tile_idx in range(ntiles_x * ntiles_y):
base_y = tile_idx // ntiles_x
base_x = tile_idx % ntiles_x
tsa_idx = tsa_data[tile_idx]
tile = tiles_8x8[tsa_idx]
for y in range(8):
for x in range(0, 8):
offset = y * (8 // 2) + (x // 2)
real_x = x + base_x * 8
real_y = y + base_y * 8
pixels[real_x + 0 + real_y * width] = tile[y * 8 + x]
img.putpalette(pal_data)
img.putdata(pixels)
return img
def dump_img(prefix, img_addr, tsa_addr, pal_addr, ntiles_x, ntiles_y):
img_addr &= 0x00FFFFFF
tsa_addr &= 0x00FFFFFF
pal_addr &= 0x00FFFFFF
pal_bytes = lzss_lib.copy_direct(pal_addr, 0x20)
img_bytes = lzss_lib.lz77_decomp_data(img_addr)
tsa_bytes = lzss_lib.lz77_decomp_data(tsa_addr)
img_data = array.array('B', img_bytes)
tsa_data = []
for i in range(0, len(tsa_bytes), 2):
tsa = struct.unpack('<H', tsa_bytes[i:i+2])[0]
tsa_data.append(tsa)
img = create_image_from_4bpp(img_data, tsa_data, pal_bytes, ntiles_x, ntiles_y)
fpath = f"{prefix}.png"
img.save(fpath)
def main(args):
try:
prefix = args[1]
img_addr = eval(args[2]) & 0x00FFFFFF
tsa_addr = eval(args[3]) & 0x00FFFFFF
pal_addr = eval(args[4]) & 0x00FFFFFF
ntiles_x = eval(args[5])
ntiles_y = eval(args[6])
except IndexError:
sys.exit(f"Usage: {args[0]} [prefix] [img_addr] [tsa_addr] [pal_addr] [ntiles_x] [ntiles_y]")
dump_img(prefix, img_addr, tsa_addr, pal_addr, ntiles_x, ntiles_y)
if __name__ == '__main__':
main(sys.argv)

20
scripts/dump_img_efxskill.py Executable file
View File

@ -0,0 +1,20 @@
#!/bin/python3
import sys
from dump_img import dump_img
rom = "baserom.gba"
ImgLut_EfxSkill = 0x5d9370
TsaLut_EfxSkill = 0x5d9330
PalLut_EfxSkill = 0x5d93b0
with open(rom, 'rb') as f:
rom_data = f.read()
for i in range(11, 16):
img_addr = int.from_bytes(rom_data[ImgLut_EfxSkill + 4 * i:ImgLut_EfxSkill + 4 * i + 4], 'little')
tsa_addr = int.from_bytes(rom_data[TsaLut_EfxSkill + 4 * i:TsaLut_EfxSkill + 4 * i + 4], 'little')
pal_addr = int.from_bytes(rom_data[PalLut_EfxSkill + 4 * i:PalLut_EfxSkill + 4 * i + 4], 'little')
print(f"IMG 0x{img_addr:08X}, TSA 0x{tsa_addr:08X}, PAL 0x{pal_addr:08X}")
dump_img(f"graphics/efxskill/efxskill_{i + 1}", img_addr, tsa_addr, pal_addr, 30, 20)

255
scripts/lzss_lib.py Normal file
View File

@ -0,0 +1,255 @@
#!/bin/python3
from sys import stderr
from collections import defaultdict
from operator import itemgetter
from struct import pack, unpack
ROM="baserom.gba"
def lz77_decompress(src):
if src[0] != 0x10:
raise ValueError("Not a valid GBA LZ77 compressed stream.")
dest_size = (src[1] | (src[2] << 8) | (src[3] << 16))
dest = [0] * dest_size
src_pos = 4
dest_pos = 0
while True:
if src_pos >= len(src):
raise ValueError("overflow")
flags = src[src_pos]
src_pos += 1
for i in range(8):
if flags & 0x80: # compressed blocks
if src_pos + 1 >= len(src):
raise ValueError("overflow in flags")
block_size = (src[src_pos] >> 4) + 3
block_distance = (((src[src_pos] & 0xF) << 8) | src[src_pos + 1]) + 1
src_pos += 2
block_pos = dest_pos - block_distance
if block_pos < 0:
raise ValueError("invalid distance")
if dest_pos + block_size > dest_size:
block_size = dest_size - dest_pos
print("Destination buffer overflow. Truncating block size.")
for j in range(block_size):
dest[dest_pos] = dest[block_pos + j]
dest_pos += 1
else: # uncompressed blocks
if src_pos >= len(src) or dest_pos >= dest_size:
raise ValueError("overflow in uncompressed blocks")
dest[dest_pos] = src[src_pos]
src_pos += 1
dest_pos += 1
if dest_pos == dest_size:
return bytes(dest)
flags <<= 1
def lz77_decomp_data(offset):
offset = offset & 0x00FFFFFF
with open(ROM, "rb") as f:
f.seek(offset)
return lz77_decompress(f.read())
def copy_direct(offset, len):
offset = offset & 0x00FFFFFF
with open(ROM, "rb") as f:
f.seek(offset)
return f.read(len)
class SlidingWindow:
# The size of the sliding window
size = 4096
# The minimum displacement.
disp_min = 2
# The hard minimum — a disp less than this can't be represented in the
# compressed stream.
disp_start = 1
# The minimum length for a successful match in the window
match_min = 1
# The maximum length of a successful match, inclusive.
match_max = None
def __init__(self, buf):
self.data = buf
self.hash = defaultdict(list)
self.full = False
self.start = 0
self.stop = 0
#self.index = self.disp_min - 1
self.index = 0
assert self.match_max is not None
def next(self):
if self.index < self.disp_start - 1:
self.index += 1
return
if self.full:
olditem = self.data[self.start]
assert self.hash[olditem][0] == self.start
self.hash[olditem].pop(0)
item = self.data[self.stop]
self.hash[item].append(self.stop)
self.stop += 1
self.index += 1
if self.full:
self.start += 1
else:
if self.size <= self.stop:
self.full = True
def advance(self, n=1):
for _ in range(n):
self.next()
def search(self):
match_max = self.match_max
match_min = self.match_min
counts = []
indices = self.hash[self.data[self.index]]
for i in indices:
matchlen = self.match(i, self.index)
if matchlen >= match_min:
disp = self.index - i
#assert self.index - disp >= 0
#assert self.disp_min <= disp < self.size + self.disp_min
if self.disp_min <= disp:
counts.append((matchlen, -disp))
if matchlen >= match_max:
#assert matchlen == match_max
return counts[-1]
if counts:
match = max(counts, key=itemgetter(0))
return match
return None
def match(self, start, bufstart):
size = self.index - start
if size == 0:
return 0
matchlen = 0
it = range(min(len(self.data) - bufstart, self.match_max))
for i in it:
if self.data[start + (i % size)] == self.data[bufstart + i]:
matchlen += 1
else:
break
return matchlen
class NLZ10Window(SlidingWindow):
size = 4096
match_min = 3
match_max = 3 + 0xf
class NLZ11Window(SlidingWindow):
size = 4096
match_min = 3
match_max = 0x111 + 0xFFFF
class NOverlayWindow(NLZ10Window):
disp_min = 3
def _compress(input, windowclass=NLZ10Window):
window = windowclass(input)
i = 0
while True:
if len(input) <= i:
break
match = window.search()
if match:
yield match
#if match[1] == -283:
# raise Exception(match, i)
window.advance(match[0])
i += match[0]
else:
yield input[i]
window.next()
i += 1
def packflags(flags):
n = 0
for i in range(8):
n <<= 1
try:
if flags[i]:
n |= 1
except IndexError:
pass
return n
def chunkit(it, n):
buf = []
for x in it:
buf.append(x)
if n <= len(buf):
yield buf
buf = []
if buf:
yield buf
def lz77_compress(input):
# header
out = b''
out += (pack("<L", (len(input) << 8) + 0x10))
# body
length = 0
for tokens in chunkit(_compress(input), 8):
flags = [type(t) == tuple for t in tokens]
out += (pack(">B", packflags(flags)))
for t in tokens:
if type(t) == tuple:
count, disp = t
count -= 3
disp = (-disp) - 1
assert 0 <= disp < 4096
sh = (count << 12) | disp
out += (pack(">H", sh))
else:
out += (pack(">B", t))
length += 1
length += sum(2 if f else 1 for f in flags)
# padding
# padding = 4 - (length % 4 or 4)
# if padding:
# out += (b'\x00' * padding)
return out

31
test.py Normal file
View File

@ -0,0 +1,31 @@
from PIL import Image
import sys, re
def reduce_palette(image_path, output_path):
# 打开原始图像
image = Image.open(image_path)
# 确保图像是调色板模式
if image.mode != 'P':
raise ValueError("Image must be in 'P' mode (palette mode)")
# 获取原始调色板
palette = image.getpalette()
# 只保留前 16 个颜色
new_palette = palette[:16 * 3] # 每个颜色有三个值R, G, B
# 创建新的图像
new_image = Image.new('P', image.size)
# 将新调色板应用到新图像
new_image.putpalette(new_palette)
# 重新调色,确保使用新调色板
new_image.putdata(image.getdata())
# 保存新的图像
new_image.save(output_path)
# 示例使用
reduce_palette(f'{sys.argv[1]}.png', f'{sys.argv[1]}.png')

22
tools/gfxtools/lzss_compress.py Executable file
View File

@ -0,0 +1,22 @@
#!/bin/python3
import sys
import lzss_lib
def main(args):
try:
in_fpath = args[1]
out_fpath = args[2]
except IndexError:
sys.exit(f"Usage: {args[0]} <input> <output>")
with open(in_fpath, 'rb') as f:
bin_data = f.read()
with open(out_fpath, "wb") as f:
compressed_data = lzss_lib.lz77_compress(bin_data)
f.write(compressed_data)
if __name__ == '__main__':
main(sys.argv)

View File

@ -0,0 +1,18 @@
#!/bin/python3
import sys
import lzss_lib
def main(args):
try:
offset = eval(args[1])
out_fpath = args[2]
except IndexError:
sys.exit(f"Usage: {args[0]} <offset> <output>")
with open(out_fpath, "wb") as f:
f.write(lzss_lib.lz77_decomp_data(offset))
if __name__ == '__main__':
main(sys.argv)

255
tools/gfxtools/lzss_lib.py Normal file
View File

@ -0,0 +1,255 @@
#!/bin/python3
from sys import stderr
from collections import defaultdict
from operator import itemgetter
from struct import pack, unpack
import rom_def
def lz77_decompress(src):
if src[0] != 0x10:
raise ValueError("Not a valid GBA LZ77 compressed stream.")
dest_size = (src[1] | (src[2] << 8) | (src[3] << 16))
dest = [0] * dest_size
src_pos = 4
dest_pos = 0
while True:
if src_pos >= len(src):
raise ValueError("overflow")
flags = src[src_pos]
src_pos += 1
for i in range(8):
if flags & 0x80: # compressed blocks
if src_pos + 1 >= len(src):
raise ValueError("overflow in flags")
block_size = (src[src_pos] >> 4) + 3
block_distance = (((src[src_pos] & 0xF) << 8) | src[src_pos + 1]) + 1
src_pos += 2
block_pos = dest_pos - block_distance
if block_pos < 0:
raise ValueError("invalid distance")
if dest_pos + block_size > dest_size:
block_size = dest_size - dest_pos
print("Destination buffer overflow. Truncating block size.")
for j in range(block_size):
dest[dest_pos] = dest[block_pos + j]
dest_pos += 1
else: # uncompressed blocks
if src_pos >= len(src) or dest_pos >= dest_size:
raise ValueError("overflow in uncompressed blocks")
dest[dest_pos] = src[src_pos]
src_pos += 1
dest_pos += 1
if dest_pos == dest_size:
return bytes(dest)
flags <<= 1
def copy_direct(offset, len):
offset = offset & 0x00FFFFFF
with open(rom_def.ROM, "rb") as f:
f.seek(offset)
return f.read(len)
def lz77_decomp_data(offset):
offset = offset & 0x00FFFFFF
with open(rom_def.ROM, "rb") as f:
f.seek(offset)
return lz77_decompress(f.read())
class SlidingWindow:
# The size of the sliding window
size = 4096
# The minimum displacement.
disp_min = 2
# The hard minimum — a disp less than this can't be represented in the
# compressed stream.
disp_start = 1
# The minimum length for a successful match in the window
match_min = 1
# The maximum length of a successful match, inclusive.
match_max = None
def __init__(self, buf):
self.data = buf
self.hash = defaultdict(list)
self.full = False
self.start = 0
self.stop = 0
#self.index = self.disp_min - 1
self.index = 0
assert self.match_max is not None
def next(self):
if self.index < self.disp_start - 1:
self.index += 1
return
if self.full:
olditem = self.data[self.start]
assert self.hash[olditem][0] == self.start
self.hash[olditem].pop(0)
item = self.data[self.stop]
self.hash[item].append(self.stop)
self.stop += 1
self.index += 1
if self.full:
self.start += 1
else:
if self.size <= self.stop:
self.full = True
def advance(self, n=1):
for _ in range(n):
self.next()
def search(self):
match_max = self.match_max
match_min = self.match_min
counts = []
indices = self.hash[self.data[self.index]]
for i in indices:
matchlen = self.match(i, self.index)
if matchlen >= match_min:
disp = self.index - i
#assert self.index - disp >= 0
#assert self.disp_min <= disp < self.size + self.disp_min
if self.disp_min <= disp:
counts.append((matchlen, -disp))
if matchlen >= match_max:
#assert matchlen == match_max
return counts[-1]
if counts:
match = max(counts, key=itemgetter(0))
return match
return None
def match(self, start, bufstart):
size = self.index - start
if size == 0:
return 0
matchlen = 0
it = range(min(len(self.data) - bufstart, self.match_max))
for i in it:
if self.data[start + (i % size)] == self.data[bufstart + i]:
matchlen += 1
else:
break
return matchlen
class NLZ10Window(SlidingWindow):
size = 4096
match_min = 3
match_max = 3 + 0xf
class NLZ11Window(SlidingWindow):
size = 4096
match_min = 3
match_max = 0x111 + 0xFFFF
class NOverlayWindow(NLZ10Window):
disp_min = 3
def _compress(input, windowclass=NLZ10Window):
window = windowclass(input)
i = 0
while True:
if len(input) <= i:
break
match = window.search()
if match:
yield match
#if match[1] == -283:
# raise Exception(match, i)
window.advance(match[0])
i += match[0]
else:
yield input[i]
window.next()
i += 1
def packflags(flags):
n = 0
for i in range(8):
n <<= 1
try:
if flags[i]:
n |= 1
except IndexError:
pass
return n
def chunkit(it, n):
buf = []
for x in it:
buf.append(x)
if n <= len(buf):
yield buf
buf = []
if buf:
yield buf
def lz77_compress(input):
# header
out = b''
out += (pack("<L", (len(input) << 8) + 0x10))
# body
length = 0
for tokens in chunkit(_compress(input), 8):
flags = [type(t) == tuple for t in tokens]
out += (pack(">B", packflags(flags)))
for t in tokens:
if type(t) == tuple:
count, disp = t
count -= 3
disp = (-disp) - 1
assert 0 <= disp < 4096
sh = (count << 12) | disp
out += (pack(">H", sh))
else:
out += (pack(">B", t))
length += 1
length += sum(2 if f else 1 for f in flags)
# padding
# padding = 4 - (length % 4 or 4)
# if padding:
# out += (b'\x00' * padding)
return out

16
tools/gfxtools/rom_def.py Normal file
View File

@ -0,0 +1,16 @@
ROM = "baserom.gba"
BANIM_MODES = {
0: "NORMAL_ATK",
1: "NORMAL_ATK_PRIORITY_L",
2: "CRIT_ATK",
3: "CRIT_ATK_PRIORITY_L",
4: "RANGED_ATK",
5: "RANGED_CRIT_ATK",
6: "CLOSE_DODGE",
7: "RANGED_DODGE",
8: "STANDING",
9: "STANDING2",
10: "RANGED_STANDING",
11: "MISSED_ATK"
}

47
tools/gfxtools/tsa_analysis.py Executable file
View File

@ -0,0 +1,47 @@
#!/bin/python3
import sys, struct
from collections import Counter
import lzss_lib
def count_and_sort_numbers(numbers):
counts = Counter(numbers)
sorted_counts = sorted(counts.items())
return sorted_counts
def main(args):
try:
offset = eval(args[1])
width = eval(args[2])
height = eval(args[3])
except IndexError:
sys.exit(f"Usage: {args[0]} <offset> <width> <height>")
decomped_data = lzss_lib.lz77_decomp_data(offset)
numbers = [(struct.unpack('<H', decomped_data[i:i+2])[0]) for i in range(0, len(decomped_data), 2)]
n_cols = width // 8
for col in range(n_cols):
x_start = 8 * col
x_end = 8 * col + 8
print(f"[col: {col}]")
for y in range(height):
for x in range(x_start, x_end):
idx = y * width + x
# print(f"[{x}, {y}] = ", end="")
tile = numbers[idx]
tile_0 = numbers[0]
if tile == tile_0:
# print(" ", end="")
print(f" {numbers[idx]:04X}", end=" ")
else:
print(f"0x{numbers[idx]:04X}", end=" ")
print("")
if __name__ == '__main__':
main(sys.argv)

View File

@ -0,0 +1,45 @@
#!/bin/python3
import sys, struct
from collections import Counter
def read_bin_file(filename):
with open(filename, 'rb') as f:
data = f.read()
numbers = [(struct.unpack('<H', data[i:i+2])[0] & 0x3FF) for i in range(0, len(data), 2)]
return numbers
def main(args):
try:
filename = args[1]
width = eval(args[2])
height = eval(args[3])
except IndexError:
sys.exit(f"Usage: {args[0]} <offset> <width> <height>")
numbers = read_bin_file(filename)
n_cols = width // 8
for col in range(n_cols):
x_start = 8 * col
x_end = 8 * col + 8
print(f"[col: {col}]")
for y in range(height):
for x in range(x_start, x_end):
idx = y * width + x
# print(f"[{x}, {y}] = ", end="")
tile = numbers[idx]
tile_0 = numbers[0]
if tile == tile_0:
# print(" ", end="")
print(f" {numbers[idx]:04X}", end=" ")
else:
print(f"0x{numbers[idx]:04X}", end=" ")
print("")
if __name__ == '__main__':
main(sys.argv)

147
tools/gfxtools/tsa_generator.py Executable file
View File

@ -0,0 +1,147 @@
#!/bin/python3
import sys, re
import numpy as np
from PIL import Image
'''
方案1:
常见于 banim 相关的图片
1. 整张图按行从左向右扫描, 将获得的独特的 tile 入栈, 从而获得 tile array
2. 上述获得的第一个 tile 丢到末尾作为最后一个 tile
3. tile array 0 tile 置空
'''
def process_tiles_method1(tiles, ntile_x, ntile_y):
unique_tiles = []
tsa_data = []
tile_dict = {}
for tile_y in range(0, ntile_y):
for tile_x in range(0, ntile_x):
tile = tiles[tile_y][tile_x]
tile_4bpp = convert_to_4bpp(tile)
tile_tuple = tuple(tile_4bpp)
tile_2d = tile.reshape((8, 8))
tile_hf = np.flip(tile_2d, axis=1).flatten()
tile_vf = np.flip(tile_2d, axis=0).flatten()
tile_se = np.flip(tile_2d).flatten()
tile_hf_4bpp = convert_to_4bpp(tile_hf)
tile_hf_tuple = tuple(tile_hf_4bpp)
tile_vf_4bpp = convert_to_4bpp(tile_vf)
tile_vf_tuple = tuple(tile_vf_4bpp)
tile_se_4bpp = convert_to_4bpp(tile_se)
tile_se_tuple = tuple(tile_se_4bpp)
if tile_tuple in tile_dict:
tsa_data.append(tile_dict[tile_tuple])
elif tile_hf_tuple in tile_dict:
tsa_data.append(tile_dict[tile_hf_tuple] | (1 << 10))
elif tile_vf_tuple in tile_dict:
tsa_data.append(tile_dict[tile_vf_tuple] | (2 << 10))
elif tile_se_tuple in tile_dict:
tsa_data.append(tile_dict[tile_se_tuple] | (3 << 10))
else:
# fine, we did not find it
unique_tiles.append(tile_4bpp)
tile_index = len(unique_tiles) - 1
tile_dict[tile_tuple] = tile_index
tsa_data.append(tile_index)
# put the first tile to the tale
last_idx = len(unique_tiles)
print(last_idx)
for i, tsa in enumerate(tsa_data):
if tsa == 0:
tsa_data[i] = last_idx
unique_tiles.append(unique_tiles[0])
unique_tiles[0] = [0] * 32
return unique_tiles, tsa_data
'''
方案2: (todo)
1. 8 tile 为一行将图片分割为多个列
2. 按照方案1的方式扫描所有列
'''
def extract_tiles(image, ntile_x, ntile_y):
tiles = [[None for _ in range(ntile_x)] for _ in range(ntile_y)]
for tile_y in range(0, ntile_y):
for tile_x in range(0, ntile_x):
x = tile_x * 8
y = tile_y * 8
tile = image.crop((x, y, x + 8, y + 8))
tile_data = np.array(tile).flatten()
tiles[tile_y][tile_x] = tile_data
return tiles
def convert_to_4bpp(tile):
result = []
for i in range(0, len(tile), 2):
byte = (tile[i] & 0xF) | ((tile[i + 1] & 0xF) << 4)
result.append(byte)
return result
def extract_suffix_from_filename(file_name):
match = re.search(r'\.(fetsa(\d+)\.bin)$', file_name)
if match:
return match.group(2)
return None
def main(args):
try:
png_file = args[1]
out_img = args[2]
out_tsa = args[3]
except IndexError:
sys.exit(f"Usage: {args[0]} [*.png] [*.feimg.bin] [*.fetsa<x>.bin]")
method = extract_suffix_from_filename(out_tsa)
if method is None:
method = 0 # default
image = Image.open(png_file)
if image.mode != 'P':
raise ValueError("IMAGE ERRIR (P mode)")
width, height = image.size
ntile_x = width // 8
ntile_y = height // 8
tiles = extract_tiles(image, ntile_x, ntile_y)
if method == 1:
unique_tiles, tsa_data = process_tiles_method1(tiles, ntile_x, ntile_y)
else:
# todo
unique_tiles, tsa_data = process_tiles_method1(tiles, ntile_x, ntile_y)
with open(out_img, 'wb') as f:
cnt = 0
for tile in unique_tiles:
cnt += 1
f.write(bytearray(tile))
if cnt > 0x100:
raise ValueError("Compressed image overflowed!")
for i in range(cnt, 0x100):
f.write(b'\x00' * 32)
with open(out_tsa, 'wb') as f:
for entry in tsa_data:
f.write(entry.to_bytes(2, byteorder='little'))
if __name__ == '__main__':
main(sys.argv)