Jak and Daxter: Replace Pymem, Add Linux Support (#5850)

* Replace pymem with PyMemoryEditor (nonworking)

* Add back pymem for faster windows address searching.

* Replace other uses of pymem, parameterize executable names.

* Updated to add linux and potential MacOS support to launching gk and … (#84)

* Updated to add linux and potential MacOS support to launching gk and goalc. Still needs tested on MacOS.

* Switched to using x-terminal-emulator instead of trying to find gnome-terminal or konsole

Made argument building for suprocessing goalc easier to read

Fixed OS X support to use osascript instead of attempting to run Terminal directly

* Changed Terminal usage to use Archipelago's Launh utility, which handles terminal launching for me for both linux and OS X

* Added try/except to re-connect the memory process. The process file/id changes over time on linux, and this works to re-connect without needing to restart

* Removed Unsetting  env var in favor of reporting to the source authors

* Putting PyMemoryEditor local. (#85)

* Putting PyMemoryEditor local

---------

Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com>

* Fixing minor problems (#87)

* Refactor away circular launcher import.

* Push latest PyMemoryEditor scan utility (#91)

Co-authored-by: Louis M <Louis M>

* Remove Pymem, rely solely on PyMemoryEditor. Add konsole support.

* Jak 1: Remove vendored copy of PME, update imports, requirements, and manifest.

* Jak 1: Prevent server connect until game is properly setup.

* Jak 1: reduce REPL/Compiler confusion, small updates to setup guide.

* Write hack for Konsole on AppImage to avoid OpenSSL error.

* Refactor LD_LIBRARY_PATH hack.

* Update worlds/jakanddaxter/agents/memory_reader.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/jakanddaxter/agents/memory_reader.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: Morgan <morgan07kelley@gmail.com>
Co-authored-by: Louis M <prog@tioui.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
massimilianodelliubaldini
2026-02-04 12:45:09 -05:00
committed by GitHub
parent 3b1971be66
commit f3389f5d8b
7 changed files with 170 additions and 90 deletions
+38 -28
View File
@@ -1,13 +1,14 @@
import logging
import random
import struct
import sys
from typing import ByteString, Callable
import json
import pymem
from pymem import pattern
from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError
from PyMemoryEditor import OpenProcess, ProcessNotFoundError, ProcessIDNotExistsError, ClosedProcess
from dataclasses import dataclass
import Utils
from ..game_id import jak1_gk
from ..locs import (orb_locations as orbs,
cell_locations as cells,
scout_locations as flies,
@@ -166,7 +167,7 @@ class JakAndDaxterMemoryReader:
initiated_connect: bool = False
# The memory reader just needs the game running.
gk_process: pymem.process = None
gk_process: OpenProcess | None = None
location_outbox: list[int] = []
outbox_index: int = 0
@@ -227,8 +228,10 @@ class JakAndDaxterMemoryReader:
if self.connected:
try:
self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive.
except (ProcessError, MemoryReadError, WinAPIError):
# TODO - When PyMemoryEditor issue #15 is resolved, swap out this line for the commented one.
# self.gk_process.read_process_memory(0, bytes, 1) # Ping to see if it's alive.
OpenProcess(process_name=jak1_gk)
except (ProcessNotFoundError, ProcessIDNotExistsError, ClosedProcess):
msg = (f"Error reading game memory! (Did the game crash?)\n"
f"Please close all open windows and reopen the Jak and Daxter Client "
f"from the Archipelago Launcher.\n"
@@ -272,28 +275,35 @@ class JakAndDaxterMemoryReader:
async def connect(self):
try:
self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel
logger.debug("Found the gk process: " + str(self.gk_process.process_id))
except ProcessNotFound:
self.gk_process = OpenProcess(process_name=jak1_gk) # The GOAL Kernel
logger.debug(f"Found the gk process: {self.gk_process.pid}")
except ProcessNotFoundError:
self.log_error(logger, "Could not find the game process.")
self.connected = False
return
# If we don't find the marker in the first loaded module, we've failed.
modules = list(self.gk_process.list_modules())
marker_address = pattern.pattern_scan_module(self.gk_process.process_handle, modules[0], self.marker)
if marker_address:
# At this address is another address that contains the struct we're looking for: the game's state.
# From here we need to add the length in bytes for the marker and 4 bytes of padding,
# and the struct address is 8 bytes long (it's an uint64).
goal_pointer = marker_address + len(self.marker) + 4
self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64),
byteorder="little",
signed=False)
logger.debug("Found the archipelago memory address: " + str(self.goal_address))
await self.verify_memory_version()
if Utils.is_windows or Utils.is_linux:
marker_addresses = list(self.gk_process.search_by_value(bytes, len(self.marker), self.marker,
writeable_only=True))
if len(marker_addresses) > 0:
# If we don't find the marker in the first loaded module, we've failed.
goal_pointer = marker_addresses[0] + len(self.marker) + 4
# At this address is another address that contains the struct we're looking for: the game's state.
# From here we need to add the length in bytes for the marker and 4 bytes of padding,
# and the struct address is 8 bytes long (it's an uint64).
self.goal_address = int.from_bytes(
self.gk_process.read_process_memory(goal_pointer, bytes, sizeof_uint64),
byteorder="little",
signed=False)
logger.debug("Found the archipelago memory address: " + str(self.goal_address))
await self.verify_memory_version()
else:
self.log_error(logger, "Could not find the Archipelago marker address!")
self.connected = False
else:
self.log_error(logger, "Could not find the Archipelago marker address!")
self.log_error(logger, f"Unsupported operating system: {sys.platform}!")
self.connected = False
async def verify_memory_version(self):
@@ -309,8 +319,8 @@ class JakAndDaxterMemoryReader:
self.log_success(logger, "The Memory Reader is ready!")
self.connected = True
else:
raise MemoryReadError(memory_version_offset, sizeof_uint32)
except (ProcessError, MemoryReadError, WinAPIError):
raise Exception(memory_version_offset, sizeof_uint32)
except (ProcessNotFoundError, ProcessIDNotExistsError, ClosedProcess, Exception):
if memory_version is None:
msg = (f"Could not find a version number in the OpenGOAL memory structure!\n"
f" Expected Version: {str(expected_memory_version)}\n"
@@ -336,7 +346,7 @@ class JakAndDaxterMemoryReader:
self.connected = False
async def print_status(self):
proc_id = str(self.gk_process.process_id) if self.gk_process else "None"
proc_id = str(self.gk_process.pid) if self.gk_process else "None"
last_loc = str(self.location_outbox[self.outbox_index - 1] if self.outbox_index else "None")
msg = (f"Memory Reader Status:\n"
f" Game process ID: {proc_id}\n"
@@ -451,7 +461,7 @@ class JakAndDaxterMemoryReader:
self.finished_game = True
self.log_success(logger, "Congratulations! You finished the game!")
except (ProcessError, MemoryReadError, WinAPIError):
except (ProcessNotFoundError, ProcessIDNotExistsError, ClosedProcess):
msg = (f"Error reading game memory! (Did the game crash?)\n"
f"Please close all open windows and reopen the Jak and Daxter Client "
f"from the Archipelago Launcher.\n"
@@ -467,7 +477,7 @@ class JakAndDaxterMemoryReader:
def read_goal_address(self, offset: int, length: int) -> int:
return int.from_bytes(
self.gk_process.read_bytes(self.goal_address + offset, length),
self.gk_process.read_process_memory(self.goal_address + offset, bytes, length),
byteorder="little",
signed=False)