diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index c451162ae9..9efbd4bb20 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,3 +1,4 @@ +import logging import typing import asyncio import colorama @@ -38,6 +39,26 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): # 2. Listen (have the REPL compiler connect and listen on the game's REPL server's socket). # 3. Compile (have the REPL compiler compile the game into object code it can run). # All 3 need to be done, and in this order, for this to work. + def _cmd_repl(self, *arguments: str): + """Sends a command to the OpenGOAL REPL. Arguments: + - connect : connect a new client to the REPL. + - listen : listen to the game's internal socket. + - compile : compile the game into executable object code. + - verify : verify successful compilation.""" + if arguments: + if arguments[0] == "connect": + if arguments[1] and arguments[2]: + self.ctx.repl.ip = str(arguments[1]) + self.ctx.repl.port = int(arguments[2]) + self.ctx.repl.connect() + else: + logging.error("You must provide the ip address and port (default 127.0.0.1 port 8181).") + if arguments[0] == "listen": + self.ctx.repl.listen() + if arguments[0] == "compile": + self.ctx.repl.compile() + if arguments[0] == "verify": + self.ctx.repl.verify() class JakAndDaxterContext(CommonContext): diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 020805b32b..2a3b32a259 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -3,6 +3,14 @@ import subprocess import pymem from pymem import pattern from pymem.exception import ProcessNotFound +from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies + +# Some helpful constants. +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. +cells_offset = 16 +buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101) +end_marker_offset = 868 # buzzers_offset + (sizeof uint32 * 112 flies) = 420 + (4 * 112) class JakAndDaxterMemoryReader: @@ -14,7 +22,7 @@ class JakAndDaxterMemoryReader: marker_address = None goal_address = None - location_outbox = {} + location_outbox = [] outbox_index = 0 def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): @@ -22,7 +30,6 @@ class JakAndDaxterMemoryReader: self.connected = self.connect() if self.connected and self.marker: self.marked = self.find_marker() - pass async def main_tick(self, location_callback: typing.Callable): self.read_memory() @@ -55,5 +62,17 @@ class JakAndDaxterMemoryReader: return True return False - def read_memory(self) -> typing.Dict: - pass + def read_memory(self) -> typing.List[int]: + next_cell_index = int.from_bytes(self.process.read_bytes(self.goal_address, 8)) + next_buzzer_index = int.from_bytes(self.process.read_bytes(self.goal_address + next_buzzer_index_offset, 8)) + next_cell = int.from_bytes(self.process.read_bytes(self.goal_address + cells_offset + (next_cell_index * 4), 4)) + next_buzzer = int.from_bytes(self.process.read_bytes(self.goal_address + cells_offset + (next_buzzer_index * 4), 4)) + + if next_cell not in self.location_outbox: + self.location_outbox.append(Cells.to_ap_id(next_cell)) + if next_buzzer not in self.location_outbox: + self.location_outbox.append(Flies.to_ap_id(next_buzzer)) + + return self.location_outbox + + diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 0a7ca9f97c..7d39ac866b 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -22,29 +22,29 @@ class JakAndDaxterReplClient: self.port = port async def init(self): - self.connected = await self.connect() + self.connected = self.connect() if self.connected: - self.listening = await self.listen() + self.listening = self.listen() if self.connected and self.listening: - self.compiled = await self.compile() + self.compiled = self.compile() async def main_tick(self): # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: - await self.receive_item() + self.receive_item() self.inbox_index += 1 # 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 needs to block on receiving an acknowledgement from the REPL server. # Problem is, it doesn't ack anything right now. So we need that to happen first. - async def send_form(self, form: str) -> None: + def send_form(self, form: str) -> None: header = struct.pack(" bool: + def connect(self) -> bool: if not self.ip or not self.port: return False @@ -57,71 +57,67 @@ class JakAndDaxterReplClient: except ConnectionRefusedError: return False - async def listen(self) -> bool: - await self.send_form("(lt)") + def listen(self) -> bool: + self.send_form("(lt)") return True - async def compile(self) -> bool: + def compile(self) -> bool: # Show this visual cue when compilation is started. # It's the version number of the OpenGOAL Compiler. - await self.send_form("(set! *debug-segment* #t)") + self.send_form("(set! *debug-segment* #t)") # Play this audio cue when compilation is started. # It's the sound you hear when you press START + CIRCLE to open the Options menu. - await self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"start-options\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"start-options\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") # Start compilation. This is blocking, so nothing will happen until the REPL is done. - await self.send_form("(mi)") + self.send_form("(mi)") # Play this audio cue when compilation is complete. # It's the sound you hear when you press START + START to close the Options menu. - await self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") # Disable cheat-mode and debug (close the visual cue). - await self.send_form("(set! *cheat-mode* #f)") - # await self.send_form("(set! *debug-segment* #f)") + self.send_form("(set! *cheat-mode* #f)") + # self.send_form("(set! *debug-segment* #f)") return True - async def verify(self) -> bool: - await self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + def verify(self) -> bool: + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") return True - async def receive_item(self): + def receive_item(self): ap_id = self.item_inbox[self.inbox_index]["item"] # Determine the type of item to receive. if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): - await self.receive_power_cell(ap_id) + self.receive_power_cell(ap_id) elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Orbs.orb_offset): - await self.receive_scout_fly(ap_id) + self.receive_scout_fly(ap_id) elif ap_id > jak1_id + Orbs.orb_offset: pass # TODO - # TODO - In ArchipelaGOAL, override the 'get-pickup event so that it doesn't give you the item, - # it just plays the victory animation. Then define a new event type like 'get-archipelago - # to actually give ourselves the item. See game-info.gc and target-handler.gc. - - async def receive_power_cell(self, ap_id: int) -> None: + def receive_power_cell(self, ap_id: int) -> None: cell_id = Cells.to_game_id(ap_id) - await self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type fuel-cell) " - "(the float " + str(cell_id) + "))") + self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") - async def receive_scout_fly(self, ap_id: int) -> None: + def receive_scout_fly(self, ap_id: int) -> None: fly_id = Flies.to_game_id(ap_id) - await self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type buzzer) " - "(the float " + str(fly_id) + "))") + self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))")