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.
This commit is contained in:
massimilianodelliubaldini
2024-05-12 19:23:51 -04:00
committed by GitHub
parent f38655d6b6
commit 1ac5302681
17 changed files with 1871 additions and 0 deletions

9
JakAndDaxterClient.py Normal file
View File

@@ -0,0 +1,9 @@
import Utils
from worlds.jakanddaxter.Client import launch
import ModuleUpdate
ModuleUpdate.update()
if __name__ == '__main__':
Utils.init_logging("JakAndDaxterClient", exception_logger="Client")
launch()

View File

@@ -0,0 +1,200 @@
import logging
import os
import subprocess
import typing
import asyncio
import colorama
import Utils
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled
from worlds.jakanddaxter.GameID import jak1_name
from worlds.jakanddaxter.client.ReplClient import JakAndDaxterReplClient
from worlds.jakanddaxter.client.MemoryReader import JakAndDaxterMemoryReader
import ModuleUpdate
ModuleUpdate.update()
all_tasks = set()
def create_task_log_exception(awaitable: typing.Awaitable) -> asyncio.Task:
async def _log_exception(a):
try:
return await a
except Exception as e:
logger.exception(e)
finally:
all_tasks.remove(task)
task = asyncio.create_task(_log_exception(awaitable))
all_tasks.add(task)
return task
class JakAndDaxterClientCommandProcessor(ClientCommandProcessor):
ctx: "JakAndDaxterContext"
# The command processor is not async and cannot use async tasks, so long-running operations
# like the /repl connect command (which takes 10-15 seconds to compile the game) have to be requested
# with user-initiated flags. The text client will hang while the operation runs, but at least we can
# inform the user to wait. 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."""
if arguments:
if arguments[0] == "connect":
logger.info("This may take a bit... Wait for the success audio cue before continuing!")
self.ctx.repl.initiated_connect = True
if arguments[0] == "status":
self.ctx.repl.print_status()
def _cmd_memr(self, *arguments: str):
"""Sends a command to the Memory Reader. Arguments:
- connect : connect the memory reader to the game process (gk).
- status : check the internal status of the Memory Reader."""
if arguments:
if arguments[0] == "connect":
self.ctx.memr.initiated_connect = True
if arguments[0] == "status":
self.ctx.memr.print_status()
class JakAndDaxterContext(CommonContext):
tags = {"AP"}
game = jak1_name
items_handling = 0b111 # Full item handling
command_processor = JakAndDaxterClientCommandProcessor
# We'll need two agents working in tandem to handle two-way communication with the game.
# The REPL Client will handle the server->game direction by issuing commands directly to the running game.
# But the REPL cannot send information back to us, it only ingests information we send it.
# Luckily OpenGOAL sets up memory addresses to write to, that AutoSplit can read from, for speedrunning.
# We'll piggyback off this system with a Memory Reader, and that will handle the game->server direction.
repl: JakAndDaxterReplClient
memr: JakAndDaxterMemoryReader
# And two associated tasks, so we have handles on them.
repl_task: asyncio.Task
memr_task: asyncio.Task
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
self.repl = JakAndDaxterReplClient()
self.memr = JakAndDaxterMemoryReader()
super().__init__(server_address, password)
def run_gui(self):
from kvui import GameManager
class JakAndDaxterManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Jak and Daxter ArchipelaGOAL Client"
self.ui = JakAndDaxterManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(JakAndDaxterContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
logger.info(args)
self.repl.item_inbox[index] = item
async def ap_inform_location_checks(self, location_ids: typing.List[int]):
message = [{"cmd": "LocationChecks", "locations": location_ids}]
await self.send_msgs(message)
def on_locations(self, location_ids: typing.List[int]):
create_task_log_exception(self.ap_inform_location_checks(location_ids))
async def run_repl_loop(self):
while True:
await self.repl.main_tick()
await asyncio.sleep(0.1)
async def run_memr_loop(self):
while True:
await self.memr.main_tick(self.on_locations)
await asyncio.sleep(0.1)
async def run_game(ctx: JakAndDaxterContext):
exec_directory = ""
try:
exec_directory = Utils.get_settings()["jakanddaxter_options"]["root_directory"]
files_in_path = os.listdir(exec_directory)
if ".git" in files_in_path:
# Indicates the user is running from source, append expected subdirectory appropriately.
exec_directory = os.path.join(exec_directory, "out", "build", "Release", "bin")
else:
# Indicates the user is running from the official launcher, a mod launcher, or otherwise.
# We'll need to handle version numbers in the path somehow...
exec_directory = os.path.join(exec_directory, "versions", "official")
latest_version = list(reversed(os.listdir(exec_directory)))[0]
exec_directory = os.path.join(exec_directory, str(latest_version))
except FileNotFoundError:
logger.error(f"Unable to locate directory {exec_directory}, "
f"unable to locate game executable.")
return
except KeyError as e:
logger.error(f"Hosts.yaml does not contain {e.args[0]}, "
f"unable to locate game executable.")
return
gk = os.path.join(exec_directory, "gk.exe")
goalc = os.path.join(exec_directory, "goalc.exe")
# Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl".
await asyncio.create_subprocess_exec(
gk,
"-v", "--game jak1", "--", "-boot", "-fakeiso", "-debug",
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL)
# You MUST launch goalc as a console application, so powershell/cmd/bash/etc is the program
# and goalc is just an argument. It HAS to be this way.
# TODO - Support other OS's.
await asyncio.create_subprocess_exec(
"powershell.exe",
goalc, "--user-auto", "--game jak1")
# Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load,
# and it's not something we can await.
logger.info("This may take a bit... Wait for the success audio cue before continuing!")
await asyncio.sleep(5)
ctx.repl.initiated_connect = True
ctx.memr.initiated_connect = True
async def main():
Utils.init_logging("JakAndDaxterClient", exception_logger="Client")
ctx = JakAndDaxterContext(None, None)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
ctx.repl_task = create_task_log_exception(ctx.run_repl_loop())
ctx.memr_task = create_task_log_exception(ctx.run_memr_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
# Find and run the game (gk) and compiler/repl (goalc).
await run_game(ctx)
await ctx.exit_event.wait()
await ctx.shutdown()
def launch():
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -0,0 +1,5 @@
# All Jak And Daxter Archipelago IDs must be offset by this number.
jak1_id = 741000000
# The name of the game.
jak1_name = "Jak and Daxter The Precursor Legacy"

View File

@@ -0,0 +1,7 @@
from BaseClasses import Item
from .GameID import jak1_name
class JakAndDaxterItem(Item):
game: str = jak1_name

View File

@@ -0,0 +1,20 @@
import os
from dataclasses import dataclass
from Options import Toggle, PerGameCommonOptions
# class EnableScoutFlies(Toggle):
# """Enable to include each Scout Fly as a check. Adds 112 checks to the pool."""
# display_name = "Enable Scout Flies"
# class EnablePrecursorOrbs(Toggle):
# """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool."""
# display_name = "Enable Precursor Orbs"
@dataclass
class JakAndDaxterOptions(PerGameCommonOptions):
# enable_scout_flies: EnableScoutFlies
# enable_precursor_orbs: EnablePrecursorOrbs
pass

View File

@@ -0,0 +1,46 @@
from BaseClasses import Location
from .GameID import jak1_name
from .locs import CellLocations as Cells, ScoutLocations as Scouts
class JakAndDaxterLocation(Location):
game: str = jak1_name
# All Locations
# Because all items in Jak And Daxter are unique and do not regenerate, we can use this same table as our item table.
# Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed.
location_table = {
**{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable},
**{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable},
**{Cells.to_ap_id(k): Cells.locFJ_cellTable[k] for k in Cells.locFJ_cellTable},
**{Cells.to_ap_id(k): Cells.locSB_cellTable[k] for k in Cells.locSB_cellTable},
**{Cells.to_ap_id(k): Cells.locMI_cellTable[k] for k in Cells.locMI_cellTable},
**{Cells.to_ap_id(k): Cells.locFC_cellTable[k] for k in Cells.locFC_cellTable},
**{Cells.to_ap_id(k): Cells.locRV_cellTable[k] for k in Cells.locRV_cellTable},
**{Cells.to_ap_id(k): Cells.locPB_cellTable[k] for k in Cells.locPB_cellTable},
**{Cells.to_ap_id(k): Cells.locLPC_cellTable[k] for k in Cells.locLPC_cellTable},
**{Cells.to_ap_id(k): Cells.locBS_cellTable[k] for k in Cells.locBS_cellTable},
**{Cells.to_ap_id(k): Cells.locMP_cellTable[k] for k in Cells.locMP_cellTable},
**{Cells.to_ap_id(k): Cells.locVC_cellTable[k] for k in Cells.locVC_cellTable},
**{Cells.to_ap_id(k): Cells.locSC_cellTable[k] for k in Cells.locSC_cellTable},
**{Cells.to_ap_id(k): Cells.locSM_cellTable[k] for k in Cells.locSM_cellTable},
**{Cells.to_ap_id(k): Cells.locLT_cellTable[k] for k in Cells.locLT_cellTable},
**{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable},
**{Scouts.to_ap_id(k): Scouts.locGR_scoutTable[k] for k in Scouts.locGR_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locSV_scoutTable[k] for k in Scouts.locSV_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locFJ_scoutTable[k] for k in Scouts.locFJ_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locSB_scoutTable[k] for k in Scouts.locSB_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locMI_scoutTable[k] for k in Scouts.locMI_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locFC_scoutTable[k] for k in Scouts.locFC_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locRV_scoutTable[k] for k in Scouts.locRV_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locPB_scoutTable[k] for k in Scouts.locPB_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locLPC_scoutTable[k] for k in Scouts.locLPC_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locBS_scoutTable[k] for k in Scouts.locBS_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locMP_scoutTable[k] for k in Scouts.locMP_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locVC_scoutTable[k] for k in Scouts.locVC_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locSC_scoutTable[k] for k in Scouts.locSC_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locSM_scoutTable[k] for k in Scouts.locSM_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}
}

View File

@@ -0,0 +1,229 @@
import typing
from enum import Enum, auto
from BaseClasses import MultiWorld, Region
from .GameID import jak1_name
from .JakAndDaxterOptions import JakAndDaxterOptions
from .Locations import JakAndDaxterLocation, location_table
from .locs import CellLocations as Cells, ScoutLocations as Scouts
class Jak1Level(int, Enum):
GEYSER_ROCK = auto()
SANDOVER_VILLAGE = auto()
FORBIDDEN_JUNGLE = auto()
SENTINEL_BEACH = auto()
MISTY_ISLAND = auto()
FIRE_CANYON = auto()
ROCK_VILLAGE = auto()
PRECURSOR_BASIN = auto()
LOST_PRECURSOR_CITY = auto()
BOGGY_SWAMP = auto()
MOUNTAIN_PASS = auto()
VOLCANIC_CRATER = auto()
SPIDER_CAVE = auto()
SNOWY_MOUNTAIN = auto()
LAVA_TUBE = auto()
GOL_AND_MAIAS_CITADEL = auto()
class Jak1SubLevel(int, Enum):
MAIN_AREA = auto()
FORBIDDEN_JUNGLE_SWITCH_ROOM = auto()
FORBIDDEN_JUNGLE_PLANT_ROOM = auto()
SENTINEL_BEACH_CANNON_TOWER = auto()
PRECURSOR_BASIN_BLUE_RINGS = auto()
LOST_PRECURSOR_CITY_SUNKEN_ROOM = auto()
LOST_PRECURSOR_CITY_HELIX_ROOM = auto()
BOGGY_SWAMP_FLUT_FLUT = auto()
MOUNTAIN_PASS_RACE = auto()
MOUNTAIN_PASS_SHORTCUT = auto()
SNOWY_MOUNTAIN_FLUT_FLUT = auto()
SNOWY_MOUNTAIN_LURKER_FORT = auto()
SNOWY_MOUNTAIN_FROZEN_BOX = auto()
GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = auto()
GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto()
level_table: typing.Dict[Jak1Level, str] = {
Jak1Level.GEYSER_ROCK: "Geyser Rock",
Jak1Level.SANDOVER_VILLAGE: "Sandover Village",
Jak1Level.FORBIDDEN_JUNGLE: "Forbidden Jungle",
Jak1Level.SENTINEL_BEACH: "Sentinel Beach",
Jak1Level.MISTY_ISLAND: "Misty Island",
Jak1Level.FIRE_CANYON: "Fire Canyon",
Jak1Level.ROCK_VILLAGE: "Rock Village",
Jak1Level.PRECURSOR_BASIN: "Precursor Basin",
Jak1Level.LOST_PRECURSOR_CITY: "Lost Precursor City",
Jak1Level.BOGGY_SWAMP: "Boggy Swamp",
Jak1Level.MOUNTAIN_PASS: "Mountain Pass",
Jak1Level.VOLCANIC_CRATER: "Volcanic Crater",
Jak1Level.SPIDER_CAVE: "Spider Cave",
Jak1Level.SNOWY_MOUNTAIN: "Snowy Mountain",
Jak1Level.LAVA_TUBE: "Lava Tube",
Jak1Level.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel"
}
subLevel_table: typing.Dict[Jak1SubLevel, str] = {
Jak1SubLevel.MAIN_AREA: "Main Area",
Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room",
Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room",
Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower",
Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings",
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room",
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room",
Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut",
Jak1SubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race",
Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut",
Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut",
Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort",
Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box",
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower",
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss"
}
class JakAndDaxterRegion(Region):
game: str = jak1_name
# Use the original game ID's for each item to tell the Region which Locations are available in it.
# You do NOT need to add the item offsets or game ID, that will be handled by create_*_locations.
def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
create_region(player, multiworld, "Menu")
region_gr = create_region(player, multiworld, level_table[Jak1Level.GEYSER_ROCK])
create_cell_locations(region_gr, Cells.locGR_cellTable)
create_fly_locations(region_gr, Scouts.locGR_scoutTable)
region_sv = create_region(player, multiworld, level_table[Jak1Level.SANDOVER_VILLAGE])
create_cell_locations(region_sv, Cells.locSV_cellTable)
create_fly_locations(region_sv, Scouts.locSV_scoutTable)
region_fj = create_region(player, multiworld, level_table[Jak1Level.FORBIDDEN_JUNGLE])
create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}})
create_fly_locations(region_fj, Scouts.locFJ_scoutTable)
sub_region_fjsr = create_subregion(region_fj, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM])
create_cell_locations(sub_region_fjsr, {k: Cells.locFJ_cellTable[k] for k in {2}})
sub_region_fjpr = create_subregion(sub_region_fjsr, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM])
create_cell_locations(sub_region_fjpr, {k: Cells.locFJ_cellTable[k] for k in {6}})
region_sb = create_region(player, multiworld, level_table[Jak1Level.SENTINEL_BEACH])
create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}})
create_fly_locations(region_sb, Scouts.locSB_scoutTable)
sub_region_sbct = create_subregion(region_sb, subLevel_table[Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER])
create_cell_locations(sub_region_sbct, {k: Cells.locSB_cellTable[k] for k in {19}})
region_mi = create_region(player, multiworld, level_table[Jak1Level.MISTY_ISLAND])
create_cell_locations(region_mi, Cells.locMI_cellTable)
create_fly_locations(region_mi, Scouts.locMI_scoutTable)
region_fc = create_region(player, multiworld, level_table[Jak1Level.FIRE_CANYON])
create_cell_locations(region_fc, Cells.locFC_cellTable)
create_fly_locations(region_fc, Scouts.locFC_scoutTable)
region_rv = create_region(player, multiworld, level_table[Jak1Level.ROCK_VILLAGE])
create_cell_locations(region_rv, Cells.locRV_cellTable)
create_fly_locations(region_rv, Scouts.locRV_scoutTable)
region_pb = create_region(player, multiworld, level_table[Jak1Level.PRECURSOR_BASIN])
create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}})
create_fly_locations(region_pb, Scouts.locPB_scoutTable)
sub_region_pbbr = create_subregion(region_pb, subLevel_table[Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS])
create_cell_locations(sub_region_pbbr, {k: Cells.locPB_cellTable[k] for k in {59}})
region_lpc = create_region(player, multiworld, level_table[Jak1Level.LOST_PRECURSOR_CITY])
create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}})
create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k]
for k in {262193, 131121, 393265, 196657, 49, 65585}})
sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM])
create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47, 49}})
create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {327729}})
sub_region_lpchr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM])
create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}})
region_bs = create_region(player, multiworld, level_table[Jak1Level.BOGGY_SWAMP])
create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}})
create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {43, 393259, 65579, 262187, 196651}})
sub_region_bsff = create_subregion(region_bs, subLevel_table[Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT])
create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {43, 37}})
create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}})
region_mp = create_region(player, multiworld, level_table[Jak1Level.MOUNTAIN_PASS])
create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}})
sub_region_mpr = create_subregion(region_mp, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_RACE])
create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87, 88}})
create_fly_locations(sub_region_mpr, Scouts.locMP_scoutTable)
sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT])
create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}})
region_vc = create_region(player, multiworld, level_table[Jak1Level.VOLCANIC_CRATER])
create_cell_locations(region_vc, Cells.locVC_cellTable)
create_fly_locations(region_vc, Scouts.locVC_scoutTable)
region_sc = create_region(player, multiworld, level_table[Jak1Level.SPIDER_CAVE])
create_cell_locations(region_sc, Cells.locSC_cellTable)
create_fly_locations(region_sc, Scouts.locSC_scoutTable)
region_sm = create_region(player, multiworld, level_table[Jak1Level.SNOWY_MOUNTAIN])
create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}})
create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {65, 327745, 65601, 131137, 393281}})
sub_region_smfb = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX])
create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}})
sub_region_smff = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT])
create_cell_locations(sub_region_smff, {k: Cells.locSM_cellTable[k] for k in {63}})
sub_region_smlf = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT])
create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62, 65}})
create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {196673, 262209}})
region_lt = create_region(player, multiworld, level_table[Jak1Level.LAVA_TUBE])
create_cell_locations(region_lt, Cells.locLT_cellTable)
create_fly_locations(region_lt, Scouts.locLT_scoutTable)
region_gmc = create_region(player, multiworld, level_table[Jak1Level.GOL_AND_MAIAS_CITADEL])
create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}})
create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k]
for k in {91, 65627, 196699, 262235, 393307, 131163}})
sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER])
create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70, 91}})
create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {327771}})
create_subregion(sub_region_gmcrt, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS])
def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion:
region = JakAndDaxterRegion(name, player, multiworld)
multiworld.regions.append(region)
return region
def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion:
region = JakAndDaxterRegion(name, parent.player, parent.multiworld)
parent.multiworld.regions.append(region)
return region
def create_cell_locations(region: Region, locations: typing.Dict[int, str]):
region.locations += [JakAndDaxterLocation(region.player,
location_table[Cells.to_ap_id(loc)],
Cells.to_ap_id(loc),
region) for loc in locations]
def create_fly_locations(region: Region, locations: typing.Dict[int, str]):
region.locations += [JakAndDaxterLocation(region.player,
location_table[Scouts.to_ap_id(loc)],
Scouts.to_ap_id(loc),
region) for loc in locations]

