Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jak and Daxter: Implement New Game #3291

Open
wants to merge 76 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
57b2ecf
Jak 1: Initial commit: Cell Locations, Items, and Regions modeled.
massimilianodelliubaldini Apr 14, 2024
bff2d4a
Jak 1: Wrote Regions, Rules, init. Untested.
massimilianodelliubaldini Apr 15, 2024
ab4a5e9
Jak 1: Fixed mistakes, need better understanding of Entrances.
massimilianodelliubaldini Apr 15, 2024
723f641
Jak 1: Fixed bugs, refactored Regions, added missing Special Checks. …
massimilianodelliubaldini Apr 15, 2024
4031f19
Jak 1: Add Scout Fly Locations, code and style cleanup.
massimilianodelliubaldini Apr 15, 2024
352a26a
Jak 1: Add Scout Flies to Regions.
massimilianodelliubaldini Apr 15, 2024
3b197bb
Jak 1: Add version info.
massimilianodelliubaldini Apr 16, 2024
e1e8c04
Jak 1: Reduced code smell.
massimilianodelliubaldini Apr 16, 2024
293a282
Jak 1: Fixed UT bugs, added Free The Sages as Locations.
massimilianodelliubaldini Apr 16, 2024
db8d74e
Jak 1: Refactor ID scheme to better fit game's scheme. Add more subre…
massimilianodelliubaldini Apr 18, 2024
3cd3e73
Jak 1: Add some one-ways, adjust scout fly offset.
massimilianodelliubaldini Apr 18, 2024
d22c2ca
Jak 1: Found Scout Fly ID's for first 4 maps.
massimilianodelliubaldini Apr 19, 2024
0b26a99
Jak 1: Add more scout fly ID's, refactor game/AP ID translation for e…
massimilianodelliubaldini Apr 20, 2024
df31d87
Jak 1: Fixed a few things. Four maps to go.
massimilianodelliubaldini Apr 23, 2024
ef7b359
Jak 1: Last of the scout flies mapped!
massimilianodelliubaldini Apr 23, 2024
c497188
Jak 1: simplify citadel sages logic.
massimilianodelliubaldini Apr 24, 2024
7f8a41f
Jak 1: WebWorld setup, some documentation.
massimilianodelliubaldini Apr 24, 2024
3de94fb
Jak 1: Initial checkin of Client. Removed the colon from the game name.
massimilianodelliubaldini Apr 26, 2024
39364b3
Jak 1: Refactored client into components, working on async communicat…
massimilianodelliubaldini Apr 27, 2024
801e50b
Jak 1: In tandem with new ArchipelaGOAL memory structure, define read…
massimilianodelliubaldini Apr 29, 2024
75461d6
Jak 1: There's magic in the air...
massimilianodelliubaldini Apr 30, 2024
b1f1464
Jak 1: Fixed bug translating scout fly ID's.
massimilianodelliubaldini Apr 30, 2024
f0659e3
Jak 1: Make the REPL a little more verbose, easier to debug.
massimilianodelliubaldini May 1, 2024
f4a51a8
Jak 1: Did you know Snowy Mountain had such specific unlock requireme…
massimilianodelliubaldini May 3, 2024
6637452
Jak 1: Update Documentation.
massimilianodelliubaldini May 9, 2024
240bb6c
Jak 1: Simplify user interaction with agents, make process more robus…
massimilianodelliubaldini May 9, 2024
4b4489a
Jak 1: Simplified startup process, updated docs, prayed.
massimilianodelliubaldini May 12, 2024
89c15f5
Jak 1: quick fix to settings.
massimilianodelliubaldini May 12, 2024
1ac5302
Jak and Daxter: Implement New Game (#1)
massimilianodelliubaldini May 12, 2024
75989b8
Merge branch 'main' into jak-and-daxter
massimilianodelliubaldini May 13, 2024
0f4c221
Merge remote-tracking branch 'remotes/upstream/main'
massimilianodelliubaldini May 18, 2024
7cf50b0
Jak and Daxter: Genericize Items, Update Scout Fly logic, Add Victory…
massimilianodelliubaldini May 18, 2024
064ae55
Merge remote-tracking branch 'remotes/upstream/main'
massimilianodelliubaldini May 22, 2024
e76df68
Jak and Daxter - Gondola, Pontoons, Rules, Regions, and Client Update
massimilianodelliubaldini May 22, 2024
50f5606
Alpha Updates (#15)
massimilianodelliubaldini May 28, 2024
33a858e
Logging Update (#16)
massimilianodelliubaldini Jun 1, 2024
8293b9d
Deathlink (#18)
massimilianodelliubaldini Jun 7, 2024
1c42bdb
Move Randomizer (#26)
massimilianodelliubaldini Jun 27, 2024
1b0d6ad
Merge remote-tracking branch 'remotes/upstream/main'
massimilianodelliubaldini Jul 2, 2024
f875169
Move rando fixes (#29)
massimilianodelliubaldini Jul 3, 2024
f7b688d
Orbsanity (#32)
massimilianodelliubaldini Jul 11, 2024
35bf078
Finishing Touches (#36)
massimilianodelliubaldini Jul 23, 2024
22b43a8
Rename completion_condition to jak_completion_condition (#41)
DeamonHunter Jul 31, 2024
ea82846
The Afterparty (#42)
massimilianodelliubaldini Aug 3, 2024
b3b1ee4
Added more specific troubleshooting/setup instructions.
massimilianodelliubaldini Aug 3, 2024
da7de27
Add known issue about large releases taking time. (Dodge 6,666th comm…
massimilianodelliubaldini Aug 3, 2024
a675849
Remove "Bundle of", Add location name groups, set better default Root…
massimilianodelliubaldini Aug 7, 2024
17b4845
Make orb trade amounts configurable, make orbsanity defaults more rea…
massimilianodelliubaldini Aug 19, 2024
b7ca9cb
Add HUD info to doc.
massimilianodelliubaldini Aug 20, 2024
746b281
Exempt's Code Review Updates (#43)
massimilianodelliubaldini Aug 27, 2024
3eadf54
Added a host.yaml option to override friendly limits, plus a couple o…
massimilianodelliubaldini Aug 27, 2024
30f5d84
Added singleplayer limits, player names to enforcement rules.
massimilianodelliubaldini Aug 29, 2024
b63ed86
Updated friendly limits to be more strict, optimized recalculate logic.
massimilianodelliubaldini Aug 30, 2024
0e1a3df
Today's the big day Jak: updates docs for mod support in OpenGOAL Lau…
massimilianodelliubaldini Sep 1, 2024
9317486
Rearranged and clarified some instructions, ADDED PATH-SPACE FIX TO C…
massimilianodelliubaldini Sep 2, 2024
e6b58aa
Fix deathlink reset stalls on a busy client. (#47)
massimilianodelliubaldini Sep 7, 2024
8922b1a
Jak & Daxter Client : queue game text messages to get items faster du…
t-rbernard Sep 10, 2024
d15de80
Item Classifications (and REPL fixes) (#49)
massimilianodelliubaldini Sep 12, 2024
f22e5f6
Merge remote-tracking branch 'remotes/upstream/main'
massimilianodelliubaldini Sep 12, 2024
8af8dd7
Use math.ceil like a normal person.
massimilianodelliubaldini Sep 12, 2024
2431ba0
Missed a space.
massimilianodelliubaldini Sep 12, 2024
fd47d02
Fix non-accessibility due to bad orb calculation.
massimilianodelliubaldini Sep 12, 2024
39f0955
Updated documentation.
massimilianodelliubaldini Sep 14, 2024
e0410ea
More Options, More Docs, More Tests (#51)
massimilianodelliubaldini Sep 17, 2024
8f0a2bc
Clean imports of unit tests.
massimilianodelliubaldini Sep 17, 2024
4e4a59d
Create OptionGroups.
massimilianodelliubaldini Sep 17, 2024
95ec860
Fix region rule bug with Punch for Klaww.
massimilianodelliubaldini Sep 18, 2024
15145f9
Include Punch For Klaww in slot data.
massimilianodelliubaldini Sep 22, 2024
5a2da8e
Update worlds/jakanddaxter/__init__.py
massimilianodelliubaldini Sep 23, 2024
9bec937
Temper and Harden Text Client (#52)
massimilianodelliubaldini Oct 4, 2024
86460b4
Stellar Messaging (#54)
massimilianodelliubaldini Oct 14, 2024
0771796
ISO Data Enhancement (#58)
massimilianodelliubaldini Oct 29, 2024
ff5165e
Update memory version to 4.
massimilianodelliubaldini Oct 29, 2024
d0d609a
Docs update for iso_data.
massimilianodelliubaldini Nov 11, 2024
5f0ff2e
Merge remote-tracking branch 'remotes/upstream/main'
massimilianodelliubaldini Nov 13, 2024
0ae5faa
Auto Detect OpenGOAL Install (#63)
massimilianodelliubaldini Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Currently, the following games are supported:
* Super Mario World
* Pokémon Red and Blue
* Hylics 2
* Jak and Daxter: The Precursor Legacy
* Overcooked! 2
* Zillion
* Lufia II Ancient Cave
Expand Down
3 changes: 3 additions & 0 deletions docs/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
# Hylics 2
/worlds/hylics2/ @TRPG0

# Jak and Daxter: The Precursor Legacy
/worlds/jakanddaxter/ @massimilianodelliubaldini

# Kirby's Dream Land 3
/worlds/kdl3/ @Silvris

Expand Down
342 changes: 342 additions & 0 deletions worlds/jakanddaxter/Client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
import os
import subprocess
import colorama

import asyncio
from asyncio import Task

from typing import Set, Awaitable, Optional, List

import pymem
from pymem.exception import ProcessNotFound

import Utils
from NetUtils import ClientStatus
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled
from .Options import EnableOrbsanity

from .GameID import jak1_name
from .client.ReplClient import JakAndDaxterReplClient
from .client.MemoryReader import JakAndDaxterMemoryReader

import ModuleUpdate
ModuleUpdate.update()


all_tasks: Set[Task] = set()


def create_task_log_exception(awaitable: 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 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 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":
create_task_log_exception(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):
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: Optional[str], password: Optional[str]) -> None:
self.repl = JakAndDaxterReplClient()
self.memr = JakAndDaxterMemoryReader()
# self.repl.load_data()
# self.memr.load_data()
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()
self.tags = set()
await self.send_connect()

def on_package(self, cmd: str, args: dict):

if cmd == "Connected":
slot_data = args["slot_data"]
orbsanity_option = slot_data["enable_orbsanity"]
if orbsanity_option == EnableOrbsanity.option_per_level:
orbsanity_bundle = slot_data["level_orbsanity_bundle_size"]
elif orbsanity_option == EnableOrbsanity.option_global:
orbsanity_bundle = slot_data["global_orbsanity_bundle_size"]
else:
orbsanity_bundle = 1

create_task_log_exception(
self.repl.setup_options(orbsanity_option,
orbsanity_bundle,
slot_data["fire_canyon_cell_count"],
slot_data["mountain_pass_cell_count"],
slot_data["lava_tube_cell_count"],
slot_data["citizen_orb_trade_amount"],
slot_data["oracle_orb_trade_amount"],
slot_data["jak_completion_condition"]))

# Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server
# to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost,
# while "ReceivedItems" will tell us the orbs we gained. This will give us the correct balance.
if orbsanity_option in [EnableOrbsanity.option_per_level, EnableOrbsanity.option_global]:
async def get_orb_balance():
await self.send_msgs([{"cmd": "Get", "keys": [f"jakanddaxter_{self.auth}_orbs_paid"]}])

create_task_log_exception(get_orb_balance())

if cmd == "Retrieved":
if f"jakanddaxter_{self.auth}_orbs_paid" in args["keys"]:
orbs_traded = args["keys"][f"jakanddaxter_{self.auth}_orbs_paid"]
orbs_traded = orbs_traded if orbs_traded is not None else 0
create_task_log_exception(self.repl.subtract_traded_orbs(orbs_traded))

if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
logger.debug(f"index: {str(index)}, item: {str(item)}")
self.repl.item_inbox[index] = item

async def json_to_game_text(self, args: dict):
if "type" in args and args["type"] in {"ItemSend"}:
my_item_name: Optional[str] = None
my_item_finder: Optional[str] = None
their_item_name: Optional[str] = None
their_item_owner: Optional[str] = None

item = args["item"]
recipient = args["receiving"]

# Receiving an item from the server.
if self.slot_concerns_self(recipient):
my_item_name = self.item_names.lookup_in_game(item.item)

# Did we find it, or did someone else?
if self.slot_concerns_self(item.player):
my_item_finder = "MYSELF"
else:
my_item_finder = self.player_names[item.player]

# Sending an item to the server.
if self.slot_concerns_self(item.player):
their_item_name = self.item_names.lookup_in_slot(item.item, recipient)

# Does it belong to us, or to someone else?
if self.slot_concerns_self(recipient):
their_item_owner = "MYSELF"
else:
their_item_owner = self.player_names[recipient]

# Write to game display.
self.repl.queue_game_text(my_item_name, my_item_finder, their_item_name, their_item_owner)

def on_print_json(self, args: dict) -> None:

# Even though N items come in as 1 ReceivedItems packet, there are still N PrintJson packets to process,
# and they all arrive before the ReceivedItems packet does. Defer processing of these packets as
# async tasks to speed up large releases of items.
create_task_log_exception(self.json_to_game_text(args))
super(JakAndDaxterContext, self).on_print_json(args)

def on_deathlink(self, data: dict):
if self.memr.deathlink_enabled:
self.repl.received_deathlink = True
super().on_deathlink(data)

async def ap_inform_location_check(self, location_ids: List[int]):
message = [{"cmd": "LocationChecks", "locations": location_ids}]
await self.send_msgs(message)

def on_location_check(self, location_ids: List[int]):
create_task_log_exception(self.ap_inform_location_check(location_ids))

async def ap_inform_finished_game(self):
if not self.finished_game and self.memr.finished_game:
message = [{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]
await self.send_msgs(message)
self.finished_game = True

def on_finish_check(self):
create_task_log_exception(self.ap_inform_finished_game())

async def ap_inform_deathlink(self):
if self.memr.deathlink_enabled:
player = self.player_names[self.slot] if self.slot is not None else "Jak"
death_text = self.memr.cause_of_death.replace("Jak", player)
await self.send_death(death_text)
logger.info(death_text)

# Reset all flags, but leave the death count alone.
self.memr.send_deathlink = False
self.memr.cause_of_death = ""

def on_deathlink_check(self):
create_task_log_exception(self.ap_inform_deathlink())

async def ap_inform_deathlink_toggle(self):
await self.update_death_link(self.memr.deathlink_enabled)

def on_deathlink_toggle(self):
create_task_log_exception(self.ap_inform_deathlink_toggle())

async def ap_inform_orb_trade(self, orbs_changed: int):
if self.memr.orbsanity_enabled:
await self.send_msgs([{"cmd": "Set",
"key": f"jakanddaxter_{self.auth}_orbs_paid",
"default": 0,
"want_reply": False,
"operations": [{"operation": "add", "value": orbs_changed}]
}])

def on_orb_trade(self, orbs_changed: int):
create_task_log_exception(self.ap_inform_orb_trade(orbs_changed))

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_location_check,
self.on_finish_check,
self.on_deathlink_check,
self.on_deathlink_toggle,
self.on_orb_trade)
await asyncio.sleep(0.1)


async def run_game(ctx: JakAndDaxterContext):

# These may already be running. If they are not running, try to start them.
gk_running = False
try:
pymem.Pymem("gk.exe") # The GOAL Kernel
gk_running = True
except ProcessNotFound:
logger.info("Game not running, attempting to start.")

goalc_running = False
try:
pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL
goalc_running = True
except ProcessNotFound:
logger.info("Compiler not running, attempting to start.")

# Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl".
# TODO - Support other OS's. cmd for some reason does not work with goalc. Pymem is Windows-only.
if not gk_running:
try:
gk_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"]
gk_path = os.path.normpath(gk_path)
gk_path = os.path.join(gk_path, "gk.exe")
except AttributeError as e:
logger.error(f"Hosts.yaml does not contain {e.args[0]}, unable to locate game executables.")
return

if gk_path:
# Prefixing ampersand and wrapping gk_path in quotes is necessary for paths with spaces in them.
gk_process = subprocess.Popen(
["powershell.exe", f"& \"{gk_path}\"", "--game jak1", "--", "-v", "-boot", "-fakeiso", "-debug"],
creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability.

if not goalc_running:
try:
goalc_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"]
goalc_path = os.path.normpath(goalc_path)
goalc_path = os.path.join(goalc_path, "goalc.exe")
except AttributeError as e:
logger.error(f"Hosts.yaml does not contain {e.args[0]}, unable to locate game executables.")
return

if goalc_path:
# Prefixing ampersand and wrapping goalc_path in quotes is necessary for paths with spaces in them.
goalc_process = subprocess.Popen(
["powershell.exe", f"& \"{goalc_path}\"", "--game jak1"],
creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability.

# 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()
8 changes: 8 additions & 0 deletions worlds/jakanddaxter/GameID.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# All Jak And Daxter Archipelago IDs must be offset by this number.
jak1_id = 741000000

# This is maximum ID we will allow.
jak1_max = jak1_id + 999999

# The name of the game.
jak1_name = "Jak and Daxter The Precursor Legacy"
Loading