From b5d02be9ed12cb8f9764ab58c968b6fd871352fe Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 8 Mar 2025 19:05:08 -0500 Subject: [PATCH] March Refactors (#77) * Reorg imports, small fix to Rock Village movement. * Fix wait-on-title message never going to ready message. * Colorama init fix. * Swap trap list for a dictionary of trap weights. * The more laws, the less justice. * Quick readability update. * Have memory reader provide instructions for slow booting games. * Revert some things. --- worlds/jakanddaxter/Client.py | 132 +++++++++++---------- worlds/jakanddaxter/Options.py | 23 ++-- worlds/jakanddaxter/Rules.py | 2 + worlds/jakanddaxter/__init__.py | 62 +++++----- worlds/jakanddaxter/client/MemoryReader.py | 45 ++++--- worlds/jakanddaxter/client/ReplClient.py | 21 ++-- worlds/jakanddaxter/test/test_traps.py | 6 +- 7 files changed, 168 insertions(+), 123 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index c3e2ba9c62..81f71150e4 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,36 +1,39 @@ +# Python standard libraries +import asyncio +import json import logging import os -import sys -import json import subprocess -from logging import Logger -from datetime import datetime +import sys -import colorama - -import asyncio from asyncio import Task +from datetime import datetime +from logging import Logger +from typing import Awaitable -from typing import Set, Awaitable - +# Misc imports +import colorama import pymem + from pymem.exception import ProcessNotFound -import Utils -from NetUtils import ClientStatus -from CommonClient import ClientCommandProcessor, CommonContext, server_loop, gui_enabled -from .Options import EnableOrbsanity - -from .GameID import jak1_name -from .client.ReplClient import JakAndDaxterReplClient -from .client.MemoryReader import JakAndDaxterMemoryReader - +# Archipelago imports import ModuleUpdate +import Utils + +from CommonClient import ClientCommandProcessor, CommonContext, server_loop, gui_enabled +from NetUtils import ClientStatus + +# Jak imports +from .GameID import jak1_name +from .Options import EnableOrbsanity +from .client.MemoryReader import JakAndDaxterMemoryReader +from .client.ReplClient import JakAndDaxterReplClient + + ModuleUpdate.update() - - logger = logging.getLogger("JakClient") -all_tasks: Set[Task] = set() +all_tasks: set[Task] = set() def create_task_log_exception(awaitable: Awaitable) -> asyncio.Task: @@ -141,7 +144,7 @@ class JakAndDaxterContext(CommonContext): orbsanity_bundle = 1 # Connected packet is unaware of starting inventory or if player is returning to an existing game. - # Set initial item count to 0 if it hasn't been set higher by a ReceivedItems packet yet. + # Set initial_item_count to 0, see below comments for more info. if not self.repl.received_initial_items and self.repl.initial_item_count < 0: self.repl.initial_item_count = 0 @@ -178,11 +181,15 @@ class JakAndDaxterContext(CommonContext): create_task_log_exception(self.repl.subtract_traded_orbs(orbs_traded)) if cmd == "ReceivedItems": - if not self.repl.received_initial_items: - # ReceivedItems packet should set the initial item count to > 0, even if already set to 0 by the - # Connected packet. Then we should tell the game to update the title screen, telling the player - # to wait while we process the initial items. This is skipped if no initial items are sent. + # If you have a starting inventory or are returning to a game where you have items, a ReceivedItems will be + # in the same network packet as Connected. This guarantees it is the first of any ReceivedItems we process. + # In this case, we should set the initial_item_count to > 0, even if already set to 0 by Connected, as well + # as the received_initial_items flag. Finally, use send_connection_status to tell the player to wait while + # we process the initial items. However, we will skip all this if there was no initial ReceivedItems and + # the REPL indicates it already handled any initial items (0 or otherwise). + if not self.repl.received_initial_items and not self.repl.processed_initial_items: + self.repl.received_initial_items = True self.repl.initial_item_count = len(args["items"]) create_task_log_exception(self.repl.send_connection_status("wait")) @@ -225,14 +232,14 @@ class JakAndDaxterContext(CommonContext): # Write to game display. self.repl.queue_game_text(my_item_name, my_item_finder, their_item_name, their_item_owner) + # 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. 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) + # We need to do a little more than just use CommonClient's on_deathlink. def on_deathlink(self, data: dict): if self.memr.deathlink_enabled: self.repl.received_deathlink = True @@ -245,6 +252,11 @@ class JakAndDaxterContext(CommonContext): def on_location_check(self, location_ids: list[int]): create_task_log_exception(self.ap_inform_location_check(location_ids)) + # TODO - Use CommonClient's check_locations function as our async task - AP 0.6.0 ONLY. + # def on_location_check(self, location_ids: list[int]): + # create_task_log_exception(self.check_locations(location_ids)) + + # CommonClient has no finished_game function, so we will have to craft our own. TODO - Update if that changes. async def ap_inform_finished_game(self): if not self.finished_game and self.memr.finished_game: message = [{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}] @@ -254,6 +266,7 @@ class JakAndDaxterContext(CommonContext): def on_finish_check(self): create_task_log_exception(self.ap_inform_finished_game()) + # We need to do a little more than just use CommonClient's send_death. 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" @@ -268,12 +281,11 @@ class JakAndDaxterContext(CommonContext): 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) - + # Use CommonClient's update_death_link function as our async task. def on_deathlink_toggle(self): - create_task_log_exception(self.ap_inform_deathlink_toggle()) + create_task_log_exception(self.update_death_link(self.memr.deathlink_enabled)) + # Orb trades are situations unique to Jak, so we have to craft our own function. async def ap_inform_orb_trade(self, orbs_changed: int): if self.memr.orbsanity_enabled: await self.send_msgs([{"cmd": "Set", @@ -286,32 +298,32 @@ class JakAndDaxterContext(CommonContext): def on_orb_trade(self, orbs_changed: int): create_task_log_exception(self.ap_inform_orb_trade(orbs_changed)) + def _markup_panels(self, msg: str, c: str = None): + color = self.jsontotextparser.color_codes[c] if c else None + message = f"[color={color}]{msg}[/color]" if c else msg + + self.ui.log_panels["Archipelago"].on_message_markup(message) + self.ui.log_panels["All"].on_message_markup(message) + def on_log_error(self, lg: Logger, message: str): lg.error(message) if self.ui: - color = self.jsontotextparser.color_codes["red"] - self.ui.log_panels["Archipelago"].on_message_markup(f"[color={color}]{message}[/color]") - self.ui.log_panels["All"].on_message_markup(f"[color={color}]{message}[/color]") + self._markup_panels(message, "red") def on_log_warn(self, lg: Logger, message: str): lg.warning(message) if self.ui: - color = self.jsontotextparser.color_codes["orange"] - self.ui.log_panels["Archipelago"].on_message_markup(f"[color={color}]{message}[/color]") - self.ui.log_panels["All"].on_message_markup(f"[color={color}]{message}[/color]") + self._markup_panels(message, "orange") def on_log_success(self, lg: Logger, message: str): lg.info(message) if self.ui: - color = self.jsontotextparser.color_codes["green"] - self.ui.log_panels["Archipelago"].on_message_markup(f"[color={color}]{message}[/color]") - self.ui.log_panels["All"].on_message_markup(f"[color={color}]{message}[/color]") + self._markup_panels(message, "green") def on_log_info(self, lg: Logger, message: str): lg.info(message) if self.ui: - self.ui.log_panels["Archipelago"].on_message_markup(f"{message}") - self.ui.log_panels["All"].on_message_markup(f"{message}") + self._markup_panels(message) async def run_repl_loop(self): while True: @@ -327,20 +339,21 @@ class JakAndDaxterContext(CommonContext): def find_root_directory(ctx: JakAndDaxterContext): # The path to this file is platform-dependent. - if sys.platform == "win32": + if Utils.is_windows: appdata = os.getenv("APPDATA") settings_path = os.path.normpath(f"{appdata}/OpenGOAL-Launcher/settings.json") - elif sys.platform == "linux": + elif Utils.is_linux: home = os.path.expanduser("~") settings_path = os.path.normpath(f"{home}/.config/OpenGOAL-Launcher/settings.json") - elif sys.platform == "darwin": - home = os.path.expanduser("~") # MacOS + elif Utils.is_macos: + home = os.path.expanduser("~") settings_path = os.path.normpath(f"{home}/Library/Application Support/OpenGOAL-Launcher/settings.json") else: ctx.on_log_error(logger, f"Unknown operating system: {sys.platform}!") return - # Boilerplate message that all error messages in this function should add at the end. + # Boilerplate messages that all error messages in this function should have. + err_title = "Unable to locate the ArchipelaGOAL install directory" alt_instructions = (f"Please verify that OpenGOAL and ArchipelaGOAL are installed properly. " f"If the problem persists, follow these steps:\n" f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" @@ -354,7 +367,7 @@ def find_root_directory(ctx: JakAndDaxterContext): f" Close all launchers, games, clients, and console windows, then restart Archipelago.") if not os.path.exists(settings_path): - msg = (f"Unable to locate the ArchipelaGOAL install directory: the OpenGOAL settings file does not exist.\n" + msg = (f"{err_title}: the OpenGOAL settings file does not exist.\n" f"{alt_instructions}") ctx.on_log_error(logger, msg) return @@ -364,16 +377,14 @@ def find_root_directory(ctx: JakAndDaxterContext): jak1_installed = load["games"]["Jak 1"]["isInstalled"] if not jak1_installed: - msg = (f"Unable to locate the ArchipelaGOAL install directory: " - f"The OpenGOAL Launcher is missing a normal install of Jak 1!\n" + msg = (f"{err_title}: The OpenGOAL Launcher is missing a normal install of Jak 1!\n" f"{alt_instructions}") ctx.on_log_error(logger, msg) return mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"] if mod_sources is None: - msg = (f"Unable to locate the ArchipelaGOAL install directory: " - f"No mod sources have been configured in the OpenGOAL Launcher!\n" + msg = (f"{err_title}: No mod sources have been configured in the OpenGOAL Launcher!\n" f"{alt_instructions}") ctx.on_log_error(logger, msg) return @@ -387,8 +398,7 @@ def find_root_directory(ctx: JakAndDaxterContext): archipelagoal_source = src # Using this file, we could verify the right version is installed, but we don't need to. if archipelagoal_source is None: - msg = (f"Unable to locate the ArchipelaGOAL install directory: " - f"The ArchipelaGOAL mod is not installed in the OpenGOAL Launcher!\n" + msg = (f"{err_title}: The ArchipelaGOAL mod is not installed in the OpenGOAL Launcher!\n" f"{alt_instructions}") ctx.on_log_error(logger, msg) return @@ -423,11 +433,12 @@ async def run_game(ctx: JakAndDaxterContext): ctx.on_log_warn(logger, "Compiler not running, attempting to start.") try: - auto_detect_root_directory = Utils.get_settings()["jakanddaxter_options"]["auto_detect_root_directory"] + settings = Utils.get_settings() + auto_detect_root_directory = settings["jakanddaxter_options"]["auto_detect_root_directory"] if auto_detect_root_directory: root_path = find_root_directory(ctx) else: - root_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] + root_path = settings["jakanddaxter_options"]["root_directory"] # Always trust your instincts... the user may not have entered their root_directory properly. # We don't have to do this check if the root directory was auto-detected. @@ -584,6 +595,7 @@ async def main(): def launch(): - colorama.init() + # use colorama to display colored text highlighting + colorama.just_fix_windows_console() asyncio.run(main()) colorama.deinit() diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/Options.py index fc0e4c66cb..55aa11271c 100644 --- a/worlds/jakanddaxter/Options.py +++ b/worlds/jakanddaxter/Options.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionSet +from functools import cached_property +from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionDict from .Items import trap_item_table @@ -194,13 +195,21 @@ class TrapEffectDuration(Range): default = 30 -class ChosenTraps(OptionSet): +# TODO - Revisit once ArchipelagoMW/Archipelago#3756 is merged. +class TrapWeights(OptionDict): """ - The list of traps that will be randomly added to the item pool. If the list is empty, no traps are created. + The list of traps and corresponding weights that will be randomly added to the item pool. A trap with weight 10 is + twice as likely to appear as a trap with weight 5. Set a weight to 0 to prevent that trap from appearing altogether. + If all weights are 0, no traps are created, overriding the values of "Filler * Replaced With Traps." """ - display_name = "Chosen Traps" - default = {trap for trap in trap_item_table.values()} - valid_keys = {trap for trap in trap_item_table.values()} + display_name = "Trap Weights" + default = {trap: 1 for trap in trap_item_table.values()} + valid_keys = sorted({trap for trap in trap_item_table.values()}) + + @cached_property + def weights_pair(self) -> tuple[list[str], list[int]]: + return (list(self.value.keys()), + list(max(0, v) for v in self.value.values())) class CompletionCondition(Choice): @@ -232,6 +241,6 @@ class JakAndDaxterOptions(PerGameCommonOptions): filler_power_cells_replaced_with_traps: FillerPowerCellsReplacedWithTraps filler_orb_bundles_replaced_with_traps: FillerOrbBundlesReplacedWithTraps trap_effect_duration: TrapEffectDuration - chosen_traps: ChosenTraps + trap_weights: TrapWeights jak_completion_condition: CompletionCondition start_inventory_from_pool: StartInventoryPool diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index be17b81259..437c2cca8c 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -177,6 +177,7 @@ def enforce_multiplayer_limits(world: JakAndDaxterWorld): raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n" f"Please adjust the following Options for a multiplayer game. \n" f"{friendly_message}" + f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n" f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. " f"(Use at your own risk!)") @@ -207,6 +208,7 @@ def enforce_singleplayer_limits(world: JakAndDaxterWorld): raise OptionError(f"The options you have chosen may result in seed generation failures. \n" f"Please adjust the following Options for a singleplayer game. \n" f"{friendly_message}" + f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n" f"Or set 'enforce_friendly_options' in your host.yaml to false. " f"(Use at your own risk!)") diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 9e88f6c9ec..cb0697b7b6 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,23 +1,22 @@ -import typing -from typing import Any, ClassVar, Callable +# Python standard libraries from math import ceil -import Utils -import settings -from Options import OptionGroup +from typing import Any, ClassVar, Callable, Union, cast +# Archipelago imports +import settings +import Utils + +from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths from BaseClasses import (Item, ItemClassification as ItemClass, Tutorial, CollectionState) +from Options import OptionGroup + +# Jak imports +from .Options import * from .GameID import jak1_id, jak1_name, jak1_max -from . import Options -from .Locations import (JakAndDaxterLocation, - location_table, - cell_location_table, - scout_location_table, - special_location_table, - cache_location_table, - orb_location_table) from .Items import (JakAndDaxterItem, item_table, cell_item_table, @@ -27,14 +26,19 @@ from .Items import (JakAndDaxterItem, orb_item_table, trap_item_table) from .Levels import level_table, level_table_with_global -from .regs.RegionBase import JakAndDaxterRegion +from .Locations import (JakAndDaxterLocation, + location_table, + cell_location_table, + scout_location_table, + special_location_table, + cache_location_table, + orb_location_table) from .locs import (CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials, OrbCacheLocations as Caches, OrbLocations as Orbs) -from worlds.AutoWorld import World, WebWorld -from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths +from .regs.RegionBase import JakAndDaxterRegion def launch_client(): @@ -70,8 +74,8 @@ class JakAndDaxterSettings(settings.Group): root_directory: RootDirectory = RootDirectory( "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal") # Don't ever change these type hints again. - auto_detect_root_directory: typing.Union[AutoDetectRootDirectory, bool] = True - enforce_friendly_options: typing.Union[EnforceFriendlyOptions, bool] = True + auto_detect_root_directory: Union[AutoDetectRootDirectory, bool] = True + enforce_friendly_options: Union[EnforceFriendlyOptions, bool] = True class JakAndDaxterWebWorld(WebWorld): @@ -107,7 +111,7 @@ class JakAndDaxterWebWorld(WebWorld): Options.FillerPowerCellsReplacedWithTraps, Options.FillerOrbBundlesReplacedWithTraps, Options.TrapEffectDuration, - Options.ChosenTraps, + Options.TrapWeights, ]), ] @@ -220,7 +224,7 @@ class JakAndDaxterWorld(World): total_trap_cells: int = 0 total_filler_cells: int = 0 power_cell_thresholds: list[int] = [] - chosen_traps: list[str] = [] + trap_weights: tuple[list[str], list[int]] = ({}, {}) # Handles various options validation, rules enforcement, and caching of important information. def generate_early(self) -> None: @@ -295,7 +299,7 @@ class JakAndDaxterWorld(World): else: self.options.filler_orb_bundles_replaced_with_traps.value = 0 - self.chosen_traps = list(self.options.chosen_traps.value) + self.trap_weights = self.options.trap_weights.weights_pair # Options drive which trade rules to use, so they need to be setup before we create_regions. from .Rules import set_orb_trade_rule @@ -388,20 +392,20 @@ class JakAndDaxterWorld(World): items_made += count # Handle Traps (for real). - # Manually fill the item pool with a random assortment of trap items, equal to the sum of - # total_trap_cells + total_trap_orb_bundles. Only do this if one or more traps have been selected. - if len(self.chosen_traps) > 0: + # Manually fill the item pool with a weighted assortment of trap items, equal to the sum of + # total_trap_cells + total_trap_orb_bundles. Only do this if one or more traps have weights > 0. + names, weights = self.trap_weights + if sum(weights) > 0: total_traps = self.total_trap_cells + self.total_trap_orb_bundles - for _ in range(total_traps): - trap_name = self.random.choice(self.chosen_traps) - self.multiworld.itempool.append(self.create_item(trap_name)) + trap_list = self.random.choices(names, weights=weights, k=total_traps) + self.multiworld.itempool += [self.create_item(trap_name) for trap_name in trap_list] items_made += total_traps # Handle Unfilled Locations. # Add an amount of filler items equal to the number of locations yet to be filled. # This is the final set of items we will add to the pool. all_regions = self.multiworld.get_regions(self.player) - total_locations = sum(reg.location_count for reg in typing.cast(list[JakAndDaxterRegion], all_regions)) + total_locations = sum(reg.location_count for reg in cast(list[JakAndDaxterRegion], all_regions)) total_filler = total_locations - items_made self.multiworld.itempool += [self.create_filler() for _ in range(total_filler)] @@ -482,7 +486,7 @@ class JakAndDaxterWorld(World): "filler_power_cells_replaced_with_traps", "filler_orb_bundles_replaced_with_traps", "trap_effect_duration", - "chosen_traps", + "trap_weights", "jak_completion_condition", "require_punch_for_klaww", ) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 2fa35e1c3c..ba8c513962 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -161,7 +161,7 @@ def autopsy(cause: int) -> str: class JakAndDaxterMemoryReader: marker: ByteString - goal_address = None + goal_address: int | None = None connected: bool = False initiated_connect: bool = False @@ -223,7 +223,6 @@ class JakAndDaxterMemoryReader: async def main_tick(self): if self.initiated_connect: await self.connect() - await self.verify_memory_version() self.initiated_connect = False if self.connected: @@ -243,7 +242,6 @@ class JakAndDaxterMemoryReader: else: return - # TODO - How drastic of a change is this, to wrap all of main_tick in a self.connected check? if self.connected: # Save some state variables temporarily. @@ -293,32 +291,47 @@ class JakAndDaxterMemoryReader: byteorder="little", signed=False) logger.debug("Found the archipelago memory address: " + str(self.goal_address)) - self.connected = True + await self.verify_memory_version() else: - self.log_error(logger, "Could not find the archipelago memory address!") + self.log_error(logger, "Could not find the Archipelago marker address!") self.connected = False async def verify_memory_version(self): - if not self.connected: - self.log_error(logger, "The Memory Reader is not connected!") + if self.goal_address is None: + self.log_error(logger, "Could not find the Archipelago memory address!") + self.connected = False + return memory_version: int | None = None try: memory_version = self.read_goal_address(memory_version_offset, sizeof_uint32) if memory_version == expected_memory_version: self.log_success(logger, "The Memory Reader is ready!") + self.connected = True else: raise MemoryReadError(memory_version_offset, sizeof_uint32) except (ProcessError, MemoryReadError, WinAPIError): - msg = (f"The OpenGOAL memory structure is incompatible with the current Archipelago client!\n" - f" Expected Version: {str(expected_memory_version)}\n" - f" Found Version: {str(memory_version)}\n" - f"Please follow these steps:\n" - f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" - f" Click Update (if one is available).\n" - f" Click Advanced > Compile. When this is done, click Continue.\n" - f" Click Versions and verify the latest version is marked 'Active'.\n" - f" Close all launchers, games, clients, and console windows, then restart Archipelago.") + if memory_version is None: + msg = (f"Could not find a version number in the OpenGOAL memory structure!\n" + f" Expected Version: {str(expected_memory_version)}\n" + f" Found Version: {str(memory_version)}\n" + f"Please follow these steps:\n" + f" If the game is running, try entering '/memr connect' in the client.\n" + f" You should see 'The Memory Reader is ready!'\n" + f" If that did not work, or the game is not running, run the OpenGOAL Launcher.\n" + f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Then click Advanced > Play in Debug Mode.\n" + f" Try entering '/memr connect' in the client again.") + else: + msg = (f"The OpenGOAL memory structure is incompatible with the current Archipelago client!\n" + f" Expected Version: {str(expected_memory_version)}\n" + f" Found Version: {str(memory_version)}\n" + f"Please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Click Update (if one is available).\n" + f" Click Advanced > Compile. When this is done, click Continue.\n" + f" Click Versions and verify the latest version is marked 'Active'.\n" + f" Close all launchers, games, clients, and console windows, then restart Archipelago.") self.log_error(logger, msg) self.connected = False diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 777b768e8b..41acf95652 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -58,7 +58,11 @@ class JakAndDaxterReplClient: initiated_connect: bool = False # Signals when user tells us to try reconnecting. received_deathlink: bool = False balanced_orbs: bool = False + + # Variables to handle the title screen and initial game connection. + initial_item_count = -1 # Brand new games have 0 items, so initialize this to -1. received_initial_items = False + processed_initial_items = False # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. @@ -67,7 +71,6 @@ class JakAndDaxterReplClient: item_inbox: dict[int, NetworkItem] = {} inbox_index = 0 - initial_item_count = -1 # New games have 0 items, so initialize this to -1. json_message_queue: Queue[JsonMessageData] = queue.Queue() # Logging callbacks @@ -127,19 +130,21 @@ class JakAndDaxterReplClient: else: return + # When connecting the game to the AP server on the title screen, we may be processing items from starting + # inventory or items received in an async game. Once we have caught up to the initial count, tell the player + # that we are ready to start. New items may even come in during the title screen, so if we go over the count, + # we should still send the ready signal. + if not self.processed_initial_items: + if self.inbox_index >= self.initial_item_count >= 0: + self.processed_initial_items = True + await self.send_connection_status("ready") + # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: await self.receive_item() await self.save_data() self.inbox_index += 1 - # When connecting the game to the AP server on the title screen, we may be processing items from starting - # inventory or items received in an async game. Once we are done, tell the player that we are ready to start. - if not self.received_initial_items and self.initial_item_count >= 0: - if self.inbox_index == self.initial_item_count: - self.received_initial_items = True - await self.send_connection_status("ready") - if self.received_deathlink: await self.receive_deathlink() self.received_deathlink = False diff --git a/worlds/jakanddaxter/test/test_traps.py b/worlds/jakanddaxter/test/test_traps.py index af6e1361a2..5087abadd8 100644 --- a/worlds/jakanddaxter/test/test_traps.py +++ b/worlds/jakanddaxter/test/test_traps.py @@ -6,7 +6,7 @@ class NoTrapsTest(JakAndDaxterTestBase): options = { "filler_power_cells_replaced_with_traps": 0, "filler_orb_bundles_replaced_with_traps": 0, - "chosen_traps": ["Trip Trap"] + "trap_weights": {"Trip Trap": 1}, } def test_trap_count(self): @@ -32,7 +32,7 @@ class SomeTrapsTest(JakAndDaxterTestBase): options = { "filler_power_cells_replaced_with_traps": 10, "filler_orb_bundles_replaced_with_traps": 10, - "chosen_traps": ["Trip Trap"] + "trap_weights": {"Trip Trap": 1}, } def test_trap_count(self): @@ -58,7 +58,7 @@ class MaximumTrapsTest(JakAndDaxterTestBase): options = { "filler_power_cells_replaced_with_traps": 100, "filler_orb_bundles_replaced_with_traps": 100, - "chosen_traps": ["Trip Trap"] + "trap_weights": {"Trip Trap": 1}, } def test_trap_count(self):