View File

@@ -0,0 +1,232 @@
from BaseClasses import MultiWorld, CollectionState
from .JakAndDaxterOptions import JakAndDaxterOptions
from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table
from .Locations import location_table as item_table
from .locs import CellLocations as Cells, ScoutLocations as Scouts
# Helper function for a handful of special cases
# where we need "at least any N" number of a specific set of cells.
def has_count_of(cell_list: set, required_count: int, player: int, state: CollectionState) -> bool:
c: int = 0
for k in cell_list:
if state.has(item_table[k], player):
c += 1
if c >= required_count:
return True
return False
def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
# Setting up some useful variables here because the offset numbers can get confusing
# for access rules. Feel free to add more variables here to keep the code more readable.
# You DO need to convert the game ID's to AP ID's here.
gr_cells = {Cells.to_ap_id(k) for k in Cells.locGR_cellTable}
fj_temple_top = Cells.to_ap_id(4)
fj_blue_switch = Cells.to_ap_id(2)
fj_plant_boss = Cells.to_ap_id(6)
fj_fisherman = Cells.to_ap_id(5)
sb_flut_flut = Cells.to_ap_id(17)
fc_end = Cells.to_ap_id(69)
pb_purple_rings = Cells.to_ap_id(58)
lpc_sunken = Cells.to_ap_id(47)
lpc_helix = Cells.to_ap_id(50)
mp_klaww = Cells.to_ap_id(86)
mp_end = Cells.to_ap_id(87)
pre_sm_cells = {Cells.to_ap_id(k) for k in {**Cells.locVC_cellTable, **Cells.locSC_cellTable}}
sm_yellow_switch = Cells.to_ap_id(60)
sm_fort_gate = Cells.to_ap_id(63)
lt_end = Cells.to_ap_id(89)
gmc_rby_sages = {Cells.to_ap_id(k) for k in {71, 72, 73}}
gmc_green_sage = Cells.to_ap_id(70)
# Start connecting regions and set their access rules.
connect_start(multiworld, player, Jak1Level.GEYSER_ROCK)
connect_regions(multiworld, player,
Jak1Level.GEYSER_ROCK,
Jak1Level.SANDOVER_VILLAGE,
lambda state: has_count_of(gr_cells, 4, player, state))
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
Jak1Level.FORBIDDEN_JUNGLE)
connect_region_to_sub(multiworld, player,
Jak1Level.FORBIDDEN_JUNGLE,
Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM,
lambda state: state.has(item_table[fj_temple_top], player))
connect_subregions(multiworld, player,
Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM,
Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM,
lambda state: state.has(item_table[fj_blue_switch], player))
connect_sub_to_region(multiworld, player,
Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM,
Jak1Level.FORBIDDEN_JUNGLE,
lambda state: state.has(item_table[fj_plant_boss], player))
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
Jak1Level.SENTINEL_BEACH)
connect_region_to_sub(multiworld, player,
Jak1Level.SENTINEL_BEACH,
Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER,
lambda state: state.has(item_table[fj_blue_switch], player))
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
Jak1Level.MISTY_ISLAND,
lambda state: state.has(item_table[fj_fisherman], player))
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
Jak1Level.FIRE_CANYON,
lambda state: state.count_group("Power Cell", player) >= 20)
connect_regions(multiworld, player,
Jak1Level.FIRE_CANYON,
Jak1Level.ROCK_VILLAGE,
lambda state: state.has(item_table[fc_end], player))
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
Jak1Level.PRECURSOR_BASIN)
connect_region_to_sub(multiworld, player,
Jak1Level.PRECURSOR_BASIN,
Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS,
lambda state: state.has(item_table[pb_purple_rings], player))
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
Jak1Level.LOST_PRECURSOR_CITY)
connect_region_to_sub(multiworld, player,
Jak1Level.LOST_PRECURSOR_CITY,
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM)
connect_subregions(multiworld, player,
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM,
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM)
connect_sub_to_region(multiworld, player,
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM,
Jak1Level.LOST_PRECURSOR_CITY,
lambda state: state.has(item_table[lpc_helix], player))
connect_sub_to_region(multiworld, player,
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM,
Jak1Level.ROCK_VILLAGE,
lambda state: state.has(item_table[lpc_sunken], player))
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
Jak1Level.BOGGY_SWAMP)
connect_region_to_sub(multiworld, player,
Jak1Level.BOGGY_SWAMP,
Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT,
lambda state: state.has(item_table[sb_flut_flut], player))
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
Jak1Level.MOUNTAIN_PASS,
lambda state: state.count_group("Power Cell", player) >= 45)
connect_region_to_sub(multiworld, player,
Jak1Level.MOUNTAIN_PASS,
Jak1SubLevel.MOUNTAIN_PASS_RACE,
lambda state: state.has(item_table[mp_klaww], player))
connect_subregions(multiworld, player,
Jak1SubLevel.MOUNTAIN_PASS_RACE,
Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT,
lambda state: state.has(item_table[sm_yellow_switch], player))
connect_sub_to_region(multiworld, player,
Jak1SubLevel.MOUNTAIN_PASS_RACE,
Jak1Level.VOLCANIC_CRATER,
lambda state: state.has(item_table[mp_end], player))
connect_regions(multiworld, player,
Jak1Level.VOLCANIC_CRATER,
Jak1Level.SPIDER_CAVE)
connect_regions(multiworld, player,
Jak1Level.VOLCANIC_CRATER,
Jak1Level.SNOWY_MOUNTAIN,
lambda state: has_count_of(pre_sm_cells, 2, player, state)
or state.count_group("Power Cell", player) >= 71) # Yeah, this is a weird one.
connect_region_to_sub(multiworld, player,
Jak1Level.SNOWY_MOUNTAIN,
Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX,
lambda state: state.has(item_table[sm_yellow_switch], player))
connect_region_to_sub(multiworld, player,
Jak1Level.SNOWY_MOUNTAIN,
Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT,
lambda state: state.has(item_table[sb_flut_flut], player))
connect_region_to_sub(multiworld, player,
Jak1Level.SNOWY_MOUNTAIN,
Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT,
lambda state: state.has(item_table[sm_fort_gate], player))
connect_regions(multiworld, player,
Jak1Level.VOLCANIC_CRATER,
Jak1Level.LAVA_TUBE,
lambda state: state.count_group("Power Cell", player) >= 72)
connect_regions(multiworld, player,
Jak1Level.LAVA_TUBE,
Jak1Level.GOL_AND_MAIAS_CITADEL,
lambda state: state.has(item_table[lt_end], player))
connect_region_to_sub(multiworld, player,
Jak1Level.GOL_AND_MAIAS_CITADEL,
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER,
lambda state: has_count_of(gmc_rby_sages, 3, player, state))
connect_subregions(multiworld, player,
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER,
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS,
lambda state: state.has(item_table[gmc_green_sage], player))
multiworld.completion_condition[player] = lambda state: state.can_reach(
multiworld.get_region(subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player),
"Region",
player)
def connect_start(multiworld: MultiWorld, player: int, target: Jak1Level):
menu_region = multiworld.get_region("Menu", player)
start_region = multiworld.get_region(level_table[target], player)
menu_region.connect(start_region)
def connect_regions(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1Level, rule=None):
source_region = multiworld.get_region(level_table[source], player)
target_region = multiworld.get_region(level_table[target], player)
source_region.connect(target_region, rule=rule)
def connect_region_to_sub(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1SubLevel, rule=None):
source_region = multiworld.get_region(level_table[source], player)
target_region = multiworld.get_region(subLevel_table[target], player)
source_region.connect(target_region, rule=rule)
def connect_sub_to_region(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1Level, rule=None):
source_region = multiworld.get_region(subLevel_table[source], player)
target_region = multiworld.get_region(level_table[target], player)
source_region.connect(target_region, rule=rule)
def connect_subregions(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1SubLevel, rule=None):
source_region = multiworld.get_region(subLevel_table[source], player)
target_region = multiworld.get_region(subLevel_table[target], player)
source_region.connect(target_region, rule=rule)

