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

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)

View File

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

View File

@@ -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"]
}

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
Pymem>=1.13.0
PyMemoryEditor>=1.6.0