Skip to content

Commit 65f17c0

Browse files
committed
Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into client-cleanup
2 parents 4e69a72 + 8bb2364 commit 65f17c0

112 files changed

Lines changed: 2259 additions & 947 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ on:
1919

2020
env:
2121
ENEMIZER_VERSION: 7.1
22-
APPIMAGETOOL_VERSION: 13
22+
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
23+
# we check the sha256 and require manual intervention if it was updated.
24+
APPIMAGETOOL_VERSION: continuous
25+
APPIMAGETOOL_X86_64_HASH: '363dafac070b65cc36ca024b74db1f043c6f5cd7be8fca760e190dce0d18d684'
26+
APPIMAGE_RUNTIME_VERSION: continuous
27+
APPIMAGE_RUNTIME_X86_64_HASH: 'e3c4dfb70eddf42e7e5a1d28dff396d30563aa9a901970aebe6f01f3fecf9f8e'
2328

2429
permissions: # permissions required for attestation
2530
id-token: 'write'
@@ -134,10 +139,13 @@ jobs:
134139
- name: Install build-time dependencies
135140
run: |
136141
echo "PYTHON=python3.12" >> $GITHUB_ENV
137-
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
142+
wget -nv https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
143+
echo "$APPIMAGETOOL_X86_64_HASH appimagetool-x86_64.AppImage" | sha256sum -c
144+
wget -nv https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-x86_64
145+
echo "$APPIMAGE_RUNTIME_X86_64_HASH runtime-x86_64" | sha256sum -c
138146
chmod a+rx appimagetool-x86_64.AppImage
139147
./appimagetool-x86_64.AppImage --appimage-extract
140-
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
148+
echo -e '#/bin/sh\n./squashfs-root/AppRun --runtime-file runtime-x86_64 "$@"' > appimagetool
141149
chmod a+rx appimagetool
142150
- name: Download run-time dependencies
143151
run: |

.github/workflows/release.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ on:
99

1010
env:
1111
ENEMIZER_VERSION: 7.1
12-
APPIMAGETOOL_VERSION: 13
12+
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
13+
# we check the sha256 and require manual intervention if it was updated.
14+
APPIMAGETOOL_VERSION: continuous
15+
APPIMAGETOOL_X86_64_HASH: '363dafac070b65cc36ca024b74db1f043c6f5cd7be8fca760e190dce0d18d684'
16+
APPIMAGE_RUNTIME_VERSION: continuous
17+
APPIMAGE_RUNTIME_X86_64_HASH: 'e3c4dfb70eddf42e7e5a1d28dff396d30563aa9a901970aebe6f01f3fecf9f8e'
1318

1419
permissions: # permissions required for attestation
1520
id-token: 'write'
@@ -122,10 +127,13 @@ jobs:
122127
- name: Install build-time dependencies
123128
run: |
124129
echo "PYTHON=python3.12" >> $GITHUB_ENV
125-
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
130+
wget -nv https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
131+
echo "$APPIMAGETOOL_X86_64_HASH appimagetool-x86_64.AppImage" | sha256sum -c
132+
wget -nv https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-x86_64
133+
echo "$APPIMAGE_RUNTIME_X86_64_HASH runtime-x86_64" | sha256sum -c
126134
chmod a+rx appimagetool-x86_64.AppImage
127135
./appimagetool-x86_64.AppImage --appimage-extract
128-
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
136+
echo -e '#/bin/sh\n./squashfs-root/AppRun --runtime-file runtime-x86_64 "$@"' > appimagetool
129137
chmod a+rx appimagetool
130138
- name: Download run-time dependencies
131139
run: |

.github/workflows/unittests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@ on:
88
paths:
99
- '**'
1010
- '!docs/**'
11+
- '!deploy/**'
1112
- '!setup.py'
13+
- '!Dockerfile'
1214
- '!*.iss'
1315
- '!.gitignore'
16+
- '!.dockerignore'
1417
- '!.github/workflows/**'
1518
- '.github/workflows/unittests.yml'
1619
pull_request:
1720
paths:
1821
- '**'
1922
- '!docs/**'
23+
- '!deploy/**'
2024
- '!setup.py'
25+
- '!Dockerfile'
2126
- '!*.iss'
2227
- '!.gitignore'
28+
- '!.dockerignore'
2329
- '!.github/workflows/**'
2430
- '.github/workflows/unittests.yml'
2531

