Files
dockipelago/worlds/papermario/modules/modify_game_strings.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

376 lines
14 KiB
Python

# from https://github.com/icebound777/PMR-SeedGenerator/blob/dev_multiworld/util/multiworld_item_info_to_pmString.py
"""
Utility module for turning item info of multiworld items (from tools like
Archipelago) into formatted pmStrings, which can be used to overwrite
in-game item descriptions for item shop displays.
"""
from enum import IntEnum, unique
from BaseClasses import ItemClassification
@unique
class FormattingToken(IntEnum):
END = 0
LINEBREAK = 1
SAVE_COLOR = 2
RESTORE_COLOR = 3
SET_COLOR = 4
COLOR_RED = 5
COLOR_CYAN = 6
COLOR_BLUE = 7
COLOR_PURPLE = 8
COLOR_YELLOW = 9
COLOR_NONE = 10
def multiworld_item_info_to_pmString(
player_name: str,
item_name: str,
progression_type: ItemClassification,
node_id: str,
) -> tuple[int, list[int]]: # (rom_location, byte_list)
"""
For the given data returns the translated byte list to write into the ROM
to create a custom multiworld shop item description, as well as the ROM
location to write this byte list to.
If some of the provided data is too long to fit into a single line of
item description, the data will be right-trimmed until it fits into the
item description box.
If an unsupported character is found within the player_name or item_name
arguments, that character is simply ignored.
If an unsupported progression_type is provided, no color change will be
applied to the text of item_name.
If the provided node_id does not correspond to any shop location defined
in the seed generator, then the first value of returned tuple is set
to -1. This is an error state.
The returned byte_list represents an item description in the following
scheme:
player_name's
item_name
with item_name's text colored in accordance with the provided progression
type.
"""
assert isinstance(player_name, str)
assert isinstance(item_name, str)
assert isinstance(progression_type, ItemClassification) or isinstance(
progression_type, int
)
assert isinstance(node_id, str)
pmString: list[int] = []
LINE_MAX_WIDTH: int = 600
# Player name
cur_line_width = 0
# reserve space for the 's at the end
cur_line_width += _get_char_width("'")
cur_line_width += _get_char_width("s")
for char in player_name:
pmChar, pmChar_width = _char_to_pmChar(char)
if cur_line_width + pmChar_width > LINE_MAX_WIDTH:
break
pmString.append(pmChar)
cur_line_width += pmChar_width
pmString.append(_char_to_pmChar("'")[0])
pmString.append(_char_to_pmChar("s")[0])
pmString.extend(_get_formatting_token(FormattingToken.LINEBREAK))
# Item name
cur_line_width = 0
pmString.extend(_get_formatting_token(FormattingToken.SAVE_COLOR))
pmString.extend(_get_formatting_token(FormattingToken.SET_COLOR))
if ItemClassification.trap in progression_type and ItemClassification.progression in progression_type:
pmString.extend(_get_formatting_token(FormattingToken.COLOR_YELLOW))
elif ItemClassification.progression in progression_type:
pmString.extend(_get_formatting_token(FormattingToken.COLOR_PURPLE))
elif ItemClassification.trap in progression_type:
pmString.extend(_get_formatting_token(FormattingToken.COLOR_RED))
elif ItemClassification.useful in progression_type:
pmString.extend(_get_formatting_token(FormattingToken.COLOR_BLUE))
elif ItemClassification.filler in progression_type:
pmString.extend(_get_formatting_token(FormattingToken.COLOR_CYAN))
else: # fallback
pmString.extend(_get_formatting_token(FormattingToken.COLOR_NONE))
for char in item_name:
pmChar, pmChar_width = _char_to_pmChar(char)
if cur_line_width + pmChar_width > LINE_MAX_WIDTH:
break
if pmChar != 0:
pmString.append(pmChar)
cur_line_width += pmChar_width
pmString.extend(_get_formatting_token(FormattingToken.RESTORE_COLOR))
# Finish string
pmString.extend(_get_formatting_token(FormattingToken.END))
rom_location = _get_rom_location(node_id)
return (rom_location, pmString)
def _char_to_pmChar(input_char: str) -> tuple[int, int]:
"""
Returns the byte value used by Paper Mario for a given character, as well
as its character width when printed into an item description box.
If a character is not supported or recognized, instead (0, 0) will be
returned.
"""
char_byte_map: dict = {
"0": 0x10,
"1": 0x11,
"2": 0x12,
"3": 0x13,
"4": 0x14,
"5": 0x15,
"6": 0x16,
"7": 0x17,
"8": 0x18,
"9": 0x19,
"A": 0x21,
"B": 0x22,
"C": 0x23,
"D": 0x24,
"E": 0x25,
"F": 0x26,
"G": 0x27,
"H": 0x28,
"I": 0x29,
"J": 0x2A,
"K": 0x2B,
"L": 0x2C,
"M": 0x2D,
"N": 0x2E,
"O": 0x2F,
"P": 0x30,
"Q": 0x31,
"R": 0x32,
"S": 0x33,
"T": 0x34,
"U": 0x35,
"V": 0x36,
"W": 0x37,
"X": 0x38,
"Y": 0x39,
"Z": 0x3A,
"a": 0x41,
"b": 0x42,
"c": 0x43,
"d": 0x44,
"e": 0x45,
"f": 0x46,
"g": 0x47,
"h": 0x48,
"i": 0x49,
"j": 0x4A,
"k": 0x4B,
"l": 0x4C,
"m": 0x4D,
"n": 0x4E,
"o": 0x4F,
"p": 0x50,
"q": 0x51,
"r": 0x52,
"s": 0x53,
"t": 0x54,
"u": 0x55,
"v": 0x56,
"w": 0x57,
"x": 0x58,
"y": 0x59,
"z": 0x5A,
" ": 0xF7,
"!": 0x01,
"?": 0x1F,
"'": 0x07,
",": 0x0C,
"-": 0x0D,
".": 0x0E,
":": 0x1A,
"(": 0x3B,
")": 0x3D,
"_": 0x3F,
}
if input_char in char_byte_map:
return (char_byte_map[input_char], _get_char_width(input_char))
else:
return (0x00, 0)
def _get_char_width(input_char: str) -> int:
"""
Returns an input_char's width when printed into an item description box.
If the input_char is not recognized, return 0 instead.
This width is more or less arbitrary and does not actually represent
any tangible value used by Paper Mario's source code. Instead, these values
are relative to an item description box's draw width, and are only supposed
to help gauge how much space is left within the item description box.
"""
char_width_map: dict = {
"ABCDEFGHJKLMNOPQRSTUVWXYZadmpw023456789?_-": 22.3, # fits ~27 times
"bcefgkngnqrstuvxyz!()": 20, # fits ~30 times
"Ihjo1": 17.7, # fits ~34 times
" ,": 15, # fits ~40 times
".:": 12.3, # fits ~49 times
"il'": 10, # fits ~60 times
}
char_width = 0
for char_list, width in char_width_map.items():
if input_char in char_list:
char_width = width
break
return char_width
def _get_formatting_token(input_formatting: FormattingToken) -> list[int]:
"""
Returns a list of one or more bytes representing input_formatting in
Paper Mario's message system as control characters and formatting tokens.
"""
char_map: dict = {
FormattingToken.END: [0xFD],
FormattingToken.LINEBREAK: [0xF0],
FormattingToken.SAVE_COLOR: [0xFF, 0x24],
FormattingToken.RESTORE_COLOR: [0xFF, 0x25],
FormattingToken.SET_COLOR: [0xFF, 0x05],
FormattingToken.COLOR_RED: [0x07],
FormattingToken.COLOR_PURPLE: [0x08],
FormattingToken.COLOR_CYAN: [0x01],
FormattingToken.COLOR_BLUE: [0x02],
FormattingToken.COLOR_YELLOW: [0x05],
FormattingToken.COLOR_NONE: [0x0A],
}
if input_formatting in char_map:
return char_map[input_formatting]
else:
return list()
def _get_rom_location(node_id: str) -> int:
"""
Returns the ROM location of a given node_id's shop item's multiworld string.
If an invalid shop node_id is provided, returns -1 instead.
"""
node_romoffset_map: dict = {
# Toad Town - Shroom Grocery
"MAC_00/ShopItemA": 0x1C7E5C8, # ErrCode 24 181
"MAC_00/ShopItemB": 0x1C7E64C, # ErrCode 24 182
"MAC_00/ShopItemC": 0x1C7E6D0, # ErrCode 24 183
"MAC_00/ShopItemD": 0x1C7E754, # ErrCode 24 184
"MAC_00/ShopItemE": 0x1C7E7D8, # ErrCode 24 185
"MAC_00/ShopItemF": 0x1C7E85C, # ErrCode 24 186
# Toad Town - Rowf
"MAC_01/ShopBadgeA": 0x1C7E908, # ErrCode 24 191
"MAC_01/ShopBadgeB": 0x1C7E98C, # ErrCode 24 192
"MAC_01/ShopBadgeC": 0x1C7EA10, # ErrCode 24 193
"MAC_01/ShopBadgeD": 0x1C7EA94, # ErrCode 24 194
"MAC_01/ShopBadgeE": 0x1C7EB18, # ErrCode 24 195
"MAC_01/ShopBadgeF": 0x1C7EB9C, # ErrCode 24 196
"MAC_01/ShopBadgeG": 0x1C7EC20, # ErrCode 24 197
"MAC_01/ShopBadgeH": 0x1C7ECA4, # ErrCode 24 198
"MAC_01/ShopBadgeI": 0x1C7ED28, # ErrCode 24 199
"MAC_01/ShopBadgeJ": 0x1C7EDAC, # ErrCode 24 19A
"MAC_01/ShopBadgeK": 0x1C7EE30, # ErrCode 24 19B
"MAC_01/ShopBadgeL": 0x1C7EEB4, # ErrCode 24 19C
"MAC_01/ShopBadgeM": 0x1C7EF38, # ErrCode 24 19D
"MAC_01/ShopBadgeN": 0x1C7EFBC, # ErrCode 24 19E
"MAC_01/ShopBadgeO": 0x1C7F040, # ErrCode 24 19F
"MAC_01/ShopBadgeP": 0x1C7F0C4, # ErrCode 24 1A0
# Toad Town - Harry's shop
"MAC_04/ShopItemA": 0x1C7F148, # ErrCode 24 1A1
"MAC_04/ShopItemB": 0x1C7F1CC, # ErrCode 24 1A2
"MAC_04/ShopItemC": 0x1C7F250, # ErrCode 24 1A3
"MAC_04/ShopItemD": 0x1C7F2D4, # ErrCode 24 1A4
"MAC_04/ShopItemE": 0x1C7F358, # ErrCode 24 1A5
"MAC_04/ShopItemF": 0x1C7F3DC, # ErrCode 24 1A6
# Star Haven
"HOS_03/ShopItemA": 0x1C7F488, # ErrCode 24 1B1
"HOS_03/ShopItemB": 0x1C7F50C, # ErrCode 24 1B2
"HOS_03/ShopItemC": 0x1C7F590, # ErrCode 24 1B3
"HOS_03/ShopItemD": 0x1C7F614, # ErrCode 24 1B4
"HOS_03/ShopItemE": 0x1C7F698, # ErrCode 24 1B5
"HOS_03/ShopItemF": 0x1C7F71C, # ErrCode 24 1B6
# Merlow's
"HOS_06/ShopBadgeA": 0x1C80B44, # ErrCode 24 220
"HOS_06/ShopBadgeB": 0x1C80BC8, # ErrCode 24 221
"HOS_06/ShopBadgeC": 0x1C80C4C, # ErrCode 24 222
"HOS_06/ShopBadgeD": 0x1C80CD0, # ErrCode 24 223
"HOS_06/ShopBadgeE": 0x1C80D54, # ErrCode 24 224
"HOS_06/ShopBadgeF": 0x1C80DD8, # ErrCode 24 225
"HOS_06/ShopBadgeG": 0x1C80E5C, # ErrCode 24 226
"HOS_06/ShopBadgeH": 0x1C80EE0, # ErrCode 24 227
"HOS_06/ShopBadgeI": 0x1C80F64, # ErrCode 24 228
"HOS_06/ShopBadgeJ": 0x1C80FE8, # ErrCode 24 229
"HOS_06/ShopBadgeK": 0x1C8106C, # ErrCode 24 22A
"HOS_06/ShopBadgeL": 0x1C810F0, # ErrCode 24 22B
"HOS_06/ShopBadgeM": 0x1C81174, # ErrCode 24 22C
"HOS_06/ShopBadgeN": 0x1C811F8, # ErrCode 24 22D
"HOS_06/ShopBadgeO": 0x1C8127C, # ErrCode 24 22E
"HOS_06/ShopRewardA": 0x1C81304, # ErrCode 24 230
"HOS_06/ShopRewardB": 0x1C81388, # ErrCode 24 231
"HOS_06/ShopRewardC": 0x1C8140C, # ErrCode 24 232
"HOS_06/ShopRewardD": 0x1C81490, # ErrCode 24 233
"HOS_06/ShopRewardE": 0x1C81514, # ErrCode 24 234
"HOS_06/ShopRewardF": 0x1C81598, # ErrCode 24 235
# Koopa Village
"NOK_01/ShopItemA": 0x1C7F7C8, # ErrCode 24 1C1
"NOK_01/ShopItemB": 0x1C7F84C, # ErrCode 24 1C2
"NOK_01/ShopItemC": 0x1C7F8D0, # ErrCode 24 1C3
"NOK_01/ShopItemD": 0x1C7F954, # ErrCode 24 1C4
"NOK_01/ShopItemE": 0x1C7F9D8, # ErrCode 24 1C5
"NOK_01/ShopItemF": 0x1C7FA5C, # ErrCode 24 1C6
# Dry Dry Outpost
"DRO_01/ShopItemA": 0x1C7FB08, # ErrCode 24 1D1
"DRO_01/ShopItemB": 0x1C7FB8C, # ErrCode 24 1D2
"DRO_01/ShopItemC": 0x1C7FC10, # ErrCode 24 1D3
"DRO_01/ShopItemD": 0x1C7FC94, # ErrCode 24 1D4
"DRO_01/ShopItemE": 0x1C7FD18, # ErrCode 24 1D5
"DRO_01/ShopItemF": 0x1C7FD9C, # ErrCode 24 1D6
# Boo's Mansion
"OBK_03/ShopItemA": 0x1C7FE48, # ErrCode 24 1E1
"OBK_03/ShopItemB": 0x1C7FECC, # ErrCode 24 1E2
"OBK_03/ShopItemC": 0x1C7FF50, # ErrCode 24 1E3
"OBK_03/ShopItemD": 0x1C7FFD4, # ErrCode 24 1E4
"OBK_03/ShopItemE": 0x1C80058, # ErrCode 24 1E5
"OBK_03/ShopItemF": 0x1C800DC, # ErrCode 24 1E6
# Yoshi Village
"JAN_03/ShopItemA": 0x1C80188, # ErrCode 24 1F1
"JAN_03/ShopItemB": 0x1C8020C, # ErrCode 24 1F2
"JAN_03/ShopItemC": 0x1C80290, # ErrCode 24 1F3
"JAN_03/ShopItemD": 0x1C80314, # ErrCode 24 1F4
"JAN_03/ShopItemE": 0x1C80398, # ErrCode 24 1F5
"JAN_03/ShopItemF": 0x1C8041C, # ErrCode 24 1F6
# Shiver City
"SAM_02/ShopItemA": 0x1C804C8, # ErrCode 24 201
"SAM_02/ShopItemB": 0x1C8054C, # ErrCode 24 202
"SAM_02/ShopItemC": 0x1C805D0, # ErrCode 24 203
"SAM_02/ShopItemD": 0x1C80654, # ErrCode 24 204
"SAM_02/ShopItemE": 0x1C806D8, # ErrCode 24 205
"SAM_02/ShopItemF": 0x1C8075C, # ErrCode 24 206
# Bowser's Castle
"KPA_96/ShopItemA": 0x1C80808, # ErrCode 24 211
"KPA_96/ShopItemB": 0x1C8088C, # ErrCode 24 212
"KPA_96/ShopItemC": 0x1C80910, # ErrCode 24 213
"KPA_96/ShopItemD": 0x1C80994, # ErrCode 24 214
"KPA_96/ShopItemE": 0x1C80A18, # ErrCode 24 215
"KPA_96/ShopItemF": 0x1C80A9C, # ErrCode 24 216
}
if node_id in node_romoffset_map:
return node_romoffset_map[node_id]
else:
return -1