View File

@@ -0,0 +1,108 @@
import typing
import settings
from BaseClasses import Item, ItemClassification, Tutorial
from .GameID import jak1_id, jak1_name
from .JakAndDaxterOptions import JakAndDaxterOptions
from .Items import JakAndDaxterItem
from .Locations import JakAndDaxterLocation, location_table as item_table
from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs
from .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
from ..LauncherComponents import components, Component, launch_subprocess, Type
class JakAndDaxterSettings(settings.Group):
class RootDirectory(settings.UserFolderPath):
"""Path to folder containing the ArchipelaGOAL mod."""
description = "ArchipelaGOAL Root Directory"
root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL")
class JakAndDaxterWebWorld(WebWorld):
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up ArchipelaGOAL (Archipelago on OpenGOAL).",
"English",
"setup_en.md",
"setup/en",
["markustulliuscicero"]
)
tutorials = [setup_en]
class JakAndDaxterWorld(World):
"""
Jak and Daxter: The Precursor Legacy is a 2001 action platformer developed by Naughty Dog
for the PlayStation 2. The game follows the eponymous protagonists, a young boy named Jak
and his friend Daxter, who has been transformed into an ottsel. With the help of Samos
the Sage of Green Eco and his daughter Keira, the pair travel north in search of a cure for Daxter,
discovering artifacts created by an ancient race known as the Precursors along the way. When the
rogue sages Gol and Maia Acheron plan to flood the world with Dark Eco, they must stop their evil plan
and save the world.
"""
# ID, name, version
game = jak1_name
data_version = 1
required_client_version = (0, 4, 5)
# Options
settings: typing.ClassVar[JakAndDaxterSettings]
options_dataclass = JakAndDaxterOptions
options: JakAndDaxterOptions
# Web world
web = JakAndDaxterWebWorld()
# Items and Locations
# Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs.
# Remember, the game ID and various offsets for each item type have already been calculated.
item_name_to_id = {item_table[k]: k for k in item_table}
location_name_to_id = {item_table[k]: k for k in item_table}
item_name_groups = {
"Power Cell": {item_table[k]: k for k in item_table
if k in range(jak1_id, jak1_id + Scouts.fly_offset)},
"Scout Fly": {item_table[k]: k for k in item_table
if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)}
# "Precursor Orb": {} # TODO
}
def create_regions(self):
create_regions(self.multiworld, self.options, self.player)
def set_rules(self):
set_rules(self.multiworld, self.options, self.player)
def create_items(self):
self.multiworld.itempool += [self.create_item(item_table[k]) for k in item_table]
def create_item(self, name: str) -> Item:
item_id = self.item_name_to_id[name]
if item_id in range(jak1_id, jak1_id + Scouts.fly_offset):
# Power Cell
classification = ItemClassification.progression_skip_balancing
elif item_id in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset):
# Scout Fly
classification = ItemClassification.progression_skip_balancing
elif item_id > jak1_id + Orbs.orb_offset:
# Precursor Orb
classification = ItemClassification.filler # TODO
else:
classification = ItemClassification.filler
item = JakAndDaxterItem(name, classification, item_id, self.player)
return item
def launch_client():
from .Client import launch
launch_subprocess(launch, name="JakAndDaxterClient")
components.append(Component("Jak and Daxter Client",
"JakAndDaxterClient",
func=launch_client,
component_type=Type.CLIENT))