BaseClasses.py

Lines changed: 132 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import secrets
88
import warnings
99
from argparse import Namespace
10-
from collections import Counter, deque
10+
from collections import Counter, deque, defaultdict
1111
from collections.abc import Collection, MutableSequence
1212
from enum import IntEnum, IntFlag
1313
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple,
14-
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
14+
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING, Literal, overload)
1515
import dataclasses
1616

1717
from typing_extensions import NotRequired, TypedDict
@@ -183,7 +183,7 @@ def set_player_attr(attr: str, val) -> None:
183183
set_player_attr('completion_condition', lambda state: True)
184184
self.worlds = {}
185185
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
186-
"world's random object instead (usually self.random)")
186+
"world's random object instead (usually self.random)", True)
187187
self.plando_options = PlandoOptions.none
188188

189189
def get_all_ids(self) -> Tuple[int, ...]:
@@ -585,26 +585,9 @@ def can_beat_game(self,
585585
if self.has_beaten_game(state):
586586
return True
587587

588-
base_locations = self.get_locations() if locations is None else locations
589-
prog_locations = {location for location in base_locations if location.item
590-
and location.item.advancement and location not in state.locations_checked}
591-
592-
while prog_locations:
593-
sphere: Set[Location] = set()
594-
# build up spheres of collection radius.
595-
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
596-
for location in prog_locations:
597-
if location.can_reach(state):
598-
sphere.add(location)
599-
600-
if not sphere:
601-
# ran out of places and did not finish yet, quit
602-
return False
603-
604-
for location in sphere:
605-
state.collect(location.item, True, location)
606-
prog_locations -= sphere
607-
588+
for _ in state.sweep_for_advancements(locations,
589+
yield_each_sweep=True,
590+
checked_locations=state.locations_checked):
608591
if self.has_beaten_game(state):
609592
return True
610593

@@ -889,20 +872,133 @@ def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> No
889872
"Please switch over to sweep_for_advancements.")
890873
return self.sweep_for_advancements(locations)
891874

