From 650fd5d792692a9542a89f4bcb0b23b5b0eee2b0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 11 Nov 2021 16:09:08 +0100 Subject: [PATCH] LttP: refine DeathLink handling. --- CommonClient.py | 1 + LttPClient.py | 49 +++++++++++++++++++++++++++++++++++++------------ MultiServer.py | 20 ++++++++++++-------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 093a2eb2..bde3adb5 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -271,6 +271,7 @@ class CommonContext(): logger.info(f"DeathLink: Received from {data['source']}") async def send_death(self, death_text: str = ""): + logger.info("Sending death to your friends...") self.last_death_link = time.time() await self.send_msgs([{ "cmd": "Bounce", "tags": ["DeathLink"], diff --git a/LttPClient.py b/LttPClient.py index e82cbdc0..b3b2e3ff 100644 --- a/LttPClient.py +++ b/LttPClient.py @@ -1,6 +1,5 @@ from __future__ import annotations -import atexit import threading import time import multiprocessing @@ -32,6 +31,12 @@ snes_logger = logging.getLogger("SNES") from MultiServer import mark_raw +class DeathState(enum.IntEnum): + killing_player = 1 + alive = 2 + dead = 3 + + class LttPCommandProcessor(ClientCommandProcessor): ctx: Context @@ -88,6 +93,10 @@ class LttPCommandProcessor(ClientCommandProcessor): self.output("Data Sent") return True + def _cmd_test_death(self): + self.ctx.on_deathlink({"source": "Console", + "time": time.time()}) + class Context(CommonContext): command_processor = LttPCommandProcessor @@ -106,7 +115,7 @@ class Context(CommonContext): self.snes_request_lock = asyncio.Lock() self.snes_write_buffer = [] self.snes_connector_lock = threading.Lock() - self.death_state = False # for death link flop behaviour + self.death_state = DeathState.alive # for death link flop behaviour self.awaiting_rom = False self.rom = None @@ -140,13 +149,17 @@ class Context(CommonContext): }]) def on_deathlink(self, data: dict): - snes_buffered_write(self, WRAM_START + 0xF36D, bytes([0])) # set current health to 0 - snes_buffered_write(self, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity - asyncio.create_task(snes_flush_writes(self)) - self.death_state = True + asyncio.create_task(deathlink_kill_player(self)) + self.death_state = DeathState.killing_player super(Context, self).on_deathlink(data) +async def deathlink_kill_player(ctx: Context): + snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0 + snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity + await snes_flush_writes(ctx) + + def color_item(item_id: int, green: bool = False) -> str: item_name = get_item_name_from_id(item_id) item_colors = ['green' if green else 'cyan'] @@ -842,7 +855,7 @@ async def game_watcher(ctx: Context): ctx.rom = rom death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1) if death_link: - death_link = bool(death_link[0]) + death_link = bool(death_link[0] & 0b1) old_tags = ctx.tags.copy() if death_link: ctx.tags.add("DeathLink") @@ -864,12 +877,24 @@ async def game_watcher(ctx: Context): gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): - if gamemode[0] in DEATH_MODES: - if not ctx.death_state: # new death + currently_dead = gamemode[0] in DEATH_MODES + # in this state we only care about triggering a death send + if ctx.death_state == DeathState.alive: + if currently_dead: + ctx.death_state = DeathState.dead await ctx.send_death() - ctx.death_state = True - else: - ctx.death_state = False # reset death state, so next death can trigger + # in this state we care about confirming a kill, to move state to dead + elif DeathState.killing_player: + if currently_dead: + ctx.death_state = DeathState.dead + else: + await deathlink_kill_player(ctx) # try again + ctx.last_death_link = time.time() # delay handling + # in this state we wait until the player is alive again + elif DeathState.dead: + if not currently_dead: + ctx.death_state = DeathState.alive + gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) if gamemode is None or gameend is None or game_timer is None or \ diff --git a/MultiServer.py b/MultiServer.py index d629be1a..c1be4d9e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -431,6 +431,17 @@ class Context: else: return self.player_names[team, slot] + def on_goal_achieved(self, client: Client): + finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ + f' has completed their goal.' + self.notify_all(finished_msg) + if "auto" in self.forfeit_mode: + forfeit_player(self, client.team, client.slot) + elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit: + forfeit_player(self, client.team, client.slot) + if "auto" in self.collect_mode: + collect_player(self, client.team, client.slot) + def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]): concerns = collections.defaultdict(list) @@ -1356,14 +1367,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus) current = ctx.client_game_state[client.team, client.slot] if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion if new_status == ClientStatus.CLIENT_GOAL: - finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.' - ctx.notify_all(finished_msg) - if "auto" in ctx.forfeit_mode: - forfeit_player(ctx, client.team, client.slot) - elif proxy_worlds[ctx.games[client.slot]].forced_auto_forfeit: - forfeit_player(ctx, client.team, client.slot) - if "auto" in ctx.collect_mode: - collect_player(ctx, client.team, client.slot) + ctx.on_goal_achieved(client) ctx.client_game_state[client.team, client.slot] = new_status