Jak and Daxter: Implement New Game (#3291)
* Jak 1: Initial commit: Cell Locations, Items, and Regions modeled. * Jak 1: Wrote Regions, Rules, init. Untested. * Jak 1: Fixed mistakes, need better understanding of Entrances. * Jak 1: Fixed bugs, refactored Regions, added missing Special Checks. First spoiler log generated. * Jak 1: Add Scout Fly Locations, code and style cleanup. * Jak 1: Add Scout Flies to Regions. * Jak 1: Add version info. * Jak 1: Reduced code smell. * Jak 1: Fixed UT bugs, added Free The Sages as Locations. * Jak 1: Refactor ID scheme to better fit game's scheme. Add more subregions and rules, but still missing one-way Entrances. * Jak 1: Add some one-ways, adjust scout fly offset. * Jak 1: Found Scout Fly ID's for first 4 maps. * Jak 1: Add more scout fly ID's, refactor game/AP ID translation for easier reading and code reuse. * Jak 1: Fixed a few things. Four maps to go. * Jak 1: Last of the scout flies mapped! * Jak 1: simplify citadel sages logic. * Jak 1: WebWorld setup, some documentation. * Jak 1: Initial checkin of Client. Removed the colon from the game name. * Jak 1: Refactored client into components, working on async communication between the client and the game. * Jak 1: In tandem with new ArchipelaGOAL memory structure, define read_memory. * Jak 1: There's magic in the air... * Jak 1: Fixed bug translating scout fly ID's. * Jak 1: Make the REPL a little more verbose, easier to debug. * Jak 1: Did you know Snowy Mountain had such specific unlock requirements? I didn't. * Jak 1: Update Documentation. * Jak 1: Simplify user interaction with agents, make process more robust/less dependent on order of ops. * Jak 1: Simplified startup process, updated docs, prayed. * Jak 1: quick fix to settings. * Jak and Daxter: Implement New Game (#1) * Jak 1: Initial commit: Cell Locations, Items, and Regions modeled. * Jak 1: Wrote Regions, Rules, init. Untested. * Jak 1: Fixed mistakes, need better understanding of Entrances. * Jak 1: Fixed bugs, refactored Regions, added missing Special Checks. First spoiler log generated. * Jak 1: Add Scout Fly Locations, code and style cleanup. * Jak 1: Add Scout Flies to Regions. * Jak 1: Add version info. * Jak 1: Reduced code smell. * Jak 1: Fixed UT bugs, added Free The Sages as Locations. * Jak 1: Refactor ID scheme to better fit game's scheme. Add more subregions and rules, but still missing one-way Entrances. * Jak 1: Add some one-ways, adjust scout fly offset. * Jak 1: Found Scout Fly ID's for first 4 maps. * Jak 1: Add more scout fly ID's, refactor game/AP ID translation for easier reading and code reuse. * Jak 1: Fixed a few things. Four maps to go. * Jak 1: Last of the scout flies mapped! * Jak 1: simplify citadel sages logic. * Jak 1: WebWorld setup, some documentation. * Jak 1: Initial checkin of Client. Removed the colon from the game name. * Jak 1: Refactored client into components, working on async communication between the client and the game. * Jak 1: In tandem with new ArchipelaGOAL memory structure, define read_memory. * Jak 1: There's magic in the air... * Jak 1: Fixed bug translating scout fly ID's. * Jak 1: Make the REPL a little more verbose, easier to debug. * Jak 1: Did you know Snowy Mountain had such specific unlock requirements? I didn't. * Jak 1: Update Documentation. * Jak 1: Simplify user interaction with agents, make process more robust/less dependent on order of ops. * Jak 1: Simplified startup process, updated docs, prayed. * Jak 1: quick fix to settings. * Jak and Daxter: Genericize Items, Update Scout Fly logic, Add Victory Condition. (#3) * Jak 1: Update to 0.4.6. Decouple locations from items, support filler items. * Jak 1: Total revamp of Items. This is where everything broke. * Jak 1: Decouple 7 scout fly checks from normal checks, update regions/rules for orb counts/traders. * Jak 1: correct regions/rules, account for sequential oracle/miner locations. * Jak 1: make nicer strings. * Jak 1: Add logic for finished game. First full run complete! * Jak 1: update group names. * Jak and Daxter - Gondola, Pontoons, Rules, Regions, and Client Update * Jak 1: Overhaul of regions, rules, and special locations. Updated game info page. * Jak 1: Preparations for Alpha. Reintroducing automatic startup in client. Updating docs, readme, codeowners. * Alpha Updates (#15) * Jak 1: Consolidate client into apworld, create launcher icon, improve setup docs. * Jak 1: Update setup guide. * Jak 1: Load title screen, save states of in/outboxes. * Logging Update (#16) * Jak 1: Separate info and debug logs. * Jak 1: Update world info to refer to Archipelago Options menu. * 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. * Move Randomizer (#26) * Finally remove debug-segment text, update Python imports to relative paths. * HUGE refactor to Regions/Rules to support move rando, first hub area coded. * More refactoring. * Another refactor - may squash. * Fix some Rules, reuse some code by returning key regions from build_regions. * More regions added. A couple of TODOs. * Fixed trade logic, added LPC regions. * Added Spider, Snowy, Boggy. Fixed Misty's orbs. * Fix circular import, assert orb counts per level, fix a few naming errors. * Citadel added, missing locs and connections fixed. First move rando seed generated. * Add Move Rando to Options class. * Fixed rules for prerequisite moves. * Implement client functionality for move rando, add blurbs to game info page. * Fix wrong address for cache checks. * Fix byte alignment of offsets, refactor read_memory for better code reuse. * Refactor memory offsets and add some unit tests. * Make green eco the filler item, also define a maximum ID. Fix Boggy tether locations. * Move rando fixes (#29) * Fix virtual regions in Snowy. Fix some GMC problems. * Fix Deathlink on sunken slides. * Removed unncessary code causing build failure. * Orbsanity (#32) * My big dumb shortcut: a 2000 item array. * A better idea: bundle orbs as a numerical option and make array variable size. * Have Item/Region generation respect the chosen Orbsanity bundle size. Fix trade logic. * Separate Global/Local Orbsanity options. TODO - re-introduce orb factory for per-level option. * Per-level Orbsanity implemented w/ orb bundle factory. * Implement Orbsanity for client, fix some things up for regions. * Fix location name/id mappings. * Fix client orb collection on connection. * Fix minor Deathlink bug, add Update instructions. * Finishing Touches (#36) * Set up connector level thresholds, completion goal choices. * Send AP sender/recipient info to game via client. * Slight refactors. * Refactor option checking, add DataStorage handling of traded orbs. * Update instructions to change order of load/connect. * Add Option check to ensure enough Locations exist for Cell Count thresholds. Fix Final Door region. * Need some height move to get LPC sunken chamber cell. * Rename completion_condition to jak_completion_condition (#41) * The Afterparty (#42) * Fixes to Jak client, rules, options, and more. * Post-rebase fixes. * Remove orbsanity reset code, optimize game text in client. * More game text optimization. * Added more specific troubleshooting/setup instructions. * Add known issue about large releases taking time. (Dodge 6,666th commit.) * Remove "Bundle of", Add location name groups, set better default RootDirectory for new players. * Make orb trade amounts configurable, make orbsanity defaults more reasonable. * Add HUD info to doc. * Exempt's Code Review Updates (#43) * Round 1 of code review updates, the easy stuff. * Factor options checking away from region/rule creation. * Code review updates round 2, more complex stuff. * Code review updates round 3: the mental health annihilator * Code review updates part 4: redemption. * More code review feedback, simplifying code, etc. * Added a host.yaml option to override friendly limits, plus a couple of code review updates. * Added singleplayer limits, player names to enforcement rules. * Updated friendly limits to be more strict, optimized recalculate logic. * Today's the big day Jak: updates docs for mod support in OpenGOAL Launcher * Rearranged and clarified some instructions, ADDED PATH-SPACE FIX TO CLIENT. * Fix deathlink reset stalls on a busy client. (#47) * Jak & Daxter Client : queue game text messages to get items faster during release (#48) * queue game text messages to write them during the main_tick function and empty the message queue faster during release * wrap comment for code style character limit Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * remove useless blank line Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * whitespace code style Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * Move JsonMessageData dataclass outside of ReplClient class for code clarity --------- Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * Item Classifications (and REPL fixes) (#49) * Changes to item classifications * Bugfixes to power cell thresholds. * Fix bugs in item_type_helper. * Refactor 100 cell door to pass unit tests. * Quick fix to ReplClient. * Not so quick fix to ReplClient. * Display friendly limits in options tooltips. * Use math.ceil like a normal person. * Missed a space. * Fix non-accessibility due to bad orb calculation. * Updated documentation. * More Options, More Docs, More Tests (#51) * Reorder cell counts, require punch for Klaww. * Friendlier friendly friendlies. * Removed custom_worlds references from docs/setup guide, focused OpenGOAL Launcher language. * Increased breadth of unit tests. * Clean imports of unit tests. * Create OptionGroups. * Fix region rule bug with Punch for Klaww. * Include Punch For Klaww in slot data. * Update worlds/jakanddaxter/__init__.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Temper and Harden Text Client (#52) * Provide config path so OpenGOAL can use mod-specific saves and settings. * Add versioning to MemoryReader. Harden the client against user errors. * Updated comments. * Add Deathlink as a "statement of intent" to the YAML. Small updates to client. * Revert deathlink changes. * Update error message. * Added color markup to log messages printed in text client. * Separate loggers by agent, write markup to GUI and non-markup to disk simultaneously. * Refactor MemoryReader callbacks from main_tick to constructor. * Make callback names more... informative. * Give users explicit instructions in error messages. * Stellar Messaging (#54) * Use new ap-messenger functions for text writing. * Remove Powershell requirement, bump memory version to 3. * Error message update w/ instructions for game crash. * Create no console window for gk. * ISO Data Enhancement (#58) * Add iso-path as argument to GOAL compiler. # Conflicts: # worlds/jakanddaxter/Client.py * More resilient handling of iso_path. * Fixed scout fly ID mismatches. * Corrected iso_data subpath. * Update memory version to 4. * Docs update for iso_data. * Auto Detect OpenGOAL Install (#63) * Auto detect OpenGOAL install path. Also fix Deathlink on server connection. * Updated docs, add instructions to error messages. * Slight tweak to error text. * J&D : add per region location groups (#64) * add per region power cells location group * add per region scout flies location group * add per zone orb bundle groups (I'm not particularly happy about this code, but I figured doing it this way was the point of least friction/duplication) * guess who forgot 9 very important characters in each line of the last commit * Rearrange location group names, quick fix to client error handling. * Fix pycharm warnings. * Fix more pycharm warnings. * Light cleanup: fix icons, add bug report page, remove py 3.8 code. * Update worlds/jakanddaxter/Options.py Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/jakanddaxter/Options.py Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/jakanddaxter/Options.py Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/jakanddaxter/Options.py Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Code review updates on comments, tooltips, and type hints. * Update type hint for lists in regions. * Missed todo removal. * More type hint updates. * Small region updates for location accessibility, small updates to world guide and README.md. * Add GMC scout fly location group. * Improved sanitization of game text. * Traps 2 (#70) * Add trap items, relevant options, and citadel orb caches. * Update REPL to send traps to game. * Fix item counter. * Allow player to select which traps to use. * Fix host.yaml doc strings, ap-setup-options typing, bump memory version to 5. * Alter some trap names. * Update world doc. * Add health trap. * Added 3 more trap types. * Protect against empty trap list. * Reword traps paragraph in world doc. * Another update to trap paragraph. * Concisify trap option docstring. * Timestamp on game log file. * Update client to handle waiting on title screen. * Send slot name and seed to game. * Use self.random instead. * Update setup doc for new title screen. * Quick clarification of orb caches in world doc. * Sanitize slot info earlier. * Added to and improved unit tests. * Light cleanup on world. * Optimizations to movement rules, docs: known issues update. * Quick fixes for beta 0.5.0 release: template options and LPC logic. * Quick fix to spoiler counts. * Reorganize world guide for faster navigation. * Fix links. * Update HUD section. * Found a way to render apostrophes in item names. * March Refactors (#77) * Reorg imports, small fix to Rock Village movement. * Fix wait-on-title message never going to ready message. * Colorama init fix. * Swap trap list for a dictionary of trap weights. * The more laws, the less justice. * Quick readability update. * Have memory reader provide instructions for slow booting games. * Revert some things. * Update setup_en.md * Update HUD mode lingo for combined msgs. * Remade launcher icon, sized correctly. * I don't know why I can't be satisfied with things. * Apply suggestions from Scipio Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Properly use the settings API instead of Utils. * Newline on requirements.txt. * Add __init__ files for frozen builds. * Replace an ap_inform function with a CommonClient built-in. * Resize icon to match kivymd expected size. * First round of Treble code reviews. * Second round of Treble code reviews. * Third round of Treble code reviews. * Missed an unncessary if condition. * Missed unnecessary comments. * Fourth round of Treble code reviews. * Switch trap dictionary to OptionCounter. * Use existing slot name/seed from network protocol. * Violet code review updates. * Violet code review updates part 2. * Refactor to avoid floating imports (Violet part 3). * Found a few more valid characters for messaging. * Move tests out of init, add colon to game name (now that it's safe). * But don't include those chars for file text. * Implement Vi suggestion on webhost-capable friendly limits. * Revert "Implement Vi suggestion on webhost-capable friendly limits." This reverts commit 2d012b7f4a9a4c13985ecd7303bb1fc646831c86. * Rename all files for PEP8. * Refactor how maximums work on webhost. * Fix rogue UT. * Don't rush. * Fix client post-PEP8. --------- Co-authored-by: Justus Lind <DeamonHunter@users.noreply.github.com> Co-authored-by: Romain BERNARD <30secondstodraw@gmail.com> Co-authored-by: Scipio Wright <scipiowright@gmail.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7f4bf71807
commit
3069deb019
0
worlds/jakanddaxter/agents/__init__.py
Normal file
0
worlds/jakanddaxter/agents/__init__.py
Normal file
489
worlds/jakanddaxter/agents/memory_reader.py
Normal file
489
worlds/jakanddaxter/agents/memory_reader.py
Normal file
@@ -0,0 +1,489 @@
|
||||
import logging
|
||||
import random
|
||||
import struct
|
||||
from typing import ByteString, Callable
|
||||
import json
|
||||
import pymem
|
||||
from pymem import pattern
|
||||
from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..locs import (orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as flies,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
|
||||
|
||||
logger = logging.getLogger("MemoryReader")
|
||||
|
||||
|
||||
# Some helpful constants.
|
||||
sizeof_uint64 = 8
|
||||
sizeof_uint32 = 4
|
||||
sizeof_uint8 = 1
|
||||
sizeof_float = 4
|
||||
|
||||
|
||||
# *****************************************************************************
|
||||
# **** This number must match (-> *ap-info-jak1* version) in ap-struct.gc! ****
|
||||
# *****************************************************************************
|
||||
expected_memory_version = 5
|
||||
|
||||
|
||||
# IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to
|
||||
# their size in bits. The address for an N-bit field must be divisible by N. Use this class to define the memory offsets
|
||||
# of important values in the struct. It will also do the byte alignment properly for you.
|
||||
# See https://opengoal.dev/docs/reference/type_system/#arrays
|
||||
@dataclass
|
||||
class OffsetFactory:
|
||||
current_offset: int = 0
|
||||
|
||||
def define(self, size: int, length: int = 1) -> int:
|
||||
|
||||
# If necessary, align current_offset to the current size first.
|
||||
bytes_to_alignment = self.current_offset % size
|
||||
if bytes_to_alignment != 0:
|
||||
self.current_offset += (size - bytes_to_alignment)
|
||||
|
||||
# Increment current_offset so the next definition can be made.
|
||||
offset_to_use = self.current_offset
|
||||
self.current_offset += (size * length)
|
||||
return offset_to_use
|
||||
|
||||
|
||||
# Start defining important memory address offsets here. They must be in the same order, have the same sizes, and have
|
||||
# the same lengths, as defined in `ap-info-jak1`.
|
||||
offsets = OffsetFactory()
|
||||
|
||||
# Cell, Buzzer, and Special information.
|
||||
next_cell_index_offset = offsets.define(sizeof_uint64)
|
||||
next_buzzer_index_offset = offsets.define(sizeof_uint64)
|
||||
next_special_index_offset = offsets.define(sizeof_uint64)
|
||||
|
||||
cells_checked_offset = offsets.define(sizeof_uint32, 101)
|
||||
buzzers_checked_offset = offsets.define(sizeof_uint32, 112)
|
||||
specials_checked_offset = offsets.define(sizeof_uint32, 32)
|
||||
|
||||
buzzers_received_offset = offsets.define(sizeof_uint8, 16)
|
||||
specials_received_offset = offsets.define(sizeof_uint8, 32)
|
||||
|
||||
# Deathlink information.
|
||||
death_count_offset = offsets.define(sizeof_uint32)
|
||||
death_cause_offset = offsets.define(sizeof_uint8)
|
||||
deathlink_enabled_offset = offsets.define(sizeof_uint8)
|
||||
|
||||
# Move Rando information.
|
||||
next_orb_cache_index_offset = offsets.define(sizeof_uint64)
|
||||
orb_caches_checked_offset = offsets.define(sizeof_uint32, 16)
|
||||
moves_received_offset = offsets.define(sizeof_uint8, 16)
|
||||
moverando_enabled_offset = offsets.define(sizeof_uint8)
|
||||
|
||||
# Orbsanity information.
|
||||
orbsanity_option_offset = offsets.define(sizeof_uint8)
|
||||
orbsanity_bundle_offset = offsets.define(sizeof_uint32)
|
||||
collected_bundle_offset = offsets.define(sizeof_uint32, 17)
|
||||
|
||||
# Progression and Completion information.
|
||||
fire_canyon_unlock_offset = offsets.define(sizeof_float)
|
||||
mountain_pass_unlock_offset = offsets.define(sizeof_float)
|
||||
lava_tube_unlock_offset = offsets.define(sizeof_float)
|
||||
citizen_orb_amount_offset = offsets.define(sizeof_float)
|
||||
oracle_orb_amount_offset = offsets.define(sizeof_float)
|
||||
completion_goal_offset = offsets.define(sizeof_uint8)
|
||||
completed_offset = offsets.define(sizeof_uint8)
|
||||
|
||||
# Text to display in the HUD (32 char max per string).
|
||||
their_item_name_offset = offsets.define(sizeof_uint8, 32)
|
||||
their_item_owner_offset = offsets.define(sizeof_uint8, 32)
|
||||
my_item_name_offset = offsets.define(sizeof_uint8, 32)
|
||||
my_item_finder_offset = offsets.define(sizeof_uint8, 32)
|
||||
|
||||
# Version of the memory struct, to cut down on mod/apworld version mismatches.
|
||||
memory_version_offset = offsets.define(sizeof_uint32)
|
||||
|
||||
# Connection status to AP server (not the game!)
|
||||
server_connection_offset = offsets.define(sizeof_uint8)
|
||||
slot_name_offset = offsets.define(sizeof_uint8, 16)
|
||||
slot_seed_offset = offsets.define(sizeof_uint8, 8)
|
||||
|
||||
# Trap information.
|
||||
trap_duration_offset = offsets.define(sizeof_float)
|
||||
|
||||
# The End.
|
||||
end_marker_offset = offsets.define(sizeof_uint8, 4)
|
||||
|
||||
|
||||
# Can't believe this is easier to do in GOAL than Python but that's how it be sometimes.
|
||||
def as_float(value: int) -> int:
|
||||
return int(struct.unpack('f', value.to_bytes(sizeof_float, "little"))[0])
|
||||
|
||||
|
||||
# "Jak" to be replaced by player name in the Client.
|
||||
def autopsy(cause: int) -> str:
|
||||
if cause 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 cause == 5:
|
||||
return "Jak fell into an endless pit."
|
||||
if cause == 6:
|
||||
return "Jak drowned in the spicy water."
|
||||
if cause == 7:
|
||||
return "Jak tried to tackle a Lurker Shark."
|
||||
if cause == 8:
|
||||
return "Jak hit 500 degrees."
|
||||
if cause == 9:
|
||||
return "Jak took a bath in a pool of dark eco."
|
||||
if cause == 10:
|
||||
return "Jak got bombarded with flaming 30-ton boulders."
|
||||
if cause == 11:
|
||||
return "Jak hit 800 degrees."
|
||||
if cause == 12:
|
||||
return "Jak ceased to be."
|
||||
if cause == 13:
|
||||
return "Jak got eaten by the dark eco plant."
|
||||
if cause == 14:
|
||||
return "Jak burned up."
|
||||
if cause == 15:
|
||||
return "Jak hit the ground hard."
|
||||
if cause == 16:
|
||||
return "Jak crashed the zoomer."
|
||||
if cause == 17:
|
||||
return "Jak got Flut Flut hurt."
|
||||
if cause == 18:
|
||||
return "Jak poisoned the whole darn catch."
|
||||
if cause == 19:
|
||||
return "Jak collided with too many obstacles."
|
||||
return "Jak died."
|
||||
|
||||
|
||||
class JakAndDaxterMemoryReader:
|
||||
marker: ByteString
|
||||
goal_address: int | None = None
|
||||
connected: bool = False
|
||||
initiated_connect: bool = False
|
||||
|
||||
# The memory reader just needs the game running.
|
||||
gk_process: pymem.process = None
|
||||
|
||||
location_outbox: list[int] = []
|
||||
outbox_index: int = 0
|
||||
finished_game: bool = False
|
||||
|
||||
# Deathlink handling
|
||||
deathlink_enabled: bool = False
|
||||
send_deathlink: bool = False
|
||||
cause_of_death: str = ""
|
||||
death_count: int = 0
|
||||
|
||||
# Orbsanity handling
|
||||
orbsanity_enabled: bool = False
|
||||
orbs_paid: int = 0
|
||||
|
||||
# Game-related callbacks (inform the AP server of changes to game state)
|
||||
inform_checked_location: Callable
|
||||
inform_finished_game: Callable
|
||||
inform_died: Callable
|
||||
inform_toggled_deathlink: Callable
|
||||
inform_traded_orbs: Callable
|
||||
|
||||
# Logging callbacks
|
||||
# These will write to the provided logger, as well as the Client GUI with color markup.
|
||||
log_error: Callable # Red
|
||||
log_warn: Callable # Orange
|
||||
log_success: Callable # Green
|
||||
log_info: Callable # White (default)
|
||||
|
||||
def __init__(self,
|
||||
location_check_callback: Callable,
|
||||
finish_game_callback: Callable,
|
||||
send_deathlink_callback: Callable,
|
||||
toggle_deathlink_callback: Callable,
|
||||
orb_trade_callback: Callable,
|
||||
log_error_callback: Callable,
|
||||
log_warn_callback: Callable,
|
||||
log_success_callback: Callable,
|
||||
log_info_callback: Callable,
|
||||
marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'):
|
||||
self.marker = marker
|
||||
|
||||
self.inform_checked_location = location_check_callback
|
||||
self.inform_finished_game = finish_game_callback
|
||||
self.inform_died = send_deathlink_callback
|
||||
self.inform_toggled_deathlink = toggle_deathlink_callback
|
||||
self.inform_traded_orbs = orb_trade_callback
|
||||
|
||||
self.log_error = log_error_callback
|
||||
self.log_warn = log_warn_callback
|
||||
self.log_success = log_success_callback
|
||||
self.log_info = log_info_callback
|
||||
|
||||
async def main_tick(self):
|
||||
if self.initiated_connect:
|
||||
await self.connect()
|
||||
self.initiated_connect = False
|
||||
|
||||
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):
|
||||
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"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
else:
|
||||
return
|
||||
|
||||
if self.connected:
|
||||
|
||||
# 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()
|
||||
|
||||
# Checked Locations in game. Handle the entire outbox every tick until we're up to speed.
|
||||
if len(self.location_outbox) > self.outbox_index:
|
||||
self.inform_checked_location(self.location_outbox)
|
||||
self.save_data()
|
||||
self.outbox_index += 1
|
||||
|
||||
if self.finished_game:
|
||||
self.inform_finished_game()
|
||||
|
||||
if old_deathlink_enabled != self.deathlink_enabled:
|
||||
self.inform_toggled_deathlink()
|
||||
logger.debug("Toggled DeathLink " + ("ON" if self.deathlink_enabled else "OFF"))
|
||||
|
||||
if self.send_deathlink:
|
||||
self.inform_died()
|
||||
|
||||
if self.orbs_paid > 0:
|
||||
self.inform_traded_orbs(self.orbs_paid)
|
||||
self.orbs_paid = 0
|
||||
|
||||
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.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()
|
||||
else:
|
||||
self.log_error(logger, "Could not find the Archipelago marker address!")
|
||||
self.connected = False
|
||||
|
||||
async def verify_memory_version(self):
|
||||
if self.goal_address is None:
|
||||
self.log_error(logger, "Could not find the Archipelago memory address!")
|
||||
self.connected = False
|
||||
return
|
||||
|
||||
memory_version: int | None = None
|
||||
try:
|
||||
memory_version = self.read_goal_address(memory_version_offset, sizeof_uint32)
|
||||
if memory_version == expected_memory_version:
|
||||
self.log_success(logger, "The Memory Reader is ready!")
|
||||
self.connected = True
|
||||
else:
|
||||
raise MemoryReadError(memory_version_offset, sizeof_uint32)
|
||||
except (ProcessError, MemoryReadError, WinAPIError):
|
||||
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"
|
||||
f" Found Version: {str(memory_version)}\n"
|
||||
f"Please follow these steps:\n"
|
||||
f" If the game is running, try entering '/memr connect' in the client.\n"
|
||||
f" You should see 'The Memory Reader is ready!'\n"
|
||||
f" If that did not work, or the game is not running, run the OpenGOAL Launcher.\n"
|
||||
f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Try entering '/memr connect' in the client again.")
|
||||
else:
|
||||
msg = (f"The OpenGOAL memory structure is incompatible with the current Archipelago client!\n"
|
||||
f" Expected Version: {str(expected_memory_version)}\n"
|
||||
f" Found Version: {str(memory_version)}\n"
|
||||
f"Please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Click Update (if one is available).\n"
|
||||
f" Click Advanced > Compile. When this is done, click Continue.\n"
|
||||
f" Click Versions and verify the latest version is marked 'Active'.\n"
|
||||
f" Close all launchers, games, clients, and console windows, then restart Archipelago.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
|
||||
async def print_status(self):
|
||||
proc_id = str(self.gk_process.process_id) 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"
|
||||
f" Game state memory address: {str(self.goal_address)}\n"
|
||||
f" Last location checked: {last_loc}")
|
||||
await self.verify_memory_version()
|
||||
self.log_info(logger, msg)
|
||||
|
||||
def read_memory(self) -> list[int]:
|
||||
try:
|
||||
# Need to grab these first and convert to floats, see below.
|
||||
citizen_orb_amount = self.read_goal_address(citizen_orb_amount_offset, sizeof_float)
|
||||
oracle_orb_amount = self.read_goal_address(oracle_orb_amount_offset, sizeof_float)
|
||||
|
||||
next_cell_index = self.read_goal_address(next_cell_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_cell_index):
|
||||
next_cell = self.read_goal_address(cells_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
cell_ap_id = cells.to_ap_id(next_cell)
|
||||
if cell_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(cell_ap_id)
|
||||
logger.debug("Checked power cell: " + str(next_cell))
|
||||
|
||||
# If orbsanity is ON and next_cell is one of the traders or oracles, then run a callback
|
||||
# to add their amount to the DataStorage value holding our current orb trade total.
|
||||
if next_cell in {11, 12, 31, 32, 33, 96, 97, 98, 99}:
|
||||
citizen_orb_amount = as_float(citizen_orb_amount)
|
||||
self.orbs_paid += citizen_orb_amount
|
||||
logger.debug(f"Traded {citizen_orb_amount} orbs!")
|
||||
|
||||
if next_cell in {13, 14, 34, 35, 100, 101}:
|
||||
oracle_orb_amount = as_float(oracle_orb_amount)
|
||||
self.orbs_paid += oracle_orb_amount
|
||||
logger.debug(f"Traded {oracle_orb_amount} orbs!")
|
||||
|
||||
next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_buzzer_index):
|
||||
next_buzzer = self.read_goal_address(buzzers_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
buzzer_ap_id = flies.to_ap_id(next_buzzer)
|
||||
if buzzer_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(buzzer_ap_id)
|
||||
logger.debug("Checked scout fly: " + str(next_buzzer))
|
||||
|
||||
next_special_index = self.read_goal_address(next_special_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_special_index):
|
||||
next_special = self.read_goal_address(specials_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
special_ap_id = specials.to_ap_id(next_special)
|
||||
if special_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(special_ap_id)
|
||||
logger.debug("Checked special: " + str(next_special))
|
||||
|
||||
death_count = self.read_goal_address(death_count_offset, sizeof_uint32)
|
||||
death_cause = self.read_goal_address(death_cause_offset, sizeof_uint8)
|
||||
if death_count > self.death_count:
|
||||
self.cause_of_death = autopsy(death_cause) # The way he names his variables? Wack!
|
||||
self.send_deathlink = True
|
||||
self.death_count += 1
|
||||
|
||||
# Listen for any changes to this setting.
|
||||
deathlink_flag = self.read_goal_address(deathlink_enabled_offset, sizeof_uint8)
|
||||
self.deathlink_enabled = bool(deathlink_flag)
|
||||
|
||||
next_cache_index = self.read_goal_address(next_orb_cache_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_cache_index):
|
||||
next_cache = self.read_goal_address(orb_caches_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
cache_ap_id = caches.to_ap_id(next_cache)
|
||||
if cache_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(cache_ap_id)
|
||||
logger.debug("Checked orb cache: " + str(next_cache))
|
||||
|
||||
# Listen for any changes to this setting.
|
||||
# moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8)
|
||||
# self.moverando_enabled = bool(moverando_flag)
|
||||
|
||||
orbsanity_option = self.read_goal_address(orbsanity_option_offset, sizeof_uint8)
|
||||
bundle_size = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32)
|
||||
self.orbsanity_enabled = orbsanity_option > 0
|
||||
|
||||
# Per Level Orbsanity option. Only need to do this loop if we chose this setting.
|
||||
if orbsanity_option == 1:
|
||||
for level in range(0, 16):
|
||||
collected_bundles = self.read_goal_address(collected_bundle_offset + (level * sizeof_uint32),
|
||||
sizeof_uint32)
|
||||
|
||||
# Count up from the first bundle, by bundle size, until you reach the latest collected bundle.
|
||||
# e.g. {25, 50, 75, 100, 125...}
|
||||
if collected_bundles > 0:
|
||||
for bundle in range(bundle_size,
|
||||
bundle_size + collected_bundles, # Range max is non-inclusive.
|
||||
bundle_size):
|
||||
|
||||
bundle_ap_id = orbs.to_ap_id(orbs.find_address(level, bundle, bundle_size))
|
||||
if bundle_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(bundle_ap_id)
|
||||
logger.debug(f"Checked orb bundle: L{level} {bundle}")
|
||||
|
||||
# Global Orbsanity option. Index 16 refers to all orbs found regardless of level.
|
||||
if orbsanity_option == 2:
|
||||
collected_bundles = self.read_goal_address(collected_bundle_offset + (16 * sizeof_uint32),
|
||||
sizeof_uint32)
|
||||
if collected_bundles > 0:
|
||||
for bundle in range(bundle_size,
|
||||
bundle_size + collected_bundles, # Range max is non-inclusive.
|
||||
bundle_size):
|
||||
|
||||
bundle_ap_id = orbs.to_ap_id(orbs.find_address(16, bundle, bundle_size))
|
||||
if bundle_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(bundle_ap_id)
|
||||
logger.debug(f"Checked orb bundle: G {bundle}")
|
||||
|
||||
completed = self.read_goal_address(completed_offset, sizeof_uint8)
|
||||
if completed > 0 and not self.finished_game:
|
||||
self.finished_game = True
|
||||
self.log_success(logger, "Congratulations! You finished the game!")
|
||||
|
||||
except (ProcessError, MemoryReadError, WinAPIError):
|
||||
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"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
|
||||
return self.location_outbox
|
||||
|
||||
def read_goal_address(self, offset: int, length: int) -> int:
|
||||
return int.from_bytes(
|
||||
self.gk_process.read_bytes(self.goal_address + offset, length),
|
||||
byteorder="little",
|
||||
signed=False)
|
||||
|
||||
def save_data(self):
|
||||
with open("jakanddaxter_location_outbox.json", "w+") as f:
|
||||
dump = {
|
||||
"outbox_index": self.outbox_index,
|
||||
"location_outbox": self.location_outbox
|
||||
}
|
||||
json.dump(dump, f, indent=4)
|
||||
|
||||
def load_data(self):
|
||||
try:
|
||||
with open("jakanddaxter_location_outbox.json", "r") as f:
|
||||
load = json.load(f)
|
||||
self.outbox_index = load["outbox_index"]
|
||||
self.location_outbox = load["location_outbox"]
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
527
worlds/jakanddaxter/agents/repl_client.py
Normal file
527
worlds/jakanddaxter/agents/repl_client.py
Normal file
@@ -0,0 +1,527 @@
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import time
|
||||
import struct
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from queue import Queue
|
||||
from typing import Callable
|
||||
|
||||
import pymem
|
||||
from pymem.exception import ProcessNotFound, ProcessError
|
||||
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter, Lock
|
||||
|
||||
from NetUtils import NetworkItem
|
||||
from ..game_id import jak1_id, jak1_max
|
||||
from ..items import item_table, trap_item_table
|
||||
from ..locs import (
|
||||
orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as flies,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
|
||||
|
||||
logger = logging.getLogger("ReplClient")
|
||||
|
||||
|
||||
@dataclass
|
||||
class JsonMessageData:
|
||||
my_item_name: str | None = None
|
||||
my_item_finder: str | None = None
|
||||
their_item_name: str | None = None
|
||||
their_item_owner: str | None = None
|
||||
|
||||
|
||||
ALLOWED_CHARACTERS = frozenset({
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d",
|
||||
"e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
|
||||
"o", "p", "q", "r", "s", "t", "u", "v", "w", "x",
|
||||
"y", "z", " ", "!", ":", ",", ".", "/", "?", "-",
|
||||
"=", "+", "'", "(", ")", "\""
|
||||
})
|
||||
|
||||
|
||||
class JakAndDaxterReplClient:
|
||||
ip: str
|
||||
port: int
|
||||
reader: StreamReader
|
||||
writer: StreamWriter
|
||||
lock: Lock
|
||||
connected: bool = False
|
||||
initiated_connect: bool = False # Signals when user tells us to try reconnecting.
|
||||
received_deathlink: bool = False
|
||||
balanced_orbs: bool = False
|
||||
|
||||
# Variables to handle the title screen and initial game connection.
|
||||
initial_item_count = -1 # Brand new games have 0 items, so initialize this to -1.
|
||||
received_initial_items = False
|
||||
processed_initial_items = 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.
|
||||
gk_process: pymem.process = None
|
||||
goalc_process: pymem.process = None
|
||||
|
||||
item_inbox: dict[int, NetworkItem] = {}
|
||||
inbox_index = 0
|
||||
json_message_queue: Queue[JsonMessageData] = queue.Queue()
|
||||
|
||||
# Logging callbacks
|
||||
# These will write to the provided logger, as well as the Client GUI with color markup.
|
||||
log_error: Callable # Red
|
||||
log_warn: Callable # Orange
|
||||
log_success: Callable # Green
|
||||
log_info: Callable # White (default)
|
||||
|
||||
def __init__(self,
|
||||
log_error_callback: Callable,
|
||||
log_warn_callback: Callable,
|
||||
log_success_callback: Callable,
|
||||
log_info_callback: Callable,
|
||||
ip: str = "127.0.0.1",
|
||||
port: int = 8181):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
self.lock = asyncio.Lock()
|
||||
self.log_error = log_error_callback
|
||||
self.log_warn = log_warn_callback
|
||||
self.log_success = log_success_callback
|
||||
self.log_info = log_info_callback
|
||||
|
||||
async def main_tick(self):
|
||||
if self.initiated_connect:
|
||||
await self.connect()
|
||||
self.initiated_connect = False
|
||||
|
||||
if self.connected:
|
||||
try:
|
||||
self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive.
|
||||
except ProcessError:
|
||||
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"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
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:
|
||||
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"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
else:
|
||||
return
|
||||
|
||||
# When connecting the game to the AP server on the title screen, we may be processing items from starting
|
||||
# inventory or items received in an async game. Once we have caught up to the initial count, tell the player
|
||||
# that we are ready to start. New items may even come in during the title screen, so if we go over the count,
|
||||
# we should still send the ready signal.
|
||||
if not self.processed_initial_items:
|
||||
if self.inbox_index >= self.initial_item_count >= 0:
|
||||
self.processed_initial_items = True
|
||||
await self.send_connection_status("ready")
|
||||
|
||||
# Receive Items from AP. Handle 1 item per tick.
|
||||
if len(self.item_inbox) > self.inbox_index:
|
||||
await self.receive_item()
|
||||
await self.save_data()
|
||||
self.inbox_index += 1
|
||||
|
||||
if self.received_deathlink:
|
||||
await self.receive_deathlink()
|
||||
self.received_deathlink = False
|
||||
|
||||
# Progressively empty the queue during each tick
|
||||
# if text messages happen to be too slow we could pool dequeuing here,
|
||||
# but it'd slow down the ItemReceived message during release
|
||||
if not self.json_message_queue.empty():
|
||||
json_txt_data = self.json_message_queue.get_nowait()
|
||||
await self.write_game_text(json_txt_data)
|
||||
|
||||
# This helper function formats and sends `form` as a command to the REPL.
|
||||
# ALL commands to the REPL should be sent using this function.
|
||||
async def send_form(self, form: str, print_ok: bool = True) -> bool:
|
||||
header = struct.pack("<II", len(form), 10)
|
||||
async with self.lock:
|
||||
self.writer.write(header + form.encode())
|
||||
await self.writer.drain()
|
||||
|
||||
response_data = await self.reader.read(1024)
|
||||
response = response_data.decode()
|
||||
|
||||
if "OK!" in response:
|
||||
if print_ok:
|
||||
logger.debug(response)
|
||||
return True
|
||||
else:
|
||||
self.log_error(logger, f"Unexpected response from REPL: {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.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.log_error(logger, "Could not find the compiler process.")
|
||||
return
|
||||
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection(self.ip, self.port)
|
||||
time.sleep(1)
|
||||
connect_data = await self.reader.read(1024)
|
||||
welcome_message = connect_data.decode()
|
||||
|
||||
# Should be the OpenGOAL welcome message (ignore version number).
|
||||
if "Connected to OpenGOAL" and "nREPL!" in welcome_message:
|
||||
logger.debug(welcome_message)
|
||||
else:
|
||||
self.log_error(logger,
|
||||
f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"")
|
||||
except ConnectionRefusedError as e:
|
||||
self.log_error(logger, f"Unable to connect to REPL websocket: {e.strerror}")
|
||||
return
|
||||
|
||||
ok_count = 0
|
||||
if self.reader and self.writer:
|
||||
|
||||
# Have the REPL listen to the game's internal websocket.
|
||||
if await self.send_form("(lt)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Show this visual cue when compilation is started.
|
||||
# It's the version number of the OpenGOAL Compiler.
|
||||
if await self.send_form("(set! *debug-segment* #t)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Start compilation. This is blocking, so nothing will happen until the REPL is done.
|
||||
if await self.send_form("(mi)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Play this audio cue when compilation is complete.
|
||||
# It's the sound you hear when you press START + START to close the Options menu.
|
||||
if 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))", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Disable cheat-mode and debug (close the visual cues).
|
||||
if await self.send_form("(set! *debug-segment* #f)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
if await self.send_form("(set! *cheat-mode* #f)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Run the retail game start sequence (while still connected with REPL).
|
||||
if await self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"):
|
||||
ok_count += 1
|
||||
|
||||
# Now wait until we see the success message... 7 times.
|
||||
if ok_count == 7:
|
||||
self.connected = True
|
||||
else:
|
||||
self.connected = False
|
||||
|
||||
if self.connected:
|
||||
self.log_success(logger, "The REPL 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"
|
||||
f" Game process ID: {gk_proc_id}\n")
|
||||
try:
|
||||
if self.reader and self.writer:
|
||||
addr = self.writer.get_extra_info("peername")
|
||||
addr = str(addr) if addr else "None"
|
||||
msg += f" Game websocket: {addr}\n"
|
||||
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))", print_ok=False)
|
||||
except ConnectionResetError:
|
||||
msg += f" Connection to the game was lost or reset!"
|
||||
last_item = str(getattr(self.item_inbox[self.inbox_index], "item")) if self.inbox_index else "None"
|
||||
msg += f" Last item received: {last_item}\n"
|
||||
msg += f" Did you hear the success audio cue?"
|
||||
self.log_info(logger, msg)
|
||||
|
||||
# To properly display in-game text:
|
||||
# - It must be a valid character from the ALLOWED_CHARACTERS list.
|
||||
# - All lowercase letters must be uppercase.
|
||||
# - It must be wrapped in double quotes (for the REPL command).
|
||||
# - Apostrophes must be handled specially - GOAL uses invisible ASCII character 0x12.
|
||||
# I also only allotted 32 bytes to each string in OpenGOAL, so we must truncate.
|
||||
@staticmethod
|
||||
def sanitize_game_text(text: str) -> str:
|
||||
result = "".join([c if c in ALLOWED_CHARACTERS else "?" for c in text[:32]]).upper()
|
||||
result = result.replace("'", "\\c12")
|
||||
return f"\"{result}\""
|
||||
|
||||
# Like sanitize_game_text, but the settings file will NOT allow any whitespace in the slot_name or slot_seed data.
|
||||
# And don't replace any chars with "?" for good measure.
|
||||
@staticmethod
|
||||
def sanitize_file_text(text: str) -> str:
|
||||
allowed_chars_no_extras = ALLOWED_CHARACTERS - {" ", "'", "(", ")", "\""}
|
||||
result = "".join([c if c in allowed_chars_no_extras else "" for c in text[:16]]).upper()
|
||||
return f"\"{result}\""
|
||||
|
||||
# Pushes a JsonMessageData object to the json message queue to be processed during the repl main_tick
|
||||
def queue_game_text(self, my_item_name, my_item_finder, their_item_name, their_item_owner):
|
||||
self.json_message_queue.put(JsonMessageData(my_item_name, my_item_finder, their_item_name, their_item_owner))
|
||||
|
||||
# OpenGOAL can handle both its own string datatype and C-like character pointers (charp).
|
||||
async def write_game_text(self, data: JsonMessageData):
|
||||
logger.debug(f"Sending info to the in-game messenger!")
|
||||
body = ""
|
||||
if data.my_item_name and data.my_item_finder:
|
||||
body += (f" (append-messages (-> *ap-messenger* 0) \'recv "
|
||||
f" {self.sanitize_game_text(data.my_item_name)} "
|
||||
f" {self.sanitize_game_text(data.my_item_finder)})")
|
||||
if data.their_item_name and data.their_item_owner:
|
||||
body += (f" (append-messages (-> *ap-messenger* 0) \'sent "
|
||||
f" {self.sanitize_game_text(data.their_item_name)} "
|
||||
f" {self.sanitize_game_text(data.their_item_owner)})")
|
||||
await self.send_form(f"(begin {body} (none))", print_ok=False)
|
||||
|
||||
async def receive_item(self):
|
||||
ap_id = getattr(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)
|
||||
elif ap_id in range(jak1_id + flies.fly_offset, jak1_id + specials.special_offset):
|
||||
await self.receive_scout_fly(ap_id)
|
||||
elif ap_id in range(jak1_id + specials.special_offset, jak1_id + caches.orb_cache_offset):
|
||||
await self.receive_special(ap_id)
|
||||
elif ap_id in range(jak1_id + caches.orb_cache_offset, jak1_id + orbs.orb_offset):
|
||||
await self.receive_move(ap_id)
|
||||
elif ap_id in range(jak1_id + orbs.orb_offset, jak1_max - max(trap_item_table)):
|
||||
await self.receive_precursor_orb(ap_id) # Ponder the orbs.
|
||||
elif ap_id in range(jak1_max - max(trap_item_table), jak1_max):
|
||||
await self.receive_trap(ap_id)
|
||||
elif ap_id == jak1_max:
|
||||
await self.receive_green_eco() # Ponder why I chose to do ID's this way.
|
||||
else:
|
||||
self.log_error(logger, f"Tried to receive item with unknown AP ID {ap_id}!")
|
||||
|
||||
async def receive_power_cell(self, ap_id: int) -> bool:
|
||||
cell_id = cells.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type fuel-cell) "
|
||||
"(the float " + str(cell_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received a Power Cell!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a Power Cell!")
|
||||
return ok
|
||||
|
||||
async def receive_scout_fly(self, ap_id: int) -> bool:
|
||||
fly_id = flies.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type buzzer) "
|
||||
"(the float " + str(fly_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received a {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
async def receive_special(self, ap_id: int) -> bool:
|
||||
special_id = specials.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type ap-special) "
|
||||
"(the float " + str(special_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received special unlock {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive special unlock {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
async def receive_move(self, ap_id: int) -> bool:
|
||||
move_id = caches.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type ap-move) "
|
||||
"(the float " + str(move_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received the ability to {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive the ability to {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
async def receive_precursor_orb(self, ap_id: int) -> bool:
|
||||
orb_amount = orbs.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type money) "
|
||||
"(the float " + str(orb_amount) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received {orb_amount} Precursor orbs!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive {orb_amount} Precursor orbs!")
|
||||
return ok
|
||||
|
||||
async def receive_trap(self, ap_id: int) -> bool:
|
||||
trap_id = jak1_max - ap_id
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type ap-trap) "
|
||||
"(the float " + str(trap_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received a {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
# Green eco pills are our filler item. Use the get-pickup event instead to handle being full health.
|
||||
async def receive_green_eco(self) -> bool:
|
||||
ok = await self.send_form("(send-event *target* \'get-pickup (pickup-type eco-pill) (the float 1))")
|
||||
if ok:
|
||||
logger.debug(f"Received a green eco pill!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a green eco pill!")
|
||||
return ok
|
||||
|
||||
async 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 = await self.send_form("(ap-deathlink-received! " + chosen_death + ")")
|
||||
if ok:
|
||||
logger.debug(f"Received deathlink signal!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive deathlink signal!")
|
||||
return ok
|
||||
|
||||
async def subtract_traded_orbs(self, orb_count: int) -> bool:
|
||||
|
||||
# To protect against momentary server disconnects,
|
||||
# this should only be done once per client session.
|
||||
if not self.balanced_orbs:
|
||||
self.balanced_orbs = True
|
||||
|
||||
ok = await self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))")
|
||||
if ok:
|
||||
logger.debug(f"Subtracting {orb_count} traded orbs!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to subtract {orb_count} traded orbs!")
|
||||
return ok
|
||||
|
||||
return True
|
||||
|
||||
# OpenGOAL has a limit of 8 parameters per function. We've already hit this limit. So, define a new datatype
|
||||
# in OpenGOAL that holds all these options, instantiate the type here, and have ap-setup-options! function take
|
||||
# that instance as input.
|
||||
async def setup_options(self,
|
||||
os_option: int, os_bundle: int,
|
||||
fc_count: int, mp_count: int,
|
||||
lt_count: int, ct_amount: int,
|
||||
ot_amount: int, trap_time: int,
|
||||
goal_id: int, slot_name: str,
|
||||
slot_seed: str) -> bool:
|
||||
sanitized_name = self.sanitize_file_text(slot_name)
|
||||
sanitized_seed = self.sanitize_file_text(slot_seed)
|
||||
|
||||
# I didn't want to have to do this with floats but GOAL's compile-time vs runtime types leave me no choice.
|
||||
ok = await self.send_form(f"(ap-setup-options! (new 'static 'ap-seed-options "
|
||||
f":orbsanity-option {os_option} "
|
||||
f":orbsanity-bundle {os_bundle} "
|
||||
f":fire-canyon-unlock {fc_count}.0 "
|
||||
f":mountain-pass-unlock {mp_count}.0 "
|
||||
f":lava-tube-unlock {lt_count}.0 "
|
||||
f":citizen-orb-amount {ct_amount}.0 "
|
||||
f":oracle-orb-amount {ot_amount}.0 "
|
||||
f":trap-duration {trap_time}.0 "
|
||||
f":completion-goal {goal_id} "
|
||||
f":slot-name {sanitized_name} "
|
||||
f":slot-seed {sanitized_seed} ))")
|
||||
message = (f"Setting options: \n"
|
||||
f" orbsanity Option {os_option}, orbsanity Bundle {os_bundle}, \n"
|
||||
f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n"
|
||||
f" LT Cell Count {lt_count}, Citizen Orb Amt {ct_amount}, \n"
|
||||
f" Oracle Orb Amt {ot_amount}, Trap Duration {trap_time}, \n"
|
||||
f" Completion GOAL {goal_id}, Slot Name {sanitized_name}, \n"
|
||||
f" Slot Seed {sanitized_seed}... ")
|
||||
if ok:
|
||||
logger.debug(message + "Success!")
|
||||
else:
|
||||
self.log_error(logger, message + "Failed!")
|
||||
|
||||
return ok
|
||||
|
||||
async def send_connection_status(self, status: str) -> bool:
|
||||
ok = await self.send_form(f"(ap-set-connection-status! (connection-status {status}))")
|
||||
if ok:
|
||||
logger.debug(f"Connection Status {status} set!")
|
||||
else:
|
||||
self.log_error(logger, f"Connection Status {status} failed to set!")
|
||||
|
||||
return ok
|
||||
|
||||
async def save_data(self):
|
||||
with open("jakanddaxter_item_inbox.json", "w+") as f:
|
||||
dump = {
|
||||
"inbox_index": self.inbox_index,
|
||||
"item_inbox": [{
|
||||
"item": self.item_inbox[k].item,
|
||||
"location": self.item_inbox[k].location,
|
||||
"player": self.item_inbox[k].player,
|
||||
"flags": self.item_inbox[k].flags
|
||||
} for k in self.item_inbox
|
||||
]
|
||||
}
|
||||
json.dump(dump, f, indent=4)
|
||||
|
||||
def load_data(self):
|
||||
try:
|
||||
with open("jakanddaxter_item_inbox.json", "r") as f:
|
||||
load = json.load(f)
|
||||
self.inbox_index = load["inbox_index"]
|
||||
self.item_inbox = {k: NetworkItem(
|
||||
item=load["item_inbox"][k]["item"],
|
||||
location=load["item_inbox"][k]["location"],
|
||||
player=load["item_inbox"][k]["player"],
|
||||
flags=load["item_inbox"][k]["flags"]
|
||||
) for k in range(0, len(load["item_inbox"]))
|
||||
}
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
Reference in New Issue
Block a user