mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 00:43:20 -07:00
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:
committed by
GitHub
parent
33a858e526
commit
8293b9d436
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user