diff --git a/src/mars_patcher/zm/auto_generated_types.py b/src/mars_patcher/zm/auto_generated_types.py index 69b5e51..05ed395 100644 --- a/src/mars_patcher/zm/auto_generated_types.py +++ b/src/mars_patcher/zm/auto_generated_types.py @@ -351,7 +351,7 @@ class MarsschemazmStartingItems(typ.TypedDict, total=False): downloaded_maps: typ.Annotated[list[AreaId], 'Unique items'] = [] """Which area maps will be downloaded from the start.""" - suit_type: MarsschemazmStartingItemsSuitType = 'normal' + suit_type: MarsschemazmStartingItemsSuitType = 'NORMAL' """Which suit type the player should start with.""" ziplines_activated: bool = False diff --git a/src/mars_patcher/zm/constants/items.py b/src/mars_patcher/zm/constants/items.py index de378b1..d4e6388 100644 --- a/src/mars_patcher/zm/constants/items.py +++ b/src/mars_patcher/zm/constants/items.py @@ -179,3 +179,9 @@ class HintLocation(IntEnum): "MORPH_BALL": 1 << 6, "POWER_GRIP": 1 << 7, } + + +class SuitType(IntEnum): + NORMAL = 0 + FULLY_POWERED = auto() + SUITLESS = auto() diff --git a/src/mars_patcher/zm/constants/sprites.py b/src/mars_patcher/zm/constants/sprites.py index 52ee908..98f6654 100644 --- a/src/mars_patcher/zm/constants/sprites.py +++ b/src/mars_patcher/zm/constants/sprites.py @@ -47,6 +47,7 @@ class SpriteIdZM(IntEnum): RINKA_ORANGE = 0x66 VIOLA_BLUE = 0x68 VIOLA_ORANGE = 0x69 + SAVE_PLATFORM = 0x6E KRAID = 0x6F IMAGO_COCOON_AFTER_FIGHT = 0x70 RIPPER_II = 0x71 @@ -56,6 +57,7 @@ class SpriteIdZM(IntEnum): IMAGO_LARVA_RIGHT_SIDE = 0x7F IMAGO = 0x86 IMAGO_LARVA_LEFT = 0x8B + SAVE_PLATFORM_CHOZODIA = 0x92 PLASMA_BEAM_CHOZO_STATUE = 0x94 KRAID_ELEVATOR_STATUE = 0x95 RIDLEY_ELEVATOR_STATUE = 0x96 diff --git a/src/mars_patcher/zm/data/schema.json b/src/mars_patcher/zm/data/schema.json index 0bed6c3..e4b008c 100644 --- a/src/mars_patcher/zm/data/schema.json +++ b/src/mars_patcher/zm/data/schema.json @@ -190,7 +190,7 @@ "NORMAL", "FULLY_POWERED" ], - "default": "normal" + "default": "NORMAL" }, "ziplines_activated": { "type": "boolean", diff --git a/src/mars_patcher/zm/patcher.py b/src/mars_patcher/zm/patcher.py index 22f6a85..88ddc37 100644 --- a/src/mars_patcher/zm/patcher.py +++ b/src/mars_patcher/zm/patcher.py @@ -8,6 +8,7 @@ from mars_patcher.zm.constants.reserved_space import ReservedConstantsZM from mars_patcher.zm.item_patcher import ItemPatcher, set_tank_increments from mars_patcher.zm.locations import LocationSettings +from mars_patcher.zm.starting import set_starting_items, set_starting_location def patch_zm( @@ -47,21 +48,21 @@ def patch_zm( item_patcher = ItemPatcher(rom, loc_settings) item_patcher.write_items() - # Starting location - # if "StartingLocation" in patch_data: - # status_update("Writing starting location...", -1) - # set_starting_location(rom, patch_data["StartingLocation"]) - - # Starting items - # if "StartingItems" in patch_data: - # status_update("Writing starting items...", -1) - # set_starting_items(rom, patch_data["StartingItems"]) - # Music if "MusicReplacement" in patch_data: status_update("Writing music...", -1) set_sounds(rom, patch_data["MusicReplacement"]) + # Starting location + if "starting_location" in patch_data: + status_update("Writing starting location...", -1) + set_starting_location(rom, patch_data["starting_location"]) + + # Starting items + if "starting_items" in patch_data: + status_update("Writing starting items...", -1) + set_starting_items(rom, patch_data["starting_items"]) + # Tank increments if "tank_increments" in patch_data: status_update("Writing tank increments...", -1) diff --git a/src/mars_patcher/zm/starting.py b/src/mars_patcher/zm/starting.py new file mode 100644 index 0000000..d17e86f --- /dev/null +++ b/src/mars_patcher/zm/starting.py @@ -0,0 +1,127 @@ +from mars_patcher.constants.game_data import area_doors_ptrs, spriteset_ptrs +from mars_patcher.rom import Rom +from mars_patcher.room_entry import RoomEntry +from mars_patcher.zm.auto_generated_types import ( + MarsschemazmStartingItems, + MarsschemazmStartingLocation, +) +from mars_patcher.zm.constants.items import BEAM_BOMB_FLAGS, SUIT_MISC_FLAGS, SuitType +from mars_patcher.zm.constants.reserved_space import ReservedPointersZM +from mars_patcher.zm.constants.sprites import SpriteIdZM + + +def set_starting_location(rom: Rom, data: MarsschemazmStartingLocation) -> None: + area = data["area"] + room = data["room"] + # Don't do anything for area 0 room 0 + if area == 0 and room == 0: + return + # Find any door in the provided room + door = find_door_in_room(rom, area, room) + # Check if save pad in room + pos = find_save_pad_position(rom, area, room) + if pos is not None: + x_pos, y_pos = pos + else: + x_pos = data["block_x"] + y_pos = data["block_y"] + # Write to rom (see struct StartingInfo in structs/randomizer.h) + starting_info_addr = rom.read_ptr(ReservedPointersZM.STARTING_INFO_PTR.value) + rom.write_8(starting_info_addr, area) + rom.write_8(starting_info_addr + 1, room) + rom.write_8(starting_info_addr + 2, door) + rom.write_8(starting_info_addr + 3, x_pos) + rom.write_8(starting_info_addr + 4, y_pos) + # See struct InGameCutsceneData in structs/in_game_cutscene.h + intro_cutscene_addr = rom.read_ptr(ReservedPointersZM.INTRO_CUTSCENE_DATA_PTR.value) + rom.write_8(intro_cutscene_addr + 1, area) + + +def find_door_in_room(rom: Rom, area: int, room: int) -> int: + door_addr = rom.read_ptr(area_doors_ptrs(rom) + area * 4) + door = None + for d in range(256): + if rom.read_8(door_addr) == 0: + break + if rom.read_8(door_addr + 1) == room: + door = d + break + door_addr += 0xC + if door is None: + raise ValueError(f"No door found for area {area} room {room:X}") + return door + + +def find_save_pad_position(rom: Rom, area: int, room: int) -> tuple[int, int] | None: + # Check if room's spriteset has save pad + save_platform_ids = {SpriteIdZM.SAVE_PLATFORM.value, SpriteIdZM.SAVE_PLATFORM_CHOZODIA.value} + room_entry = RoomEntry(rom, area, room) + spriteset = room_entry.default_spriteset() + ss_addr = rom.read_ptr(spriteset_ptrs(rom) + spriteset * 4) + ss_idx = None + for i in range(15): + sprite_id = rom.read_8(ss_addr) + if sprite_id == 0: + break + if sprite_id in save_platform_ids: + ss_idx = i + break + ss_addr += 2 + if ss_idx is None: + return None + # Find save pad in sprite layout list + layout_addr = room_entry.default_sprite_layout_addr() + for i in range(24): + sp_y = rom.read_8(layout_addr) + sp_x = rom.read_8(layout_addr + 1) + prop = rom.read_8(layout_addr + 2) + if sp_x == 0xFF and sp_y == 0xFF and prop == 0xFF: + break + if (prop & 0xF) - 1 == ss_idx: + return sp_x, sp_y - 1 + layout_addr += 3 + # No save pad found + return None + + +def set_starting_items(rom: Rom, data: MarsschemazmStartingItems) -> None: + def get_ability_flags(ability_flags: dict[str, int]) -> int: + status = 0 + for ability, flag in ability_flags.items(): + if ability in abilities: + status |= flag + return status + + # Get health/ammo amounts + energy = data.get("energy", 99) + missiles = data.get("missiles", 0) + super_missiles = data.get("super_missiles", 0) + power_bombs = data.get("power_bombs", 0) + # Get ability status flags + abilities = data.get("abilities", []) + beam_bomb_status = get_ability_flags(BEAM_BOMB_FLAGS) + suit_misc_status = get_ability_flags(SUIT_MISC_FLAGS) + # Get downloaded map flags + maps = data.get("downloaded_maps", range(7)) + map_status = 0 + for map in maps: + map_status |= 1 << map + # Get suit type + suit_str = data.get("suit_type") + suit_type = SuitType[suit_str] if suit_str else SuitType.NORMAL + # Get ziplines activated + ziplines = data.get("ziplines_activated", False) + + # Write to rom (see struct StartingInfo in structs/randomizer.h) + addr = rom.read_ptr(ReservedPointersZM.STARTING_INFO_PTR.value) + rom.write_16(addr + 6, energy) + rom.write_16(addr + 8, missiles) + rom.write_8(addr + 0xA, super_missiles) + rom.write_8(addr + 0xB, power_bombs) + rom.write_8(addr + 0xC, beam_bomb_status) + rom.write_8(addr + 0xD, suit_misc_status) + rom.write_8(addr + 0xE, map_status) + rom.write_8(addr + 0xF, suit_type.value) + rom.write_8(addr + 0x10, ziplines) + # TODO: Disabled hints + rom.write_8(addr + 0x11, 0)