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.
This commit is contained in:
massimilianodelliubaldini
2025-03-08 19:05:08 -05:00
committed by GitHub
parent e0e4926fbb
commit b5d02be9ed
7 changed files with 168 additions and 123 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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