Skip to content

Commit ba20012

Browse files
authored
Merge branch 'ArchipelagoMW:main' into HK-mrks-WebsiteTracker
2 parents 4203f16 + d6da3bc commit ba20012

File tree

125 files changed

+5934
-2441
lines changed

Some content is hidden

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

125 files changed

+5934
-2441
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
worlds/blasphemous/region_data.py linguist-generated=true
2+
worlds/yachtdice/YachtWeights.py linguist-generated=true

.github/workflows/build.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ env:
2424
jobs:
2525
# build-release-macos: # LF volunteer
2626

27-
build-win-py310: # RCs will still be built and signed by hand
27+
build-win: # RCs will still be built and signed by hand
2828
runs-on: windows-latest
2929
steps:
3030
- uses: actions/checkout@v4
3131
- name: Install python
3232
uses: actions/setup-python@v5
3333
with:
34-
python-version: '3.10'
34+
python-version: '3.12'
3535
- name: Download run-time dependencies
3636
run: |
3737
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
@@ -111,10 +111,10 @@ jobs:
111111
- name: Get a recent python
112112
uses: actions/setup-python@v5
113113
with:
114-
python-version: '3.11'
114+
python-version: '3.12'
115115
- name: Install build-time dependencies
116116
run: |
117-
echo "PYTHON=python3.11" >> $GITHUB_ENV
117+
echo "PYTHON=python3.12" >> $GITHUB_ENV
118118
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
119119
chmod a+rx appimagetool-x86_64.AppImage
120120
./appimagetool-x86_64.AppImage --appimage-extract

.github/workflows/release.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ jobs:
4444
- name: Get a recent python
4545
uses: actions/setup-python@v5
4646
with:
47-
python-version: '3.11'
47+
python-version: '3.12'
4848
- name: Install build-time dependencies
4949
run: |
50-
echo "PYTHON=python3.11" >> $GITHUB_ENV
50+
echo "PYTHON=python3.12" >> $GITHUB_ENV
5151
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
5252
chmod a+rx appimagetool-x86_64.AppImage
5353
./appimagetool-x86_64.AppImage --appimage-extract

BaseClasses.py

+72-14
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,49 @@ def get_spheres(self) -> Iterator[Set[Location]]:
604604
state.collect(location.item, True, location)
605605
locations -= sphere
606606

607+
def get_sendable_spheres(self) -> Iterator[Set[Location]]:
608+
"""
609+
yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere
610+
611+
If there are unreachable locations, the last sphere of reachable locations is followed by an empty set,
612+
and then a set of all of the unreachable locations.
613+
"""
614+
state = CollectionState(self)
615+
locations: Set[Location] = set()
616+
events: Set[Location] = set()
617+
for location in self.get_filled_locations():
618+
if type(location.item.code) is int:
619+
locations.add(location)
620+
else:
621+
events.add(location)
622+
623+
while locations:
624+
sphere: Set[Location] = set()
625+
626+
# cull events out
627+
done_events: Set[Union[Location, None]] = {None}
628+
while done_events:
629+
done_events = set()
630+
for event in events:
631+
if event.can_reach(state):
632+
state.collect(event.item, True, event)
633+
done_events.add(event)
634+
events -= done_events
635+
636+
for location in locations:
637+
if location.can_reach(state):
638+
sphere.add(location)
639+
640+
yield sphere
641+
if not sphere:
642+
if locations:
643+
yield locations # unreachable locations
644+
break
645+
646+
for location in sphere:
647+
state.collect(location.item, True, location)
648+
locations -= sphere
649+
607650
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
608651
"""Check if accessibility rules are fulfilled with current or supplied state."""
609652
if not state:
@@ -1110,7 +1153,7 @@ def create_exit(self, name: str) -> Entrance:
11101153
return exit_
11111154

