diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/vwf_renderer_test.py b/tests/vwf_renderer_test.py new file mode 100644 index 0000000..f1eb9fe --- /dev/null +++ b/tests/vwf_renderer_test.py @@ -0,0 +1,32 @@ +import unittest +from pathlib import Path +from xml.etree import ElementTree + +from build import read_fixed_from_xml +from utils.smallvwf import VwfAsset + + +class VwfRendererTestCase(unittest.TestCase): + def test_render(self): + vwf = VwfAsset("./fonts/8x8vwf.png") + strings = [] + # with open("./text/fr/monsters.xml", encoding='utf-8') as datasource: + # tree = ElementTree.parse(datasource) + # root = tree.getroot() + # + # for child in root: + # text = child.text + # if text: + # strings.append(text) + + strings = [ + "Objets", "Magie", "Equiper", "Statut", "Placer", "Changer", "Options", "Sauver" + ] + + vwf.set_strings(strings) + + vwf.render() + renderd = Path("./renderd.bin") + serialized = vwf.serialize() + + renderd.write_bytes(serialized) diff --git a/utils/dump.py b/utils/dump.py index 9bede5b..683e59b 100755 --- a/utils/dump.py +++ b/utils/dump.py @@ -1,8 +1,12 @@ #!/usr/bin/env python3.4 +import struct + +import script from a816.cpu.cpu_65c816 import snes_to_rom +from a816.symbols import low_rom_bus from script import Table from script.formulas import base_relative_16bits_pointer_formula -from script.pointers import Script, write_pointers_as_xml +from script.pointers import Script, write_pointers_as_xml, Pointer def write_fixed_text_as_xml(pointers, table, length, output_file): @@ -16,9 +20,137 @@ def write_fixed_text_as_xml(pointers, table, length, output_file): fd.write('\n') fd.write('\n') +def write_nul_terminated_strings_as_xml(strings, output_file): + with open(output_file, 'wt', encoding='utf-8') as fd: + fd.writelines('\n') + fd.write('\n') + for s in strings: + fd.write('') + fd.write(s) #.strip()) + fd.write('\n') + fd.write('\n') + +def dump_en(): + # jtable = Table('../text/ff4_jap.tbl') + jtable = Table('../text/ff4_menus.tbl') + item_description = low_rom_bus.get_address(0x2407ad) + item_desc = [] + + # with open('../ff4j.smc', 'rb') as rom: + with open('../Final Fantasy IV (Japan) (Rev 1) [En by J2e v3.21].sfc', 'rb') as rom: + rom.seek(item_description.physical) + for k in range(443): + s = bytes() + while (char := rom.read(1)) != b'\x00': + print(char) + s += char + item_desc.append(jtable.to_text(s)) + + write_nul_terminated_strings_as_xml(item_desc, "../text/en/item_descriptions.xml") + ... + +def dump_jp(): + jtable = Table('../text/ff4_jap.tbl') + #jtable = Table('.text/ff4_menus.tbl') + item_description = low_rom_bus.get_address(0x0fae2a) + item_desc = [] + + with open('../ff4j.smc', 'rb') as rom: + #with open('../Final Fantasy IV (Japan) (Rev 1) [En by J2e v3.21].sfc', 'rb') as rom: + rom.seek(item_description.physical) + for k in range(47): + s = bytes() + while (char := rom.read(1)) != b'\x00': + print(char) + s += char + item_desc.append(jtable.to_text(s)) + + write_nul_terminated_strings_as_xml(item_desc, "../text/jp/item_descriptions.xml") + ... + + +def dump_handedness(): + jtable = Table('../text/ff4_jap.tbl') + addr = low_rom_bus.get_address(0x01e2d9) + with open('../ff4j.smc', 'rb') as rom: + rom.seek(addr.physical) + data = rom.read(8*4) + text = jtable.to_text(data) + ... if __name__ == '__main__': - table = Table('text/ff4_menus.tbl') + jtable = Table('../text/ff4_jap.tbl') +# addr = low_rom_bus.get_address(0xef200) +# with open('../ff4j.smc', 'rb') as rom: +# rom.seek(addr.physical) +# i = 0 +# battle_messages_pointers = [] +# while i < 0xba: +# data = rom.read(2) +# pointer = struct.unpack(' np.ndarray: shape = image.shape width = shape[1] height = shape[0] @@ -34,7 +35,7 @@ def char_as_1bbp(char): return bytes(binary_data) -def get_max_width(char): +def get_max_width(char: np.ndarray) -> int: max_width = 0 for byte in char: trimmed = np.trim_zeros(byte, 'b') @@ -43,23 +44,43 @@ def get_max_width(char): return max_width -def convert_font_to_1bpp(font_file, has_grid=True): +def convert_font_to_1bpp(font_file, has_grid=True, char_height=16): image = np.array(Image.open(font_file)) # image = ndimage.imread(font_file) - char = get_char(image, 0x00, has_grid, 8, 16) + char = get_char(image, 0x00, has_grid, 8, char_height) data = b'' char_index = 1 while len(char) > 0: data += char_as_1bbp(char) - char = get_char(image, char_index, has_grid, 8, 16) + char = get_char(image, char_index, has_grid, 8, char_height) + char_index += 1 + + len_table = {} + for i in range(char_index - 1): + len_table[i] = get_max_width(get_char(image, i, has_grid, 8, char_height)) + + return len_table, data + +def convert_font_to_2bpp(font_file, has_grid=True, char_height=16): + image = np.array(Image.open(font_file)) + + # image = ndimage.imread(font_file) + + char = get_char(image, 0x00, has_grid, 8, char_height) + + data = b'' + char_index = 1 + while len(char) > 0: + data += write_as_2bpp(char) + char = get_char(image, char_index, has_grid, 8, char_height) char_index += 1 len_table = {} for i in range(char_index - 1): - len_table[i] = get_max_width(get_char(image, i, has_grid, 8, 16)) + len_table[i] = get_max_width(get_char(image, i, has_grid, 8, char_height)) return len_table, data @@ -89,4 +110,17 @@ def remove_grid(font_file): if __name__ == '__main__': - remove_grid('/Users/emmanuel/PycharmProjects/ff4/fonts/wicked_vwf.png') \ No newline at end of file + remove_grid('/Users/emmanuel/PycharmProjects/ff4/fonts/wicked_vwf.png') + + +def write_as_2bpp(data: np.ndarray) -> bytearray: + binary_data = bytearray() + for y_value in range(0, len(data[0]), 8): + char = data[0:8, y_value:y_value + 8] + + for byte in char: + byte_value = int(''.join(byte.astype(str)).ljust(8, '0'), 2) + binary_data.append(0xFF) + binary_data.append(byte_value) + + return binary_data diff --git a/utils/scratchpad.py b/utils/scratchpad.py new file mode 100644 index 0000000..83b98d9 --- /dev/null +++ b/utils/scratchpad.py @@ -0,0 +1,22 @@ +if __name__ == "__main__": + texts = [ + "Requis", + "Garde", + "Rang" + ] + t = ' '.join(texts) + + available = sorted({c for c in t}) + print(''.join(sorted(available))) + print (len(available)) + mp_needed = [available.index(c) + 0xdd for c in "Requis"] + # mp_needed[mp_needed.index(0xdd)]= 0xff + + garde_text = [available.index(c) + 0xdd for c in "Garde "] + garde_text[garde_text.index(0xdd)]= 0xff + + passer_text = [available.index(c) + 0xdd for c in "Rang"] + + print(mp_needed) + print(garde_text) + print(passer_text) diff --git a/utils/smallvwf.py b/utils/smallvwf.py index 4896421..6b1253c 100644 --- a/utils/smallvwf.py +++ b/utils/smallvwf.py @@ -1,12 +1,15 @@ -from math import ceil import binascii import struct -from PIL import Image -from utils.font import get_char, get_max_width +from math import ceil + import numpy as np +from PIL import Image +from script import Table + +from utils.font import get_char, get_max_width, write_as_2bpp + def text_to_char(text): - ord('Z') - ord('A') data = [] for char in text: if 'A' <= char <= 'Z': @@ -44,18 +47,6 @@ def build_text_image(font_file, text_data): return buffer -def write_as_2bpp(data): - binary_data = bytearray() - for y_value in range(0, len(data[0]), 8): - char = data[0:8, y_value:y_value + 8] - - for byte in char: - byte_value = int(''.join(byte.astype(str)).ljust(8, '0'), 2) - binary_data.append(0xFF) - binary_data.append(byte_value) - - return binary_data - classes = [ 'Chevalier noir ', 'Chevalier dragon ', @@ -117,6 +108,82 @@ def write_as_2bpp(data): ] +# new file format: +# ptr: 2 len:1 tile_count: 1 +# +# ptr: data: len +# on the programming side: +# we need to know where we can write in the vram +# vram_ptr +# need to display item n: lookup the n-th pointer and the data size, +# setup DMA transfer +# know the tile_id and tile_count ? +# ring buffer ? + +class VwfAsset: + def __init__(self, font_file: str, table: Table) -> None: + self.font = np.array(Image.open(font_file)) + self.strings = [] + self.rendered_strings: dict[str, np.ndarray] = {} + self.table = table + + def set_strings(self, strings: list[str]) -> None: + self.strings = strings + + def get_char(self, char: int) -> np.ndarray: + current_char = get_char(self.font, char, True, 8, 8) + if char == 0xFF: + width = 2 + else: + width = get_max_width(current_char) + + return current_char[0:8, 0:width + 1] + + def render_string(self, string: str) -> np.ndarray | None: + buffer: np.ndarray | None = None + chars = self.table.to_bytes(string) + for char in chars: + culled_char = self.get_char(char) + + if buffer is not None: + buffer = np.concatenate((buffer, culled_char), 1) + else: + buffer = culled_char + + return buffer + + def render(self) -> None: + buffer: np.ndarray | None = None + + for string in self.strings: + string_buffer = self.render_string(string) + self.rendered_strings[string] = string_buffer + + return buffer + + def serialize(self) -> bytearray: + pointers: list[tuple[int, int, int]] = [] + data = bytearray() + data_origin = len(self.rendered_strings.keys()) * 3 + + for string, rendered_string in self.rendered_strings.items(): + serialized_string = write_as_2bpp(rendered_string) + pointers.append((len(data) + data_origin, len(serialized_string), len(serialized_string) // 16)) + data += serialized_string + + pointer_data = bytearray() + + for pointer in pointers: + pointer_data += struct.pack(">HBB", pointer[0] & 0xffff, pointer[1], pointer[2]) + + + return pointer_data + data + + + + + + def generate_8x8_vwf_asset(string_list, prefix, table_start, max_tile_length=None): k = 0 current_id = table_start @@ -129,7 +196,7 @@ def generate_8x8_vwf_asset(string_list, prefix, table_start, max_tile_length=Non for string in string_list: if max_tile_length: output.seek(k * line_length) - data = build_text_image('fonts/8x8vwf.png', string.strip()) + data = build_text_image('fonts/8x8vwf2.png', string.strip()) data_2bpp = write_as_2bpp(data) output.write(data_2bpp) length_table.write(struct.pack('