diff --git a/src/mars_patcher/auto_generated_types.py b/src/mars_patcher/auto_generated_types.py index fcea670..a2b0c84 100644 --- a/src/mars_patcher/auto_generated_types.py +++ b/src/mars_patcher/auto_generated_types.py @@ -188,7 +188,11 @@ 'Spanish' ] -Itemmessages: typ.TypeAlias = dict[Validlanguages, typ.Annotated[str, 'len() <= 112']] +Messagelanguages: typ.TypeAlias = dict[Validlanguages, str] +class Itemmessages(typ.TypedDict, total=False): + Languages: Messagelanguages + Centered: bool = True + class BlocklayerItem(typ.TypedDict, total=False): X: Typeu8 """The X position in the room that should get edited.""" diff --git a/src/mars_patcher/constants/items.py b/src/mars_patcher/constants/items.py index d484706..83a69d9 100644 --- a/src/mars_patcher/constants/items.py +++ b/src/mars_patcher/constants/items.py @@ -111,6 +111,8 @@ class ItemSprite(Enum): KEY_ITEM: Final = "Item" KEY_ITEM_SPRITE: Final = "ItemSprite" KEY_ITEM_MESSAGES: Final = "ItemMessages" +KEY_LANGUAGES: Final = "Languages" +KEY_CENTERED: Final = "Centered" SOURCE_ENUMS = { diff --git a/src/mars_patcher/data/schema.json b/src/mars_patcher/data/schema.json index 772c320..377f64f 100644 --- a/src/mars_patcher/data/schema.json +++ b/src/mars_patcher/data/schema.json @@ -629,29 +629,29 @@ "default": false }, "RoomNames": { - "type": "array", - "description": "Specifies a name to be displayed when the A Button is pressed on the pause menu.", - "uniqueItems": true, - "items": { - "type": "object", - "properties": { - "Area": { - "$ref": "#/$defs/AreaID", - "description": "The area ID where this room is located." - }, - "Room": { - "$ref": "#/$defs/TypeU8", - "description": "The room ID." - }, - "Name": { - "type": "string", - "description": "Specifies what text should appear for this room. Two lines are available, with an absolute maximum of 56 characters per line, if all characters used are small. Text will auto-wrap if the next word doesn't fit on the line. If the text is too long, it will be truncated. Use \n to force a line break. If not provided, will display 'Unknown Room'.", - "maxLength": 112 - } - }, - "required": ["Area", "Room", "Name"] + "type": "array", + "description": "Specifies a name to be displayed when the A Button is pressed on the pause menu.", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "Area": { + "$ref": "#/$defs/AreaID", + "description": "The area ID where this room is located." + }, + "Room": { + "$ref": "#/$defs/TypeU8", + "description": "The room ID." + }, + "Name": { + "type": "string", + "description": "Specifies what text should appear for this room. Two lines are available, with an absolute maximum of 56 characters per line, if all characters used are small. Text will auto-wrap if the next word doesn't fit on the line. If the text is too long, it will be truncated. Use \n to force a line break. If not provided, will display 'Unknown Room'.", + "maxLength": 112 } - } + }, + "required": ["Area", "Room", "Name"] + } + } }, "required": [ "SeedHash", @@ -903,7 +903,7 @@ "Spanish" ] }, - "ItemMessages": { + "MessageLanguages": { "type": "object", "propertyNames": { "$ref": "#/$defs/ValidLanguages" @@ -911,8 +911,19 @@ "required": ["English"], "additionalProperties": { "type": "string", - "description": "Specifies what text should appear on acquiring this item. Two lines are available, with an absolute maximum of 56 characters per line, if all characters used are small. Text will auto-wrap if the next word doesn't fit on the line. If the text is too long, it will be truncated. Use \n to force a line break. If not provided, a message based on the Item will be shown. If a language is not provided, it will use the provided English message.", - "maxLength": 112 + "description": "Specifies what text should appear for a 2 line message. Text will auto-wrap if the next word doesn't fit on the line. If the text is too long, it will be truncated. Use \n to force a line break. If not provided, a message based on the Item will be shown. If a language is not provided, it will use the provided English message." + } + }, + "ItemMessages": { + "type": "object", + "properties": { + "Languages": { + "$ref": "#/$defs/MessageLanguages" + }, + "Centered": { + "type": "boolean", + "default": true + } } }, "BlockLayer": { diff --git a/src/mars_patcher/item_patcher.py b/src/mars_patcher/item_patcher.py index 6d50b60..87381be 100644 --- a/src/mars_patcher/item_patcher.py +++ b/src/mars_patcher/item_patcher.py @@ -112,7 +112,6 @@ def write_items(self) -> None: item_addr = MINOR_LOCS_ARRAY + ((room_entry_index + item_index) * MINOR_LOC_SIZE) read_area = rom.read_8(item_addr) read_room = rom.read_8(item_addr + 1) - _read_room_index = rom.read_8(item_addr + 2) read_block_x = rom.read_8(item_addr + 3) read_block_y = rom.read_8(item_addr + 4) @@ -180,6 +179,7 @@ def write_custom_message( if lang in messages.item_messages else messages.item_messages[Language.ENGLISH] ), + centered=messages.centered, ) message_addr = rom.reserve_free_space(len(encoded_text) * 2) rom.write_ptr(message_table_addrs[lang] + (4 * custom_message_id), message_addr) diff --git a/src/mars_patcher/locations.py b/src/mars_patcher/locations.py index f46d797..bdace40 100644 --- a/src/mars_patcher/locations.py +++ b/src/mars_patcher/locations.py @@ -9,10 +9,12 @@ KEY_AREA, KEY_BLOCK_X, KEY_BLOCK_Y, + KEY_CENTERED, KEY_HIDDEN, KEY_ITEM, KEY_ITEM_MESSAGES, KEY_ITEM_SPRITE, + KEY_LANGUAGES, KEY_MAJOR_LOCS, KEY_MINOR_LOCS, KEY_ORIGINAL, @@ -27,7 +29,7 @@ from mars_patcher.text import Language if TYPE_CHECKING: - from mars_patcher.auto_generated_types import MarsschemaLocations + from mars_patcher.auto_generated_types import Itemmessages, MarsschemaLocations class Location: @@ -95,16 +97,18 @@ class ItemMessages: "Spanish": Language.SPANISH, } - def __init__(self, item_messages: dict[Language, str]): + def __init__(self, item_messages: dict[Language, str], centered: bool): self.item_messages = item_messages + self.centered = centered @classmethod - def from_json(cls, data: dict) -> ItemMessages: + def from_json(cls, data: Itemmessages) -> ItemMessages: item_messages: dict[Language, str] = {} - for lang, message in data.items(): - lang = cls.LANG_ENUMS[lang] + for lang_name, message in data[KEY_LANGUAGES].items(): + lang = cls.LANG_ENUMS[lang_name] item_messages[lang] = message - return cls(item_messages) + centered = data.get(KEY_CENTERED, True) + return cls(item_messages, centered) class LocationSettings: diff --git a/src/mars_patcher/text.py b/src/mars_patcher/text.py index 6d13b55..9b30bee 100644 --- a/src/mars_patcher/text.py +++ b/src/mars_patcher/text.py @@ -6,12 +6,13 @@ from mars_patcher.data import get_data_path from mars_patcher.rom import Region, Rom -SPACE = 0x40 +SPACE_CHAR = 0x40 +SPACE_TAG = 0x8000 NEXT = 0xFD00 NEWLINE = 0xFE00 END = 0xFF00 VALUE_MARKUP_TAG = { - "SPACE": (0x8000, 8), + "SPACE": (SPACE_TAG, 8), "COLOR": (0x8100, 8), "SPEED": (0x8200, 8), "INDENT": (0x8300, 8), @@ -19,7 +20,8 @@ "STOP_SOUND": (0xA000, 12), "WAIT": (0xE100, 8), } -BREAKING_CHARS = {SPACE, NEXT, NEWLINE} +BREAKING_CHARS = {SPACE_CHAR, NEXT, NEWLINE} +NEWLINE_CHARS = {NEXT, NEWLINE} KANJI_START = 0x4A0 KANJI_WIDTH = 10 @@ -77,11 +79,43 @@ def parse_value_markup_tag(tag: str) -> int | None: raise ValueError(f"Invalid value markup tag '{tag}'") +def get_char_width(rom: Rom, char_widths_addr: int, char_val: int) -> int: + if char_val >= 0x8000: + return 0 + if char_val < KANJI_START: + return rom.read_8(char_widths_addr + char_val) + return KANJI_WIDTH + + +def center_text(rom: Rom, char_vals: list[int], max_width: int) -> None: + char_widths_addr = character_widths(rom) + line_start = 0 + line_width = 0 + index = 0 + while index < len(char_vals): + char_val = char_vals[index] + index += 1 + if char_val in NEWLINE_CHARS or index == len(char_vals): + if line_width > 0: + assert line_width <= max_width + space_val = SPACE_TAG + (max_width - line_width) // 2 + char_vals.insert(line_start, space_val) + index += 1 + line_width = 0 + line_start = index + else: + line_width += get_char_width(rom, char_widths_addr, char_val) + + def encode_text( - rom: Rom, message_type: MessageType, string: str, max_width: int = MAX_LINE_WIDTH + rom: Rom, + message_type: MessageType, + string: str, + max_width: int = MAX_LINE_WIDTH, + centered: bool = False, ) -> list[int]: char_map = get_char_map(rom.region) - char_widths = character_widths(rom) + char_widths_addr = character_widths(rom) text: list[int] = [] line_width = 0 line_number = 0 @@ -124,14 +158,14 @@ def encode_text( escaped = False char_val = char_map[char] - char_width = rom.read_8(char_widths + char_val) if char_val < KANJI_START else KANJI_WIDTH + char_width = get_char_width(rom, char_widths_addr, char_val) line_width += char_width width_since_break += char_width if char_val in BREAKING_CHARS: prev_break = len(text) width_since_break = 0 - if char_val == NEXT or char_val == NEWLINE: + if char_val in NEWLINE_CHARS: line_width = 0 line_number += 1 @@ -174,6 +208,9 @@ def encode_text( if message_type == MessageType.ONE_LINE and (NEXT in text or NEWLINE in text): raise ValueError(f'String cannot have newlines:\n"{string}"') + if centered: + center_text(rom, text, max_width) + if message_type == MessageType.TWO_LINE and NEWLINE not in text: # Two line messages MUST have two lines, append NEWLINE if none exists text.append(NEWLINE)