11121155
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
1113-
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
1156+
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
11141157
"""
11151158
Connects current region to regions in exit dictionary. Passed region names must exist first.
11161159
@@ -1120,10 +1163,14 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
11201163
"""
11211164
if not isinstance(exits, Dict):
11221165
exits = dict.fromkeys(exits)
1123-
for connecting_region, name in exits.items():
1124-
self.connect(self.multiworld.get_region(connecting_region, self.player),
1125-
name,
1126-
rules[connecting_region] if rules and connecting_region in rules else None)
1166+
return [
1167+
self.connect(
1168+
self.multiworld.get_region(connecting_region, self.player),
1169+
name,
1170+
rules[connecting_region] if rules and connecting_region in rules else None,
1171+
)
1172+
for connecting_region, name in exits.items()
1173+
]
11271174

11281175
def __repr__(self):
11291176
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
@@ -1262,6 +1309,10 @@ def useful(self) -> bool:
12621309
def trap(self) -> bool:
12631310
return ItemClassification.trap in self.classification
12641311

1312+
@property
1313+
def filler(self) -> bool:
1314+
return not (self.advancement or self.useful or self.trap)
1315+
12651316
@property
12661317
def excludable(self) -> bool:
12671318
return not (self.advancement or self.useful)
@@ -1384,14 +1435,21 @@ def create_playthrough(self, create_paths: bool = True) -> None:
13841435

13851436
# second phase, sphere 0
13861437
removed_precollected: List[Item] = []
1387-
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
1388-
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
1389-
multiworld.precollected_items[item.player].remove(item)
1390-
multiworld.state.remove(item)
1391-
if not multiworld.can_beat_game():
1392-
multiworld.push_precollected(item)
1393-
else:
1394-
removed_precollected.append(item)
1438+
1439+
for precollected_items in multiworld.precollected_items.values():
1440+
# The list of items is mutated by removing one item at a time to determine if each item is required to beat
1441+
# the game, and re-adding that item if it was required, so a copy needs to be made before iterating.
1442+
for item in precollected_items.copy():
1443+
if not item.advancement:
1444+
continue
1445+
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
1446+
precollected_items.remove(item)
1447+
multiworld.state.remove(item)
1448+
if not multiworld.can_beat_game():
1449+
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
1450+
multiworld.push_precollected(item)
1451+
else:
1452+
removed_precollected.append(item)
13951453