View File

@@ -0,0 +1,133 @@
import typing
import pymem
from pymem import pattern
from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError
from CommonClient import logger
from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies
# Some helpful constants.
next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes.
next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes.
cells_offset = 16
buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101)
# buzzers_offset
# + (sizeof uint32 * 112 flies) <-- The buzzers themselves.
# + (sizeof uint8 * 116 tasks) <-- A "cells-received" array for the game to handle new ownership logic.
# = 420 + (4 * 112) + (1 * 116)
end_marker_offset = 984
class JakAndDaxterMemoryReader:
marker: typing.ByteString
goal_address = None
connected: bool = False
initiated_connect: bool = False
# The memory reader just needs the game running.
gk_process: pymem.process = None
location_outbox = []
outbox_index = 0
def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'):
self.marker = marker
self.connect()
async def main_tick(self, location_callback: typing.Callable):
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):
logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.")
self.connected = False
else:
return
# Read the memory address to check the state of the game.
self.read_memory()
location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below...
# Checked Locations in game. Handle the entire outbox every tick until we're up to speed.
if len(self.location_outbox) > self.outbox_index:
location_callback(self.location_outbox)
self.outbox_index += 1
async def connect(self):
try:
self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel
logger.info("Found the gk process: " + str(self.gk_process.process_id))
except ProcessNotFound:
logger.error("Could not find the gk 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 u64).
goal_pointer = marker_address + len(self.marker) + 4
self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, 8),
byteorder="little",
signed=False)
logger.info("Found the archipelago memory address: " + str(self.goal_address))
self.connected = True
else:
logger.error("Could not find the archipelago memory address.")
self.connected = False
if self.connected:
logger.info("The Memory Reader is ready!")
def print_status(self):
logger.info("Memory Reader Status:")
logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None"))
logger.info(" Game state memory address: " + str(self.goal_address))
logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index])
if self.outbox_index else "None"))
def read_memory(self) -> typing.List[int]:
try:
next_cell_index = int.from_bytes(
self.gk_process.read_bytes(self.goal_address, 8),
byteorder="little",
signed=False)
next_buzzer_index = int.from_bytes(
self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, 8),
byteorder="little",
signed=False)
for k in range(0, next_cell_index):
next_cell = int.from_bytes(
self.gk_process.read_bytes(self.goal_address + cells_offset + (k * 4), 4),
byteorder="little",
signed=False)
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.info("Checked power cell: " + str(next_cell))
for k in range(0, next_buzzer_index):
next_buzzer = int.from_bytes(
self.gk_process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4),
byteorder="little",
signed=False)
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.info("Checked scout fly: " + str(next_buzzer))
except (ProcessError, MemoryReadError, WinAPIError):
logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.")
self.connected = False
return self.location_outbox

