Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
787ddc4
Core: Bump version from 0.6.7 to 0.6.8 (#6114)
Berserker66 May 27, 2026
b24b3d1
Factorio: Locale changes (#5908)
Enderdraak May 30, 2026
d241829
Bump mistune from 3.2.0 to 3.2.1 in /WebHostLib (#6185)
dependabot[bot] May 31, 2026
65277a5
Muse Dash: Update to Touhou Mugakudan -V- (#6234)
DeamonHunter Jun 1, 2026
ad0af18
Rule Builder: Resolve empty and() to True_() instead of False_()(#6239)
Ishigh1 Jun 2, 2026
2a8ebe2
Core+RB: Make has_from_list, has_from_list_unique, has_group and has_…
Ishigh1 Jun 3, 2026
09aba95
Factorio: Fixed a needlessly small bug that does not affect gameplay …
Enderdraak Jun 3, 2026
5ccef98
WebHost: fix Authors on supported game page (#6218)
EmilyV99 Jun 7, 2026
e6e0bc3
WebHost: Config option for custom port ranges v2 (#6009)
ImUrX Jun 20, 2026
fde71f6
Core: typing and cleaning, mostly in `Fill.py` (#4459)
beauxq Jul 11, 2026
f9c5868
ModuleUpdate: Add core requirements.txt as a constraint file to pip c…
qwint Jul 11, 2026
f110a7e
Gauntlet Legends: Implement New Game (#3499)
jamesbrq Jul 11, 2026
c311685
WebHost: Bump mistune from 3.2.1 to 3.3.0 in /WebHostLib #6324
dependabot[bot] Jul 15, 2026
9c80dc6
Core: Allow running unit tests scoped to specific worlds (#6290)
gerbiljames Jul 17, 2026
6907d89
Core: fix Set import (#6235)
beauxq Jul 17, 2026
4efce42
Core: Adjust YAML error message to clarify the name is not the cause …
gerbiljames Jul 17, 2026
c6a7c0c
Docs: add guidance for choosing minimum/maximum_ap_version (#6265)
patrickwehbe Jul 17, 2026
13f1929
Core: Pre Output Steps Log (#6047)
g0goTAS Jul 17, 2026
feab54d
Core: Add parameter to visualize_regions to display Rule Builder expl…
silasary Jul 17, 2026
c8cf2c4
Launcher: Loading Screen (#6115)
Berserker66 Jul 18, 2026
ae7c560
Launcher: fix world import ordering conflict between #6115 and #6058
Berserker66 Jul 18, 2026
bbd5fc3
Launcher: Delete unused functions (#6332)
silasary Jul 18, 2026
6875f9d
RuleBuilder: Optimize Has("Item", 0) (#6228)
Ishigh1 Jul 18, 2026
2cde25f
Unittests: allow running unittests for specific worlds when using uni…
Silvris Jul 18, 2026
318a4ff
Build APWorlds: fix moved import (#6343)
qwint Jul 19, 2026
72dbfcc
SC2: Disabling UT as it doesn't work (#6346)
MatthewMarinets Jul 20, 2026
abb6f26
Tests: Force race mode off in test_generate_yaml (#6349)
Ars-Ignis Jul 30, 2026
a7abc86
Core: update platformdirs (#6328)
Berserker66 Jul 30, 2026
26792ef
The Messenger: Fix shop price not correctly handled by UT (#6340)
Jouramie Jul 30, 2026
b34b84a
Starcraft 2: item pool reduction overcounting fix (#6318)
Severencir Jul 30, 2026
18c3b19
Starcraft 2: Moving pool filter to use a counter instead of a dict (#…
MatthewMarinets Jul 30, 2026
53bc375
Core: remove per_slot_randoms and DeprecateDict (#6186)
Berserker66 Aug 3, 2026
26e4574
ALTTP: Reflowed headers in docs and explicitly added what client is u…
Quasky Aug 4, 2026
838d553
MultiServer: Update hint_points when hint cost or check points is cha…
duckboycool Aug 4, 2026
e759562
Setup: bump cx-Freeze to fix frozen linux shortcuts (#6365)
qwint Aug 4, 2026
8b3557c
The Messenger: Various small logic fix (#6337)
Jouramie Aug 4, 2026
f328ce7
Options: add typing and remove unnecessary deepcopy call in OptionCou…
qwint Aug 4, 2026
408954c
Factorio: Locale fixes (#6237)
Enderdraak Aug 5, 2026
d62772a
Core: Fix Open Patch (#6356)
Ishigh1 Aug 5, 2026
0f76d35
ALttP: Fix Randomzie Enemy Damage (#6296)
Alchav Aug 6, 2026
3359cac
CommonClient: Ignore invalid deathlink packets (#6376)
colin969 Aug 7, 2026
fe5b49e
Core: limit depth of received JSON to 16 (#6378)
black-sliver Aug 10, 2026
ce81860
Core: Add helper for unescape kivy strings + cleanup its usages (#6023)
qwint Aug 14, 2026
e3a6068
Jak and Daxter: Update client to use PME 2.0.x. (#6393)
massimilianodelliubaldini Aug 15, 2026
5b155bc
CommonClient: Match ServerLabel's team to other UIs (#5875)
qwint Aug 16, 2026
a0ca21b
HK: Various silly fixes (#6276)
qwint Aug 16, 2026
c6d9411
BizHawkClient: Fix compatibility for Lua 5.5+ (#6200)
Zunawe Aug 16, 2026
21e728c
CommonClient: reset session state when connecting to a different room…
gerbiljames Aug 16, 2026
bf52661
MultiServer: reject None targets on Bounce and validate types before …
SomeJakeGuy Aug 16, 2026
9ae40fd
MultiServer: Allow "and" chaining of Bounce packet conditions & add t…
NewSoupVi Aug 17, 2026
17f7532
The Witness: Universal Tracker support + Clean up which options actua…
NewSoupVi Aug 17, 2026
f2b2278
APQuest: Reimplement Dijkstra by hand (so that it is LLM-free again) …
NewSoupVi Aug 17, 2026
9c17dc5
The Messenger: Remove connection from Climb Shop to Rocket Maze Check…
Jouramie Aug 17, 2026
90b051a
Civ6: Update BOOST_TECH_CASTLES (#6270)
Gearwcu Aug 19, 2026
3ab4ab5
Stardew Valley: Rebalanced a dozen or so meme bundles (#6214)
agilbert1412 Aug 21, 2026
0c9a232
Tests: remove exception for LADX in test_itempool_not_modified (#6390)
threeandthreee Aug 21, 2026
eabced5
Stardew Valley: Fixed filler buffs only rolling one of each instead o…
agilbert1412 Aug 21, 2026
52a7637
KH2: Prevent the client from crashing if the player duplicated items …
shananas Aug 21, 2026
ca33ebc
Subnautica: Handle empty filler item weights with specific error (#6361)
Rooby-Roo Aug 22, 2026
f0c21fd
Core: Update PlandoConnection.Direction to use StrEnum (#5801)
duckboycool Aug 22, 2026
8d981a8
Satisfactory: Fix broken links on the Setup guide (#6110)
budak7273 Aug 22, 2026
4a3ac71
KH2: Make Lucky Emblems behave like bounties and keyblade stats (#6190)
shananas Aug 22, 2026
e4fd85d
Launcher: Show error message when world fails to load during install …
duckboycool Aug 22, 2026
1db7d33
Core: Fix loading of worlds installed in launcher (#6319)
duckboycool Aug 22, 2026
260b5cb
Stardew Valley: Reorganise option groups and visibility a little bit …
agilbert1412 Aug 22, 2026
4910875
Stardew Valley: Apworld Version To 7.4.25 (#6407)
agilbert1412 Aug 23, 2026
94a3981
stardew valley: use /explain from universal tracker instead of starde…
itepastra Aug 24, 2026
cf42061
Wargroove: Add item groups (#6254)
rosen200 Aug 25, 2026
63b8e81
Stardew Valley: Fixed Artifact spot logic (#6213)
agilbert1412 Aug 25, 2026
1d8a6a5
Test: flask duplicate route is now a ValueError (#6380)
black-sliver Aug 26, 2026
1e1efa3
Wargroove: actually import Items (#6422)
qwint Aug 30, 2026
5a5bcf4
Terraria: transfer maintainership to Desperandos (#6170)
Seldom-SE Sep 11, 2026
47a6d6d
BHC: don't override --connect when manifest has empty server (#6435)
qwint Sep 11, 2026
4cced3d
Tests: Remove unneeded world exclusions (#6396)
duckboycool Sep 11, 2026
e0b6083
MMBN3: Handle timeouts on dual stack networks by catching OSError (#6…
gerbiljames Sep 12, 2026
527f119
MultiServer: Correct Bounce Target logic, add unit tests (#6417)
NewSoupVi Sep 20, 2026
f16917f
The Wind Waker: fix sending already checked and non-existing location…
Berserker66 Sep 20, 2026
5b33339
Kingdom Hearts 2: fix sending already checked locations (#6411)
Berserker66 Sep 20, 2026
fe8489b
SM64: Disable default tests for flaky move rando options (#6457)
duckboycool Sep 20, 2026
90ea894
docs: apquest: make it clear that launcher components are optional (#…
BadMagic100 Sep 20, 2026
c745984
Launcher: fix for more terminal on linux (#6436)
Osiris32-and-a-half Sep 21, 2026
afeb1fb
Core: Add .apworlds to loading screen (#6460)
silasary Sep 22, 2026
2fb9bda
Rule Builder : Made AtLeast a dataclass (#6421)
Ishigh1 Sep 22, 2026
db1d856
Core: Restrictive fill in batches to reduce state sweeping costs (#3872)
Mysteryem Sep 22, 2026
5442df6
MultiServer, customserver: move data package handling, optimize RAM u…
black-sliver Sep 24, 2026
5b47bf9
Launcher: Fix initial position of "Search" text upon loading (#6456)
duckboycool Sep 26, 2026
dab9cc8
Merge branch 'main' into osrs-2.1.0
FarisTheAncient Oct 4, 2026
8e16bf7
Not needed for yamlless worlds (removing to hopefully get fewer peopl…
FarisTheAncient Oct 4, 2026
98e382d
This is the fix for the actual UT issue people reported
FarisTheAncient Oct 4, 2026
54b3541
i added this typing when trying to debug but i like it so we're keepi…
FarisTheAncient Oct 4, 2026
bd7ed42
it turns out i don't know my rights from my lefts, fixing this
FarisTheAncient Oct 4, 2026
c91d9a4
digi forgor this one, putting it here so gen passes
FarisTheAncient Oct 4, 2026
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
25 changes: 25 additions & 0 deletions .run/APQuest Tests.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="APQuest Tests" type="tests" factoryName="py.test">
<module name="Archipelago" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="AP_TEST_WORLDS" value="apquest" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="DEBUG_JUST_MY_CODE" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="RUN_TOOL" value="" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;$PROJECT_DIR$/worlds -q --continue-on-collection-errors&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/test&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>
22 changes: 13 additions & 9 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import warnings
from argparse import Namespace
from collections import Counter, deque, defaultdict
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, MutableSequence, Set
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, MutableSequence, Set as AbstractSet
from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, ClassVar, Dict, List, Literal, NamedTuple,
Optional, Protocol, Tuple, Union, TYPE_CHECKING, overload)
from typing import (Any, ClassVar, Dict, List, Literal, NamedTuple,
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING, overload)
import dataclasses

from typing_extensions import NotRequired, TypedDict
Expand Down Expand Up @@ -49,7 +49,7 @@ def __getattr__(self, name: str) -> Any:
return getattr(self.obj, name)
else:
raise RuntimeError("You are in a threaded context and global random state was removed for your safety. "
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
"Please use world.random or randomize ahead of output.")


class HasNameAndPlayer(Protocol):
Expand Down Expand Up @@ -100,8 +100,6 @@ class MultiWorld():
game: Dict[int, str]

random: random.Random
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
"""Deprecated. Please use `self.random` instead."""

class AttributeProxy():
def __init__(self, rule):
Expand Down Expand Up @@ -177,8 +175,6 @@ def set_player_attr(attr: str, val) -> None:
set_player_attr('game', "Archipelago")
set_player_attr('completion_condition', lambda state: True)
self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
"world's random object instead (usually self.random)", True)
self.plando_options = PlandoOptions.none

def get_all_ids(self) -> Tuple[int, ...]:
Expand Down Expand Up @@ -514,7 +510,7 @@ def register_indirect_condition(self, region: Region, entrance: Entrance):
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
self.indirect_connections.setdefault(region, set()).add(entrance)

def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
def get_locations(self, player: int | None = None) -> Collection[Location]:
if player is not None:
return self.regions.location_cache[player].values()
return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
Expand Down Expand Up @@ -1038,6 +1034,8 @@ def count(self, item: str, player: int) -> int:

def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
Expand All @@ -1049,6 +1047,8 @@ def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
Expand Down Expand Up @@ -1077,6 +1077,8 @@ def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
Expand All @@ -1089,6 +1091,8 @@ def has_group_unique(self, item_name_group: str, player: int, count: int = 1) ->
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
if count <= 0:
return True
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
Expand Down
39 changes: 36 additions & 3 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int])
"""Name used in Connect packet"""
seed_name: str | None
"""Seed name that will be validated on opening a socket if present"""
server_seed_name: str | None
"""Actual seed_name reported by the server in RoomInfo"""
connected_identity: tuple[str, int, int] | None
"""(server_seed_name, team, slot) of the last successful connection, used to detect a session switch"""

# locations
locations_checked: set[int]
Expand Down Expand Up @@ -399,6 +403,10 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing
self.slot = None
self.auth = None
self.seed_name = None
self.server_seed_name = None
self.connected_identity = None
# decouple from the class-level mutable set so tag changes (e.g. DeathLink) don't leak across instances
self.tags = set(type(self).tags)

self.locations_checked = set() # local state
self.locations_scouted = set()
Expand Down Expand Up @@ -459,6 +467,7 @@ def reset_server_state(self):
self.team = None
self.items_received = []
self.locations_info = {}
self.server_seed_name = None
self.server_version = Version(0, 0, 0)
self.generator_version = Version(0, 0, 0)
self.server = None
Expand All @@ -470,6 +479,21 @@ def reset_server_state(self):
"remaining": "disabled",
}

def reset_session_state(self):
"""Clear local progress and data-storage state tied to a single session."""
self.locations_checked = set()
self.locations_scouted = set()
self.locations_info = {}
self.items_received = []
self.missing_locations = set()
self.checked_locations = set()
self.server_locations = set()
self.finished_game = False
self.ready = False
self.stored_data = {}
self.stored_data_notification_keys = set()
self.current_energy_link_value = None

async def disconnect(self, allow_autoreconnect: bool = False):
if not allow_autoreconnect:
self.disconnected_intentionally = True
Expand Down Expand Up @@ -924,6 +948,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.exception(f"Could not get command from {args}")
raise
if cmd == 'RoomInfo':
ctx.server_seed_name = args["seed_name"]
if ctx.seed_name and ctx.seed_name != args["seed_name"]:
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
Expand Down Expand Up @@ -1004,6 +1029,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.username = ctx.auth
ctx.team = args["team"]
ctx.slot = args["slot"]
# on a switch to a different session, clear session state before stale checks/goal are replayed below
identity = (ctx.server_seed_name, ctx.team, ctx.slot)
if ctx.connected_identity is not None and identity != ctx.connected_identity:
ctx.reset_session_state()
ctx.connected_identity = identity
# int keys get lost in JSON transfer
ctx.slot_info = {0: NetworkSlot("Archipelago", "Archipelago", SlotType.player)}
ctx.slot_info.update({int(pid): data for pid, data in args["slot_info"].items()})
Expand Down Expand Up @@ -1092,9 +1122,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):

elif cmd == "Bounced":
tags = args.get("tags", [])
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been sent this
if "DeathLink" in tags:
data = args.get("data", {})
time = data.get("time")
if time is not None and ctx.last_death_link != time:
ctx.on_deathlink(args["data"])

elif cmd == "Retrieved":
ctx.stored_data.update(args["keys"])
Expand Down
Loading
Loading