diff --git a/.gitignore b/.gitignore index ca62135..806305e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iso .idea *.arc +**/__pycache__ /src_dir/ /assets diff --git a/src/wiithon/binary/reader.py b/src/wiithon/binary/reader.py index ed59179..a5758ed 100644 --- a/src/wiithon/binary/reader.py +++ b/src/wiithon/binary/reader.py @@ -15,8 +15,14 @@ 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() @@ -24,6 +30,11 @@ def tell(self) -> int: 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: data = self.stream.read(size) if len(data) != size: @@ -94,4 +105,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 new file mode 100644 index 0000000..df2b125 --- /dev/null +++ b/src/wiithon/formats/bmg.py @@ -0,0 +1,127 @@ +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 +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: + """ + 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(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: BinaryIO): + reader = BinaryReader(raw_bytes) + data_magic = reader.string(0x4) + assert data_magic == DATA_MAGIC + + file_magic = reader.string(0x4) + assert file_magic == FILE_MAGIC + + self.flw1_section_offset = reader.u32() + self.section_count = reader.u32() + self.unknown = reader.u8() + reader.seek(0x20) + + self.sections = [] + + for section in range(self.section_count): + 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) + + 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) + + 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 + + padding = 0 + if section_size % 32: + padding = 32 - section_size % 32 + section_size += padding + + 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] = [] + + for section in self.sections: + if section.magic == section_magic: + out.append(section) + + return out + + def export_bmg(self) -> BinaryIO: + bmg_bytes = BytesIO() + writer = BinaryWriter(bmg_bytes) + + 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) + + for section in self.sections: + if section.magic == "FLW1": + position = writer.tell() + writer.seek(0x8) + writer.u32(position) + writer.seek(position) + + section_bytes = self.add_header(section) + writer.raw(section_bytes.read()) + + return bmg_bytes diff --git a/src/wiithon/formats/bmg_sections/__init__.py b/src/wiithon/formats/bmg_sections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wiithon/formats/bmg_sections/bmg_section.py b/src/wiithon/formats/bmg_sections/bmg_section.py new file mode 100644 index 0000000..e203e7b --- /dev/null +++ b/src/wiithon/formats/bmg_sections/bmg_section.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import BinaryIO + +class BMGSection(ABC): + """ + 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 + @abstractmethod + 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. + Raises NotImplementedError: If not properly overridden in a subclass. + """ + raise NotImplementedError("Import section is not implemented") + + @abstractmethod + def export_section(self) -> BinaryIO: + """ + Export a section from raw bytes. + This method must be overridden in subclasses to provide proper implementation. + Raises NotImplementedError: If not properly overridden in a subclass. + """ + raise NotImplementedError("Export section is not implemented") diff --git a/src/wiithon/formats/bmg_sections/dat1.py b/src/wiithon/formats/bmg_sections/dat1.py new file mode 100644 index 0000000..0b99df3 --- /dev/null +++ b/src/wiithon/formats/bmg_sections/dat1.py @@ -0,0 +1,153 @@ +from io import BytesIO +from enum import IntEnum +from typing import BinaryIO, NamedTuple + +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" +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: bytes = 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: BinaryIO, offset: int) -> "Tag": + reader = BinaryReader(raw_bytes) + size = reader.u8() + identifier = reader.u8() + data = reader.raw(size - 4) + + return cls(offset, size, identifier, data) + + def export_tag(self) -> BinaryIO: + assert isinstance(self.data, bytes) + tag_bytes: BytesIO = BytesIO() + writer = BinaryWriter(tag_bytes) + + writer.raw(TAG_IDENTIFIER) + writer.u8(self.size) + writer.u8(self.identifier) + writer.raw(self.data) + + return tag_bytes + +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: BinaryIO): + reader = BinaryReader(raw_bytes) + section = cls() + + string = '' + tags: list[Tag] = [] + + 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) + 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) -> 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 + 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() + writer.raw(tag_data.read()) + + writer.string(char, 2) + + if not string: + 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() + 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 new file mode 100644 index 0000000..a467e67 --- /dev/null +++ b/src/wiithon/formats/bmg_sections/fli1.py @@ -0,0 +1,92 @@ +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 + +FLI1_MAGIC: str = "FLI1" + +class FLI1Entry: + def __init__(self, unknown1: int, unknown2: int): + self.unknown1 = unknown1 + self.unknown2 = unknown2 + + def export_entry(self) -> BinaryIO: + entry_bytes = BytesIO() + writer = BinaryWriter(entry_bytes) + + writer.u16(self.unknown1) + writer.u16(0) + writer.u16(self.unknown2) + writer.u16(0) + + return entry_bytes + +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: BinaryIO): + reader = BinaryReader(raw_bytes) + + entry_count = reader.u16() + entry_size = reader.u8() + reader.skip(0x1) + + assert entry_size == cls.entry_size + + section = cls() + + for entry_index in range(entry_count): + unknown1 = reader.u16() + reader.skip(0x2) + unknown2 = reader.u16() + reader.skip(0x2) + + entry = FLI1Entry(unknown1, unknown2) + section.add_entry(entry) + + return section + + def export_section(self) -> BinaryIO: + section_bytes = BytesIO() + writer = BinaryWriter(section_bytes) + + self.entry_count = len(self.entries) + writer.u16(self.entry_count) + writer.u8(self.entry_size) + writer.seek(0x8) + + for entry in self.entries: + entry_data = entry.export_entry() + writer.raw(entry_data.read) + + return section_bytes diff --git a/src/wiithon/formats/bmg_sections/flw1.py b/src/wiithon/formats/bmg_sections/flw1.py new file mode 100644 index 0000000..0b7d24d --- /dev/null +++ b/src/wiithon/formats/bmg_sections/flw1.py @@ -0,0 +1,221 @@ +from enum import IntEnum +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 + +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: BinaryIO) -> "FLWTextNode": + reader = BinaryReader(raw_bytes) + assert reader.size() == NODE_SIZE + assert reader.u8() == NodeType.text + + 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) -> BinaryIO: + node_bytes = BytesIO() + writer = BinaryWriter(node_bytes) + + 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 node_bytes + +class FLWConditionNode: + node_type: int = NodeType.condition + + 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: BinaryIO) -> "FLWConditionNode": + reader = BinaryReader(raw_bytes) + assert reader.size() == NODE_SIZE + assert reader.u8() == NodeType.condition + + 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) -> BinaryIO: + node_bytes = BytesIO() + writer = BinaryWriter(node_bytes) + + 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 node_bytes + +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: BinaryIO) -> "FLWEventNode": + reader = BinaryReader(raw_bytes) + assert reader.size() == NODE_SIZE + assert reader.u8() == NodeType.event + + 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) -> BinaryIO: + node_bytes = BytesIO() + writer = BinaryWriter(node_bytes) + + writer.u8(self.node_type) + writer.u8(self.event_type) + writer.u16(self.branch_node_ID) + writer.u32(self.event_argument) + + return node_bytes + +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: BinaryIO) -> "FLW1Section": + reader = BinaryReader(raw_bytes) + section = cls() + + flow_node_count = reader.u16() + branch_node_count = reader.u16() + reader.seek(0x8) + + for flow_node_index in range(flow_node_count): + node_type = reader.u8() + 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) + case NodeType.condition: + node = FLWConditionNode.import_node(node_bytes) + case NodeType.event: + node = FLWEventNode.import_node(node_bytes) + + section.flow_nodes.append(node) + + for branch_node_index in range(branch_node_count): + branch_node_id = reader.u16() + section.branch_nodes.append(branch_node_id) + + return section + + 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) + + writer.u16(self.flow_node_count) + writer.u16(self.branch_node_count) + writer.seek(0x8) + + for flow_node in self.flow_nodes: + flow_data = flow_node.export_node() + writer.raw(flow_data.read) + + for branch_node in self.branch_nodes: + writer.u16(branch_node) + + return section_bytes diff --git a/src/wiithon/formats/bmg_sections/inf1.py b/src/wiithon/formats/bmg_sections/inf1.py new file mode 100644 index 0000000..0fb1dc6 --- /dev/null +++ b/src/wiithon/formats/bmg_sections/inf1.py @@ -0,0 +1,171 @@ +from enum import IntEnum +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" + +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: 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, + sound_ID, + camera_type, + talk_type, + balloon_type, + area_ID, + gameeventvalue_index) + + def export_entry(self) -> BinaryIO: + entry_bytes = BytesIO() + writer = BinaryWriter(entry_bytes) + + 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 entry_bytes + +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: 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. + """ + reader = BinaryReader(raw_bytes) + entry_count = reader.u16() + entry_size = reader.u16() + 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(BytesIO(entry_bytes)) + section.add_entry(entry) + + return section + + def export_section(self) -> BinaryIO: + section_bytes = BytesIO() + writer = BinaryWriter(section_bytes) + + entry_count = len(self.entries) + writer.u16(entry_count) + writer.u16(self.entry_size) + writer.seek(0x8) + + for entry in self.entries: + entry_data = entry.export_entry() + writer.raw(entry_data.read) + + return section_bytes