892-
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
875+
def _sweep_for_advancements_impl(self, advancements_per_player: List[Tuple[int, List[Location]]],
876+
yield_each_sweep: bool) -> Iterator[None]:
877+
"""
878+
The implementation for sweep_for_advancements is separated here because it returns a generator due to the use
879+
of a yield statement.
880+
"""
881+
all_players = {player for player, _ in advancements_per_player}
882+
players_to_check = all_players
883+
# As an optimization, it is assumed that each player's world only logically depends on itself. However, worlds
884+
# are allowed to logically depend on other worlds, so once there are no more players that should be checked
885+
# under this assumption, an extra sweep iteration is performed that checks every player, to confirm that the
886+
# sweep is finished.
887+
checking_if_finished = False
888+
while players_to_check:
889+
next_advancements_per_player: List[Tuple[int, List[Location]]] = []
890+
next_players_to_check = set()
891+
892+
for player, locations in advancements_per_player:
893+
if player not in players_to_check:
894+
next_advancements_per_player.append((player, locations))
895+
continue
896+
897+
# Accessibility of each location is checked first because a player's region accessibility cache becomes
898+
# stale whenever one of their own items is collected into the state.
899+
reachable_locations: List[Location] = []
900+
unreachable_locations: List[Location] = []
901+
for location in locations:
902+
if location.can_reach(self):
903+
# Locations containing items that do not belong to `player` could be collected immediately
904+
# because they won't stale `player`'s region accessibility cache, but, for simplicity, all the
905+
# items at reachable locations are collected in a single loop.
906+
reachable_locations.append(location)
907+
else:
908+
unreachable_locations.append(location)
909+
if unreachable_locations:
910+
next_advancements_per_player.append((player, unreachable_locations))
911+
912+
# A previous player's locations processed in the current `while players_to_check` iteration could have
913+
# collected items belonging to `player`, but now that all of `player`'s reachable locations have been
914+
# found, it can be assumed that `player` will not gain any more reachable locations until another one of
915+
# their items is collected.
916+
# It would be clearer to not add players to `next_players_to_check` in the first place if they have yet
917+
# to be processed in the current `while players_to_check` iteration, but checking if a player should be
918+
# added to `next_players_to_check` would need to be run once for every item that is collected, so it is
919+
# more performant to instead discard `player` from `next_players_to_check` once their locations have
920+
# been processed.
921+
next_players_to_check.discard(player)
922+
923+
# Collect the items from the reachable locations.
924+
for advancement in reachable_locations:
925+
self.advancements.add(advancement)
926+
item = advancement.item
927+
assert isinstance(item, Item), "tried to collect advancement Location with no Item"
928+
if self.collect(item, True, advancement):
929+
# The player the item belongs to may be able to reach additional locations in the next sweep
930+
# iteration.
931+
next_players_to_check.add(item.player)
932+
933+
if not next_players_to_check:
934+
if not checking_if_finished:
935+
# It is assumed that each player's world only logically depends on itself, which may not be the
936+
# case, so confirm that the sweep is finished by doing an extra iteration that checks every player.
937+
checking_if_finished = True
938+
next_players_to_check = all_players
939+
else:
940+
checking_if_finished = False
941+
942+
players_to_check = next_players_to_check
943+
advancements_per_player = next_advancements_per_player
944+
945+
if yield_each_sweep:
946+
yield
947+
948+
@overload
949+
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None, *,
950+
yield_each_sweep: Literal[True],
951+
checked_locations: Optional[Set[Location]] = None) -> Iterator[None]: ...
952+
953+
@overload
954+
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None,
955+
yield_each_sweep: Literal[False] = False,
956+
checked_locations: Optional[Set[Location]] = None) -> None: ...
957+
958+
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None, yield_each_sweep: bool = False,
959+
checked_locations: Optional[Set[Location]] = None) -> Optional[Iterator[None]]:
960+
"""
961+
Sweep through the locations that contain uncollected advancement items, collecting the items into the state
962+
until there are no more reachable locations that contain uncollected advancement items.
963+
964+
:param locations: The locations to sweep through, defaulting to all locations in the multiworld.
965+
:param yield_each_sweep: When True, return a generator that yields at the end of each sweep iteration.
966+
:param checked_locations: Optional override of locations to filter out from the locations argument, defaults to
967+
self.advancements when None.
968+
"""
969+
if checked_locations is None:
970+
checked_locations = self.advancements
971+
972+
# Since the sweep loop usually performs many iterations, the locations are filtered in advance.
973+
# A list of tuples is used, instead of a dictionary, because it is faster to iterate.
974+
advancements_per_player: List[Tuple[int, List[Location]]]
893975
if locations is None:
894-
locations = self.multiworld.get_filled_locations()
895-
reachable_advancements = True
896-
# since the loop has a good chance to run more than once, only filter the advancements once
897-
locations = {location for location in locations if location.advancement and location not in self.advancements}
898-
899-
while reachable_advancements:
900-
reachable_advancements = {location for location in locations if location.can_reach(self)}
901-
locations -= reachable_advancements
902-
for advancement in reachable_advancements:
903-
self.advancements.add(advancement)
904-
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
905-
self.collect(advancement.item, True, advancement)
976+
# `location.advancement` can only be True for filled locations, so unfilled locations are filtered out.
977+
advancements_per_player = []
978+
for player, locations_dict in self.multiworld.regions.location_cache.items():
979+
filtered_locations = [location for location in locations_dict.values()
980+
if location.advancement and location not in checked_locations]
981+
if filtered_locations:
982+
advancements_per_player.append((player, filtered_locations))
983+
else:
984+
# Filter and separate the locations into a list for each player.
985+
advancements_per_player_dict: Dict[int, List[Location]] = defaultdict(list)
986+
for location in locations:
987+
if location.advancement and location not in checked_locations:
988+
advancements_per_player_dict[location.player].append(location)
989+
# Convert to a list of tuples.
990+
advancements_per_player = list(advancements_per_player_dict.items())
991+
del advancements_per_player_dict
992+
993+
if yield_each_sweep:
994+
# Return a generator that will yield at the end of each sweep iteration.
995+
return self._sweep_for_advancements_impl(advancements_per_player, True)
996+
else:
997+
# Create the generator, but tell it not to yield anything, so it will run to completion in zero iterations
998+
# once started, then start and exhaust the generator by attempting to iterate it.
999+
for _ in self._sweep_for_advancements_impl(advancements_per_player, False):
1000+
assert False, "Generator yielded when it should have run to completion without yielding"
1001+
return None
9061002

9071003
# item name related
9081004
def has(self, item: str, player: int, count: int = 1) -> bool:

0 commit comments

Comments
 (0)