13961454
# we are now down to just the required progress items in collection_spheres. Unfortunately
13971455
# the previous pruning stage could potentially have made certain items dependant on others
@@ -1530,7 +1588,7 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
15301588
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
15311589
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
15321590
if self.unreachables:
1533-
outfile.write('\n\nUnreachable Items:\n\n')
1591+
outfile.write('\n\nUnreachable Progression Items:\n\n')
15341592
outfile.write(
15351593
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
15361594

CommonClient.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from MultiServer import CommandProcessor
2525
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
26-
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
26+
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
2727
from Utils import Version, stream_input, async_start
2828
from worlds import network_data_package, AutoWorldRegister
2929
import os
@@ -412,6 +412,7 @@ async def disconnect(self, allow_autoreconnect: bool = False):
412412
await self.server.socket.close()
413413
if self.server_task is not None:
414414
await self.server_task
415+
self.ui.update_hints()
415416

416417
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
417418
""" `msgs` JSON serializable """
@@ -551,7 +552,14 @@ async def shutdown(self):
551552
await self.ui_task
552553
if self.input_task:
553554
self.input_task.cancel()
554-
555+
556+
# Hints
557+
def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None:
558+
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
559+
if status is not None:
560+
msg["status"] = status
561+
async_start(self.send_msgs([msg]), name="update_hint")
562+
555563
# DataPackage
556564
async def prepare_data_package(self, relevant_games: typing.Set[str],
557565
remote_date_package_versions: typing.Dict[str, int],

Fill.py

+43-16
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
3636
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
3737
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
3838
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
39-
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
39+
allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True,
40+
name: str = "Unknown") -> None:
4041
"""
4142
:param multiworld: Multiworld to be filled.
4243
:param base_state: State assumed before fill.
@@ -63,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
6364
placed = 0
6465

6566
while any(reachable_items.values()) and locations:
66-
# grab one item per player
67-
items_to_place = [items.pop()
68-
for items in reachable_items.values() if items]
67+
if one_item_per_player:
68+
# grab one item per player
69+
items_to_place = [items.pop()
70+
for items in reachable_items.values() if items]
71+
else:
72+
next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items])
73+
items_to_place = []
74+
if item_pool:
75+
items_to_place.append(reachable_items[next_player].pop())
76+
6977
for item in items_to_place:
7078
for p, pool_item in enumerate(item_pool):
7179
if pool_item is item:
7280
item_pool.pop(p)
7381
break
82+
7483
maximum_exploration_state = sweep_from_pool(
7584
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
7685
if single_player_placement else None)
@@ -480,7 +489,8 @@ def mark_for_locking(location: Location):
480489
if prioritylocations:
481490
# "priority fill"
482491
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
483-
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
492+
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
493+
name="Priority", one_item_per_player=False)
484494
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
485495
defaultlocations = prioritylocations + defaultlocations
486496

@@ -978,15 +988,32 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
978988
multiworld.random.shuffle(items)
979989
count = 0
980990
err: typing.List[str] = []
981-
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
991+
successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = []
992+
claimed_indices: typing.Set[typing.Optional[int]] = set()
982993
for item_name in items:
983-
item = multiworld.worlds[player].create_item(item_name)
994+
index_to_delete: typing.Optional[int] = None
995+
if from_pool:
996+
try:
997+
# If from_pool, try to find an existing item with this name & player in the itempool and use it
998+
index_to_delete, item = next(
999+
(i, item) for i, item in enumerate(multiworld.itempool)
1000+
if item.player == player and item.name == item_name and i not in claimed_indices
1001+
)
1002+
except StopIteration:
1003+
warn(
1004+
f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.",
1005+
placement['force'])
1006+
item = multiworld.worlds[player].create_item(item_name)
1007+
else:
1008+
item = multiworld.worlds[player].create_item(item_name)
1009+
9841010
for location in reversed(candidates):
9851011
if (location.address is None) == (item.code is None): # either both None or both not None
9861012
if not location.item:
9871013
if location.item_rule(item):
9881014
if location.can_fill(multiworld.state, item, False):
989-
successful_pairs.append((item, location))
1015+
successful_pairs.append((index_to_delete, item, location))
1016+
claimed_indices.add(index_to_delete)
9901017
candidates.remove(location)
9911018
count = count + 1
9921019
break
@@ -998,24 +1025,24 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
9981025
err.append(f"Cannot place {item_name} into already filled location {location}.")
9991026
else:
10001027
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
1028+
10011029
if count == maxcount:
10021030
break
10031031
if count < placement['count']['min']:
10041032
m = placement['count']['min']
10051033
failed(
10061034
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
10071035
placement['force'])
1008-
for (item, location) in successful_pairs:
1036+
1037+
# Sort indices in reverse so we can remove them one by one
1038+
successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True)
1039+
1040+
for (index, item, location) in successful_pairs:
10091041
multiworld.push_item(location, item, collect=False)
10101042
location.locked = True
10111043
logging.debug(f"Plando placed {item} at {location}")
1012-
if from_pool:
1013-
try:
1014-
multiworld.itempool.remove(item)
1015-
except ValueError:
1016-
warn(
1017-
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
1018-
placement['force'])
1044+
if index is not None: # If this item is from_pool and was found in the pool, remove it.
1045+
multiworld.itempool.pop(index)
10191046

10201047
except Exception as e:
10211048
raise Exception(

Launcher.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,8 @@ def launch(exe, in_terminal=False):
246246

247247

248248
def run_gui():
249-
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
249+
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
250250
from kivy.core.window import Window
251-
from kivy.uix.image import AsyncImage
252251
from kivy.uix.relativelayout import RelativeLayout
253252

254253
class Launcher(App):
@@ -281,8 +280,8 @@ def build_button(component: Component) -> Widget:
281280
button.component = component
282281
button.bind(on_release=self.component_action)
283282
if component.icon != "icon":
284-
image = AsyncImage(source=icon_paths[component.icon],
285-
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
283+
image = ApAsyncImage(source=icon_paths[component.icon],
284+
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
286285
box_layout = RelativeLayout(size_hint_y=None, height=40)
287286
box_layout.add_widget(button)
288287
box_layout.add_widget(image)

0 commit comments

Comments
 (0)