View File

@@ -0,0 +1,202 @@
import time
import struct
from socket import socket, AF_INET, SOCK_STREAM
import pymem
from pymem.exception import ProcessNotFound, ProcessError
from CommonClient import logger
from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, OrbLocations as Orbs
from worlds.jakanddaxter.GameID import jak1_id
class JakAndDaxterReplClient:
ip: str
port: int
sock: socket
connected: bool = False
initiated_connect: bool = False # Signals when user tells us to try reconnecting.
# 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 = {}
inbox_index = 0
def __init__(self, ip: str = "127.0.0.1", port: int = 8181):
self.ip = ip
self.port = port
self.connect()
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:
logger.error("The gk process has died. Restart the game and run \"/repl connect\" again.")
self.connected = False
try:
self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive.
except ProcessError:
logger.error("The goalc process has died. Restart the compiler and run \"/repl connect\" again.")
self.connected = False
else:
return
# Receive Items from AP. Handle 1 item per tick.
if len(self.item_inbox) > self.inbox_index:
self.receive_item()
self.inbox_index += 1
# This helper function formats and sends `form` as a command to the REPL.
# ALL commands to the REPL should be sent using this function.
# TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print
# any log info in the meantime. Is that a problem?
def send_form(self, form: str, print_ok: bool = True) -> bool:
header = struct.pack("<II", len(form), 10)
self.sock.sendall(header + form.encode())
response = self.sock.recv(1024).decode()
if "OK!" in response:
if print_ok:
logger.info(response)
return True
else:
logger.error(f"Unexpected response from REPL: {response}")
return False
async def connect(self):
try:
self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel
logger.info("Found the gk process: " + str(self.gk_process.process_id))
except ProcessNotFound:
logger.error("Could not find the gk process.")
return
try:
self.goalc_process = pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL
logger.info("Found the goalc process: " + str(self.goalc_process.process_id))
except ProcessNotFound:
logger.error("Could not find the goalc process.")
return
try:
self.sock = socket(AF_INET, SOCK_STREAM)
self.sock.connect((self.ip, self.port))
time.sleep(1)
welcome_message = self.sock.recv(1024).decode()
# Should be the OpenGOAL welcome message (ignore version number).
if "Connected to OpenGOAL" and "nREPL!" in welcome_message:
logger.info(welcome_message)
else:
logger.error(f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"")
except ConnectionRefusedError as e:
logger.error(f"Unable to connect to REPL websocket: {e.strerror}")
return
ok_count = 0
if self.sock:
# Have the REPL listen to the game's internal websocket.
if 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 self.send_form("(set! *debug-segment* #t)", print_ok=False):
ok_count += 1
# Play this audio cue when compilation is started.
# It's the sound you hear when you press START + CIRCLE to open the Options menu.
if self.send_form("(dotimes (i 1) "
"(sound-play-by-name "
"(static-sound-name \"start-options\") "
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False):
ok_count += 1
# Start compilation. This is blocking, so nothing will happen until the REPL is done.
if 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 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 cue).
# self.send_form("(set! *debug-segment* #f)")
if self.send_form("(set! *cheat-mode* #f)"):
ok_count += 1
# Now wait until we see the success message... 6 times.
if ok_count == 6:
self.connected = True
else:
self.connected = False
if self.connected:
logger.info("The REPL is ready!")
def print_status(self):
logger.info("REPL Status:")
logger.info(" REPL process ID: " + (str(self.goalc_process.process_id) if self.goalc_process else "None"))
logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None"))
try:
if self.sock:
ip, port = self.sock.getpeername()
logger.info(" Game websocket: " + (str(ip) + ", " + str(port) if ip else "None"))
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:
logger.warn(" Game websocket not found!")
logger.info(" Did you hear the success audio cue?")
logger.info(" Last item received: " + (str(getattr(self.item_inbox[self.inbox_index], "item"))
if self.inbox_index else "None"))
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):
self.receive_power_cell(ap_id)
elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Orbs.orb_offset):
self.receive_scout_fly(ap_id)
elif ap_id > jak1_id + Orbs.orb_offset:
pass # TODO
def receive_power_cell(self, ap_id: int) -> bool:
cell_id = Cells.to_game_id(ap_id)
ok = self.send_form("(send-event "
"*target* \'get-archipelago "
"(pickup-type fuel-cell) "
"(the float " + str(cell_id) + "))")
if ok:
logger.info(f"Received power cell {cell_id}!")
else:
logger.error(f"Unable to receive power cell {cell_id}!")
return ok
def receive_scout_fly(self, ap_id: int) -> bool:
fly_id = Flies.to_game_id(ap_id)
ok = self.send_form("(send-event "
"*target* \'get-archipelago "
"(pickup-type buzzer) "
"(the float " + str(fly_id) + "))")
if ok:
logger.info(f"Received scout fly {fly_id}!")
else:
logger.error(f"Unable to receive scout fly {fly_id}!")
return ok

View File

@@ -0,0 +1,78 @@
# Jak And Daxter (ArchipelaGOAL)
## Where is the options page?
The [Player Options Page](../player-options) for this game contains
all the options you need to configure and export a config file.
At this time, Scout Flies are always randomized, and Precursor Orbs
are never randomized.
## What does randomization do to this game?
All 101 Power Cells and 112 Scout Flies are now Location Checks
and may contain Items for different games, as well as different Items from within Jak and Daxter.
## What is the goal of the game once randomized?
To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo.
In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition,
you will need the four specific Power Cells obtained by freeing the Red, Blue, Yellow, and Green Sages.
## How do I progress through the game?
You can progress by performing tasks and completing the challenges that would normally give you Power Cells and
Scout Flies in the game. If you are playing with others, those players may find Power Cells and Scout Flies
in their games, and those Items will be automatically sent to your game.
If you have completed all possible tasks available to you but still cannot progress, you may have to wait for
another player to find enough of your game's Items to allow you to progress. If that does not apply,
double-check your spoiler log to make sure you have all the items you should have. If you don't,
you may have encountered a bug. Please see the options for bug reporting below.
## What happens when I pick up an item?
Jak and Daxter will perform their victory animation, if applicable. You will not receive that item, and
the Item count for that item will not change. The pause menu will say "Task Completed" below the
picked-up Power Cell, but the icon will remain "dormant." You will see a message in your text client saying
what you found and who it belongs to.
## What happens when I receive an item?
Jak and Daxter won't perform their victory animation, and gameplay will continue as normal. Your text client will
inform you where you received the Item from, and which one it is. Your Item count for that type of Item will also
tick up. The pause menu will not say "Task Completed" below the selected Power Cell, but the icon will be "activated."
## I can't reach a certain area within an accessible region, how do I get there?
Some areas are locked behind possession of specific Power Cells. For example, you cannot access Misty Island
until you have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined
_through possession of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._
## I got soft-locked and can't leave, how do I get out of here?
As stated before, some areas are locked behind possession of specific Power Cells. But you may already be past
a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, where
the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, you cannot access
the Plant Boss's room and escape.
In this scenario, you will need to open your menu and find the "Warp To Home" option at the bottom of the list.
Selecting this option will instantly teleport you to Geyser Rock. From there, you can teleport back to the nearest
sage's hut to continue your journey.
## I think I found a bug, where should I report it?
Depending on the nature of the bug, there are a couple of different options.
* If you found a logical error in the randomizer, please create a new Issue
[here.](https://github.com/ArchipelaGOAL/Archipelago/issues)
* Use this page if:
* You are hard-locked from progressing. For example, you are stuck on Geyser Rock because one of the four
Geyser Rock Power Cells is not on Geyser Rock.
* The randomizer did not respect one of the Options you chose.
* You see a mistake, typo, etc. on this webpage.
* You see an error or stack trace appear on the text client.
* Please upload your config file and spoiler log file in the Issue, so we can troubleshoot the problem.
* If you encountered an error in OpenGOAL, please create a new Issue
[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues)
* Use this page if:
* You encounter a crash, freeze, reset, etc.
* You fail to send Items you find in the game to the Archipelago server.
* You fail to receive Items the server sends to you.
* Your game disconnects from the server and cannot reconnect.
* You go looking for a game item that has already disappeared before you could reach it.
* Please upload any log files that may have been generated.

View File

@@ -0,0 +1,85 @@
# Jak And Daxter (ArchipelaGOAL) Setup Guide
## Required Software
- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.*
- Python version 3.10 or higher. Make sure this is added to your PATH environment variable.
- [Task](https://taskfile.dev/installation/) (This makes it easier to run commands.)
## Installation
### Installation via OpenGOAL Mod Launcher
At this time, the only supported method of setup is through Manual Compilation. Aside from the legal copy of the game, all tools required to do this are free.
***Windows Preparations***
***Linux Preparations***
***Using the Launcher***
### Manual Compilation (Linux/Windows)
***Windows Preparations***
- Dump your copy of the game as an ISO file to your PC.
- Download a zipped up copy of the Archipelago Server and Client [here.](https://github.com/ArchipelaGOAL/Archipelago)
- Download a zipped up copy of the modded OpenGOAL game [here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL)
- Unzip the two projects into easily accessible directories.
***Linux Preparations***
***Compiling***
## Starting a Game
- Open 3 Powershell windows. If you have VSCode, you can run 3 terminals to consolidate this process.
- In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py --update_settings`. Then run it again without the `--update_settings` flag.
- In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its contents. When that is done, run `task repl`.
- In the third window, navigate to the ArchipelaGOAL folder and run `task boot-game`. At this point, Jak should be standing outside Samos's hut.
- Once you confirm all those tasks succeeded, you can now close all these windows.
- Edit your host.yaml file and ensure these lines exist. And don't forget to specify your ACTUAL install path. If you're on Windows, no backslashes!
```
jakanddaxter_options:
# Path to folder containing the ArchipelaGOAL mod.
root_directory: "D:/Files/Repositories/ArchipelaGOAL"
```
- In the Launcher, click Generate to create a new random seed. Save the resulting zip file.
- In the Launcher, click Host to host the Archipelago server. It will prompt you for the location of that zip file.
- Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the command window begin to compile the game.
- When it completes, you should hear the menu closing sound effect, and you should see the text client indicate that the two agents are ready to communicate with the game.
- Connect the client to the Archipelago server and enter your slot name. Once this is done, the game should be ready to play. Talk to Samos to trigger the cutscene where he sends you to Geyser Rock, and off you go!
Once you complete the setup steps, you should only need to run the Launcher again to generate a game, host a server, or run the client and connect to a server.
- You never need to download the zip copies of the projects again (unless there are updates).
- You never need to dump your ISO again.
- You never need to extract the ISO assets again.
### Joining a MultiWorld Game
MultiWorld games are untested at this time.
### Playing Offline
Offline play is untested at this time.
## Installation and Setup Troubleshooting
### Compilation Failures
### Runtime Failures
- If the client window appears but no sound plays, you will need to enter the following commands into the client to connect it to the game.
- `/repl connect`
- `/memr connect`
- Once these are done, you can enter `/repl status` and `/memr status` to check that everything is connected and ready.
## Gameplay Troubleshooting
### Known Issues
- I've streamlined the process of connecting the client's agents to the game, but it comes at the cost of more granular commands useful for troubleshooting.
- The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly.
- The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server.
- The game relates tasks and power cells closely but separately. Some issues may result from having to tell the game to check for the power cells you own, rather than the tasks you completed.

View File

@@ -0,0 +1,188 @@
from ..GameID import jak1_id
# Power Cells are given ID's between 0 and 116 by the game.
# The game tracks all game-tasks as integers.
# 101 of these ID's correspond directly to power cells, but they are not
# necessarily ordered, nor are they the first 101 in the task list.
# The remaining ones are cutscenes and other events.
# These helper functions do all the math required to get information about each
# power cell and translate its ID between AP and OpenGOAL.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
return jak1_id + game_id
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
return ap_id - jak1_id
# The ID's you see below correspond directly to that cell's game-task ID.
# Geyser Rock
locGR_cellTable = {
92: "GR: Find The Cell On The Path",
93: "GR: Open The Precursor Door",
94: "GR: Climb Up The Cliff",
95: "GR: Free 7 Scout Flies"
}
# Sandover Village
locSV_cellTable = {
11: "SV: Bring 90 Orbs To The Mayor",
12: "SV: Bring 90 Orbs to Your Uncle",
10: "SV: Herd The Yakows Into The Pen",
13: "SV: Bring 120 Orbs To The Oracle (1)",
14: "SV: Bring 120 Orbs To The Oracle (2)",
75: "SV: Free 7 Scout Flies"
}
# Forbidden Jungle
locFJ_cellTable = {
3: "FJ: Connect The Eco Beams",
4: "FJ: Get To The Top Of The Temple",
2: "FJ: Find The Blue Vent Switch",
6: "FJ: Defeat The Dark Eco Plant",
5: "FJ: Catch 200 Pounds Of Fish",
8: "FJ: Follow The Canyon To The Sea",
9: "FJ: Open The Locked Temple Door",
7: "FJ: Free 7 Scout Flies"
}
# Sentinel Beach
locSB_cellTable = {
15: "SB: Unblock The Eco Harvesters",
17: "SB: Push The Flut Flut Egg Off The Cliff",
16: "SB: Get The Power Cell From The Pelican",
18: "SB: Chase The Seagulls",
19: "SB: Launch Up To The Cannon Tower",
21: "SB: Explore The Beach",
22: "SB: Climb The Sentinel",
20: "SB: Free 7 Scout Flies"
}
# Misty Island
locMI_cellTable = {
23: "MI: Catch The Sculptor's Muse",
24: "MI: Climb The Lurker Ship",
26: "MI: Stop The Cannon",
25: "MI: Return To The Dark Eco Pool",
27: "MI: Destroy the Balloon Lurkers",
29: "MI: Use Zoomer To Reach Power Cell",
30: "MI: Use Blue Eco To Reach Power Cell",
28: "MI: Free 7 Scout Flies"
}
# Fire Canyon
locFC_cellTable = {
69: "FC: Reach The End Of Fire Canyon",
68: "FC: Free 7 Scout Flies"
}
# Rock Village
locRV_cellTable = {
31: "RV: Bring 90 Orbs To The Gambler",
32: "RV: Bring 90 Orbs To The Geologist",
33: "RV: Bring 90 Orbs To The Warrior",
34: "RV: Bring 120 Orbs To The Oracle (1)",
35: "RV: Bring 120 Orbs To The Oracle (2)",
76: "RV: Free 7 Scout Flies"
}
# Precursor Basin
locPB_cellTable = {
54: "PB: Herd The Moles Into Their Hole",
53: "PB: Catch The Flying Lurkers",
52: "PB: Beat Record Time On The Gorge",
56: "PB: Get The Power Cell Over The Lake",
55: "PB: Cure Dark Eco Infected Plants",
58: "PB: Navigate The Purple Precursor Rings",
59: "PB: Navigate The Blue Precursor Rings",
57: "PB: Free 7 Scout Flies"
}
# Lost Precursor City
locLPC_cellTable = {
47: "LPC: Raise The Chamber",
45: "LPC: Follow The Colored Pipes",
46: "LPC: Reach The Bottom Of The City",
48: "LPC: Quickly Cross The Dangerous Pool",
44: "LPC: Match The Platform Colors",
50: "LPC: Climb The Slide Tube",
51: "LPC: Reach The Center Of The Complex",
49: "LPC: Free 7 Scout Flies"
}
# Boggy Swamp
locBS_cellTable = {
37: "BS: Ride The Flut Flut",
36: "BS: Protect Farthy's Snacks",
38: "BS: Defeat The Lurker Ambush",
39: "BS: Break The Tethers To The Zeppelin (1)",
40: "BS: Break The Tethers To The Zeppelin (2)",
41: "BS: Break The Tethers To The Zeppelin (3)",
42: "BS: Break The Tethers To The Zeppelin (4)",
43: "BS: Free 7 Scout Flies"
}
# Mountain Pass
locMP_cellTable = {
86: "MP: Defeat Klaww",
87: "MP: Reach The End Of The Mountain Pass",
110: "MP: Find The Hidden Power Cell",
88: "MP: Free 7 Scout Flies"
}
# Volcanic Crater
locVC_cellTable = {
96: "VC: Bring 90 Orbs To The Miners (1)",
97: "VC: Bring 90 Orbs To The Miners (2)",
98: "VC: Bring 90 Orbs To The Miners (3)",
99: "VC: Bring 90 Orbs To The Miners (4)",
100: "VC: Bring 120 Orbs To The Oracle (1)",
101: "VC: Bring 120 Orbs To The Oracle (2)",
74: "VC: Find The Hidden Power Cell",
77: "VC: Free 7 Scout Flies"
}
# Spider Cave
locSC_cellTable = {
78: "SC: Use Your Goggles To Shoot The Gnawing Lurkers",
79: "SC: Destroy The Dark Eco Crystals",
80: "SC: Explore The Dark Cave",
81: "SC: Climb The Giant Robot",
82: "SC: Launch To The Poles",
83: "SC: Navigate The Spider Tunnel",
84: "SC: Climb the Precursor Platforms",
85: "SC: Free 7 Scout Flies"
}
# Snowy Mountain
locSM_cellTable = {
60: "SM: Find The Yellow Vent Switch",
61: "SM: Stop The 3 Lurker Glacier Troops",
66: "SM: Deactivate The Precursor Blockers",
67: "SM: Open The Frozen Crate",
63: "SM: Open The Lurker Fort Gate",
62: "SM: Get Through The Lurker Fort",
64: "SM: Survive The Lurker Infested Cave",
65: "SM: Free 7 Scout Flies"
}
# Lava Tube
locLT_cellTable = {
89: "LT: Cross The Lava Tube",
90: "LT: Free 7 Scout Flies"
}
# Gol and Maias Citadel
locGMC_cellTable = {
71: "GMC: Free The Blue Sage",
72: "GMC: Free The Red Sage",
73: "GMC: Free The Yellow Sage",
70: "GMC: Free The Green Sage",
91: "GMC: Free 7 Scout Flies"
}

View File

@@ -0,0 +1,101 @@
from ..GameID import jak1_id
# Precursor Orbs are not necessarily given ID's by the game.
# Of the 2000 orbs (or "money") you can pick up, only 1233 are standalone ones you find in the overworld.
# We can identify them by Actor ID's, which run from 549 to 24433. Other actors reside in this range,
# so like Power Cells these are not ordered, nor contiguous, nor exclusively orbs.
# In fact, other ID's in this range belong to actors that spawn orbs when they are activated or when they die,
# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head.
# These orbs that spawn from parent actors DON'T have an Actor ID themselves - the parent object keeps
# track of how many of its orbs have been picked up. If you pick up only some of its orbs, it
# will respawn when you leave the area, and only drop the remaining number of orbs when activated/killed.
# Once all the orbs are picked up, the actor will permanently "retire" and never spawn again.
# The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering
# these ID-less orbs may need to be a future enhancement. TODO ^^
# Standalone orbs need 15 bits to identify themselves by Actor ID,
# so we can use 2^15 to offset them from scout flies, just like we offset
# scout flies from power cells by 2^10.
orb_offset = 32768
# These helper functions do all the math required to get information about each
# precursor orb and translate its ID between AP and OpenGOAL.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID.
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets.
# The ID's you see below correspond directly to that orb's Actor ID in the game.
# Geyser Rock
locGR_orbTable = {
}
# Sandover Village
locSV_orbTable = {
}
# Forbidden Jungle
locFJ_orbTable = {
}
# Sentinel Beach
locSB_orbTable = {
}
# Misty Island
locMI_orbTable = {
}
# Fire Canyon
locFC_orbTable = {
}
# Rock Village
locRV_orbTable = {
}
# Precursor Basin
locPB_orbTable = {
}
# Lost Precursor City
locLPC_orbTable = {
}
# Boggy Swamp
locBS_orbTable = {
}
# Mountain Pass
locMP_orbTable = {
}
# Volcanic Crater
locVC_orbTable = {
}
# Spider Cave
locSC_orbTable = {
}
# Snowy Mountain
locSM_orbTable = {
}
# Lava Tube
locLT_orbTable = {
}
# Gol and Maias Citadel
locGMC_orbTable = {
}

View File

@@ -0,0 +1,227 @@
from ..GameID import jak1_id
# Scout Flies are given ID's between 0 and 393311 by the game, explanation below.
# Each fly (or "buzzer") is given a unique 32-bit number broken into two 16-bit numbers.
# The lower 16 bits are the game-task ID of the power cell the fly corresponds to.
# The higher 16 bits are the index of the fly itself, from 000 (0) to 110 (6).
# Ex: The final scout fly on Geyser Rock
# 0000000000000110 0000000001011111
# ( Index: 6 ) ( Cell: 95 )
# Because flies are indexed from 0, each 0th fly's full ID == the power cell's ID.
# So we need to offset all of their ID's in order for Archipelago to separate them
# from their power cells. We can use 1024 (2^10) for this purpose, because scout flies
# only ever need 10 bits to identify themselves (3 for the index, 7 for the cell ID).
# We're also going to compress the ID by bit-shifting the fly index down to lower bits,
# keeping the scout fly ID range to a smaller set of numbers (1000 -> 2000, instead of 1 -> 400000).
fly_offset = 1024
# These helper functions do all the math required to get information about each
# scout fly and translate its ID between AP and OpenGOAL.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
cell_id = get_cell_id(game_id) # Get the power cell ID from the lowest 7 bits.
buzzer_index = (game_id - cell_id) >> 9 # Get the index, bit shift it down 9 places.
compressed_id = fly_offset + buzzer_index + cell_id # Add the offset, the bit-shifted index, and the cell ID.
return jak1_id + compressed_id # Last thing: add the game's ID.
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
compressed_id = ap_id - jak1_id # Reverse process. First thing: subtract the game's ID.
cell_id = get_cell_id(compressed_id) # Get the power cell ID from the lowest 7 bits.
buzzer_index = compressed_id - fly_offset - cell_id # Get the bit-shifted index.
return (buzzer_index << 9) + cell_id # Return the index to its normal place, re-add the cell ID.
# Get the power cell ID from the lowest 7 bits.
# Make sure to use this function ONLY when the input argument does NOT include jak1_id,
# because that number may flip some of the bottom 7 bits, and that will throw off this bit mask.
def get_cell_id(buzzer_id: int) -> int:
assert buzzer_id < jak1_id, f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}."
return buzzer_id & 0b1111111
# The ID's you see below correspond directly to that fly's 32-bit ID in the game.
# I used the decompiled entity JSON's and Jak's X/Y coordinates in Debug Mode
# to determine which box ID is which location.
# Geyser Rock
locGR_scoutTable = {
95: "GR: Scout Fly On Ground, Front",
327775: "GR: Scout Fly On Ground, Back",
393311: "GR: Scout Fly On Left Ledge",
65631: "GR: Scout Fly On Right Ledge",
262239: "GR: Scout Fly On Middle Ledge, Left",
131167: "GR: Scout Fly On Middle Ledge, Right",
196703: "GR: Scout Fly On Top Ledge"
}
# Sandover Village
locSV_scoutTable = {
262219: "SV: Scout Fly In Fisherman's House",
327755: "SV: Scout Fly In Mayor's House",
131147: "SV: Scout Fly Under Bridge",
65611: "SV: Scout Fly Behind Sculptor's House",
75: "SV: Scout Fly Overlooking Farmer's House",
393291: "SV: Scout Fly Near Oracle",
196683: "SV: Scout Fly In Farmer's House"
}
# Forbidden Jungle
locFJ_scoutTable = {
393223: "FJ: Scout Fly At End Of Path",
262151: "FJ: Scout Fly On Spiral Of Stumps",
7: "FJ: Scout Fly Near Dark Eco Boxes",
196615: "FJ: Scout Fly At End Of River",
131079: "FJ: Scout Fly Behind Lurker Machine",
327687: "FJ: Scout Fly Around Temple Spire",
65543: "FJ: Scout Fly On Top Of Temple"
}
# Sentinel Beach
locSB_scoutTable = {
327700: "SB: Scout Fly At Entrance",
20: "SB: Scout Fly Overlooking Locked Boxes",
65556: "SB: Scout Fly On Path To Flut Flut",
262164: "SB: Scout Fly Under Wood Pillars",
196628: "SB: Scout Fly Overlooking Blue Eco Vent",
131092: "SB: Scout Fly Overlooking Green Eco Vents",
393236: "SB: Scout Fly On Sentinel"
}
# Misty Island
locMI_scoutTable = {
327708: "MI: Scout Fly Overlooking Entrance",
65564: "MI: Scout Fly On Ledge Near Arena Entrance",
262172: "MI: Scout Fly Near Arena Door",
28: "MI: Scout Fly On Ledge Near Arena Exit",
131100: "MI: Scout Fly On Ship",
196636: "MI: Scout Fly On Barrel Ramps",
393244: "MI: Scout Fly On Zoomer Ramps"
}
# Fire Canyon
locFC_scoutTable = {
393284: "FC: Scout Fly 1",
68: "FC: Scout Fly 2",
65604: "FC: Scout Fly 3",
196676: "FC: Scout Fly 4",
131140: "FC: Scout Fly 5",
262212: "FC: Scout Fly 6",
327748: "FC: Scout Fly 7"
}
# Rock Village
locRV_scoutTable = {
76: "RV: Scout Fly Behind Sage's Hut",
131148: "RV: Scout Fly Near Waterfall",
196684: "RV: Scout Fly Behind Geologist",
262220: "RV: Scout Fly Behind Fiery Boulder",
65612: "RV: Scout Fly On Dock",
327756: "RV: Scout Fly At Pontoon Bridge",
393292: "RV: Scout Fly At Boggy Swamp Entrance"
}
# Precursor Basin
locPB_scoutTable = {
196665: "PB: Scout Fly Overlooking Entrance",
393273: "PB: Scout Fly Near Mole Hole",
131129: "PB: Scout Fly At Purple Ring Start",
65593: "PB: Scout Fly Near Dark Eco Plant, Above",
57: "PB: Scout Fly At Blue Ring Start",
262201: "PB: Scout Fly Before Big Jump",
327737: "PB: Scout Fly Near Dark Eco Plant, Below"
}
# Lost Precursor City
locLPC_scoutTable = {
262193: "LPC: Scout Fly First Room",
131121: "LPC: Scout Fly Before Second Room",
393265: "LPC: Scout Fly Second Room, Near Orb Vent",
196657: "LPC: Scout Fly Second Room, On Path To Cell",
49: "LPC: Scout Fly Second Room, Green Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?`
65585: "LPC: Scout Fly Second Room, Blue Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?`
327729: "LPC: Scout Fly Across Steam Vents"
}
# Boggy Swamp
locBS_scoutTable = {
43: "BS: Scout Fly Near Entrance",
393259: "BS: Scout Fly Over First Jump Pad",
65579: "BS: Scout Fly Over Second Jump Pad",
262187: "BS: Scout Fly Across Black Swamp",
327723: "BS: Scout Fly Overlooking Flut Flut",
131115: "BS: Scout Fly On Flut Flut Platforms",
196651: "BS: Scout Fly In Field Of Boxes"
}
# Mountain Pass
locMP_scoutTable = {
88: "MP: Scout Fly 1",
65624: "MP: Scout Fly 2",
131160: "MP: Scout Fly 3",
196696: "MP: Scout Fly 4",
262232: "MP: Scout Fly 5",
327768: "MP: Scout Fly 6",
393304: "MP: Scout Fly 7"
}
# Volcanic Crater
locVC_scoutTable = {
262221: "VC: Scout Fly In Miner's Cave",
393293: "VC: Scout Fly Near Oracle",
196685: "VC: Scout Fly On Stone Platforms",
131149: "VC: Scout Fly Near Lava Tube",
77: "VC: Scout Fly At Minecart Junction",
65613: "VC: Scout Fly Near Spider Cave",
327757: "VC: Scout Fly Near Mountain Pass"
}
# Spider Cave
locSC_scoutTable = {
327765: "SC: Scout Fly Near Dark Dave Entrance",
262229: "SC: Scout Fly In Dark Cave",
393301: "SC: Scout Fly Main Cave, Overlooking Entrance",
196693: "SC: Scout Fly Main Cave, Near Dark Crystal",
131157: "SC: Scout Fly Main Cave, Near Robot Cave Entrance",
85: "SC: Scout Fly Robot Cave, At Bottom Level",
65621: "SC: Scout Fly Robot Cave, At Top Level",
}
# Snowy Mountain
locSM_scoutTable = {
65: "SM: Scout Fly Near Entrance",
327745: "SM: Scout Fly Near Frozen Box",
65601: "SM: Scout Fly Near Yellow Eco Switch",
131137: "SM: Scout Fly On Cliff near Flut Flut",
393281: "SM: Scout Fly Under Bridge To Fort",
196673: "SM: Scout Fly On Top Of Fort Tower",
262209: "SM: Scout Fly On Top Of Fort"
}
# Lava Tube
locLT_scoutTable = {
90: "LT: Scout Fly 1",
65626: "LT: Scout Fly 2",
327770: "LT: Scout Fly 3",
262234: "LT: Scout Fly 4",
131162: "LT: Scout Fly 5",
196698: "LT: Scout Fly 6",
393306: "LT: Scout Fly 7"
}
# Gol and Maias Citadel
locGMC_scoutTable = {
91: "GMC: Scout Fly At Entrance",
65627: "GMC: Scout Fly Main Room, Left of Robot",
196699: "GMC: Scout Fly Main Room, Right of Robot",
262235: "GMC: Scout Fly Before Jumping Lurkers",
393307: "GMC: Scout Fly At Blast Furnace",
131163: "GMC: Scout Fly At Launch Pad Room",
327771: "GMC: Scout Fly Top Of Rotating Tower"
}

View File

@@ -0,0 +1 @@
Pymem>=1.13.0