.. Copyright 2019 RoadrunnerWMC This file is part of ndspy. ndspy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. ndspy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with ndspy. If not, see . ``ndspy.bmg``: BMG (messages) ============================= .. py:module:: ndspy.bmg The ``ndspy.bmg`` module provides support for loading and saving *BMG* files. In games that use them, *BMG* files generally contain most or all of the text that can be displayed to the player (apart from text embedded into images). Games contain one or more *BMG* files for each language they support, and will load the appropriate one depending on the console's language setting. Each "message" is referenced by index. Some *BMG* files -- namely, those used in the DS Zelda games -- can contain scripts that control game progression in addition to text. ndspy can read and save the file blocks that contain these scripts (*FLW1* and *FLI1*), but decoding and encoding the instructions themselves is very game-specific and therefore out of its scope. Examples -------- Load a *BMG* from a ROM and inspect its messages and scripts: .. code-block:: python >>> import ndspy.rom, ndspy.bmg >>> rom = ndspy.rom.NintendoDSRom.fromFile('Zelda - Spirit Tracks.nds') >>> bmgData = rom.getFileByName('English/Message/castle_town.bmg') >>> bmg = ndspy.bmg.BMG(bmgData) >>> print(bmg) >>> print(bmg.messages[2]) What took you so long, [254:0000]? Did you keep me waiting just so you could change clothes? >>> bmg.messages[2].stringParts ['What took you so long,\n', Escape(254, bytearray(b'\x00\x00')), '?\n\nDid you keep me waiting\njust so you could change\nclothes?'] >>> bmg.scripts[:5] [(6553601, 9), (6553602, 140), (6553604, 117), (6553605, 124), (6553609, 183)] >>> bmg.labels[:5] [(12, 28), (-1, -1), (12, 0), (12, 68), (12, 73)] >>> bmg.instructions[:5] [bytearray(b'\x033\x00\x00e\x00\x00\x00'), bytearray(b'\x03\n\x01\x00\n\x00\r\x00'), bytearray(b'\x033\x02\x00\x03\x00\x00\x00'), bytearray(b'\x033\x03\x00\x02\x00\x00\x00'), bytearray(b'\x033\x04\x00\x04\x00\x00\x00')] >>> Load a *BMG* from a file, edit a message, and save it back into a ROM: .. code-block:: python >>> import ndspy.bmg, ndspy.rom >>> bmg = ndspy.bmg.BMG.fromFile('course.bmg') >>> print(bmg.messages[15]) Welcome to the secret Challenge mode. Think you can reach the goal? If you get stuck, press START and choose Return to Map. >>> bmg.messages[15].stringParts = ["Welcome to the secret\nChallenge mode where it's\nvery easy to softlock."] >>> print(bmg.messages[15]) Welcome to the secret Challenge mode where it's very easy to softlock. >>> rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds') >>> rom.setFileByName('script/course.bmg', bmg.save()) >>> rom.saveToFile('nsmb_edited.nds') >>> Create a new *BMG* using the ``cp1252`` encoding, and save it to a file: .. code-block:: python >>> import ndspy.bmg >>> message1 = ndspy.bmg.Message(b'', ['Want to save your game?']) >>> message2 = ndspy.bmg.Message(b'', ["Sure!\nNo thanks."]) >>> bmg = ndspy.bmg.BMG.fromMessages([message1, message2]) >>> bmg.encoding = 'cp1252' >>> bmg.saveToFile('savegame-en-us.bmg') >>> API --- .. py:class:: BMG([data], *, [id=0]) A *BMG* file. :param data: The data to be read as a *BMG* file. If this is not provided, the *BMG* object will initially be empty. :type data: bytes :param id: The initial value for the :py:attr:`id` attribute. The *BMG* data itself might optionally specify its own ID; if it does, that value takes precedence and this parameter is ignored. .. py:attribute:: encoding The encoding that should be used for storing strings in the *BMG*. Choosing an encoding is a trade-off between space efficiency, time efficiency, and the amount and choice of characters that can be encoded. Valid encodings are ``cp1252``, ``utf-16``, ``shift-jis``, and ``utf-8``. .. seealso:: :attr:`fullEncoding` -- a read-only mirror of this property that includes endianness information, intended for use with :py:meth:`str.encode` and :py:meth:`bytes.decode`. :type: :py:class:`str` :default: ``'utf-16'`` .. py:attribute:: fullEncoding A mirror property for :attr:`encoding` that takes :attr:`endianness` into account. This can be used with :py:meth:`str.encode` or :py:meth:`bytes.decode`, if for some reason you need to encode or decode raw string data matching this *BMG*'s encoding. The value of this attribute will always be the same as that of :attr:`encoding`, unless that attribute has the value ``utf-16``. In that case, this property will be either ``utf-16le`` or ``utf-16be``, depending on :attr:`endianness`. This attribute is read-only. .. seealso:: :attr:`encoding` -- a writable property you can use to modify the *BMG*'s encoding. :type: :py:class:`str` :default: ``'utf-16le'`` .. py:attribute:: endianness Whether values in the *BMG* should be stored using big- or little-endian byte order. Since the Nintendo DS is by default a little-endian console, almost every game uses little-endian *BMG* files. An exception to this is *Super Princess Peach.* ``'<'`` and ``'>'`` (representing little-endian and big-endian, respectively) are the only values this attribute is allowed to take. :type: :py:class:`str` :default: ``'<'`` .. py:attribute:: id This *BMG*'s ID number. In at least some games, every *BMG* has a unique ID. This makes it possible to refer to specific messages by specifying the desired *BMG* ID and the message index within that *BMG*. :type: :py:class:`int` :default: 0 .. py:attribute:: instructions The script instructions in this *BMG*, if it has a *FLW1* block. Instructions will be :py:class:`bytes` objects by default, but when saving, any object that implements a ``.save() -> bytes`` method is acceptable in place of :py:class:`bytes`. (This is to let you implement custom classes for instructions if you want to.) :type: :py:class:`list` of :py:class:`bytes` or of objects implementing ``.save() -> bytes`` :default: ``[]`` .. py:attribute:: labels The script instruction labels in this *BMG*, if it has a *FLW1* block. :type: :py:class:`list` of ``(bmgID, instructionIndex)`` (both :py:class:`int`\s) :default: ``[]`` .. py:attribute:: messages The list of :py:class:`Message`\s in this *BMG*. :type: :py:class:`list` of :py:class:`Message` :default: ``[]`` .. py:attribute:: scripts The starting instruction indices for each script ID defined in this *BMG*, if it has a *FLI1* block. .. seealso:: :py:func:`ndspy.indexInNamedList`, :py:func:`ndspy.findInNamedList`, :py:func:`ndspy.setInNamedList` -- helper functions you can use to find and replace values in this list. :type: :py:class:`list` of ``(scriptID, instructionIndex)`` (both :py:class:`int`\s) :default: ``[]`` .. py:attribute:: unk14 Unknown header value at 0x14. :type: :py:class:`int` :default: 0 .. py:attribute:: unk18 Unknown header value at 0x18. :type: :py:class:`int` :default: 0 .. py:attribute:: unk1C Unknown header value at 0x1C. :type: :py:class:`int` :default: 0 .. py:classmethod:: fromMessages(messages, [instructions, [labels, [scripts]]], *, [id=0]) Create a *BMG* from a list of messages. :param messages: The initial value for the :py:attr:`messages` attribute. :param instructions: The initial value for the :py:attr:`instructions` attribute. :param labels: The initial value for the :py:attr:`labels` attribute. :param scripts: The initial value for the :py:attr:`scripts` attribute. :param id: The initial value for the :py:attr:`id` attribute. :returns: The *BMG* object. :rtype: :py:class:`BMG` .. py:classmethod:: fromFile(filePath[, ...]) Load a *BMG* from a filesystem file. This is a convenience function. :param filePath: The path to the *BMG* file to open. :type filePath: :py:class:`str` or other path-like object Further parameters are the same as those of the default constructor. :returns: The *BMG* object. :rtype: :py:class:`BMG` .. py:function:: save() Generate file data representing this *BMG*. *FLW1* and *FLI1* sections will be created only if any script instructions or scripts exist, respectively. :returns: The *BMG* file data. :rtype: :py:class:`bytes` .. py:function:: saveToFile(filePath) Generate file data representing this *BMG*, and save it to a filesystem file. This is a convenience function. *FLW1* and *FLI1* sections will be created only if any script instructions or scripts exist, respectively. :param filePath: The path to the *BMG* file to save to. :type filePath: :py:class:`str` or other path-like object .. py:class:: Message([info[, stringParts[, isNull]]]) A single message in a *BMG* file. *BMG* messages are more than simple strings; they contain escape sequences that can specify font formatting and allow text to be inserted at runtime. For this reason, the message data is represented as a list of strings and :py:class:`Escape`\s instead of as a string. :param info: The initial value for the :py:attr:`info` attribute. :param stringParts: The initial value of the :py:attr:`stringParts` attribute. If you pass a bare string for this parameter, it will be automatically wrapped in a list for you. :param isNull: The initial value for the :py:attr:`isNull` attribute. .. py:attribute:: info A value containing message metadata, which comes from the *BMG*'s *INF1* block. The meaning of this value is completely game-dependent, and some games just leave this empty and don't use it at all. .. warning:: While the amount of metadata per message varies from game to game, it's always required that all messages in a *BMG* have the same amount of metadata. If you violate this, you'll experience errors when trying to save! :type: :py:class:`bytes` :default: ``b''`` .. py:attribute:: isNull This is ``True`` if the message is null; that is, if its data offset value in *INF1* is 0. A null message should have an empty :py:attr:`stringParts` list. .. note:: :py:class:`Message`\s with this attribute set to ``True`` are used to represent empty messages instead of ``None`` because empty messages can still have non-empty :py:attr:`info` values. :type: :py:class:`bool` :default: ``False`` .. py:attribute:: stringParts A list of strings and escape sequences that together form the message. Empty strings are allowed but discouraged. :type: :py:class:`list` of :py:class:`str` and of :py:class:`Escape` :default: ``[]`` .. py:function:: save(encoding) Generate binary data representing this message. :param str encoding: The encoding to use for the string data in the message (i.e. ``'utf-16'``, ``'ascii'``, etc). :returns: The message data. :rtype: :py:class:`bytes` .. py:class:: Message.Escape([type[, data]]) An escape sequence within a *BMG* message. Escape sequences have a type and optional parameter data. Currently, the parameter data is left raw and unparsed; this may change in the future. :param type: The initial value for the :py:attr:`type` attribute. :param data: The initial value of the :py:attr:`data` attribute. .. py:attribute:: data The raw data contained in this escape sequence. :type: :py:class:`bytes` :default: ``b''`` .. py:attribute:: type The type ID of this escape sequence. :type: :py:class:`int` :default: 0 .. py:function:: save(encoding) Generate binary data representing this escape sequence. :param str encoding: The encoding that should be assumed when building the binary data for the escape sequence (i.e. ``'utf-16'``, ``'ascii'``, etc). This is used to properly encode the escape character itself, U+001A. :returns: The escape sequence data. :rtype: :py:class:`bytes`