From f3389f5d8b277bce78e8eeb9ae38def38dfe33d0 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:45:09 -0500 Subject: [PATCH] Jak and Daxter: Replace Pymem, Add Linux Support (#5850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 Co-authored-by: Louis M Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/jakanddaxter/agents/memory_reader.py | 66 +++++++----- worlds/jakanddaxter/agents/repl_client.py | 50 +++++---- worlds/jakanddaxter/archipelago.json | 2 +- worlds/jakanddaxter/client.py | 110 +++++++++++++++----- worlds/jakanddaxter/docs/setup_en.md | 22 ++-- worlds/jakanddaxter/game_id.py | 8 ++ worlds/jakanddaxter/requirements.txt | 2 +- 7 files changed, 170 insertions(+), 90 deletions(-) diff --git a/worlds/jakanddaxter/agents/memory_reader.py b/worlds/jakanddaxter/agents/memory_reader.py index 01035c4a2e..493fbde39e 100644 --- a/worlds/jakanddaxter/agents/memory_reader.py +++ b/worlds/jakanddaxter/agents/memory_reader.py @@ -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) diff --git a/worlds/jakanddaxter/agents/repl_client.py b/worlds/jakanddaxter/agents/repl_client.py index b207bba281..2a624d7f74 100644 --- a/worlds/jakanddaxter/agents/repl_client.py +++ b/worlds/jakanddaxter/agents/repl_client.py @@ -8,14 +8,13 @@ from dataclasses import dataclass from queue import Queue from typing import Callable -import pymem -from pymem.exception import ProcessNotFound, ProcessError +from PyMemoryEditor import OpenProcess, ProcessNotFoundError, ProcessIDNotExistsError, ClosedProcess import asyncio from asyncio import StreamReader, StreamWriter, Lock from NetUtils import NetworkItem -from ..game_id import jak1_id, jak1_max +from ..game_id import jak1_id, jak1_max, jak1_gk, jak1_goalc from ..items import item_table, trap_item_table from ..locs import ( orb_locations as orbs, @@ -66,8 +65,8 @@ class JakAndDaxterReplClient: # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. - gk_process: pymem.process = None - goalc_process: pymem.process = None + gk_process: OpenProcess = None + goalc_process: OpenProcess = None item_inbox: dict[int, NetworkItem] = {} inbox_index = 0 @@ -102,8 +101,10 @@ class JakAndDaxterReplClient: if self.connected: try: - self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. - except ProcessError: + # 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" @@ -115,8 +116,10 @@ class JakAndDaxterReplClient: self.log_error(logger, msg) self.connected = False try: - self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive. - except ProcessError: + # TODO - When PyMemoryEditor issue #15 is resolved, swap out this line for the commented one. + # self.goalc_process.read_process_memory(0, bytes, 1) # Ping to see if it's alive. + OpenProcess(process_name=jak1_goalc) + except (ProcessNotFoundError, ProcessIDNotExistsError, ClosedProcess): msg = (f"Error sending data to compiler! (Did the compiler crash?)\n" f"Please close all open windows and reopen the Jak and Daxter Client " f"from the Archipelago Launcher.\n" @@ -172,21 +175,21 @@ class JakAndDaxterReplClient: logger.debug(response) return True else: - self.log_error(logger, f"Unexpected response from REPL: {response}") + self.log_error(logger, f"Unexpected response from Compiler: {response}") return False 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("Found the gk process: " + str(self.gk_process.pid)) + except ProcessNotFoundError: self.log_error(logger, "Could not find the game process.") return try: - self.goalc_process = pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL - logger.debug("Found the goalc process: " + str(self.goalc_process.process_id)) - except ProcessNotFound: + self.goalc_process = OpenProcess(process_name=jak1_goalc) # The GOAL Compiler and REPL + logger.debug("Found the goalc process: " + str(self.goalc_process.pid)) + except ProcessNotFoundError: self.log_error(logger, "Could not find the compiler process.") return @@ -201,9 +204,10 @@ class JakAndDaxterReplClient: logger.debug(welcome_message) else: self.log_error(logger, - f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"") + f"Unable to connect to Compiler websocket: unexpected welcome message " + f"\"{welcome_message}\"") except ConnectionRefusedError as e: - self.log_error(logger, f"Unable to connect to REPL websocket: {e.strerror}") + self.log_error(logger, f"Unable to connect to Compiler websocket: {e.strerror}") return ok_count = 0 @@ -248,13 +252,13 @@ class JakAndDaxterReplClient: self.connected = False if self.connected: - self.log_success(logger, "The REPL is ready!") + self.log_success(logger, "The Compiler is ready!") async def print_status(self): - gc_proc_id = str(self.goalc_process.process_id) if self.goalc_process else "None" - gk_proc_id = str(self.gk_process.process_id) if self.gk_process else "None" - msg = (f"REPL Status:\n" - f" REPL process ID: {gc_proc_id}\n" + gc_proc_id = str(self.goalc_process.pid) if self.goalc_process else "None" + gk_proc_id = str(self.gk_process.pid) if self.gk_process else "None" + msg = (f"Compiler Status:\n" + f" Compiler process ID: {gc_proc_id}\n" f" Game process ID: {gk_proc_id}\n") try: if self.reader and self.writer: diff --git a/worlds/jakanddaxter/archipelago.json b/worlds/jakanddaxter/archipelago.json index 8b0adcd105..6411911f2d 100644 --- a/worlds/jakanddaxter/archipelago.json +++ b/worlds/jakanddaxter/archipelago.json @@ -1,6 +1,6 @@ { "game": "Jak and Daxter: The Precursor Legacy", - "world_version": "1.0.0", + "world_version": "1.1.5", "minimum_ap_version": "0.6.2", "authors": ["markustulliuscicero"] } diff --git a/worlds/jakanddaxter/client.py b/worlds/jakanddaxter/client.py index 7cb8cb8ea9..710e0ef89d 100644 --- a/worlds/jakanddaxter/client.py +++ b/worlds/jakanddaxter/client.py @@ -5,27 +5,27 @@ import logging import os import subprocess import sys +import shlex from asyncio import Task from datetime import datetime from logging import Logger +from shutil import which from typing import Awaitable # Misc imports import colorama -import pymem - -from pymem.exception import ProcessNotFound +from psutil import NoSuchProcess # Archipelago imports import ModuleUpdate import Utils - from CommonClient import ClientCommandProcessor, CommonContext, server_loop, gui_enabled from NetUtils import ClientStatus +from PyMemoryEditor import OpenProcess, ProcessNotFoundError # Jak imports -from .game_id import jak1_name +from .game_id import jak1_name, jak1_gk, jak1_goalc from .options import EnableOrbsanity from .agents.memory_reader import JakAndDaxterMemoryReader from .agents.repl_client import JakAndDaxterReplClient @@ -57,9 +57,9 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): # (which takes 10-15 seconds to compile the game) have to be requested with user-initiated flags. # The flags are checked by the agents every main_tick. def _cmd_repl(self, *arguments: str): - """Sends a command to the OpenGOAL REPL. Arguments: - - connect : connect the client to the REPL (goalc). - - status : check internal status of the REPL.""" + """Sends a command to the OpenGOAL Compiler. Arguments: + - connect : connect the client to the compiler (goalc). + - status : check internal status of the Compiler.""" if arguments: if arguments[0] == "connect": self.ctx.on_log_info(logger, "This may take a bit... Wait for the success audio cue before continuing!") @@ -135,6 +135,12 @@ class JakAndDaxterContext(CommonContext): self.tags = set() await self.send_connect() + async def connect(self, address: str | None = None) -> None: + if not self.repl.connected or not self.memr.connected: + self.on_log_warn(logger, "Please wait for the Compiler and Memory Reader to connect to the game!") + return + await super(JakAndDaxterContext, self).connect(address) + async def disconnect(self, allow_autoreconnect: bool = False): self.locations_checked = set() # Clear this set to gracefully handle server disconnects. await super(JakAndDaxterContext, self).disconnect(allow_autoreconnect) @@ -336,13 +342,21 @@ class JakAndDaxterContext(CommonContext): async def run_repl_loop(self): while True: - await self.repl.main_tick() - await asyncio.sleep(0.1) + try: + await self.repl.main_tick() + await asyncio.sleep(0.1) + # This catch re-engages the memr loop, enabling the client to re-connect on losing the process + except NoSuchProcess: + self.on_log_info(logger, "Compiler process lost. Restarting Compiler loop.") async def run_memr_loop(self): while True: - await self.memr.main_tick() - await asyncio.sleep(0.1) + try: + await self.memr.main_tick() + await asyncio.sleep(0.1) + # This catch re-engages the memr loop, enabling the client to re-connect on losing the process + except NoSuchProcess: + self.on_log_info(logger, "Memory reader process lost. Restarting Memory reader loop.") def find_root_directory(ctx: JakAndDaxterContext): @@ -454,21 +468,19 @@ def find_root_directory(ctx: JakAndDaxterContext): async def run_game(ctx: JakAndDaxterContext): - # These may already be running. If they are not running, try to start them. - # TODO - Support other OS's. 1: Pymem is Windows-only. 2: on Linux, there's no ".exe." gk_running = False try: - pymem.Pymem("gk.exe") # The GOAL Kernel + OpenProcess(process_name=jak1_gk) # The GOAL Kernel gk_running = True - except ProcessNotFound: + except ProcessNotFoundError: ctx.on_log_warn(logger, "Game not running, attempting to start.") goalc_running = False try: - pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL + OpenProcess(process_name=jak1_goalc) # The GOAL Compiler and REPL goalc_running = True - except ProcessNotFound: + except ProcessNotFoundError: ctx.on_log_warn(logger, "Compiler not running, attempting to start.") try: @@ -501,8 +513,8 @@ async def run_game(ctx: JakAndDaxterContext): return # Now double-check the existence of the two executables we need. - gk_path = os.path.join(root_path, "gk.exe") - goalc_path = os.path.join(root_path, "goalc.exe") + gk_path = os.path.join(root_path, jak1_gk) + goalc_path = os.path.join(root_path, jak1_goalc) if not os.path.exists(gk_path) or not os.path.exists(goalc_path): msg = (f"The Game and Compiler could not be found in the ArchipelaGOAL root directory.\n" f"Please check your host.yaml file.\n" @@ -530,13 +542,26 @@ async def run_game(ctx: JakAndDaxterContext): log_path = os.path.join(Utils.user_path("logs"), f"JakAndDaxterGame_{timestamp}.txt") log_path = os.path.normpath(log_path) with open(log_path, "w") as log_file: - gk_process = subprocess.Popen( - [gk_path, "--game", "jak1", - "--config-path", config_path, - "--", "-v", "-boot", "-fakeiso", "-debug"], - stdout=log_file, - stderr=log_file, - creationflags=subprocess.CREATE_NO_WINDOW) + gk_args = [ + gk_path, + "--game", "jak1", + "--config-path", config_path, + "--", "-v", "-boot", "-fakeiso", "-debug" + ] + + if Utils.is_windows: + gk_process = subprocess.Popen( + gk_args, + stdout=log_file, + stderr=log_file, + creationflags=subprocess.CREATE_NO_WINDOW + ) + else: + gk_process = subprocess.Popen( + gk_args, + stdout=log_file, + stderr=log_file + ) if not goalc_running: # For the OpenGOAL Compiler, the existence of the "data" subfolder indicates you are running it from @@ -586,7 +611,36 @@ async def run_game(ctx: JakAndDaxterContext): goalc_args = [goalc_path, "--game", "jak1"] # This needs to be a new console. The REPL console cannot share a window with any other process. - goalc_process = subprocess.Popen(goalc_args, creationflags=subprocess.CREATE_NEW_CONSOLE) + # We make similar decisions as Launcher.launch for terminals on Linux and Mac (despite not supporting Mac). + if Utils.is_windows: + goalc_process = subprocess.Popen(goalc_args, creationflags=subprocess.CREATE_NEW_CONSOLE) + + elif Utils.is_linux: + terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('konsole') or which('xterm') + + # Don't allow the terminal application to attempt loading system libraries, this can cause OpenSSL + # errors when launching the REPL due to version mismatches between system vs shipped libraries. + env = os.environ + if "LD_LIBRARY_PATH" in env: + env = env.copy() + del env["LD_LIBRARY_PATH"] + + if terminal: + goalc_process = subprocess.Popen([terminal, '-e', shlex.join(goalc_args)], env=env) + else: + msg = (f"Your Linux installation does not have a supported terminal application.\n" + f"We support the following options:\n" + f" x-terminal-emulator\n" + f" gnome-terminal\n" + f" konsole\n" + f" xterm\n" + f"Please install one of these and try again.") + ctx.on_log_error(logger, msg) + return + + elif Utils.is_macos: + terminal = [which('open'), '-W', '-a', 'Terminal.app'] + goalc_process = subprocess.Popen([*terminal, *goalc_args]) except AttributeError as e: if " " in e.args[0]: diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 379b4eecf2..986ae75b29 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -5,7 +5,7 @@ - A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* - [The OpenGOAL Launcher](https://opengoal.dev/) -At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux. +This method of setup works on Windows and Linux. ## Installation via OpenGOAL Launcher @@ -44,7 +44,7 @@ If you are in the middle of an async game, and you do not want to update the mod - The game window itself will launch, and Jak will be standing outside Samos's Hut. - Once compilation is complete, the title sequence will start. - Finally, the Archipelago text client will open. - - If you see **BOTH** `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. If you do not, see the Troubleshooting section. + - If you see **BOTH** `The Compiler is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. If you do not, see the Troubleshooting section. - Once you see `CONNECT TO ARCHIPELAGO NOW` on the title screen, use the text client to connect to the Archipelago server. This will communicate your current settings and slot info to the game. - If you see `RECEIVING ITEMS, PLEASE WAIT...`, the game is busy receiving items from your starting inventory, assuming you have some. - Once you see `READY! PRESS START TO CONTINUE` on the title screen, you can press Start. @@ -84,7 +84,7 @@ If it cannot, you may have to tell it yourself. Follow these instructions. root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" ``` -- Second, find the `root_directory` entry. Change this to `false`. You do not need to use double quotes. +- Second, find the `auto_detect_root_directory` entry. Change this to `false`. You do not need to use double quotes. ```yaml auto_detect_root_directory: true @@ -100,7 +100,7 @@ You may start the game via the Text Client, but it never loads in the title scre -- Compilation Error! -- ``` -If this happens, follow these instructions. If you are using a PAL version of the game, you should skip these instructions and follow the `Special PAL Instructions` above. +If this happens, follow these instructions. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. @@ -112,9 +112,9 @@ If this happens, follow these instructions. If you are using a PAL version of th - Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Compile`. -### The Text Client Says "Error reading game memory!" or "Error sending data to compiler" +### The Text Client Says "Error reading game memory!" or "Error sending data to compiler!" -If at any point the text client says this, you will need to restart the **all** of these applications. +If at any point the text client says this in big red letters, you will need to restart the **all** of these applications. - Close all open windows: the client, the compiler, and the game. - Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click `ArchipelaGOAL`. @@ -123,20 +123,24 @@ If at any point the text client says this, you will need to restart the **all** - Then close and reopen the Jak and Daxter Client from the Archipelago Launcher. - Once these are done, you can enter `/repl status` and `/memr status` in the text client to verify. -### The Client Cannot Open A REPL Connection +### The Client Cannot Open A Compiler Connection -If the client cannot open a REPL connection to the game, you may need to check the following steps: +If the client cannot open a REPL (compiler) connection to the game, you may need to check the following steps: - Ensure you are not hosting anything on ports `8181` and `8112`. Those are for the REPL (goalc) and the game (gk) respectively. - Ensure that Windows Defender and Windows Firewall are not blocking those programs from hosting or listening on those ports. - You can use Windows Resource Monitor to verify those ports are open when the programs are running. - Ensure that you only opened those ports for your local network, not the wider internet. +### Your Linux Installation Does Not Have A Supported Terminal Application + +If you are running Linux and your client shows this error, you will need to install a compatible terminal application like xterm. + ## Known Issues - The game needs to boot in debug mode in order to allow the compiler to connect to it. **Clicking "Play" on the mod page in the OpenGOAL Launcher will not work.** - The Compiler console window is orphaned once you close the game - you will have to kill it manually when you stop playing. -- The console windows cannot be run as background processes due to how the REPL works, so the best we can do is minimize them. +- The console windows cannot be run as background processes due to how the compiler works, so the best we can do is minimize them. - Orbsanity checks may show up out of order in the text client. - Large item releases may take up to several minutes for the game to process them all. Item Messages will usually take longer to appear than Items themselves. - In Lost Precursor City, if you die in the Color Platforms room, the game may crash after you respawn. The cause is unknown. diff --git a/worlds/jakanddaxter/game_id.py b/worlds/jakanddaxter/game_id.py index d596a6cc82..afffca4c97 100644 --- a/worlds/jakanddaxter/game_id.py +++ b/worlds/jakanddaxter/game_id.py @@ -1,3 +1,5 @@ +import Utils + # All Jak And Daxter Archipelago IDs must be offset by this number. jak1_id = 741000000 @@ -6,3 +8,9 @@ jak1_max = jak1_id + 999999 # The name of the game. jak1_name = "Jak and Daxter: The Precursor Legacy" + +# The executable name of the GOAL Kernel. +jak1_gk = "gk" + (".exe" if Utils.is_windows else "") + +# The executable name of the GOAL Compiler. +jak1_goalc = "goalc" + (".exe" if Utils.is_windows else "") \ No newline at end of file diff --git a/worlds/jakanddaxter/requirements.txt b/worlds/jakanddaxter/requirements.txt index ca36764fbf..9ec60e8d10 100644 --- a/worlds/jakanddaxter/requirements.txt +++ b/worlds/jakanddaxter/requirements.txt @@ -1 +1 @@ -Pymem>=1.13.0 +PyMemoryEditor>=1.6.0