diff --git a/src/mars_patcher/constants/game_data.py b/src/mars_patcher/constants/game_data.py index 5359485..ec56065 100644 --- a/src/mars_patcher/constants/game_data.py +++ b/src/mars_patcher/constants/game_data.py @@ -14,7 +14,7 @@ def area_room_entry_ptrs(rom: Rom) -> int: elif rom.region == Region.C: return 0x77D5C0 elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.ROOM_AREA_ENTRIES_PTR) + return rom.read_ptr(ReservedPointersZM.ROOM_AREA_ENTRIES_PTR.value) raise ValueError("Rom has unknown game loaded.") @@ -31,7 +31,7 @@ def tileset_entries(rom: Rom) -> int: elif rom.region == Region.C: return 0x3C1E94 elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.TILESET_ENTRIES_PTR) + return rom.read_ptr(ReservedPointersZM.TILESET_ENTRIES_PTR.value) raise ValueError("Rom has unknown game loaded.") @@ -57,7 +57,7 @@ def area_doors_ptrs(rom: Rom) -> int: elif rom.region == Region.C: return 0x77D598 elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.AREA_DOORS_PTR) + return rom.read_ptr(ReservedPointersZM.AREA_DOORS_PTR.value) raise ValueError("Rom has unknown game loaded.") @@ -74,7 +74,7 @@ def area_connections(rom: Rom) -> int: elif rom.region == Region.C: return 0x3CB19C elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.AREA_CONNECTIONS_PTR) + return rom.read_ptr(ReservedPointersZM.AREA_CONNECTIONS_PTR.value) raise ValueError("Rom has unknown game loaded.") @@ -101,7 +101,7 @@ def anim_palette_entries(rom: Rom) -> int: elif rom.region == Region.C: return 0x3E5D7C elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.ANIM_PALETTE_ENTRIES_PTR) + return rom.read_ptr(ReservedPointersZM.ANIM_PALETTE_ENTRIES_PTR.value) raise ValueError("Rom has unknown game loaded.") @@ -130,7 +130,7 @@ def sprite_graphics_ptrs(rom: Rom) -> int: elif rom.region == Region.C: return 0x77C2DC elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.SPRITE_GRAPHICS_PTR) + return rom.read_ptr(ReservedPointersZM.SPRITE_GRAPHICS_PTR.value) raise ValueError(rom.game, rom.region) @@ -146,7 +146,7 @@ def sprite_palette_ptrs(rom: Rom) -> int: elif rom.region == Region.C: return 0x77C5D8 elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.SPRITE_PALETTES_PTR) + return rom.read_ptr(ReservedPointersZM.SPRITE_PALETTES_PTR.value) raise ValueError(rom.game, rom.region) @@ -171,7 +171,7 @@ def spriteset_ptrs(rom: Rom) -> int: elif rom.region == Region.C: return 0x77CADC elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.SPRITESET_PTR) + return rom.read_ptr(ReservedPointersZM.SPRITESET_PTR.value) raise ValueError(rom.game, rom.region) @@ -196,7 +196,7 @@ def samus_palettes(rom: Rom) -> list[tuple[int, int]]: elif rom.region == Region.C: return [(0x2900C8, 0x5E), (0x290E48, 0x70), (0x56CC68, 3)] elif rom.game == Game.ZM: - addr = rom.read_ptr(ReservedPointersZM.AREA_DOORS_PTR) + addr = rom.read_ptr(ReservedPointersZM.AREA_DOORS_PTR.value) return [(addr, 0xA3)] raise ValueError(rom.game, rom.region) @@ -216,7 +216,7 @@ def helmet_cursor_palettes(rom: Rom) -> list[tuple[int, int]]: elif rom.region == Region.C: return [(0x6CE360, 1), (0x6CE400, 2), (0x6CA8F8, 1), (0x6CA938, 2)] elif rom.game == Game.ZM: - addr = rom.read_ptr(ReservedPointersZM.HELMET_CURSOR_PALETTES_PTR) + addr = rom.read_ptr(ReservedPointersZM.HELMET_CURSOR_PALETTES_PTR.value) return [(addr, 1), (addr + 0x80, 1)] raise ValueError(rom.game, rom.region) @@ -233,7 +233,7 @@ def beam_palettes(rom: Rom) -> list[tuple[int, int]]: elif rom.region == Region.C: return [(0x592578, 6)] elif rom.game == Game.ZM: - addr = rom.read_ptr(ReservedPointersZM.BEAM_PALETTES_PTR) + addr = rom.read_ptr(ReservedPointersZM.BEAM_PALETTES_PTR.value) return [(addr, 6)] raise ValueError(rom.game, rom.region) @@ -250,7 +250,7 @@ def character_widths(rom: Rom) -> int: elif rom.region == Region.C: return 0x57D21C elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.CHARACTER_WIDTHS_PTR) + return rom.read_ptr(ReservedPointersZM.CHARACTER_WIDTHS_PTR.value) raise ValueError(rom.game, rom.region) @@ -266,7 +266,7 @@ def sound_data_entries(rom: Rom) -> int: elif rom.region == Region.C: return 0xAB0E4 elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.SOUND_DATA_PTR) + return rom.read_ptr(ReservedPointersZM.SOUND_DATA_PTR.value) raise ValueError(rom.game, rom.region) @@ -291,7 +291,7 @@ def minimap_ptrs(rom: Rom) -> int: elif rom.region == Region.C: return 0x77DB60 elif rom.game == Game.ZM: - return rom.read_ptr(ReservedPointersZM.MINIMAPS_PTR) + return rom.read_ptr(ReservedPointersZM.MINIMAPS_PTR.value) raise ValueError(rom.game, rom.region) diff --git a/src/mars_patcher/item_messages.py b/src/mars_patcher/item_messages.py new file mode 100644 index 0000000..932fcb1 --- /dev/null +++ b/src/mars_patcher/item_messages.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from enum import Enum, auto +from typing import ClassVar + +from frozendict import frozendict +from typing_extensions import Self + +from mars_patcher.mf.auto_generated_types import Itemmessages +from mars_patcher.text import Language + + +class ItemMessagesKind(Enum): + CUSTOM_MESSAGE = auto() + MESSAGE_ID = auto() + + +@dataclass(frozen=True) +class ItemMessages: + kind: ItemMessagesKind + item_messages: frozendict[Language, str] + centered: bool + message_id: int + + LANG_ENUMS: ClassVar[dict[str, Language]] = { + "JapaneseKanji": Language.JAPANESE_KANJI, + "JapaneseHiragana": Language.JAPANESE_HIRAGANA, + "English": Language.ENGLISH, + "German": Language.GERMAN, + "French": Language.FRENCH, + "Italian": Language.ITALIAN, + "Spanish": Language.SPANISH, + } + + KIND_ENUMS: ClassVar[dict[str, ItemMessagesKind]] = { + "CustomMessage": ItemMessagesKind.CUSTOM_MESSAGE, + "MessageID": ItemMessagesKind.MESSAGE_ID, + } + + @classmethod + def from_json(cls, data: Itemmessages) -> Self: + item_messages: dict[Language, str] = {} + centered = True + kind: ItemMessagesKind = cls.KIND_ENUMS[data["Kind"]] + message_id = 0 + if kind == ItemMessagesKind.CUSTOM_MESSAGE: + for lang_name, message in data["Languages"].items(): + lang = cls.LANG_ENUMS[lang_name] + item_messages[lang] = message + centered = data.get("Centered", True) + else: + message_id = data["MessageID"] + + return cls(kind, frozendict(item_messages), centered, message_id) diff --git a/src/mars_patcher/mf/constants/items.py b/src/mars_patcher/mf/constants/items.py index 2d6c176..7f4e704 100644 --- a/src/mars_patcher/mf/constants/items.py +++ b/src/mars_patcher/mf/constants/items.py @@ -114,18 +114,9 @@ class ItemSprite(Enum): KEY_ITEM: Final = "Item" KEY_ITEM_SPRITE: Final = "ItemSprite" KEY_ITEM_MESSAGES: Final = "ItemMessages" -KEY_ITEM_MESSAGES_KIND: Final = "Kind" -KEY_LANGUAGES: Final = "Languages" -KEY_CENTERED: Final = "Centered" -KEY_MESSAGE_ID: Final = "MessageID" KEY_ITEM_JINGLE: Final = "Jingle" -class ItemMessagesKind(Enum): - CUSTOM_MESSAGE = 0 - MESSAGE_ID = 1 - - class ItemJingle(Enum): MINOR = 0 MAJOR = 1 diff --git a/src/mars_patcher/mf/data.py b/src/mars_patcher/mf/data.py index 70c0149..5b55ed7 100644 --- a/src/mars_patcher/mf/data.py +++ b/src/mars_patcher/mf/data.py @@ -3,4 +3,4 @@ def get_data_path(*path: str | os.PathLike) -> str: - return os.fspath(Path(__file__).parent.joinpath("mf", "data", *path)) + return os.fspath(Path(__file__).parent.joinpath("data", *path)) diff --git a/src/mars_patcher/mf/door_locks.py b/src/mars_patcher/mf/door_locks.py index 36d3f9d..8ad1bf0 100644 --- a/src/mars_patcher/mf/door_locks.py +++ b/src/mars_patcher/mf/door_locks.py @@ -188,11 +188,7 @@ def factory() -> dict: # Map tiles if lock is not None: - screen_offset_x = (hatch_x - 2) // 15 - screen_offset_y = (hatch_y - 2) // 10 - - minimap_x = room_entry.map_x + screen_offset_x - minimap_y = room_entry.map_y + screen_offset_y + minimap_x, minimap_y = room_entry.map_coords_at_block(hatch_x, hatch_y) minimap_areas = [area] if area == 0: diff --git a/src/mars_patcher/mf/item_patcher.py b/src/mars_patcher/mf/item_patcher.py index ea6cf0e..663fe45 100644 --- a/src/mars_patcher/mf/item_patcher.py +++ b/src/mars_patcher/mf/item_patcher.py @@ -1,8 +1,7 @@ +from mars_patcher.item_messages import ItemMessages, ItemMessagesKind from mars_patcher.mf.auto_generated_types import MarsschemamfTankincrements from mars_patcher.mf.constants.reserved_space import ReservedConstantsMF from mars_patcher.mf.locations import ( - ItemMessages, - ItemMessagesKind, ItemSprite, ItemType, LocationSettings, diff --git a/src/mars_patcher/mf/locations.py b/src/mars_patcher/mf/locations.py index 8cfd424..2ef0444 100644 --- a/src/mars_patcher/mf/locations.py +++ b/src/mars_patcher/mf/locations.py @@ -1,11 +1,9 @@ -from __future__ import annotations - import json -from dataclasses import dataclass -from typing import TYPE_CHECKING, ClassVar -from frozendict import frozendict +from typing_extensions import Self +from mars_patcher.item_messages import ItemMessages +from mars_patcher.mf.auto_generated_types import MarsschemamfLocations from mars_patcher.mf.constants.items import ( ITEM_ENUMS, ITEM_SPRITE_ENUMS, @@ -13,32 +11,23 @@ KEY_AREA, KEY_BLOCK_X, KEY_BLOCK_Y, - KEY_CENTERED, KEY_HIDDEN, KEY_ITEM, KEY_ITEM_JINGLE, KEY_ITEM_MESSAGES, - KEY_ITEM_MESSAGES_KIND, KEY_ITEM_SPRITE, - KEY_LANGUAGES, KEY_MAJOR_LOCS, - KEY_MESSAGE_ID, KEY_MINOR_LOCS, KEY_ORIGINAL, KEY_ROOM, KEY_SOURCE, SOURCE_ENUMS, ItemJingle, - ItemMessagesKind, ItemSprite, ItemType, MajorSource, ) from mars_patcher.mf.data import get_data_path -from mars_patcher.text import Language - -if TYPE_CHECKING: - from mars_patcher.mf.auto_generated_types import Itemmessages, MarsschemamfLocations class Location: @@ -99,52 +88,13 @@ def __init__( self.item_jingle = item_jingle -@dataclass(frozen=True) -class ItemMessages: - kind: ItemMessagesKind - item_messages: frozendict[Language, str] - centered: bool - message_id: int - - LANG_ENUMS: ClassVar[dict[str, Language]] = { - "JapaneseKanji": Language.JAPANESE_KANJI, - "JapaneseHiragana": Language.JAPANESE_HIRAGANA, - "English": Language.ENGLISH, - "German": Language.GERMAN, - "French": Language.FRENCH, - "Italian": Language.ITALIAN, - "Spanish": Language.SPANISH, - } - - KIND_ENUMS: ClassVar[dict[str, ItemMessagesKind]] = { - "CustomMessage": ItemMessagesKind.CUSTOM_MESSAGE, - "MessageID": ItemMessagesKind.MESSAGE_ID, - } - - @classmethod - def from_json(cls, data: Itemmessages) -> ItemMessages: - item_messages: dict[Language, str] = {} - centered = True - kind: ItemMessagesKind = cls.KIND_ENUMS[data[KEY_ITEM_MESSAGES_KIND]] - message_id = 0 - if kind == ItemMessagesKind.CUSTOM_MESSAGE: - for lang_name, message in data[KEY_LANGUAGES].items(): - lang = cls.LANG_ENUMS[lang_name] - item_messages[lang] = message - centered = data.get(KEY_CENTERED, True) - else: - message_id = data[KEY_MESSAGE_ID] - - return cls(kind, frozendict(item_messages), centered, message_id) - - class LocationSettings: def __init__(self, major_locs: list[MajorLocation], minor_locs: list[MinorLocation]): self.major_locs = major_locs self.minor_locs = minor_locs @classmethod - def initialize(cls) -> LocationSettings: + def initialize(cls) -> Self: with open(get_data_path("locations.json")) as f: data = json.load(f) @@ -170,7 +120,7 @@ def initialize(cls) -> LocationSettings: ) minor_locs.append(minor_loc) - return LocationSettings(major_locs, minor_locs) + return cls(major_locs, minor_locs) def set_assignments(self, data: MarsschemamfLocations) -> None: for maj_loc_entry in data[KEY_MAJOR_LOCS]: diff --git a/src/mars_patcher/rom.py b/src/mars_patcher/rom.py index 4de3a6f..baceee5 100644 --- a/src/mars_patcher/rom.py +++ b/src/mars_patcher/rom.py @@ -101,9 +101,7 @@ def __init__(self, path: str | PathLike[str]): self.game = game self.region = region - # For now we only allow MF U - if self.game == Game.ZM: - raise ValueError("Not compatible with Metroid Zero Mission") + # For now we only allow (U) version if self.region != Region.U: raise ValueError("Only compatible with the North American (U) version") # Set free space address diff --git a/src/mars_patcher/room_entry.py b/src/mars_patcher/room_entry.py index e518cf2..29e427d 100644 --- a/src/mars_patcher/room_entry.py +++ b/src/mars_patcher/room_entry.py @@ -60,6 +60,11 @@ def map_x(self) -> int: def map_y(self) -> int: return self.rom.read_8(self.addr + 0x36) + def map_coords_at_block(self, block_x: int, block_y: int) -> tuple[int, int]: + x = self.map_x + ((block_x - 2) // 15) + y = self.map_y + ((block_y - 2) // 10) + return x, y + class BlockLayer: def __enter__(self) -> BlockLayer: diff --git a/src/mars_patcher/zm/auto_generated_types.py b/src/mars_patcher/zm/auto_generated_types.py index 0d9268c..b9b8e50 100644 --- a/src/mars_patcher/zm/auto_generated_types.py +++ b/src/mars_patcher/zm/auto_generated_types.py @@ -171,7 +171,7 @@ class ItemMessages(typ.TypedDict, total=False): 'BOMBS', 'ICE_BEAM', 'SPEED_BOOSTER', - 'HIGH_JUMP', + 'HI_JUMP', 'VARIA_SUIT', 'WAVE_BEAM', 'SCREW_ATTACK' @@ -191,15 +191,23 @@ class BlockLayerItem(typ.TypedDict, total=False): # Schema entries -class MarsschemazmLocationsMajorLocationsItem(typ.TypedDict): - source: ValidSources +class MarsschemazmLocationsMajorLocationsItem(typ.TypedDict, total=False): + source: typ.Required[ValidSources] """Valid major locations.""" - item: ValidItems + item: typ.Required[ValidItems] """Valid items for shuffling.""" - item_messages: typ.NotRequired[ItemMessages] + item_sprite: ValidItemSprites + """Valid graphics for item tanks/sprites.""" + + item_messages: ItemMessages jingle: Jingle + """The sound that plays when an item is collected""" + + hinted_by: HintLocations + """The hint location (Chozo statue) that hints to this item's location ('NONE' if not hinted by anything).""" + class MarsschemazmLocationsMinorLocationsItem(typ.TypedDict): area: AreaId @@ -221,7 +229,9 @@ class MarsschemazmLocationsMinorLocationsItem(typ.TypedDict): """Valid graphics for item tanks/sprites.""" item_messages: typ.NotRequired[ItemMessages] - jingle: Jingle + jingle: typ.NotRequired[Jingle] + """The sound that plays when an item is collected""" + hinted_by: typ.NotRequired[HintLocations] """The hint location (Chozo statue) that hints to this item's location ('None' if not hinted by anything).""" @@ -232,7 +242,7 @@ class MarsschemazmLocations(typ.TypedDict): major_locations: typ.Annotated[list[MarsschemazmLocationsMajorLocationsItem], 'len() == 16', 'Unique items'] """Specifies how the major item locations should be changed. A major item location is a location where an item is obtained from a sprite or interacting with a device.""" - minor_locations: typ.Annotated[list[MarsschemazmLocationsMinorLocationsItem], 'len() == 100', 'Unique items'] + minor_locations: typ.Annotated[list[MarsschemazmLocationsMinorLocationsItem], 'len() == 86', 'Unique items'] """Specifies how the minor item locations should be changed. A minor item location is a location where an item is obtained by touching a tank block. _tank clipdata is required at each location, the patcher does not modify any clipdata for minor locations.""" diff --git a/src/mars_patcher/zm/constants/game_data.py b/src/mars_patcher/zm/constants/game_data.py index 4308480..c635bf0 100644 --- a/src/mars_patcher/zm/constants/game_data.py +++ b/src/mars_patcher/zm/constants/game_data.py @@ -3,84 +3,72 @@ def tileset_tilemap_sizes_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.TILESET_TILEMAP_SIZES_PTR) + return rom.read_ptr(ReservedPointersZM.TILESET_TILEMAP_SIZES_PTR.value) def chozo_statue_targets_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.CHOZO_STATUE_TARGETS_PTR) + return rom.read_ptr(ReservedPointersZM.CHOZO_STATUE_TARGETS_PTR.value) def intro_cutscene_data_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.INTRO_CUTSCENE_DATA_PTR) + return rom.read_ptr(ReservedPointersZM.INTRO_CUTSCENE_DATA_PTR.value) def starting_info_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.STARTING_INFO_PTR) + return rom.read_ptr(ReservedPointersZM.STARTING_INFO_PTR.value) def major_locations_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.MAJOR_LOCATIONS_PTR) + return rom.read_ptr(ReservedPointersZM.MAJOR_LOCATIONS_PTR.value) def minor_locations_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.MINOR_LOCATIONS_PTR) + return rom.read_ptr(ReservedPointersZM.MINOR_LOCATIONS_PTR.value) def difficulty_options_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.DIFFICULTY_OPTIONS_PTR) + return rom.read_ptr(ReservedPointersZM.DIFFICULTY_OPTIONS_PTR.value) def metroid_sprite_stats_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.METROID_SPRITE_STATS_PTR) + return rom.read_ptr(ReservedPointersZM.METROID_SPRITE_STATS_PTR.value) def black_pirates_require_plasma_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.BLACK_PIRATES_REQUIRE_PLASMA_PTR) + return rom.read_ptr(ReservedPointersZM.BLACK_PIRATES_REQUIRE_PLASMA_PTR.value) def skip_door_transitions_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.SKIP_DOOR_TRANSITIONS_PTR) + return rom.read_ptr(ReservedPointersZM.SKIP_DOOR_TRANSITIONS_PTR.value) def ball_launcher_without_bombs_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.BALL_LAUNCHER_WITHOUT_BOMBS_PTR) + return rom.read_ptr(ReservedPointersZM.BALL_LAUNCHER_WITHOUT_BOMBS_PTR.value) def disable_midair_bomb_jump_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.DISABLE_MIDAIR_BOMB_JUMP_PTR) + return rom.read_ptr(ReservedPointersZM.DISABLE_MIDAIR_BOMB_JUMP_PTR.value) def disable_walljump_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.DISABLE_WALLJUMP_PTR) + return rom.read_ptr(ReservedPointersZM.DISABLE_WALLJUMP_PTR.value) def remove_cutscenes_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.REMOVE_CUTSCENES_PTR) + return rom.read_ptr(ReservedPointersZM.REMOVE_CUTSCENES_PTR.value) def skip_suitless_sequence_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.SKIP_SUITLESS_SEQUENCE_PTR) + return rom.read_ptr(ReservedPointersZM.SKIP_SUITLESS_SEQUENCE_PTR.value) -def energy_tank_increase_amount_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.ENERGY_TANK_INCREASE_AMOUNT_PTR) - - -def missile_tank_increase_amount_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.MISSILE_TANK_INCREASE_AMOUNT_PTR) - - -def super_missile_tank_increase_amount_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.SUPER_MISSILE_TANK_INCREASE_AMOUNT_PTR) - - -def power_bomb_tank_increase_amount_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.POWER_BOMB_TANK_INCREASE_AMOUNT_PTR) +def tank_increase_amounts_addr(rom: Rom) -> int: + return rom.read_ptr(ReservedPointersZM.TANK_INCREASE_AMOUNTS_PTR.value) def title_text_lines_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.TITLE_TEXT_LINES_PTR) + return rom.read_ptr(ReservedPointersZM.TITLE_TEXT_LINES_PTR.value) def statues_cutscene_palette_addr(rom: Rom) -> int: - return rom.read_ptr(ReservedPointersZM.STATUES_CUTSCENE_PALETTE_PTR) + return rom.read_ptr(ReservedPointersZM.STATUES_CUTSCENE_PALETTE_PTR.value) diff --git a/src/mars_patcher/zm/constants/items.py b/src/mars_patcher/zm/constants/items.py new file mode 100644 index 0000000..1b218c0 --- /dev/null +++ b/src/mars_patcher/zm/constants/items.py @@ -0,0 +1,115 @@ +from enum import Enum, IntEnum, auto + + +class MajorSource(IntEnum): + LONG_BEAM = 0 + CHARGE_BEAM = auto() + ICE_BEAM = auto() + WAVE_BEAM = auto() + PLASMA_BEAM = auto() + BOMBS = auto() + VARIA_SUIT = auto() + GRAVITY_SUIT = auto() + MORPH_BALL = auto() + SPEED_BOOSTER = auto() + HI_JUMP = auto() + SCREW_ATTACK = auto() + SPACE_JUMP = auto() + POWER_GRIP = auto() + FULLY_POWERED = auto() + ZIPLINES = auto() + + +class ItemType(IntEnum): + UNDEFINED = -1 + NONE = 0 + ENERGY_TANK = auto() + MISSILE_TANK = auto() + SUPER_MISSILE_TANK = auto() + POWER_BOMB_TANK = auto() + LONG_BEAM = auto() + CHARGE_BEAM = auto() + ICE_BEAM = auto() + WAVE_BEAM = auto() + PLASMA_BEAM = auto() + BOMBS = auto() + VARIA_SUIT = auto() + GRAVITY_SUIT = auto() + MORPH_BALL = auto() + SPEED_BOOSTER = auto() + HI_JUMP = auto() + SCREW_ATTACK = auto() + SPACE_JUMP = auto() + POWER_GRIP = auto() + FULLY_POWERED = auto() + ZIPLINES = auto() + ICE_TRAP = auto() + + +class ItemSprite(Enum): + DEFAULT = auto() + EMPTY = auto() + ENERGY_TANK = auto() + MISSILE_TANK = auto() + SUPER_MISSILE_TANK = auto() + POWER_BOMB_TANK = auto() + LONG_BEAM = auto() + CHARGE_BEAM = auto() + ICE_BEAM = auto() + WAVE_BEAM = auto() + PLASMA_BEAM = auto() + BOMBS = auto() + VARIA_SUIT = auto() + GRAVITY_SUIT = auto() + MORPH_BALL = auto() + SPEED_BOOSTER = auto() + HI_JUMP = auto() + SCREW_ATTACK = auto() + SPACE_JUMP = auto() + POWER_GRIP = auto() + FULLY_POWERED = auto() + ZIPLINES = auto() + ANONYMOUS = auto() + SHINY_MISSILE_TANK = auto() + SHINY_POWER_BOMB_TANK = auto() + + +class ItemJingle(IntEnum): + DEFAULT = 0 + MINOR = auto() + MAJOR = auto() + UNKNOWN = auto() + FULLY_POWERED = auto() + + +class HintLocation(IntEnum): + NONE = -1 + LONG_BEAM = 0 + BOMBS = auto() + ICE_BEAM = auto() + SPEED_BOOSTER = auto() + HI_JUMP = auto() + VARIA_SUIT = auto() + WAVE_BEAM = auto() + SCREW_ATTACK = auto() + + +BEAM_BOMB_FLAGS = { + "LONG_BEAM": 1 << 0, + "ICE_BEAM": 1 << 1, + "WAVE_BEAM": 1 << 2, + "PLASMA_BEAM": 1 << 3, + "CHARGE_BEAM": 1 << 4, + "BOMBS": 1 << 7, +} + +SUIT_MISC_FLAGS = { + "HI_JUMP": 1 << 0, + "SPEED_BOOSTER": 1 << 1, + "SPACE_JUMP": 1 << 2, + "SCREW_ATTACK": 1 << 3, + "VARIA_SUIT": 1 << 4, + "GRAVITY_SUIT": 1 << 5, + "MORPH_BALL": 1 << 6, + "POWER_GRIP": 1 << 7, +} diff --git a/src/mars_patcher/zm/constants/reserved_space.py b/src/mars_patcher/zm/constants/reserved_space.py index 9c81f1e..628065a 100644 --- a/src/mars_patcher/zm/constants/reserved_space.py +++ b/src/mars_patcher/zm/constants/reserved_space.py @@ -21,7 +21,7 @@ class ReservedConstantsZM: # Address for any additional data that the patcher may need to write PATCHER_FREE_SPACE_ADDR = 0x7C0000 - PATCHER_FREE_SPACE_END = RANDO_POINTERS_ADDR - PATCHER_FREE_SPACE_ADDR + PATCHER_FREE_SPACE_END = RANDO_POINTERS_ADDR class ReservedPointersZM(IntEnum): @@ -86,14 +86,11 @@ class ReservedPointersZM(IntEnum): REMOVE_CUTSCENES_PTR = auto() SKIP_SUITLESS_SEQUENCE_PTR = auto() - ENERGY_TANK_INCREASE_AMOUNT_PTR = auto() - MISSILE_TANK_INCREASE_AMOUNT_PTR = auto() - SUPER_MISSILE_TANK_INCREASE_AMOUNT_PTR = auto() - POWER_BOMB_TANK_INCREASE_AMOUNT_PTR = auto() + TANK_INCREASE_AMOUNTS_PTR = auto() TITLE_TEXT_LINES_PTR = auto() def __new__(cls, offset: int) -> Self: - obj = object.__new__(cls) + obj = int.__new__(cls) obj._value_ = ReservedConstantsZM.RANDO_POINTERS_ADDR + (offset * 4) return obj diff --git a/src/mars_patcher/zm/data.py b/src/mars_patcher/zm/data.py index 145f897..5b55ed7 100644 --- a/src/mars_patcher/zm/data.py +++ b/src/mars_patcher/zm/data.py @@ -3,4 +3,4 @@ def get_data_path(*path: str | os.PathLike) -> str: - return os.fspath(Path(__file__).parent.joinpath("zm", "data", *path)) + return os.fspath(Path(__file__).parent.joinpath("data", *path)) diff --git a/src/mars_patcher/zm/data/locations.json b/src/mars_patcher/zm/data/locations.json new file mode 100644 index 0000000..818f873 --- /dev/null +++ b/src/mars_patcher/zm/data/locations.json @@ -0,0 +1,822 @@ +{ + "major_locations": [ + { + "area": 0, + "room": 5, + "map_x": 6, + "map_y": 6, + "source": "LONG_BEAM", + "original": "LONG_BEAM" + }, + { + "area": 0, + "room": 12, + "map_x": 10, + "map_y": 12, + "source": "CHARGE_BEAM", + "original": "CHARGE_BEAM" + }, + { + "area": 2, + "room": 8, + "map_x": 18, + "map_y": 3, + "source": "ICE_BEAM", + "original": "ICE_BEAM" + }, + { + "area": 2, + "room": 27, + "map_x": 10, + "map_y": 12, + "source": "WAVE_BEAM", + "original": "WAVE_BEAM" + }, + { + "area": 5, + "room": 14, + "map_x": 20, + "map_y": 5, + "source": "PLASMA_BEAM", + "original": "PLASMA_BEAM" + }, + { + "area": 0, + "room": 25, + "map_x": 24, + "map_y": 6, + "source": "BOMBS", + "original": "BOMBS" + }, + { + "area": 0, + "room": 27, + "map_x": 14, + "map_y": 2, + "source": "VARIA_SUIT", + "original": "VARIA_SUIT" + }, + { + "area": 3, + "room": 13, + "map_x": 6, + "map_y": 7, + "source": "GRAVITY_SUIT", + "original": "GRAVITY_SUIT" + }, + { + "area": 0, + "room": 0, + "map_x": 0, + "map_y": 15, + "source": "MORPH_BALL", + "original": "MORPH_BALL" + }, + { + "area": 1, + "room": 34, + "map_x": 8, + "map_y": 15, + "source": "SPEED_BOOSTER", + "original": "SPEED_BOOSTER" + }, + { + "area": 2, + "room": 13, + "map_x": 19, + "map_y": 8, + "source": "HI_JUMP", + "original": "HI_JUMP" + }, + { + "area": 2, + "room": 18, + "map_x": 6, + "map_y": 7, + "source": "SCREW_ATTACK", + "original": "SCREW_ATTACK" + }, + { + "area": 1, + "room": 33, + "map_x": 7, + "map_y": 14, + "source": "SPACE_JUMP", + "original": "SPACE_JUMP" + }, + { + "area": 5, + "room": 12, + "map_x": 14, + "map_y": 6, + "source": "POWER_GRIP", + "original": "POWER_GRIP" + }, + { + "area": 6, + "room": 42, + "map_x": 6, + "map_y": 5, + "source": "FULLY_POWERED", + "original": "FULLY_POWERED" + }, + { + "area": 1, + "room": 4, + "map_x": 12, + "map_y": 2, + "source": "ZIPLINES", + "original": "ZIPLINES" + } + ], + "minor_locations": [ + { + "area": 0, + "room": 1, + "block_x": 13, + "block_y": 7, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 2, + "block_x": 28, + "block_y": 2, + "hidden": true, + "original": "ENERGY_TANK" + }, + { + "area": 0, + "room": 12, + "block_x": 54, + "block_y": 6, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 14, + "block_x": 14, + "block_y": 23, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 15, + "block_x": 4, + "block_y": 6, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 19, + "block_x": 39, + "block_y": 6, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 0, + "room": 19, + "block_x": 11, + "block_y": 10, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 21, + "block_x": 39, + "block_y": 5, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 23, + "block_x": 18, + "block_y": 16, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 0, + "room": 25, + "block_x": 11, + "block_y": 5, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 29, + "block_x": 4, + "block_y": 10, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 40, + "block_x": 7, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 41, + "block_x": 5, + "block_y": 18, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 0, + "room": 41, + "block_x": 5, + "block_y": 25, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 1, + "room": 1, + "block_x": 24, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 2, + "block_x": 9, + "block_y": 33, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 4, + "block_x": 22, + "block_y": 6, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 1, + "room": 7, + "block_x": 38, + "block_y": 14, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 8, + "block_x": 74, + "block_y": 20, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 9, + "block_x": 60, + "block_y": 9, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 1, + "room": 10, + "block_x": 9, + "block_y": 9, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 17, + "block_x": 2, + "block_y": 4, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 21, + "block_x": 20, + "block_y": 3, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 26, + "block_x": 7, + "block_y": 10, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 1, + "room": 38, + "block_x": 5, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 1, + "block_x": 65, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 3, + "block_x": 72, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 4, + "block_x": 74, + "block_y": 9, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 5, + "block_x": 14, + "block_y": 79, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 5, + "block_x": 8, + "block_y": 111, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 10, + "block_x": 11, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 17, + "block_x": 17, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 28, + "block_x": 28, + "block_y": 3, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 28, + "block_x": 54, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 32, + "block_x": 45, + "block_y": 3, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 2, + "room": 32, + "block_x": 4, + "block_y": 5, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 37, + "block_x": 21, + "block_y": 3, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 38, + "block_x": 5, + "block_y": 6, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 2, + "room": 42, + "block_x": 33, + "block_y": 5, + "hidden": true, + "original": "ENERGY_TANK" + }, + { + "area": 2, + "room": 47, + "block_x": 24, + "block_y": 3, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 2, + "room": 55, + "block_x": 8, + "block_y": 14, + "hidden": false, + "original": "POWER_BOMB_TANK" + }, + { + "area": 2, + "room": 55, + "block_x": 30, + "block_y": 23, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 4, + "block_x": 6, + "block_y": 8, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 3, + "room": 6, + "block_x": 8, + "block_y": 33, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 9, + "block_x": 9, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 10, + "block_x": 27, + "block_y": 6, + "hidden": true, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 3, + "room": 10, + "block_x": 15, + "block_y": 15, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 13, + "block_x": 8, + "block_y": 7, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 3, + "room": 14, + "block_x": 27, + "block_y": 9, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 3, + "room": 16, + "block_x": 54, + "block_y": 6, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 17, + "block_x": 28, + "block_y": 20, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 18, + "block_x": 72, + "block_y": 6, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 19, + "block_x": 7, + "block_y": 21, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 3, + "room": 22, + "block_x": 11, + "block_y": 6, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 22, + "block_x": 8, + "block_y": 16, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 3, + "room": 23, + "block_x": 8, + "block_y": 4, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 23, + "block_x": 13, + "block_y": 13, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 29, + "block_x": 24, + "block_y": 3, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 29, + "block_x": 20, + "block_y": 15, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 30, + "block_x": 4, + "block_y": 13, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 3, + "room": 31, + "block_x": 42, + "block_y": 7, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 4, + "room": 7, + "block_x": 14, + "block_y": 8, + "hidden": false, + "original": "POWER_BOMB_TANK" + }, + { + "area": 4, + "room": 8, + "block_x": 11, + "block_y": 109, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 5, + "room": 0, + "block_x": 20, + "block_y": 37, + "hidden": false, + "original": "POWER_BOMB_TANK" + }, + { + "area": 5, + "room": 7, + "block_x": 3, + "block_y": 27, + "hidden": true, + "original": "MISSILE_TANK" + }, + { + "area": 5, + "room": 9, + "block_x": 90, + "block_y": 9, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 5, + "room": 9, + "block_x": 64, + "block_y": 34, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 5, + "room": 14, + "block_x": 8, + "block_y": 10, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 6, + "room": 10, + "block_x": 19, + "block_y": 4, + "hidden": true, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 14, + "block_x": 13, + "block_y": 5, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 24, + "block_x": 10, + "block_y": 13, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 6, + "room": 26, + "block_x": 44, + "block_y": 8, + "hidden": false, + "original": "POWER_BOMB_TANK" + }, + { + "area": 6, + "room": 34, + "block_x": 34, + "block_y": 14, + "hidden": true, + "original": "POWER_BOMB_TANK" + }, + { + "area": 6, + "room": 47, + "block_x": 9, + "block_y": 17, + "hidden": false, + "original": "POWER_BOMB_TANK" + }, + { + "area": 6, + "room": 49, + "block_x": 10, + "block_y": 7, + "hidden": false, + "original": "POWER_BOMB_TANK" + }, + { + "area": 6, + "room": 54, + "block_x": 59, + "block_y": 20, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 65, + "block_x": 9, + "block_y": 3, + "hidden": true, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 66, + "block_x": 16, + "block_y": 13, + "hidden": true, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 71, + "block_x": 59, + "block_y": 19, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 6, + "room": 73, + "block_x": 9, + "block_y": 6, + "hidden": true, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 78, + "block_x": 44, + "block_y": 8, + "hidden": false, + "original": "ENERGY_TANK" + }, + { + "area": 6, + "room": 87, + "block_x": 18, + "block_y": 18, + "hidden": true, + "original": "POWER_BOMB_TANK" + }, + { + "area": 6, + "room": 89, + "block_x": 6, + "block_y": 27, + "hidden": true, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 90, + "block_x": 56, + "block_y": 24, + "hidden": false, + "original": "MISSILE_TANK" + }, + { + "area": 6, + "room": 90, + "block_x": 56, + "block_y": 40, + "hidden": false, + "original": "SUPER_MISSILE_TANK" + }, + { + "area": 6, + "room": 95, + "block_x": 24, + "block_y": 6, + "hidden": false, + "original": "POWER_BOMB_TANK" + } + ] +} diff --git a/src/mars_patcher/zm/data/schema.json b/src/mars_patcher/zm/data/schema.json index afe7bb3..adb6547 100644 --- a/src/mars_patcher/zm/data/schema.json +++ b/src/mars_patcher/zm/data/schema.json @@ -28,25 +28,31 @@ "item": { "$ref": "#/$defs/valid_items" }, + "item_sprite": { + "$ref": "#/$defs/valid_item_sprites" + }, "item_messages": { "$ref": "#/$defs/item_messages" }, "jingle": { "$ref": "#/$defs/jingle" + }, + "hinted_by": { + "$ref": "#/$defs/hint_locations", + "description": "The hint location (Chozo statue) that hints to this item's location ('NONE' if not hinted by anything)." } }, "required": [ "source", - "item", - "jingle" + "item" ] } }, "minor_locations": { "type": "array", "description": "Specifies how the minor item locations should be changed. A minor item location is a location where an item is obtained by touching a tank block. _tank clipdata is required at each location, the patcher does not modify any clipdata for minor locations.", - "minItems": 100, - "maxItems": 100, + "minItems": 86, + "maxItems": 86, "uniqueItems": true, "items": { "type": "object", @@ -89,8 +95,7 @@ "room", "block_x", "block_y", - "item", - "jingle" + "item" ] } } @@ -842,6 +847,7 @@ }, "jingle": { "type": "string", + "description": "The sound that plays when an item is collected", "enum": ["DEFAULT", "MINOR", "MAJOR", "UNKNOWN", "FULLY_POWERED"] }, "hint_locations": { @@ -852,7 +858,7 @@ "BOMBS", "ICE_BEAM", "SPEED_BOOSTER", - "HIGH_JUMP", + "HI_JUMP", "VARIA_SUIT", "WAVE_BEAM", "SCREW_ATTACK" diff --git a/src/mars_patcher/zm/item_patcher.py b/src/mars_patcher/zm/item_patcher.py new file mode 100644 index 0000000..3c49560 --- /dev/null +++ b/src/mars_patcher/zm/item_patcher.py @@ -0,0 +1,77 @@ +from mars_patcher.rom import Rom +from mars_patcher.room_entry import RoomEntry +from mars_patcher.zm.auto_generated_types import MarsschemazmTankIncrements +from mars_patcher.zm.constants.game_data import ( + chozo_statue_targets_addr, + major_locations_addr, + minor_locations_addr, + tank_increase_amounts_addr, +) +from mars_patcher.zm.locations import HintLocation, LocationSettings + + +class ItemPatcher: + """Class for writing item assignments to a ROM.""" + + def __init__(self, rom: Rom, settings: LocationSettings): + self.rom = rom + self.settings = settings + + def write_items(self) -> None: + rom = self.rom + hint_targets_addr = chozo_statue_targets_addr(rom) + + # Handle minor locations + # Locations need to be written in order so that binary search works + minor_locs = sorted(self.settings.minor_locs, key=lambda x: x.key) + minor_loc_addr = minor_locations_addr(rom) + for min_loc in minor_locs: + # TODO: Update tileset and get BG1 value + bg1_val = 0x4A # Use power bomb tank block for now + # Overwrite BG1 if not hidden + if not min_loc.hidden: + room = RoomEntry(rom, min_loc.area, min_loc.room) + with room.load_bg1() as bg1: + bg1.set_block_value(min_loc.block_x, min_loc.block_y, bg1_val) + + # See struct MinorLocation in include/structs/randomizer.h + rom.write_32(minor_loc_addr, min_loc.key) + rom.write_16(minor_loc_addr + 4, bg1_val) + rom.write_8(minor_loc_addr + 6, min_loc.new_item.value) + rom.write_8(minor_loc_addr + 7, min_loc.item_jingle.value) + # TODO: Handle custom messages + rom.write_32(minor_loc_addr + 8, 0) + rom.write_8(minor_loc_addr + 0xC, min_loc.hint_value) + minor_loc_addr += 0x10 + + if min_loc.hinted_by != HintLocation.NONE: + room = RoomEntry(rom, min_loc.area, min_loc.room) + map_x, map_y = room.map_coords_at_block(min_loc.block_x, min_loc.block_y) + target_addr = hint_targets_addr + (min_loc.hinted_by.value * 0xC) + rom.write_8(target_addr + 6, min_loc.area) + rom.write_8(target_addr + 7, map_x) + rom.write_8(target_addr + 8, map_y) + + # Handle major locations + major_locs_addr = major_locations_addr(rom) + for maj_loc in self.settings.major_locs: + addr = major_locs_addr + (maj_loc.major_src.value * 8) + rom.write_8(addr, maj_loc.new_item.value) + rom.write_8(addr + 1, maj_loc.item_jingle.value) + rom.write_8(addr + 2, maj_loc.hint_value) + # TODO: Handle custom messages + rom.write_32(addr + 4, 0) + + if maj_loc.hinted_by != HintLocation.NONE: + target_addr = hint_targets_addr + (maj_loc.hinted_by.value * 0xC) + rom.write_8(target_addr + 6, maj_loc.area) + rom.write_8(target_addr + 7, maj_loc.map_x) + rom.write_8(target_addr + 8, maj_loc.map_y) + + +def set_tank_increments(rom: Rom, data: MarsschemazmTankIncrements) -> None: + addr = tank_increase_amounts_addr(rom) + rom.write_16(addr, data["energy_tank"]) + rom.write_16(addr + 2, data["missile_tank"]) + rom.write_8(addr + 4, data["super_missile_tank"]) + rom.write_8(addr + 5, data["power_bomb_tank"]) diff --git a/src/mars_patcher/zm/locations.py b/src/mars_patcher/zm/locations.py new file mode 100644 index 0000000..bad0d5e --- /dev/null +++ b/src/mars_patcher/zm/locations.py @@ -0,0 +1,194 @@ +import json +from typing import TypeAlias + +from typing_extensions import Self + +from mars_patcher.item_messages import ItemMessages +from mars_patcher.zm.auto_generated_types import ( + MarsschemazmLocations, + MarsschemazmLocationsMajorLocationsItem, + MarsschemazmLocationsMinorLocationsItem, +) +from mars_patcher.zm.constants.items import ( + HintLocation, + ItemJingle, + ItemSprite, + ItemType, + MajorSource, +) +from mars_patcher.zm.data import get_data_path + +MarsSchemaZmLocation: TypeAlias = ( + MarsschemazmLocationsMajorLocationsItem | MarsschemazmLocationsMinorLocationsItem +) + + +class Location: + def __init__( + self, + area: int, + room: int, + orig_item: ItemType, + new_item: ItemType = ItemType.UNDEFINED, + item_sprite: ItemSprite = ItemSprite.DEFAULT, + item_messages: ItemMessages | None = None, + item_jingle: ItemJingle = ItemJingle.DEFAULT, + hinted_by: HintLocation = HintLocation.NONE, + ): + if type(self) is Location: + raise TypeError() + self.area = area + self.room = room + self.orig_item = orig_item + self.new_item = new_item + self.item_sprite = item_sprite + self.item_messages = item_messages + self.item_jingle = item_jingle + self.hinted_by = hinted_by + + def __str__(self) -> str: + item_str = self.orig_item.name + item_str += "/" + self.new_item.name + return f"{self.area},0x{self.room:02X}: {item_str}" + + @property + def hint_value(self) -> int: + return 0xFF if self.hinted_by == HintLocation.NONE else self.hinted_by.value + + +class MajorLocation(Location): + def __init__( + self, + area: int, + room: int, + map_x: int, + map_y: int, + major_src: MajorSource, + orig_item: ItemType, + new_item: ItemType = ItemType.UNDEFINED, + item_sprite: ItemSprite = ItemSprite.DEFAULT, + item_messages: ItemMessages | None = None, + item_jingle: ItemJingle = ItemJingle.DEFAULT, + hinted_by: HintLocation = HintLocation.NONE, + ): + super().__init__( + area, room, orig_item, new_item, item_sprite, item_messages, item_jingle, hinted_by + ) + self.map_x = map_x + self.map_y = map_y + self.major_src = major_src + + +class MinorLocation(Location): + def __init__( + self, + area: int, + room: int, + block_x: int, + block_y: int, + hidden: bool, + orig_item: ItemType, + new_item: ItemType = ItemType.UNDEFINED, + item_sprite: ItemSprite = ItemSprite.DEFAULT, + item_messages: ItemMessages | None = None, + item_jingle: ItemJingle = ItemJingle.DEFAULT, + hinted_by: HintLocation = HintLocation.NONE, + ): + super().__init__( + area, room, orig_item, new_item, item_sprite, item_messages, item_jingle, hinted_by + ) + self.block_x = block_x + self.block_y = block_y + self.hidden = hidden + + @property + def key(self) -> int: + # See MINOR_LOC_KEY macro in include/randomizer.h + return (self.area << 24) | (self.room << 16) | (self.block_y << 8) | self.block_x + + +class LocationSettings: + def __init__(self, major_locs: list[MajorLocation], minor_locs: list[MinorLocation]): + self.major_locs = major_locs + self.minor_locs = minor_locs + + @classmethod + def initialize(cls) -> Self: + with open(get_data_path("locations.json")) as f: + data = json.load(f) + + major_locs = [] + for entry in data["major_locations"]: + major_loc = MajorLocation( + entry["area"], + entry["room"], + entry["map_x"], + entry["map_y"], + MajorSource[entry["source"]], + ItemType[entry["original"]], + ) + major_locs.append(major_loc) + + minor_locs = [] + for entry in data["minor_locations"]: + minor_loc = MinorLocation( + entry["area"], + entry["room"], + entry["block_x"], + entry["block_y"], + entry["hidden"], + ItemType[entry["original"]], + ) + minor_locs.append(minor_loc) + + return cls(major_locs, minor_locs) + + def set_assignments(self, data: MarsschemazmLocations) -> None: + for maj_loc_entry in data["major_locations"]: + source = MajorSource[maj_loc_entry["source"]] + # Find location with this source + try: + maj_loc = next(m for m in self.major_locs if m.major_src == source) + except StopIteration: + raise ValueError(f"Invalid major location: Source {source}") + LocationSettings.set_location_data(maj_loc, maj_loc_entry) + + for min_loc_entry in data["minor_locations"]: + # Get area, room, block X, block Y + area = min_loc_entry["area"] + room = min_loc_entry["room"] + block_x = min_loc_entry["block_x"] + block_y = min_loc_entry["block_y"] + # Find location with this area, room, and position + try: + min_loc = next( + m + for m in self.minor_locs + if m.area == area + and m.room == room + and m.block_x == block_x + and m.block_y == block_y + ) + except StopIteration: + raise ValueError( + f"Invalid minor location: Area {area}, Room {room}, X {block_x}, Y {block_y}" + ) + LocationSettings.set_location_data(min_loc, min_loc_entry) + + @classmethod + def set_location_data(cls, loc_obj: Location, loc_entry: MarsSchemaZmLocation) -> None: + """Sets item, item sprite, custom message (if any), jingle, and hint + on a major or minor location.""" + loc_obj.new_item = ItemType[loc_entry["item"]] + if "item_sprite" in loc_entry: + loc_obj.item_sprite = ItemSprite[loc_entry["item_sprite"]] + # if "ItemMessages" in loc_entry: + # loc_obj.item_messages = ItemMessages.from_json(loc_entry["ItemMessages"]) + if "jingle" in loc_entry: + loc_obj.item_jingle = ItemJingle[loc_entry["jingle"]] + else: + loc_obj.item_jingle = ItemJingle.DEFAULT + if "hinted_by" in loc_entry: + loc_obj.hinted_by = HintLocation[loc_entry["hinted_by"]] + else: + loc_obj.hinted_by = HintLocation.NONE diff --git a/src/mars_patcher/zm/patcher.py b/src/mars_patcher/zm/patcher.py index 6ca789b..e41a0b1 100644 --- a/src/mars_patcher/zm/patcher.py +++ b/src/mars_patcher/zm/patcher.py @@ -3,6 +3,9 @@ from mars_patcher.rom import Rom from mars_patcher.zm.auto_generated_types import MarsSchemaZM +from mars_patcher.zm.constants.game_data import skip_door_transitions_addr +from mars_patcher.zm.item_patcher import ItemPatcher, set_tank_increments +from mars_patcher.zm.locations import LocationSettings def patch_zm( @@ -36,14 +39,11 @@ def patch_zm( # pal_randomizer.randomize() # Load locations and set assignments - # status_update("Writing item assignments...", -1) - # loc_settings = LocationSettings.initialize() - # loc_settings.set_assignments(patch_data["Locations"]) - # item_patcher = ItemPatcher(rom, loc_settings) - # item_patcher.write_items() - - # Required metroid count - # set_required_metroid_count(rom, patch_data["RequiredMetroidCount"]) + status_update("Writing item assignments...", -1) + loc_settings = LocationSettings.initialize() + loc_settings.set_assignments(patch_data["locations"]) + item_patcher = ItemPatcher(rom, loc_settings) + item_patcher.write_items() # Starting location # if "StartingLocation" in patch_data: @@ -56,9 +56,9 @@ def patch_zm( # set_starting_items(rom, patch_data["StartingItems"]) # Tank increments - # if "TankIncrements" in patch_data: - # status_update("Writing tank increments...", -1) - # set_tank_increments(rom, patch_data["TankIncrements"]) + if "tank_increments" in patch_data: + status_update("Writing tank increments...", -1) + set_tank_increments(rom, patch_data["tank_increments"]) # Elevator connections # conns = None @@ -67,9 +67,6 @@ def patch_zm( # conns = Connections(rom) # conns.set_elevator_connections(patch_data["ElevatorConnections"]) - # Hints - # TODO - # Room Names # if room_names := patch_data.get("RoomNames", []): # status_update("Writing room names...", -1) @@ -87,8 +84,8 @@ def patch_zm( # if patch_data.get("DisableDemos"): # disable_demos(rom) - # if patch_data.get("SkipDoorTransitions"): - # skip_door_transitions(rom) + if patch_data.get("skip_door_transitions"): + rom.write_8(skip_door_transitions_addr(rom), 1) # if patch_data.get("StereoDefault", True): # stereo_default(rom)