From 1288b8dd07adfcc699d40ba4edbaa30c29a7fa9d Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 18 Jun 2026 16:02:00 +0200 Subject: [PATCH 01/11] Instantiating self.ticket as Ticket() instead of None --- src/wiithon/structs/WiiPartitionHeader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wiithon/structs/WiiPartitionHeader.py b/src/wiithon/structs/WiiPartitionHeader.py index 6ef1766..c339e33 100644 --- a/src/wiithon/structs/WiiPartitionHeader.py +++ b/src/wiithon/structs/WiiPartitionHeader.py @@ -10,7 +10,7 @@ class WiiPartitionHeader: https://wiibrew.org/wiki/Wii_disc#Partition """ def __init__(self): - self.ticket: Ticket|None = None + self.ticket: Ticket = Ticket() self.tmd_size: int = 0 self.tmd_offset: int = 0 self.certificate_chain_size: int = 0 @@ -53,4 +53,4 @@ def write(self, stream: BinaryIO) -> None: stream.write(struct.pack('>I', self.certificate_chain_offset >> 2)) stream.write(struct.pack('>I', self.global_hash_table_offset >> 2)) stream.write(struct.pack('>I', self.data_offset >> 2)) - stream.write(struct.pack('>I', self.data_size >> 2)) \ No newline at end of file + stream.write(struct.pack('>I', self.data_size >> 2)) From 84d85b54295870b5e847572e418d04b3613f683b Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 18 Jun 2026 16:23:20 +0200 Subject: [PATCH 02/11] Replaced magic number with existing enum --- src/wiithon/structs/WiiPartitionEntry.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wiithon/structs/WiiPartitionEntry.py b/src/wiithon/structs/WiiPartitionEntry.py index b48aa8a..21c4a1f 100644 --- a/src/wiithon/structs/WiiPartitionEntry.py +++ b/src/wiithon/structs/WiiPartitionEntry.py @@ -2,7 +2,7 @@ from typing import BinaryIO from wiithon.helpers.Utils import read_u32_shifted, read_u32 - +from wiithon.helpers.Enums import WiiPartType class WiiPartitionEntry: """ @@ -30,11 +30,11 @@ def __repr__(self): def get_readable_part_type(self) -> str: match self.part_type: - case 0: + case WiiPartType.DATA: return "data" - case 1: + case WiiPartType.UPDATE: return "update" - case 2: + case WiiPartType.CHANNEL: return "channel" case _: raise ValueError("Invalid partition type") @@ -65,4 +65,4 @@ def read_parts(stream: BinaryIO) -> list[WiiPartitionEntry]: for _ in range(count): entries.append(WiiPartitionEntry.read(stream)) - return entries \ No newline at end of file + return entries From 90d995c69daf259f7072a15a5d7da66a092457ae Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 18 Jun 2026 23:20:57 +0200 Subject: [PATCH 03/11] Added optional None type for manual_area --- src/wiithon/file_helper/dol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wiithon/file_helper/dol.py b/src/wiithon/file_helper/dol.py index a6bf125..d97ff07 100644 --- a/src/wiithon/file_helper/dol.py +++ b/src/wiithon/file_helper/dol.py @@ -214,7 +214,7 @@ def patch_arena_lo(self, lis_vaddr: int, new_value: int) -> None: def inject_above_arena( self, sections: list[bytes], - manual_arena: int = None, + manual_arena: int | None = None, padding_before: int = 0x100, reserved_size: int | None = None ) -> tuple[int, list[int]]: From b0b6426edc99dc3394c07025a172b1a213603584 Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Sat, 20 Jun 2026 03:52:08 +0200 Subject: [PATCH 04/11] Small initial commit for BMG BMG is a binary message data file used for the text --- .gitignore | 1 + src/wiithon/file_helper/bmg.py | 118 ++++++++++ .../file_helper/bmg_sections/__init__.py | 0 .../file_helper/bmg_sections/bmg_section.py | 33 +++ src/wiithon/file_helper/bmg_sections/dat1.py | 152 +++++++++++++ src/wiithon/file_helper/bmg_sections/fli1.py | 87 +++++++ src/wiithon/file_helper/bmg_sections/flw1.py | 212 ++++++++++++++++++ src/wiithon/file_helper/bmg_sections/inf1.py | 170 ++++++++++++++ 8 files changed, 773 insertions(+) create mode 100644 src/wiithon/file_helper/bmg.py create mode 100644 src/wiithon/file_helper/bmg_sections/__init__.py create mode 100644 src/wiithon/file_helper/bmg_sections/bmg_section.py create mode 100644 src/wiithon/file_helper/bmg_sections/dat1.py create mode 100644 src/wiithon/file_helper/bmg_sections/fli1.py create mode 100644 src/wiithon/file_helper/bmg_sections/flw1.py create mode 100644 src/wiithon/file_helper/bmg_sections/inf1.py diff --git a/.gitignore b/.gitignore index 24b9cee..217a15e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iso .idea *.arc +**/__pycache__ /src_dir/ /assets diff --git a/src/wiithon/file_helper/bmg.py b/src/wiithon/file_helper/bmg.py new file mode 100644 index 0000000..1109488 --- /dev/null +++ b/src/wiithon/file_helper/bmg.py @@ -0,0 +1,118 @@ +from io import BytesIO + +import wiithon.helpers.Utils as fh +from wiithon.file_helper.bmg_sections.bmg_section import BMGSection +from wiithon.file_helper.bmg_sections.inf1 import INF1Section +from wiithon.file_helper.bmg_sections.dat1 import DAT1Section +from wiithon.file_helper.bmg_sections.flw1 import FLW1Section +from wiithon.file_helper.bmg_sections.fli1 import FLI1Section + +class BMG: + """ + BMG (Binary Message Data) file handler for parsing and exporting binary message data. + The BMG class manages the structure of BMG files which contain multiple sections + (INF1, DAT1, FLW1, FLI1) that store message information and data. + Attributes: + section_count (int): Number of sections in the BMG file. + sections (list[bmg_section]): List of parsed section objects. + flw1_section_offset (int): Offset to the FLW1 section in the file. + unknown (int): Unknown single byte value from file header. + Methods: + __init__(raw_bytes: BytesIO) -> None: + Parses a BMG file from raw bytes. Validates magic numbers and reads + all sections from the file. + add_header_to_section(section: bmg_section) -> BytesIO: + Wraps a section with its BMG header (magic and size) and applies + 32-byte alignment padding. Returns the complete section data. + export_bmg() -> BytesIO: + Reconstructs the complete BMG file from the current sections list. + Rebuilds the header and all sections with proper formatting and padding. + Returns the complete BMG file as bytes. + """ + section_count: int + sections: list[BMGSection] + + def __init__(self, raw_bytes: BytesIO): + data_magic = fh.read_string(raw_bytes, 4, 0x0) + assert data_magic == "MESG" + + file_magic = fh.read_string(raw_bytes, 4, 0x4) + assert file_magic == "bmg1" + + self.flw1_section_offset = fh.read_u32(raw_bytes, 0x8) + self.section_count = fh.read_u32(raw_bytes, 0xC) + self.unknown = fh.read_u8(raw_bytes, 0x10) + # 15 bytes of padding + + self.sections = [] + + offset = 0x20 + for section in range(self.section_count): + section_magic = fh.read_string(raw_bytes, 4, offset) + section_size = fh.read_u32(raw_bytes, offset + 0x4) - 0x8 + offset += 8 + + raw_bytes.seek(offset, 0) + section_bytes = raw_bytes.read(section_size) + section_bytes = BytesIO(section_bytes) + + match section_magic: + case "INF1": + section = INF1Section.import_section(section_bytes) + case "DAT1": + section = DAT1Section.import_section(section_bytes) + case "FLW1": + section = FLW1Section.import_section(section_bytes) + case "FLI1": + section = FLI1Section.import_section(section_bytes) + + self.sections.append(section) + offset += section_size + + def add_header_to_section(self, section: BMGSection) -> BytesIO: + data = BytesIO() + + section_bytes = section.export_section() + section_size = section_bytes.seek(0, 2) + 0x8 + + padding = 0 + if section_size % 32: + padding = 32 - section_size % 32 + section_size += padding + + fh.write_str(data, section.magic, 4, offset=0x0) + fh.write_u32(data, section_size, 0x4) + fh.write_bytes(data, section_bytes.getvalue(), 0x8) + fh.write_bytes(data, b'\x00' * padding, section_size - padding) + + return data + + def get_section(self, section_magic: str) -> list[BMGSection]: + out: list[BMGSection] = [] + + for section in self.sections: + if section.magic == section_magic: + out.append(section) + + return out + + def export_bmg(self) -> BytesIO: + data = BytesIO() + + fh.write_str(data, "MESG", 4, offset=0x0) + fh.write_str(data, "bmg1", 4, offset=0x4) + fh.write_u32(data, 0, 0x8) # Write the flw1_section_offset later + fh.write_u32(data, len(self.sections), 0xC) + fh.write_u8(data, self.unknown, 0x10) + fh.write_bytes(data, b'\x00' * 15, 0x11) + + offset = 0x20 + for section in self.sections: + if section.magic == "FLW1": + fh.write_u32(data ,offset, 0x8) + + section_bytes = self.add_header_to_section(section) + fh.write_bytes(data, section_bytes.getvalue(), offset) + offset += len(section_bytes.getvalue()) + + return data diff --git a/src/wiithon/file_helper/bmg_sections/__init__.py b/src/wiithon/file_helper/bmg_sections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wiithon/file_helper/bmg_sections/bmg_section.py b/src/wiithon/file_helper/bmg_sections/bmg_section.py new file mode 100644 index 0000000..79ae0a2 --- /dev/null +++ b/src/wiithon/file_helper/bmg_sections/bmg_section.py @@ -0,0 +1,33 @@ +from io import BytesIO + +class BMGSection: + """ + Base class for BMG file sections. + Provides the interface for importing and exporting binary section data. + Subclasses must override import_section() and export_section() methods. + Attributes: + magic (str): The magic identifier for this section type. + """ + magic: str + + def __init__(self, magic: str): + self.magic = magic + + @classmethod + def import_section(cls, raw_bytes: BytesIO) -> "BMGSection": + """ + Import a section from raw bytes. + This method must be overridden in subclasses to provide proper implementation. + Raises AttributeError: If not properly overridden in a subclass. + """ + raise AttributeError("Import section is not properly overwritten") + return cls() + + def export_section(self) -> BytesIO: + """ + Export a section from raw bytes. + This method must be overridden in subclasses to provide proper implementation. + Raises AttributeError: If not properly overridden in a subclass. + """ + raise AttributeError("Export section is not properly overwritten") + return BytesIO() diff --git a/src/wiithon/file_helper/bmg_sections/dat1.py b/src/wiithon/file_helper/bmg_sections/dat1.py new file mode 100644 index 0000000..5e49f03 --- /dev/null +++ b/src/wiithon/file_helper/bmg_sections/dat1.py @@ -0,0 +1,152 @@ +from io import BytesIO +from enum import IntEnum +from typing import NamedTuple + +import wiithon.helpers.Utils as fh +from wiithon.file_helper.bmg_sections.bmg_section import BMGSection + +DAT1_MAGIC: str = "DAT1" +TAG_IDENTIFIER = b"\x00\x1A" +NULL_BYTE = b"\x00\x00" + +class TagIdentifier(IntEnum): + """ + Identifies when a tag/action will be used when the console displays this in game. + This can range from delaying more text from appearing, playing a sound effect, coloring text, etc. + """ + delay = 0x01 + sound_effect = 0x02 + load_image = 0x03 + unknown1 = 0x04 + unknown2 = 0x05 + unknown3 = 0x06 + unknown4 = 0x07 + unknown5 = 0x08 + unknown6 = 0x09 + colour_text = 0xFF + +class Tag: + """ + Container object for one or more tags that will be used within a Message object. + """ + def __init__(self, + offset: int, + size: int, + identifier: TagIdentifier, + data = None): + + if not isinstance(identifier, int) and not isinstance(identifier, TagIdentifier): + raise Exception("Bad Input") + + self.offset: int = offset + self.size: int = size + self.identifier: TagIdentifier = TagIdentifier(identifier) + self.data = data + + @classmethod + def import_tag(cls, raw_bytes: BytesIO, offset: int) -> "Tag": + size = int.from_bytes(raw_bytes.read(1), "big") + identifier = int.from_bytes(raw_bytes.read(1), "big") + data = raw_bytes.read(size - 4) + + return cls(offset, size, identifier, data) + + def export_tag(self) -> BytesIO: + assert isinstance(self.data, bytes) + data = BytesIO() + + fh.write_bytes(data, TAG_IDENTIFIER, 0x0) + fh.write_u8(data, self.size, 0x2) + fh.write_u8(data, self.identifier, 0x3) + fh.write_bytes(data, self.data, 0x4) + + return data + +class Message(NamedTuple): + """ + The collection of a single or multi-lined message that will be displayed in an event, sign, message bubble, etc. + This also contains the list of actions/tags that will occur with the given message. + """ + string: str + tags: list[Tag] + +class DAT1Section(BMGSection): + """ + Represents a section of DAT1 message data containing multiple messages with their associated tags. + This class handles the serialization and deserialization of message sections encoded in a binary format + that combines UTF-8/Shift-JIS encoded text with embedded tag markers. Messages are delimited by null + characters and can contain formatting or metadata tags at various offsets within the string. + Attributes: + messages (list[Message]): A list of Message objects contained in this section. + """ + def __init__(self, messages: list[Message] = None): + super().__init__(DAT1_MAGIC) + + if messages == None: + messages = [] + self.messages: list[Message] = messages + + def add_message(self, message: Message): + self.messages.append(message) + + @classmethod + def import_section(cls, raw_bytes: BytesIO): + data_length = raw_bytes.seek(0, 2) + section = cls() + + string = '' + tags: list[Tag] = [] + + raw_bytes.seek(0) + while raw_bytes.tell() < data_length: + char_bytes = raw_bytes.read(2) + if char_bytes == TAG_IDENTIFIER: # Found a tag + offset = len(string) + tag = Tag.import_tag(raw_bytes, offset) + tags.append(tag) + else: + int_value = int.from_bytes(char_bytes) + string += chr(int_value) + + if char_bytes == NULL_BYTE: # Reading a null character + message = Message(string, tags) + section.add_message(message) + + string = '' + tags = [] + + return section + + def export_section(self) -> BytesIO: + """ + Export message section by serializing messages with their tags and characters into binary data. + Iterates through each message's characters and associated tags, writing tag data before each character + and any closing tags at the end of the string, encoding characters in Shift-JIS format. + """ + data = BytesIO() + + for message in self.messages: + string = message.string + tags = message.tags + + offset = -1 + for offset, char in enumerate(string): + current_tags = [tag for tag in tags if tag.offset == offset] + for tag in current_tags: + tag_data = tag.export_tag() + data.write(tag_data.getvalue()) + + int_value = ord(char) + char_bytes = int.to_bytes(int_value, 2, 'big') + data.write(char_bytes) + + if not string: + data.write(NULL_BYTE) + + # Since message.string does not contains the tags themselves, we must also check to see if there are tags at the end of the string + closing_tags = [tag for tag in tags if tag.offset == offset + 1] + for tag in closing_tags: + tag_data = tag.export_tag() + data.write(tag_data.getvalue()) + + return data diff --git a/src/wiithon/file_helper/bmg_sections/fli1.py b/src/wiithon/file_helper/bmg_sections/fli1.py new file mode 100644 index 0000000..d5d7abf --- /dev/null +++ b/src/wiithon/file_helper/bmg_sections/fli1.py @@ -0,0 +1,87 @@ +from io import BytesIO +import wiithon.helpers.Utils as fh +from wiithon.file_helper.bmg_sections.bmg_section import BMGSection + +FLI1_MAGIC: str = "FLI1" + +class FLI1Entry: + def __init__(self, unknown1: int, unknown2: int): + self.unknown1 = unknown1 + self.unknown2 = unknown2 + + def export_entry(self) -> BytesIO: + data = BytesIO() + + fh.write_u16(data, self.unknown1, 0x0) + fh.write_u16(data, 0, 0x2) + fh.write_u16(data, self.unknown2, 0x4) + fh.write_u16(data, 0, 0x6) + + return data + +class FLI1Section(BMGSection): + """ + A section containing a collection of FLI1 entries. + This class manages a list of FLI1Entry objects and provides functionality to serialize + and deserialize them to/from binary data. Each entry has a fixed size of 0x8 bytes. + Attributes: + entry_size (int): The fixed size of each FLI1Entry in bytes (0x8). + entry_count (int): The current number of entries in the section. + entries (list[FLI1Entry]): The list of FLI1Entry objects contained in this section. + Methods: + __init__(entries): Initializes a new FLI1Section with an optional list of entries. + add_entry(entry): Adds a new FLI1Entry to the section and updates the entry count. + import_section(raw_bytes): Class method that deserializes binary data into a FLI1Section object. + export_section(): Serializes the section and its entries back into binary data. + """ + entry_size = 0x8 + + def __init__(self, entries: list[FLI1Entry] = None): + super().__init__(FLI1_MAGIC) + + if entries == None: + entries = [] + + self.entry_count = len(entries) + self.entries = entries + + def add_entry(self, entry: FLI1Entry): + self.entries.append(entry) + self.entry_count = len(self.entries) + + @classmethod + def import_section(cls, raw_bytes: BytesIO): + entry_count = fh.read_u16(raw_bytes, 0x0) + entry_size = fh.read_u8(raw_bytes, 0x2) + assert entry_size == cls.entry_size + + section = cls() + + offset = 0x8 + for entry_index in range(entry_count): + unknown1 = fh.read_u16(raw_bytes, offset) + unknown2 = fh.read_u16(raw_bytes, offset + 0x4) + + entry = FLI1Entry(unknown1, unknown2) + section.add_entry(entry) + + offset += entry_size + + return section + + def export_section(self) -> BytesIO: + data = BytesIO() + + self.entry_count = len(self.entries) + fh.write_u16(data, self.entry_count, 0x0) + fh.write_u8(data, self.entry_size, 0x2) + fh.write_bytes(data, b'\x00' * 5, 0x3) + + offset = 0x8 + for entry in self.entries: + entry_data = entry.export_entry() + fh.write_bytes(data, entry_data.getvalue(), offset) + + offset += 0x8 + + return data diff --git a/src/wiithon/file_helper/bmg_sections/flw1.py b/src/wiithon/file_helper/bmg_sections/flw1.py new file mode 100644 index 0000000..17d7ace --- /dev/null +++ b/src/wiithon/file_helper/bmg_sections/flw1.py @@ -0,0 +1,212 @@ +from enum import IntEnum +from io import BytesIO +import wiithon.helpers.Utils as fh +from wiithon.file_helper.bmg_sections.bmg_section import BMGSection + +NODE_SIZE: int = 0x8 +FLW1_MAGIC: str = "FLW1" +type FLWNode = FLWTextNode | FLWConditionNode | FLWEventNode + +class NodeType(IntEnum): + text = 1 + condition = 2 + event = 3 + +class FLWTextNode: + node_type: int = NodeType.text + + def __init__(self, + unknown1: int, + message_ID: int, + next_flow_ID: int, + validity: int, + unknown2: int): + + self.unknown1: int = unknown1 + self.message_ID: int = message_ID + self.next_flow_ID: int = next_flow_ID + self.validity: int = validity + self.unknown2: int = unknown2 + + @classmethod + def import_node(cls, raw_bytes: BytesIO) -> "FLWTextNode": + assert raw_bytes.seek(0, 2) == NODE_SIZE + + unknown1 = fh.read_u8(raw_bytes, 0x1) + message_ID = fh.read_u16(raw_bytes, 0x2) + next_flow_ID = fh.read_u16(raw_bytes, 0x4) + validity = fh.read_u8(raw_bytes, 0x6) + unknown2 = fh.read_u8(raw_bytes, 0x7) + + return cls(unknown1, message_ID, next_flow_ID, validity, unknown2) + + def export_node(self) -> BytesIO: + data = BytesIO() + + fh.write_u8(data, self.node_type, 0x0) + fh.write_u8(data, self.unknown1, 0x1) + fh.write_u16(data, self.message_ID, 0x2) + fh.write_u16(data, self.next_flow_ID, 0x4) + fh.write_u8(data, self.validity, 0x6) + fh.write_u8(data, self.unknown2, 0x7) + + return data + +class FLWConditionNode: + node_type: int = 2 + + def __init__(self, + unknown1: int, + condition_type: int, + condition_argument: int, + branch_node_ID: int): + + self.unknown1: int = unknown1 + self.condition_type: int = condition_type + self.condition_argument: int = condition_argument + self.branch_node_ID: int = branch_node_ID + + @classmethod + def import_node(cls, raw_bytes: BytesIO) -> "FLWConditionNode": + assert raw_bytes.seek(0, 2) == NODE_SIZE + + unknown1 = fh.read_u8(raw_bytes, 0x1) + condition_type = fh.read_u16(raw_bytes, 0x2) + condition_argument = fh.read_u16(raw_bytes, 0x4) + branch_node_ID = fh.read_u16(raw_bytes, 0x6) + + return cls(unknown1, condition_type, condition_argument, branch_node_ID) + + def export_node(self) -> BytesIO: + data = BytesIO() + + fh.write_u8(data, self.node_type, 0x0) + fh.write_u8(data, self.unknown1, 0x1) + fh.write_u16(data, self.condition_type, 0x2) + fh.write_u16(data, self.condition_argument, 0x4) + fh.write_u16(data, self.branch_node_ID, 0x6) + + return data + +class FLWEventNode: + node_type: int = NodeType.event + + def __init__(self, + event_type: int, + branch_node_ID: int, + event_argument: int): + + self.event_type: int = event_type + self.branch_node_ID: int = branch_node_ID + self.event_argument: int = event_argument + + @classmethod + def import_node(cls, raw_bytes: BytesIO) -> "FLWEventNode": + assert raw_bytes.seek(0, 2) == NODE_SIZE + + event_type = fh.read_u8(raw_bytes, 0x1) + branch_node_ID = fh.read_u16(raw_bytes, 0x2) + event_argument = fh.read_u32(raw_bytes, 0x4) + + return cls(event_type, branch_node_ID, event_argument) + + def export_node(self) -> BytesIO: + data = BytesIO() + + fh.write_u8(data, self.node_type, 0x0) + fh.write_u8(data, self.event_type, 0x1) + fh.write_u16(data, self.branch_node_ID, 0x2) + fh.write_u32(data, self.event_argument, 0x4) + + return data + +class FLW1Section(BMGSection): + """ + Represents a FLW1 (Flow) section containing flow nodes and branch nodes. + This class handles the parsing and serialization of flow control data used in + Wii game files. It manages a collection of flow nodes (text, condition, event) + and branch node references. + Attributes: + flow_nodes (list[FLWNode]): List of flow nodes in this section. + branch_nodes (list[int]): List of branch node IDs. + Methods: + __init__(flow_nodes, branch_nodes): Initialize a FLW1Section with optional + flow nodes and branch nodes. + import_section(raw_bytes): Class method that deserializes a FLW1Section + from raw binary data (BytesIO). Reads the flow node count and branch + node count from the header, then parses each node based on its type + (text, condition, or event). Returns a populated FLW1Section instance. + export_section(): Serializes the FLW1Section back into binary format (BytesIO). + Writes the header with node counts, then serializes each flow node and + branch node sequentially. Returns the packed data as BytesIO. + """ + flow_nodes: list[FLWNode] + branch_nodes: list[int] + + def __init__(self, flow_nodes: list[FLWNode] = None, branch_nodes: list[int] = None): + super().__init__(FLW1_MAGIC) + + if flow_nodes == None: + flow_nodes = [] + if branch_nodes == None: + branch_nodes = [] + + self.flow_node_count = len(flow_nodes) + self.branch_node_count = len(branch_nodes) + + self.flow_nodes = flow_nodes + self.branch_nodes = branch_nodes + + @classmethod + def import_section(cls, raw_bytes: BytesIO) -> "FLW1Section": + section = cls() + + flow_node_count = fh.read_u16(raw_bytes, 0x0) + branch_node_count = fh.read_u16(raw_bytes, 0x2) + + offset = 0x8 + for flow_node_index in range(flow_node_count): + node_type = fh.read_u8(raw_bytes, offset) + node_bytes = fh.read_bytes(raw_bytes, 0x8, offset) + node_bytes = BytesIO(node_bytes) + + if node_type == NodeType.text: + node = FLWTextNode.import_node(node_bytes) + elif node_type == NodeType.condition: + node = FLWConditionNode.import_node(node_bytes) + elif node_type == NodeType.event: + node = FLWEventNode.import_node(node_bytes) + + section.flow_nodes.append(node) + offset += 0x8 + + for _ in range(branch_node_count): + branch_node_id = fh.read_u16(raw_bytes, offset) + section.branch_nodes.append(branch_node_id) + offset += 0x2 + + return section + + def export_section(self) -> BytesIO: + data = BytesIO() + + self.flow_node_count = len(self.flow_nodes) + self.branch_node_count = len(self.branch_nodes) + + fh.write_u16(data, self.flow_node_count, 0x0) + fh.write_u16(data, self.branch_node_count, 0x2) + fh.write_u32(data, 0, 0x4) + + offset = 0x8 + for flow_node in self.flow_nodes: + flow_data = flow_node.export_node() + fh.write_bytes(data, flow_data.getvalue(), offset) + + offset += 0x8 + + for branch_node in self.branch_nodes: + fh.write_u16(data, branch_node, offset) + + offset += 0x2 + + return data diff --git a/src/wiithon/file_helper/bmg_sections/inf1.py b/src/wiithon/file_helper/bmg_sections/inf1.py new file mode 100644 index 0000000..2323fcf --- /dev/null +++ b/src/wiithon/file_helper/bmg_sections/inf1.py @@ -0,0 +1,170 @@ +from io import BytesIO +from enum import IntEnum +import wiithon.helpers.Utils as fh +from wiithon.file_helper.bmg_sections.bmg_section import BMGSection + +INF1_MAGIC: str = "INF1" + +class CameraType(IntEnum): + normal = 0 + event = 1 + none = 2 + +class TalkType(IntEnum): + normal = 0 + short = 1 + event = 2 + composite = 3 + flow = 4 + null = 5 + +class BalloonType(IntEnum): + normal = 0 + unknown = 1 + call = 2 + fixed = 3 + signboard = 4 + info = 5 + icon = 6 + +class INF1Entry: + entry_size: int = 0xC + + def __init__(self, + message_data_offset: int, + camera_ID: int, + sound_ID: int, + camera_type: CameraType | int, + talk_type: TalkType | int, + balloon_type: BalloonType | int, + area_ID: int, + gameeventvalue_index: int): + + if not isinstance(camera_type, int) and not isinstance(camera_type, CameraType): + raise Exception(f"Bad Input camera type: {camera_type}") + if not isinstance(talk_type, int) and not isinstance(talk_type, TalkType): + raise Exception(f"Bad Input talk type: {talk_type}") + if not isinstance(balloon_type, int) and not isinstance(balloon_type, BalloonType): + raise Exception(f"Bad Input balloon type: {balloon_type}") + + self.message_data_offset: int = message_data_offset + self.camera_ID: int = camera_ID + self.sound_ID: int = sound_ID + self.camera_type: CameraType = CameraType(camera_type) + self.talk_type: TalkType = TalkType(talk_type) + self.balloon_type: BalloonType = BalloonType(balloon_type) + self.area_ID: int = area_ID + self.gameeventvalue_index: int = gameeventvalue_index + + @classmethod + def import_entry(cls, raw_bytes: BytesIO | bytes) -> "INF1Entry": + if isinstance(raw_bytes, bytes): + raw_bytes = BytesIO(raw_bytes) + + data_length = raw_bytes.seek(0,2) + assert data_length == cls.entry_size + + message_data_offset = fh.read_u32(raw_bytes, 0x0) + camera_ID = fh.read_u16(raw_bytes, 0x4) + sound_ID = fh.read_u8(raw_bytes, 0x6) + camera_type = fh.read_u8(raw_bytes, 0x7) + talk_type = fh.read_u8(raw_bytes, 0x8) + balloon_type = fh.read_u8(raw_bytes, 0x9) + area_ID = fh.read_u8(raw_bytes, 0xA) + gameeventvalue_index = fh.read_u8(raw_bytes, 0xB) + + return cls(message_data_offset, + camera_ID, + sound_ID, + camera_type, + talk_type, + balloon_type, + area_ID, + gameeventvalue_index) + + def export_entry(self) -> BytesIO: + data = BytesIO() + + fh.write_u32(data, self.message_data_offset, 0x0) + fh.write_u16(data, self.camera_ID, 0x4) + fh.write_u8(data, self.sound_ID, 0x6) + fh.write_u8(data, self.camera_type, 0x7) + fh.write_u8(data, self.talk_type, 0x8) + fh.write_u8(data, self.balloon_type, 0x9) + fh.write_u8(data, self.area_ID, 0xA) + fh.write_u8(data, self.gameeventvalue_index, 0xB) + + return data + +class INF1Section(BMGSection): + """ + Represents an INF1 section from a BMG file. + This class manages a collection of INF1 entries and provides methods to + pack and import the section data to/from binary format. + Attributes: + data_offset (int): The byte offset where entry data begins (0x8). + entry_size (int): The size in bytes of each entry (0xC). + entries (list[INF1Entry]): List of INF1Entry objects in this section. + entry_count (int): The number of entries in this section. + Methods: + __init__(entries): Initialize a new INF1Section with optional entries. + add_entry(entry): Add an INF1Entry to the section. + import_section(raw_bytes): Class method to deserialize an INF1 section from raw bytes. + export_section(): Serialize the section back into binary format. + """ + data_offset: int = 0x8 + entry_size: int = 0xC + entries: list[INF1Entry] + + def __init__(self, entries: list[INF1Entry] = None): + super().__init__(INF1_MAGIC) + + # Make sure list is of INF1Entry and set to empty if unspecified + if entries: + assert isinstance(entries[0], INF1Entry) + else: + entries = [] + + self.entry_count = len(entries) + self.entries = entries + + def add_entry(self, entry: INF1Entry): + """Add an entry to the section""" + self.entries.append(entry) + self.entry_count = len(self.entries) + + @classmethod + def import_section(cls, raw_bytes: BytesIO) -> "INF1Section": + """ + Imports a BMG section from raw bytes into an INF1 section object. + raw_bytes (BytesIO): A BytesIO object containing the section data to import. + """ + entry_count = fh.read_u16(raw_bytes, 0x0) + entry_size = fh.read_u16(raw_bytes, 0x2) + assert entry_size == cls.entry_size + + section = cls() + + for entry_index in range(entry_count): + raw_bytes.seek(cls.data_offset + entry_index * entry_size) + entry_bytes: bytes = raw_bytes.read(entry_size) + entry: INF1Entry = INF1Entry.import_entry(entry_bytes) + section.add_entry(entry) + + return section + + def export_section(self) -> BytesIO: + data = BytesIO() + + entry_count = len(self.entries) + fh.write_u16(data, entry_count, 0x0) + fh.write_u16(data, self.entry_size, 0x2) + fh.write_u32(data, 0, 0x4) + + offset = 0x8 + for entry in self.entries: + entry_data = entry.export_entry() + fh.write_bytes(data, entry_data.getvalue(), offset) + offset += self.entry_size + + return data From 6d1a560163c6b3c46d05b0bdbc5d6f2e4f4cbcca Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 03:04:17 +0200 Subject: [PATCH 05/11] Make BMGSection an abtract base class --- src/wiithon/formats/bmg_sections/bmg_section.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/wiithon/formats/bmg_sections/bmg_section.py b/src/wiithon/formats/bmg_sections/bmg_section.py index 79ae0a2..82843d4 100644 --- a/src/wiithon/formats/bmg_sections/bmg_section.py +++ b/src/wiithon/formats/bmg_sections/bmg_section.py @@ -1,6 +1,7 @@ +from abc import ABC, abstractmethod from io import BytesIO -class BMGSection: +class BMGSection(ABC): """ Base class for BMG file sections. Provides the interface for importing and exporting binary section data. @@ -14,20 +15,20 @@ def __init__(self, magic: str): self.magic = magic @classmethod + @abstractmethod def import_section(cls, raw_bytes: BytesIO) -> "BMGSection": """ Import a section from raw bytes. This method must be overridden in subclasses to provide proper implementation. - Raises AttributeError: If not properly overridden in a subclass. + Raises NotImplementedError: If not properly overridden in a subclass. """ - raise AttributeError("Import section is not properly overwritten") - return cls() - + raise NotImplementedError("Import section is not implemented") + + @abstractmethod def export_section(self) -> BytesIO: """ Export a section from raw bytes. This method must be overridden in subclasses to provide proper implementation. - Raises AttributeError: If not properly overridden in a subclass. + Raises NotImplementedError: If not properly overridden in a subclass. """ - raise AttributeError("Export section is not properly overwritten") - return BytesIO() + raise NotImplementedError("Export section is not implemented") From 23f0e914287c847e6a6f263056b98b568390bfb2 Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 05:03:16 +0200 Subject: [PATCH 06/11] Replaced all the read/write with the appropriate `BinaryReader` and `BinaryWriter` --- src/wiithon/binary/reader.py | 19 +- src/wiithon/formats/bmg.py | 96 +++++----- .../formats/bmg_sections/bmg_section.py | 6 +- src/wiithon/formats/bmg_sections/dat1.py | 57 +++--- src/wiithon/formats/bmg_sections/fli1.py | 58 ++++--- src/wiithon/formats/bmg_sections/flw1.py | 164 ++++++++++-------- src/wiithon/formats/bmg_sections/inf1.py | 87 +++++----- 7 files changed, 259 insertions(+), 228 deletions(-) diff --git a/src/wiithon/binary/reader.py b/src/wiithon/binary/reader.py index ed59179..bb8f365 100644 --- a/src/wiithon/binary/reader.py +++ b/src/wiithon/binary/reader.py @@ -15,14 +15,23 @@ def from_bytes(cls, data: bytes) -> "BinaryReader": stream = BytesIO(data) return cls(stream) - def seek(self, offset: int) -> None: - self.stream.seek(offset) + def seek(self, offset: int) -> int: + return self.stream.seek(offset) + + def size(self) -> int: + current_position = self.tell() + size = self.stream.seek(0, 2) + self.seek(current_position) + return size def tell(self) -> int: return self.stream.tell() - def skip(self, count: int) -> None: - self.stream.read(count) + def skip(self, count: int) -> int: + return self.stream.seek(count, 1) + + def back(self, count: int) -> int: + return self.stream.seek(-count, 1) def _read_number(self, size: int, unpack_fmt: str) -> int: data = self.stream.read(size) @@ -94,4 +103,4 @@ def string_until_null(self, encoding: str | None = None) -> str: break chars += byte - return chars.decode(encoding) \ No newline at end of file + return chars.decode(encoding) diff --git a/src/wiithon/formats/bmg.py b/src/wiithon/formats/bmg.py index 1109488..ab37ef0 100644 --- a/src/wiithon/formats/bmg.py +++ b/src/wiithon/formats/bmg.py @@ -1,11 +1,16 @@ from io import BytesIO +from typing import BinaryIO -import wiithon.helpers.Utils as fh -from wiithon.file_helper.bmg_sections.bmg_section import BMGSection -from wiithon.file_helper.bmg_sections.inf1 import INF1Section -from wiithon.file_helper.bmg_sections.dat1 import DAT1Section -from wiithon.file_helper.bmg_sections.flw1 import FLW1Section -from wiithon.file_helper.bmg_sections.fli1 import FLI1Section +from wiithon.binary.reader import BinaryReader +from wiithon.binary.writer import BinaryWriter +from wiithon.formats.bmg_sections.bmg_section import BMGSection +from wiithon.formats.bmg_sections.inf1 import INF1Section +from wiithon.formats.bmg_sections.dat1 import DAT1Section +from wiithon.formats.bmg_sections.flw1 import FLW1Section +from wiithon.formats.bmg_sections.fli1 import FLI1Section + +DATA_MAGIC = "MESG" +FILE_MAGIC = "bmg1" class BMG: """ @@ -21,7 +26,7 @@ class BMG: __init__(raw_bytes: BytesIO) -> None: Parses a BMG file from raw bytes. Validates magic numbers and reads all sections from the file. - add_header_to_section(section: bmg_section) -> BytesIO: + add_header(section: bmg_section) -> BytesIO: Wraps a section with its BMG header (magic and size) and applies 32-byte alignment padding. Returns the complete section data. export_bmg() -> BytesIO: @@ -32,28 +37,26 @@ class BMG: section_count: int sections: list[BMGSection] - def __init__(self, raw_bytes: BytesIO): - data_magic = fh.read_string(raw_bytes, 4, 0x0) - assert data_magic == "MESG" + def __init__(self, raw_bytes: BinaryIO): + reader = BinaryReader(raw_bytes) + data_magic = reader.string(0x4) + assert data_magic == DATA_MAGIC - file_magic = fh.read_string(raw_bytes, 4, 0x4) - assert file_magic == "bmg1" + file_magic = reader.string(0x4) + assert file_magic == FILE_MAGIC - self.flw1_section_offset = fh.read_u32(raw_bytes, 0x8) - self.section_count = fh.read_u32(raw_bytes, 0xC) - self.unknown = fh.read_u8(raw_bytes, 0x10) - # 15 bytes of padding + self.flw1_section_offset = reader.u32() + self.section_count = reader.u32() + self.unknown = reader.u8() + reader.seek(0x20) self.sections = [] - offset = 0x20 for section in range(self.section_count): - section_magic = fh.read_string(raw_bytes, 4, offset) - section_size = fh.read_u32(raw_bytes, offset + 0x4) - 0x8 - offset += 8 - - raw_bytes.seek(offset, 0) - section_bytes = raw_bytes.read(section_size) + section_magic = reader.string(0x4) + section_size = reader.u32() - 0x8 + + section_bytes = reader.raw(section_size) section_bytes = BytesIO(section_bytes) match section_magic: @@ -67,10 +70,10 @@ def __init__(self, raw_bytes: BytesIO): section = FLI1Section.import_section(section_bytes) self.sections.append(section) - offset += section_size - def add_header_to_section(self, section: BMGSection) -> BytesIO: - data = BytesIO() + def add_header(self, section: BMGSection) -> BinaryIO: + total_bytes = BytesIO() + writer = BinaryWriter(total_bytes) section_bytes = section.export_section() section_size = section_bytes.seek(0, 2) + 0x8 @@ -79,13 +82,13 @@ def add_header_to_section(self, section: BMGSection) -> BytesIO: if section_size % 32: padding = 32 - section_size % 32 section_size += padding - - fh.write_str(data, section.magic, 4, offset=0x0) - fh.write_u32(data, section_size, 0x4) - fh.write_bytes(data, section_bytes.getvalue(), 0x8) - fh.write_bytes(data, b'\x00' * padding, section_size - padding) - return data + writer.string(section.magic, 0x4) + writer.u32(section_size) + writer.raw(section_bytes.read) + writer.pad(padding) # should be align(0x20) + + return total_bytes def get_section(self, section_magic: str) -> list[BMGSection]: out: list[BMGSection] = [] @@ -96,23 +99,26 @@ def get_section(self, section_magic: str) -> list[BMGSection]: return out - def export_bmg(self) -> BytesIO: - data = BytesIO() + def export_bmg(self) -> BinaryIO: + bmg_bytes = BytesIO() + writer = BinaryWriter(bmg_bytes) - fh.write_str(data, "MESG", 4, offset=0x0) - fh.write_str(data, "bmg1", 4, offset=0x4) - fh.write_u32(data, 0, 0x8) # Write the flw1_section_offset later - fh.write_u32(data, len(self.sections), 0xC) - fh.write_u8(data, self.unknown, 0x10) - fh.write_bytes(data, b'\x00' * 15, 0x11) + writer.string(DATA_MAGIC) + writer.string(FILE_MAGIC) + writer.u32(0) # Write the flw1_section_offset later + writer.u32(len(self.sections)) + writer.u8(self.unknown) + writer.seek(0x20) offset = 0x20 for section in self.sections: if section.magic == "FLW1": - fh.write_u32(data ,offset, 0x8) + position = writer.tell() + writer.seek(0x8) + writer.u32(position) + writer.seek(position) - section_bytes = self.add_header_to_section(section) - fh.write_bytes(data, section_bytes.getvalue(), offset) - offset += len(section_bytes.getvalue()) + section_bytes = self.add_header(section) + writer.raw(section_bytes.read()) - return data + return bmg_bytes diff --git a/src/wiithon/formats/bmg_sections/bmg_section.py b/src/wiithon/formats/bmg_sections/bmg_section.py index 82843d4..e203e7b 100644 --- a/src/wiithon/formats/bmg_sections/bmg_section.py +++ b/src/wiithon/formats/bmg_sections/bmg_section.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from io import BytesIO +from typing import BinaryIO class BMGSection(ABC): """ @@ -16,7 +16,7 @@ def __init__(self, magic: str): @classmethod @abstractmethod - def import_section(cls, raw_bytes: BytesIO) -> "BMGSection": + def import_section(cls, raw_data: BinaryIO) -> "BMGSection": """ Import a section from raw bytes. This method must be overridden in subclasses to provide proper implementation. @@ -25,7 +25,7 @@ def import_section(cls, raw_bytes: BytesIO) -> "BMGSection": raise NotImplementedError("Import section is not implemented") @abstractmethod - def export_section(self) -> BytesIO: + def export_section(self) -> BinaryIO: """ Export a section from raw bytes. This method must be overridden in subclasses to provide proper implementation. diff --git a/src/wiithon/formats/bmg_sections/dat1.py b/src/wiithon/formats/bmg_sections/dat1.py index 5e49f03..a93a129 100644 --- a/src/wiithon/formats/bmg_sections/dat1.py +++ b/src/wiithon/formats/bmg_sections/dat1.py @@ -1,9 +1,10 @@ from io import BytesIO from enum import IntEnum -from typing import NamedTuple +from typing import BinaryIO, NamedTuple -import wiithon.helpers.Utils as fh -from wiithon.file_helper.bmg_sections.bmg_section import BMGSection +from wiithon.binary.reader import BinaryReader +from wiithon.binary.writer import BinaryWriter +from wiithon.formats.bmg_sections.bmg_section import BMGSection DAT1_MAGIC: str = "DAT1" TAG_IDENTIFIER = b"\x00\x1A" @@ -33,7 +34,7 @@ def __init__(self, offset: int, size: int, identifier: TagIdentifier, - data = None): + data: bytes = None): if not isinstance(identifier, int) and not isinstance(identifier, TagIdentifier): raise Exception("Bad Input") @@ -44,23 +45,25 @@ def __init__(self, self.data = data @classmethod - def import_tag(cls, raw_bytes: BytesIO, offset: int) -> "Tag": - size = int.from_bytes(raw_bytes.read(1), "big") - identifier = int.from_bytes(raw_bytes.read(1), "big") - data = raw_bytes.read(size - 4) + def import_tag(cls, raw_bytes: BinaryIO, offset: int) -> "Tag": + reader = BinaryReader(raw_bytes) + size = reader.u8() + identifier = reader.raw(1) + data = reader.raw(size - 4) return cls(offset, size, identifier, data) - def export_tag(self) -> BytesIO: + def export_tag(self) -> BinaryIO: assert isinstance(self.data, bytes) - data = BytesIO() + tag_bytes: BytesIO = BytesIO() + writer = BinaryWriter(tag_bytes) - fh.write_bytes(data, TAG_IDENTIFIER, 0x0) - fh.write_u8(data, self.size, 0x2) - fh.write_u8(data, self.identifier, 0x3) - fh.write_bytes(data, self.data, 0x4) + writer.raw(TAG_IDENTIFIER) + writer.u8(self.size) + writer.u8(self.identifier) + writer.raw(self.data) - return data + return tag_bytes class Message(NamedTuple): """ @@ -90,16 +93,15 @@ def add_message(self, message: Message): self.messages.append(message) @classmethod - def import_section(cls, raw_bytes: BytesIO): - data_length = raw_bytes.seek(0, 2) + def import_section(cls, raw_bytes: BinaryIO): + reader = BinaryReader(raw_bytes) section = cls() string = '' tags: list[Tag] = [] - raw_bytes.seek(0) - while raw_bytes.tell() < data_length: - char_bytes = raw_bytes.read(2) + while reader.tell() < reader.size(): + char_bytes = reader.raw(2) if char_bytes == TAG_IDENTIFIER: # Found a tag offset = len(string) tag = Tag.import_tag(raw_bytes, offset) @@ -117,13 +119,14 @@ def import_section(cls, raw_bytes: BytesIO): return section - def export_section(self) -> BytesIO: + def export_section(self) -> BinaryIO: """ Export message section by serializing messages with their tags and characters into binary data. Iterates through each message's characters and associated tags, writing tag data before each character and any closing tags at the end of the string, encoding characters in Shift-JIS format. """ data = BytesIO() + writer = BinaryWriter(data) for message in self.messages: string = message.string @@ -134,19 +137,17 @@ def export_section(self) -> BytesIO: current_tags = [tag for tag in tags if tag.offset == offset] for tag in current_tags: tag_data = tag.export_tag() - data.write(tag_data.getvalue()) - - int_value = ord(char) - char_bytes = int.to_bytes(int_value, 2, 'big') - data.write(char_bytes) + writer.raw(tag_data.read()) + + writer.string(char, 2) if not string: - data.write(NULL_BYTE) + writer.raw(NULL_BYTE) # Since message.string does not contains the tags themselves, we must also check to see if there are tags at the end of the string closing_tags = [tag for tag in tags if tag.offset == offset + 1] for tag in closing_tags: tag_data = tag.export_tag() - data.write(tag_data.getvalue()) + writer.raw(tag_data.read) return data diff --git a/src/wiithon/formats/bmg_sections/fli1.py b/src/wiithon/formats/bmg_sections/fli1.py index d5d7abf..e5662fc 100644 --- a/src/wiithon/formats/bmg_sections/fli1.py +++ b/src/wiithon/formats/bmg_sections/fli1.py @@ -1,6 +1,9 @@ from io import BytesIO -import wiithon.helpers.Utils as fh -from wiithon.file_helper.bmg_sections.bmg_section import BMGSection +from typing import BinaryIO + +from wiithon.binary.reader import BinaryReader +from wiithon.binary.writer import BinaryWriter +from wiithon.formats.bmg_sections.bmg_section import BMGSection FLI1_MAGIC: str = "FLI1" @@ -9,15 +12,16 @@ def __init__(self, unknown1: int, unknown2: int): self.unknown1 = unknown1 self.unknown2 = unknown2 - def export_entry(self) -> BytesIO: - data = BytesIO() + def export_entry(self) -> BinaryIO: + entry_bytes = BytesIO() + writer = BinaryWriter(entry_bytes) - fh.write_u16(data, self.unknown1, 0x0) - fh.write_u16(data, 0, 0x2) - fh.write_u16(data, self.unknown2, 0x4) - fh.write_u16(data, 0, 0x6) + writer.u16(self.unknown1) + writer.u16(0) + writer.u16(self.unknown2) + writer.u16(0) - return data + return entry_bytes class FLI1Section(BMGSection): """ @@ -50,38 +54,38 @@ def add_entry(self, entry: FLI1Entry): self.entry_count = len(self.entries) @classmethod - def import_section(cls, raw_bytes: BytesIO): - entry_count = fh.read_u16(raw_bytes, 0x0) - entry_size = fh.read_u8(raw_bytes, 0x2) + def import_section(cls, raw_bytes: BinaryIO): + reader = BinaryReader(raw_bytes) + + entry_count = reader.u16() + entry_size = reader.u16() + assert entry_size == cls.entry_size section = cls() - offset = 0x8 for entry_index in range(entry_count): - unknown1 = fh.read_u16(raw_bytes, offset) - unknown2 = fh.read_u16(raw_bytes, offset + 0x4) + unknown1 = reader.u16() + reader.skip(0x2) + unknown2 = reader.u16() + reader.skip() entry = FLI1Entry(unknown1, unknown2) section.add_entry(entry) - - offset += entry_size return section - def export_section(self) -> BytesIO: - data = BytesIO() + def export_section(self) -> BinaryIO: + section_bytes = BytesIO() + writer = BinaryWriter(section_bytes) self.entry_count = len(self.entries) - fh.write_u16(data, self.entry_count, 0x0) - fh.write_u8(data, self.entry_size, 0x2) - fh.write_bytes(data, b'\x00' * 5, 0x3) + writer.u16(self.entry_count) + writer.u8(self.entry_size) + writer.seek(0x8) - offset = 0x8 for entry in self.entries: entry_data = entry.export_entry() - fh.write_bytes(data, entry_data.getvalue(), offset) - - offset += 0x8 + writer.raw(entry_data.read) - return data + return section_bytes diff --git a/src/wiithon/formats/bmg_sections/flw1.py b/src/wiithon/formats/bmg_sections/flw1.py index 17d7ace..dafaf4a 100644 --- a/src/wiithon/formats/bmg_sections/flw1.py +++ b/src/wiithon/formats/bmg_sections/flw1.py @@ -1,7 +1,10 @@ from enum import IntEnum from io import BytesIO -import wiithon.helpers.Utils as fh -from wiithon.file_helper.bmg_sections.bmg_section import BMGSection +from typing import BinaryIO + +from wiithon.binary.reader import BinaryReader +from wiithon.binary.writer import BinaryWriter +from wiithon.formats.bmg_sections.bmg_section import BMGSection NODE_SIZE: int = 0x8 FLW1_MAGIC: str = "FLW1" @@ -29,31 +32,35 @@ def __init__(self, self.unknown2: int = unknown2 @classmethod - def import_node(cls, raw_bytes: BytesIO) -> "FLWTextNode": - assert raw_bytes.seek(0, 2) == NODE_SIZE - - unknown1 = fh.read_u8(raw_bytes, 0x1) - message_ID = fh.read_u16(raw_bytes, 0x2) - next_flow_ID = fh.read_u16(raw_bytes, 0x4) - validity = fh.read_u8(raw_bytes, 0x6) - unknown2 = fh.read_u8(raw_bytes, 0x7) + def import_node(cls, raw_bytes: BinaryIO) -> "FLWTextNode": + reader = BinaryReader(raw_bytes) + assert reader.size() == NODE_SIZE + assert reader.u8() == NodeType.text + + reader.skip(0x1) + unknown1 = reader.u8() + message_ID = reader.u16() + next_flow_ID = reader.u16() + validity = reader.u8() + unknown2 = reader.u8() return cls(unknown1, message_ID, next_flow_ID, validity, unknown2) - def export_node(self) -> BytesIO: - data = BytesIO() + def export_node(self) -> BinaryIO: + node_bytes = BytesIO() + writer = BinaryWriter(node_bytes) - fh.write_u8(data, self.node_type, 0x0) - fh.write_u8(data, self.unknown1, 0x1) - fh.write_u16(data, self.message_ID, 0x2) - fh.write_u16(data, self.next_flow_ID, 0x4) - fh.write_u8(data, self.validity, 0x6) - fh.write_u8(data, self.unknown2, 0x7) + writer.u8(self.node_type) + writer.u8(self.unknown1) + writer.u16(self.message_ID) + writer.u16(self.next_flow_ID) + writer.u8(self.validity) + writer.u8(self.unknown2) - return data + return node_bytes class FLWConditionNode: - node_type: int = 2 + node_type: int = NodeType.condition def __init__(self, unknown1: int, @@ -67,26 +74,29 @@ def __init__(self, self.branch_node_ID: int = branch_node_ID @classmethod - def import_node(cls, raw_bytes: BytesIO) -> "FLWConditionNode": - assert raw_bytes.seek(0, 2) == NODE_SIZE + def import_node(cls, raw_bytes: BinaryIO) -> "FLWConditionNode": + reader = BinaryReader(raw_bytes) + assert reader.size() == NODE_SIZE + assert reader.u8() == NodeType.condition - unknown1 = fh.read_u8(raw_bytes, 0x1) - condition_type = fh.read_u16(raw_bytes, 0x2) - condition_argument = fh.read_u16(raw_bytes, 0x4) - branch_node_ID = fh.read_u16(raw_bytes, 0x6) + unknown1 = reader.u8() + condition_type = reader.u16() + condition_argument = reader.u16() + branch_node_ID = reader.u16() return cls(unknown1, condition_type, condition_argument, branch_node_ID) - def export_node(self) -> BytesIO: - data = BytesIO() + def export_node(self) -> BinaryIO: + node_bytes = BytesIO() + writer = BinaryWriter(node_bytes) - fh.write_u8(data, self.node_type, 0x0) - fh.write_u8(data, self.unknown1, 0x1) - fh.write_u16(data, self.condition_type, 0x2) - fh.write_u16(data, self.condition_argument, 0x4) - fh.write_u16(data, self.branch_node_ID, 0x6) + writer.u8(self.node_type) + writer.u8(self.unknown1) + writer.u16(self.condition_type) + writer.u16(self.condition_argument) + writer.u16(self.branch_node_ID) - return data + return node_bytes class FLWEventNode: node_type: int = NodeType.event @@ -101,24 +111,27 @@ def __init__(self, self.event_argument: int = event_argument @classmethod - def import_node(cls, raw_bytes: BytesIO) -> "FLWEventNode": - assert raw_bytes.seek(0, 2) == NODE_SIZE + def import_node(cls, raw_bytes: BinaryIO) -> "FLWEventNode": + reader = BinaryReader(raw_bytes) + assert reader.size() == NODE_SIZE + assert reader.u8() == NodeType.event - event_type = fh.read_u8(raw_bytes, 0x1) - branch_node_ID = fh.read_u16(raw_bytes, 0x2) - event_argument = fh.read_u32(raw_bytes, 0x4) + event_type = reader.u8() + branch_node_ID = reader.u16() + event_argument = reader.u32() return cls(event_type, branch_node_ID, event_argument) - def export_node(self) -> BytesIO: - data = BytesIO() + def export_node(self) -> BinaryIO: + node_bytes = BytesIO() + writer = BinaryWriter(node_bytes) - fh.write_u8(data, self.node_type, 0x0) - fh.write_u8(data, self.event_type, 0x1) - fh.write_u16(data, self.branch_node_ID, 0x2) - fh.write_u32(data, self.event_argument, 0x4) + writer.u8(self.node_type) + writer.u8(self.event_type) + writer.u16(self.branch_node_ID) + writer.u32(self.event_argument) - return data + return node_bytes class FLW1Section(BMGSection): """ @@ -158,55 +171,52 @@ def __init__(self, flow_nodes: list[FLWNode] = None, branch_nodes: list[int] = N self.branch_nodes = branch_nodes @classmethod - def import_section(cls, raw_bytes: BytesIO) -> "FLW1Section": + def import_section(cls, raw_bytes: BinaryIO) -> "FLW1Section": + reader = BinaryReader(raw_bytes) section = cls() - - flow_node_count = fh.read_u16(raw_bytes, 0x0) - branch_node_count = fh.read_u16(raw_bytes, 0x2) - - offset = 0x8 + + flow_node_count = reader.u16() + branch_node_count = reader.u16() + reader.seek(0x8) + for flow_node_index in range(flow_node_count): - node_type = fh.read_u8(raw_bytes, offset) - node_bytes = fh.read_bytes(raw_bytes, 0x8, offset) + node_type = reader.u8() + reader.back(0x1) + node_bytes = reader.raw(NODE_SIZE) node_bytes = BytesIO(node_bytes) - if node_type == NodeType.text: - node = FLWTextNode.import_node(node_bytes) - elif node_type == NodeType.condition: - node = FLWConditionNode.import_node(node_bytes) - elif node_type == NodeType.event: - node = FLWEventNode.import_node(node_bytes) + match node_type: + case NodeType.text: + node = FLWTextNode.import_node(node_bytes) + case NodeType.condition: + node = FLWConditionNode.import_node(node_bytes) + case NodeType.event: + node = FLWEventNode.import_node(node_bytes) section.flow_nodes.append(node) - offset += 0x8 - for _ in range(branch_node_count): - branch_node_id = fh.read_u16(raw_bytes, offset) + for branch_node_index in range(branch_node_count): + branch_node_id = reader.u16() section.branch_nodes.append(branch_node_id) - offset += 0x2 return section - def export_section(self) -> BytesIO: - data = BytesIO() + def export_section(self) -> BinaryIO: + section_bytes = BytesIO() + writer = BinaryWriter(section_bytes) self.flow_node_count = len(self.flow_nodes) self.branch_node_count = len(self.branch_nodes) - fh.write_u16(data, self.flow_node_count, 0x0) - fh.write_u16(data, self.branch_node_count, 0x2) - fh.write_u32(data, 0, 0x4) + writer.u16(self.flow_node_count) + writer.u16(self.branch_node_count) + writer.seek(0x8) - offset = 0x8 for flow_node in self.flow_nodes: flow_data = flow_node.export_node() - fh.write_bytes(data, flow_data.getvalue(), offset) - - offset += 0x8 + writer.raw(flow_data.read) for branch_node in self.branch_nodes: - fh.write_u16(data, branch_node, offset) - - offset += 0x2 + writer.u16(branch_node) - return data + return section_bytes diff --git a/src/wiithon/formats/bmg_sections/inf1.py b/src/wiithon/formats/bmg_sections/inf1.py index 2323fcf..0fb1dc6 100644 --- a/src/wiithon/formats/bmg_sections/inf1.py +++ b/src/wiithon/formats/bmg_sections/inf1.py @@ -1,7 +1,10 @@ -from io import BytesIO from enum import IntEnum -import wiithon.helpers.Utils as fh -from wiithon.file_helper.bmg_sections.bmg_section import BMGSection +from io import BytesIO +from typing import BinaryIO + +from wiithon.binary.reader import BinaryReader +from wiithon.binary.writer import BinaryWriter +from wiithon.formats.bmg_sections.bmg_section import BMGSection INF1_MAGIC: str = "INF1" @@ -57,21 +60,18 @@ def __init__(self, self.gameeventvalue_index: int = gameeventvalue_index @classmethod - def import_entry(cls, raw_bytes: BytesIO | bytes) -> "INF1Entry": - if isinstance(raw_bytes, bytes): - raw_bytes = BytesIO(raw_bytes) - - data_length = raw_bytes.seek(0,2) - assert data_length == cls.entry_size - - message_data_offset = fh.read_u32(raw_bytes, 0x0) - camera_ID = fh.read_u16(raw_bytes, 0x4) - sound_ID = fh.read_u8(raw_bytes, 0x6) - camera_type = fh.read_u8(raw_bytes, 0x7) - talk_type = fh.read_u8(raw_bytes, 0x8) - balloon_type = fh.read_u8(raw_bytes, 0x9) - area_ID = fh.read_u8(raw_bytes, 0xA) - gameeventvalue_index = fh.read_u8(raw_bytes, 0xB) + def import_entry(cls, raw_bytes: BinaryIO) -> "INF1Entry": + reader = BinaryReader(raw_bytes) + assert reader.size() == cls.entry_size + + message_data_offset = reader.u32() + camera_ID = reader.u16() + sound_ID = reader.u8() + camera_type = reader.u8() + talk_type = reader.u8() + balloon_type = reader.u8() + area_ID = reader.u8() + gameeventvalue_index = reader.u8() return cls(message_data_offset, camera_ID, @@ -82,19 +82,20 @@ def import_entry(cls, raw_bytes: BytesIO | bytes) -> "INF1Entry": area_ID, gameeventvalue_index) - def export_entry(self) -> BytesIO: - data = BytesIO() + def export_entry(self) -> BinaryIO: + entry_bytes = BytesIO() + writer = BinaryWriter(entry_bytes) - fh.write_u32(data, self.message_data_offset, 0x0) - fh.write_u16(data, self.camera_ID, 0x4) - fh.write_u8(data, self.sound_ID, 0x6) - fh.write_u8(data, self.camera_type, 0x7) - fh.write_u8(data, self.talk_type, 0x8) - fh.write_u8(data, self.balloon_type, 0x9) - fh.write_u8(data, self.area_ID, 0xA) - fh.write_u8(data, self.gameeventvalue_index, 0xB) + writer.u32(self.message_data_offset) + writer.u16(self.camera_ID) + writer.u8(self.sound_ID) + writer.u8(self.camera_type) + writer.u8(self.talk_type) + writer.u8(self.balloon_type) + writer.u8(self.area_ID) + writer.u8(self.gameeventvalue_index) - return data + return entry_bytes class INF1Section(BMGSection): """ @@ -134,13 +135,14 @@ def add_entry(self, entry: INF1Entry): self.entry_count = len(self.entries) @classmethod - def import_section(cls, raw_bytes: BytesIO) -> "INF1Section": + def import_section(cls, raw_bytes: BinaryIO) -> "INF1Section": """ Imports a BMG section from raw bytes into an INF1 section object. raw_bytes (BytesIO): A BytesIO object containing the section data to import. """ - entry_count = fh.read_u16(raw_bytes, 0x0) - entry_size = fh.read_u16(raw_bytes, 0x2) + reader = BinaryReader(raw_bytes) + entry_count = reader.u16() + entry_size = reader.u16() assert entry_size == cls.entry_size section = cls() @@ -148,23 +150,22 @@ def import_section(cls, raw_bytes: BytesIO) -> "INF1Section": for entry_index in range(entry_count): raw_bytes.seek(cls.data_offset + entry_index * entry_size) entry_bytes: bytes = raw_bytes.read(entry_size) - entry: INF1Entry = INF1Entry.import_entry(entry_bytes) + entry: INF1Entry = INF1Entry.import_entry(BytesIO(entry_bytes)) section.add_entry(entry) return section - def export_section(self) -> BytesIO: - data = BytesIO() + def export_section(self) -> BinaryIO: + section_bytes = BytesIO() + writer = BinaryWriter(section_bytes) entry_count = len(self.entries) - fh.write_u16(data, entry_count, 0x0) - fh.write_u16(data, self.entry_size, 0x2) - fh.write_u32(data, 0, 0x4) - - offset = 0x8 + writer.u16(entry_count) + writer.u16(self.entry_size) + writer.seek(0x8) + for entry in self.entries: entry_data = entry.export_entry() - fh.write_bytes(data, entry_data.getvalue(), offset) - offset += self.entry_size + writer.raw(entry_data.read) - return data + return section_bytes From 4061340dd995b1dd87b7c8f8f77d2e10831f779a Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 05:48:15 +0200 Subject: [PATCH 07/11] Fixed small errors --- src/wiithon/formats/bmg.py | 5 ++++- src/wiithon/formats/bmg_sections/dat1.py | 4 ++-- src/wiithon/formats/bmg_sections/fli1.py | 5 +++-- src/wiithon/formats/bmg_sections/flw1.py | 3 +-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/wiithon/formats/bmg.py b/src/wiithon/formats/bmg.py index ab37ef0..df2b125 100644 --- a/src/wiithon/formats/bmg.py +++ b/src/wiithon/formats/bmg.py @@ -56,6 +56,10 @@ def __init__(self, raw_bytes: BinaryIO): section_magic = reader.string(0x4) section_size = reader.u32() - 0x8 + # Take into account the removed padding at the end of the file + if section_size > reader.size() - reader.tell(): + section_size = reader.size() - reader.tell() + section_bytes = reader.raw(section_size) section_bytes = BytesIO(section_bytes) @@ -110,7 +114,6 @@ def export_bmg(self) -> BinaryIO: writer.u8(self.unknown) writer.seek(0x20) - offset = 0x20 for section in self.sections: if section.magic == "FLW1": position = writer.tell() diff --git a/src/wiithon/formats/bmg_sections/dat1.py b/src/wiithon/formats/bmg_sections/dat1.py index a93a129..0b99df3 100644 --- a/src/wiithon/formats/bmg_sections/dat1.py +++ b/src/wiithon/formats/bmg_sections/dat1.py @@ -35,7 +35,7 @@ def __init__(self, size: int, identifier: TagIdentifier, data: bytes = None): - + if not isinstance(identifier, int) and not isinstance(identifier, TagIdentifier): raise Exception("Bad Input") @@ -48,7 +48,7 @@ def __init__(self, def import_tag(cls, raw_bytes: BinaryIO, offset: int) -> "Tag": reader = BinaryReader(raw_bytes) size = reader.u8() - identifier = reader.raw(1) + identifier = reader.u8() data = reader.raw(size - 4) return cls(offset, size, identifier, data) diff --git a/src/wiithon/formats/bmg_sections/fli1.py b/src/wiithon/formats/bmg_sections/fli1.py index e5662fc..a467e67 100644 --- a/src/wiithon/formats/bmg_sections/fli1.py +++ b/src/wiithon/formats/bmg_sections/fli1.py @@ -58,7 +58,8 @@ def import_section(cls, raw_bytes: BinaryIO): reader = BinaryReader(raw_bytes) entry_count = reader.u16() - entry_size = reader.u16() + entry_size = reader.u8() + reader.skip(0x1) assert entry_size == cls.entry_size @@ -68,7 +69,7 @@ def import_section(cls, raw_bytes: BinaryIO): unknown1 = reader.u16() reader.skip(0x2) unknown2 = reader.u16() - reader.skip() + reader.skip(0x2) entry = FLI1Entry(unknown1, unknown2) section.add_entry(entry) diff --git a/src/wiithon/formats/bmg_sections/flw1.py b/src/wiithon/formats/bmg_sections/flw1.py index dafaf4a..0b7d24d 100644 --- a/src/wiithon/formats/bmg_sections/flw1.py +++ b/src/wiithon/formats/bmg_sections/flw1.py @@ -37,7 +37,6 @@ def import_node(cls, raw_bytes: BinaryIO) -> "FLWTextNode": assert reader.size() == NODE_SIZE assert reader.u8() == NodeType.text - reader.skip(0x1) unknown1 = reader.u8() message_ID = reader.u16() next_flow_ID = reader.u16() @@ -184,7 +183,7 @@ def import_section(cls, raw_bytes: BinaryIO) -> "FLW1Section": reader.back(0x1) node_bytes = reader.raw(NODE_SIZE) node_bytes = BytesIO(node_bytes) - + match node_type: case NodeType.text: node = FLWTextNode.import_node(node_bytes) From e6a6baea3623deac70b8df652797800336b8432d Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 15:08:23 +0200 Subject: [PATCH 08/11] Removed old folder and added `__init__.py` to `bmg_section/` From d3a53d4103aca00422299770da515aa1e0fb88e1 Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 15:28:51 +0200 Subject: [PATCH 09/11] Make `seek` and `skip` not return cursor position --- src/wiithon/binary/reader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wiithon/binary/reader.py b/src/wiithon/binary/reader.py index bb8f365..86ee036 100644 --- a/src/wiithon/binary/reader.py +++ b/src/wiithon/binary/reader.py @@ -15,8 +15,8 @@ def from_bytes(cls, data: bytes) -> "BinaryReader": stream = BytesIO(data) return cls(stream) - def seek(self, offset: int) -> int: - return self.stream.seek(offset) + def seek(self, offset: int) -> None: + self.stream.seek(offset) def size(self) -> int: current_position = self.tell() @@ -27,8 +27,8 @@ def size(self) -> int: def tell(self) -> int: return self.stream.tell() - def skip(self, count: int) -> int: - return self.stream.seek(count, 1) + def skip(self, count: int) -> None: + self.stream.seek(count, 1) def back(self, count: int) -> int: return self.stream.seek(-count, 1) From bb139ff2672d32051151f0611b6cc4fd5dcba9f9 Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 15:30:25 +0200 Subject: [PATCH 10/11] Revert "Make `seek` and `skip` not return cursor position" This reverts commit d3a53d4103aca00422299770da515aa1e0fb88e1. --- src/wiithon/binary/reader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wiithon/binary/reader.py b/src/wiithon/binary/reader.py index 86ee036..bb8f365 100644 --- a/src/wiithon/binary/reader.py +++ b/src/wiithon/binary/reader.py @@ -15,8 +15,8 @@ def from_bytes(cls, data: bytes) -> "BinaryReader": stream = BytesIO(data) return cls(stream) - def seek(self, offset: int) -> None: - self.stream.seek(offset) + def seek(self, offset: int) -> int: + return self.stream.seek(offset) def size(self) -> int: current_position = self.tell() @@ -27,8 +27,8 @@ def size(self) -> int: def tell(self) -> int: return self.stream.tell() - def skip(self, count: int) -> None: - self.stream.seek(count, 1) + def skip(self, count: int) -> int: + return self.stream.seek(count, 1) def back(self, count: int) -> int: return self.stream.seek(-count, 1) From 104f2ea113d67e5320e790340dc673de8908ab0f Mon Sep 17 00:00:00 2001 From: Fl-ppie Date: Thu, 30 Jul 2026 15:33:28 +0200 Subject: [PATCH 11/11] Fix error for unit test --- src/wiithon/binary/reader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wiithon/binary/reader.py b/src/wiithon/binary/reader.py index bb8f365..a5758ed 100644 --- a/src/wiithon/binary/reader.py +++ b/src/wiithon/binary/reader.py @@ -27,10 +27,12 @@ def size(self) -> int: def tell(self) -> int: return self.stream.tell() - def skip(self, count: int) -> int: - return self.stream.seek(count, 1) + def skip(self, count: int) -> None: + self.stream.read(count) def back(self, count: int) -> int: + if count > self.stream.tell(): + return self.stream.seek(0) return self.stream.seek(-count, 1) def _read_number(self, size: int, unpack_fmt: str) -> int: