From 8293b9d436f2285dc1bf59c9c0df93b504739aba Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:07:31 -0400 Subject: [PATCH] Deathlink (#18) * Jak 1: Implement Deathlink. TODO: make it optional... * Jak 1: Issue a proper send-event for deathlink deaths. * Jak 1: Added cause of death to deathlink, fixed typo. * Jak 1: Make Deathlink toggleable. * Jak 1: Added player name to death text, added zoomer/flut/fishing text, simplified GOAL call for deathlink. * Jak 1: Fix death text in client logger. --- worlds/jakanddaxter/Client.py | 33 ++++++- worlds/jakanddaxter/client/MemoryReader.py | 102 ++++++++++++++++++--- worlds/jakanddaxter/client/ReplClient.py | 38 ++++++++ 3 files changed, 159 insertions(+), 14 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 4417c49b10..3612946548 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -65,7 +65,6 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): class JakAndDaxterContext(CommonContext): - tags = {"AP"} game = jak1_name items_handling = 0b111 # Full item handling command_processor = JakAndDaxterClientCommandProcessor @@ -115,6 +114,11 @@ class JakAndDaxterContext(CommonContext): self.memr.save_data() self.repl.save_data() + def on_deathlink(self, data: dict): + if self.memr.deathlink_enabled: + self.repl.received_deathlink = True + super().on_deathlink(data) + async def ap_inform_location_check(self, location_ids: typing.List[int]): message = [{"cmd": "LocationChecks", "locations": location_ids}] await self.send_msgs(message) @@ -128,9 +132,29 @@ class JakAndDaxterContext(CommonContext): await self.send_msgs(message) self.finished_game = True - def on_finish(self): + def on_finish_check(self): create_task_log_exception(self.ap_inform_finished_game()) + async def ap_inform_deathlink(self): + if self.memr.deathlink_enabled: + death_text = self.memr.cause_of_death.replace("Jak", self.player_names[self.slot]) + await self.send_death(death_text) + logger.info(death_text) + + # Reset all flags. + self.memr.send_deathlink = False + self.memr.cause_of_death = "" + self.repl.reset_deathlink() + + def on_deathlink_check(self): + create_task_log_exception(self.ap_inform_deathlink()) + + async def ap_inform_deathlink_toggle(self): + await self.update_death_link(self.memr.deathlink_enabled) + + def on_deathlink_toggle(self): + create_task_log_exception(self.ap_inform_deathlink_toggle()) + async def run_repl_loop(self): while True: await self.repl.main_tick() @@ -138,7 +162,10 @@ class JakAndDaxterContext(CommonContext): async def run_memr_loop(self): while True: - await self.memr.main_tick(self.on_location_check, self.on_finish) + await self.memr.main_tick(self.on_location_check, + self.on_finish_check, + self.on_deathlink_check, + self.on_deathlink_toggle) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index c1b10ff3a6..c91eba8286 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,3 +1,4 @@ +import random import typing import pymem from pymem import pattern @@ -12,18 +13,62 @@ sizeof_uint64 = 8 sizeof_uint32 = 4 sizeof_uint8 = 1 -next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. -next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. -next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes. +next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. +next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. +next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes. cells_checked_offset = 24 -buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells) -specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers) +buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells) +specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers) -buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials) +buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials) specials_received_offset = 1020 # buzzers_received_offset + (sizeof uint8 * 16 levels (for scout fly groups)) -end_marker_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials) +died_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials) + +deathlink_enabled_offset = 1053 # died_offset + sizeof uint8 + +end_marker_offset = 1054 # deathlink_enabled_offset + sizeof uint8 + + +# "Jak" to be replaced by player name in the Client. +def autopsy(died: int) -> str: + assert died > 0, f"Tried to find Jak's cause of death, but he's still alive!" + if died in [1, 2, 3, 4]: + return random.choice(["Jak said goodnight.", + "Jak stepped into the light.", + "Jak gave Daxter his insect collection.", + "Jak did not follow Step 1."]) + if died == 5: + return "Jak fell into an endless pit." + if died == 6: + return "Jak drowned in the spicy water." + if died == 7: + return "Jak tried to tackle a Lurker Shark." + if died == 8: + return "Jak hit 500 degrees." + if died == 9: + return "Jak took a bath in a pool of dark eco." + if died == 10: + return "Jak got bombarded with flaming 30-ton boulders." + if died == 11: + return "Jak hit 800 degrees." + if died == 12: + return "Jak ceased to be." + if died == 13: + return "Jak got eaten by the dark eco plant." + if died == 14: + return "Jak burned up." + if died == 15: + return "Jak hit the ground hard." + if died == 16: + return "Jak crashed the zoomer." + if died == 17: + return "Jak got Flut Flut hurt." + if died == 18: + return "Jak poisoned the whole darn catch." + + return "Jak died." class JakAndDaxterMemoryReader: @@ -36,14 +81,23 @@ class JakAndDaxterMemoryReader: gk_process: pymem.process = None location_outbox = [] - outbox_index = 0 - finished_game = False + outbox_index: int = 0 + finished_game: bool = False + + # Deathlink handling + deathlink_enabled: bool = False + send_deathlink: bool = False + cause_of_death: str = "" def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker self.connect() - async def main_tick(self, location_callback: typing.Callable, finish_callback: typing.Callable): + async def main_tick(self, + location_callback: typing.Callable, + finish_callback: typing.Callable, + deathlink_callback: typing.Callable, + deathlink_toggle: typing.Callable): if self.initiated_connect: await self.connect() self.initiated_connect = False @@ -57,9 +111,11 @@ class JakAndDaxterMemoryReader: else: return + # Save some state variables temporarily. + old_deathlink_enabled = self.deathlink_enabled + # Read the memory address to check the state of the game. self.read_memory() - # location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below... # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. if len(self.location_outbox) > self.outbox_index: @@ -69,6 +125,13 @@ class JakAndDaxterMemoryReader: if self.finished_game: finish_callback() + if old_deathlink_enabled != self.deathlink_enabled: + deathlink_toggle() + logger.debug("Toggled DeathLink " + ("ON" if self.deathlink_enabled else "OFF")) + + if self.send_deathlink: + deathlink_callback() + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel @@ -165,6 +228,23 @@ class JakAndDaxterMemoryReader: self.location_outbox.append(special_ap_id) logger.debug("Checked special: " + str(next_special)) + died = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + died_offset, sizeof_uint8), + byteorder="little", + signed=False) + + if died > 0: + self.send_deathlink = True + self.cause_of_death = autopsy(died) + + deathlink_flag = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + deathlink_enabled_offset, sizeof_uint8), + byteorder="little", + signed=False) + + # Listen for any changes to this setting. + self.deathlink_enabled = bool(deathlink_flag) + except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 2360601d5f..7c176c1224 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -2,6 +2,7 @@ import json import time import struct import typing +import random from socket import socket, AF_INET, SOCK_STREAM import pymem @@ -24,6 +25,7 @@ class JakAndDaxterReplClient: sock: socket connected: bool = False initiated_connect: bool = False # Signals when user tells us to try reconnecting. + received_deathlink: bool = False # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. @@ -62,6 +64,14 @@ class JakAndDaxterReplClient: self.receive_item() self.inbox_index += 1 + if self.received_deathlink: + self.receive_deathlink() + + # Reset all flags. + # As a precaution, we should reset our own deathlink flag as well. + self.reset_deathlink() + self.received_deathlink = False + # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. # TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print @@ -227,6 +237,34 @@ class JakAndDaxterReplClient: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok + def receive_deathlink(self) -> bool: + + # Because it should at least be funny sometimes. + death_types = ["\'death", + "\'death", + "\'death", + "\'death", + "\'endlessfall", + "\'drown-death", + "\'melt", + "\'dark-eco-pool"] + chosen_death = random.choice(death_types) + + ok = self.send_form("(ap-deathlink-received! " + chosen_death + ")") + if ok: + logger.debug(f"Received deathlink signal!") + else: + logger.error(f"Unable to receive deathlink signal!") + return ok + + def reset_deathlink(self) -> bool: + ok = self.send_form("(set! (-> *ap-info-jak1* died) 0)") + if ok: + logger.debug(f"Reset deathlink flag!") + else: + logger.error(f"Unable to reset deathlink flag!") + return ok + def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = {