diff --git a/MetroidPrimeClient.py b/MetroidPrimeClient.py index c976b9b201b1..49185fdb314a 100644 --- a/MetroidPrimeClient.py +++ b/MetroidPrimeClient.py @@ -14,10 +14,20 @@ class MetroidPrimeCommandProcessor(ClientCommandProcessor): def __init__(self, ctx: CommonContext): super().__init__(ctx) + def _cmd_deathlink(self): + """Toggle deathlink from client. Overrides default setting.""" + if isinstance(self.ctx, MetroidPrimeContext): + new_value = True + if (self.tags["DeathLink"]): + new_value = False + Utils.async_start(self.ctx.update_death_link( + new_value), name="Update Deathlink") + class MetroidPrimeContext(CommonContext): current_level_id = 0 previous_level_id = 0 + is_pending_death_link_reset = False command_processor = MetroidPrimeCommandProcessor game_interface: MetroidPrimeInterface game = "Metroid Prime" @@ -30,7 +40,7 @@ def __init__(self, server_address, password): def on_deathlink(self, data: Utils.Dict[str, Utils.Any]) -> None: super().on_deathlink(data) - logger.debug("Death link not implemented") + self.game_interface.set_alive(False) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -38,6 +48,12 @@ async def server_auth(self, password_requested: bool = False): await self.get_username() await self.send_connect() + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + if "death_link" in args["slot_data"]: + Utils.async_start(self.update_death_link( + bool(args["slot_data"]["death_link"]))) + async def dolphin_sync_task(ctx: MetroidPrimeContext): logger.info("Starting Dolphin connector") @@ -158,6 +174,15 @@ async def handle_check_goal_complete(ctx: MetroidPrimeContext): await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) +async def handle_check_deathlink(ctx: MetroidPrimeContext): + health = ctx.game_interface.get_current_health() + if health <= 0 and ctx.is_pending_death_link_reset == False: + await ctx.send_death(ctx.player_names[ctx.slot] + " ran out of energy.") + ctx.is_pending_death_link_reset + elif health > 0 and ctx.is_pending_death_link_reset == True: + ctx.is_pending_death_link_reset = False + + async def _handle_game_ready(ctx: MetroidPrimeContext): if ctx.server: if not ctx.slot: @@ -169,7 +194,7 @@ async def _handle_game_ready(ctx: MetroidPrimeContext): await handle_check_goal_complete(ctx) if "DeathLink" in ctx.tags: - logger.debug("DeathLink not implemented") + await handle_check_deathlink(ctx) await asyncio.sleep(0.5) else: logger.info("Waiting for player to connect to server") diff --git a/worlds/metroidprime/DolphinClient.py b/worlds/metroidprime/DolphinClient.py index 209c4d086cd4..da2455c80394 100644 --- a/worlds/metroidprime/DolphinClient.py +++ b/worlds/metroidprime/DolphinClient.py @@ -59,7 +59,7 @@ def read_pointer(self, pointer, offset, byte_count): try: address = self.dolphin.follow_pointers(pointer, [0]) except RuntimeError: - self.logger.error(f"Could not read pointer at {pointer:x}") + self.logger.debug(f"Could not read pointer at {pointer:x}") return None if not self.dolphin.is_hooked(): @@ -80,7 +80,7 @@ def write_pointer(self, pointer, offset, data): try: address = self.dolphin.follow_pointers(pointer, [0]) except RuntimeError: - self.logger.error(f"Could not read pointer at {pointer:x}") + self.logger.debug(f"Could not read pointer at {pointer:x}") return None if not self.dolphin.is_hooked(): diff --git a/worlds/metroidprime/MetroidPrimeInterface.py b/worlds/metroidprime/MetroidPrimeInterface.py index 180b31acf032..b34e3616c85c 100644 --- a/worlds/metroidprime/MetroidPrimeInterface.py +++ b/worlds/metroidprime/MetroidPrimeInterface.py @@ -203,6 +203,8 @@ def __is_player_table_ready(self) -> bool: """Check if the player table is ready to be read from memory, indicating the game is in a playable state""" player_table_bytes = self.dolphin_client.read_pointer( cstate_manager_global + 0x84C, 0, 4) + if(player_table_bytes is None): + return False player_table = struct.unpack(">I", player_table_bytes)[0] if player_table == cplayer_vtable: return True diff --git a/worlds/metroidprime/PrimeOptions.py b/worlds/metroidprime/PrimeOptions.py index 23072c6eff09..d4edfc5e7bdb 100644 --- a/worlds/metroidprime/PrimeOptions.py +++ b/worlds/metroidprime/PrimeOptions.py @@ -1,5 +1,5 @@ -from Options import Toggle, Range, ItemDict, StartInventoryPool, Choice, PerGameCommonOptions +from Options import DeathLink, Toggle, Range, ItemDict, StartInventoryPool, Choice, PerGameCommonOptions from dataclasses import dataclass @@ -42,4 +42,5 @@ class MetroidPrimeOptions(PerGameCommonOptions): required_artifacts: RequiredArtifacts exclude_items: ExcludeItems final_bosses: FinalBosses + death_link: DeathLink diff --git a/worlds/metroidprime/__init__.py b/worlds/metroidprime/__init__.py index 3ffcea60263d..601d2543ec9e 100644 --- a/worlds/metroidprime/__init__.py +++ b/worlds/metroidprime/__init__.py @@ -1,3 +1,4 @@ +from typing import Any, Dict, List from BaseClasses import Item, Tutorial, ItemClassification from .Items import MetroidPrimeItem, suit_upgrade_table, artifact_table, item_table, custom_suit_upgrade_table from .PrimeOptions import MetroidPrimeOptions @@ -66,22 +67,26 @@ def create_items(self) -> None: continue elif i == "Missile Expansion": for j in range(0, 8): - self.multiworld.itempool += [self.create_item('Missile Expansion', True)] + self.multiworld.itempool += [ + self.create_item('Missile Expansion', True)] items_added += 8 elif i == "Spring Ball": continue elif i == "Energy Tank": for j in range(0, 8): - self.multiworld.itempool += [self.create_item("Energy Tank", True)] + self.multiworld.itempool += [ + self.create_item("Energy Tank", True)] for j in range(0, 6): - self.multiworld.itempool += [self.create_item("Energy Tank")] + self.multiworld.itempool += [ + self.create_item("Energy Tank")] items_added += 14 continue elif i == "Ice Trap": continue elif i == "Power Bomb Expansion": for j in range(0, 4): - self.multiworld.itempool += [self.create_item("Power Bomb Expansion")] + self.multiworld.itempool += [ + self.create_item("Power Bomb Expansion")] items_added += 4 else: self.multiworld.itempool += [self.create_item(i)] @@ -95,3 +100,15 @@ def set_rules(self) -> None: set_rules(self.multiworld, self.player, every_location) self.multiworld.completion_condition[self.player] = lambda state: ( state.can_reach("Mission Complete", "Region", self.player)) + + def fill_slot_data(self) -> Dict[str, Any]: + + slot_data: Dict[str, Any] = { + "spring_ball": self.options.spring_ball.value, + "death_link": self.options.death_link.value, + "required_artifacts": self.options.required_artifacts.value, + "exclude_items": self.options.exclude_items.value, + "final_bosses": self.options.final_bosses.value, + } + + return slot_data