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.
This commit is contained in:
massimilianodelliubaldini
2024-06-07 11:07:31 -04:00
committed by GitHub
parent 33a858e526
commit 8293b9d436
3 changed files with 159 additions and 14 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = {