Skip to content

Commit a86afab

Browse files
committed
Merge branch 'main' into smw-main
2 parents 6e0fa4d + 7406a1e commit a86afab

File tree

103 files changed

+1734
-1453
lines changed

Some content is hidden

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

103 files changed

+1734
-1453
lines changed

AdventureClient.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,12 @@ def on_package(self, cmd: str, args: dict):
115115
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
116116
self._set_message(msg, SYSTEM_MESSAGE_ID)
117117
elif cmd == "Retrieved":
118-
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
119-
if self.freeincarnates_used is None:
120-
self.freeincarnates_used = 0
121-
self.freeincarnates_used += self.freeincarnate_pending
122-
self.send_pending_freeincarnates()
118+
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
119+
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
120+
if self.freeincarnates_used is None:
121+
self.freeincarnates_used = 0
122+
self.freeincarnates_used += self.freeincarnate_pending
123+
self.send_pending_freeincarnates()
123124
elif cmd == "SetReply":
124125
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
125126
self.freeincarnates_used = args["value"]

BaseClasses.py

+33-21
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,20 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio
252252
range(1, self.players + 1)}
253253

254254
def set_options(self, args: Namespace) -> None:
255+
# TODO - remove this section once all worlds use options dataclasses
256+
all_keys: Set[str] = {key for player in self.player_ids for key in
257+
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
258+
for option_key in all_keys:
259+
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
260+
f"Please use `self.options.{option_key}` instead.")
261+
option.update(getattr(args, option_key, {}))
262+
setattr(self, option_key, option)
263+
255264
for player in self.player_ids:
256265
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
257266
self.worlds[player] = world_type(self, player)
258267
self.worlds[player].random = self.per_slot_randoms[player]
259-
for option_key in world_type.options_dataclass.type_hints:
260-
option_values = getattr(args, option_key, {})
261-
setattr(self, option_key, option_values)
262-
# TODO - remove this loop once all worlds use options dataclasses
263-
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
268+
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
264269
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
265270
for option_key in options_dataclass.type_hints})
266271

@@ -491,7 +496,7 @@ def has_beaten_game(self, state: CollectionState, player: Optional[int] = None)
491496
else:
492497
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
493498

494-
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
499+
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
495500
if starting_state:
496501
if self.has_beaten_game(starting_state):
497502
return True
@@ -504,7 +509,7 @@ def can_beat_game(self, starting_state: Optional[CollectionState] = None):
504509
and location.item.advancement and location not in state.locations_checked}
505510

506511
while prog_locations:
507-
sphere = set()
512+
sphere: Set[Location] = set()
508513
# build up spheres of collection radius.
509514
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
510515
for location in prog_locations:
@@ -524,12 +529,19 @@ def can_beat_game(self, starting_state: Optional[CollectionState] = None):
524529

525530
return False
526531

527-
def get_spheres(self):
532+
def get_spheres(self) -> Iterator[Set[Location]]:
533+
"""
534+
yields a set of locations for each logical sphere
535+
536+
If there are unreachable locations, the last sphere of reachable
537+
locations is followed by an empty set, and then a set of all of the
538+
unreachable locations.
539+
"""
528540
state = CollectionState(self)
529541
locations = set(self.get_filled_locations())
530542

531543
while locations:
532-
sphere = set()
544+
sphere: Set[Location] = set()
533545

534546
for location in locations:
535547
if location.can_reach(state):
@@ -639,34 +651,34 @@ def __init__(self, parent: MultiWorld):
639651

640652
def update_reachable_regions(self, player: int):
641653
self.stale[player] = False
642-
rrp = self.reachable_regions[player]
643-
bc = self.blocked_connections[player]
654+
reachable_regions = self.reachable_regions[player]
655+
blocked_connections = self.blocked_connections[player]
644656
queue = deque(self.blocked_connections[player])
645-
start = self.multiworld.get_region('Menu', player)
657+
start = self.multiworld.get_region("Menu", player)
646658

647659
# init on first call - this can't be done on construction since the regions don't exist yet
648-
if start not in rrp:
649-
rrp.add(start)
650-
bc.update(start.exits)
660+
if start not in reachable_regions:
661+
reachable_regions.add(start)
662+
blocked_connections.update(start.exits)
651663
queue.extend(start.exits)
652664

653665
# run BFS on all connections, and keep track of those blocked by missing items
654666
while queue:
655667
connection = queue.popleft()
656668
new_region = connection.connected_region
657-
if new_region in rrp:
658-
bc.remove(connection)
669+
if new_region in reachable_regions:
670+
blocked_connections.remove(connection)
659671
elif connection.can_reach(self):
660672
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
661-
rrp.add(new_region)
662-
bc.remove(connection)
663-
bc.update(new_region.exits)
673+
reachable_regions.add(new_region)
674+
blocked_connections.remove(connection)
675+
blocked_connections.update(new_region.exits)
664676
queue.extend(new_region.exits)
665677
self.path[new_region] = (new_region.name, self.path.get(connection, None))
666678

667679
# Retry connections if the new region can unblock them
668680
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
669-
if new_entrance in bc and new_entrance not in queue:
681+
if new_entrance in blocked_connections and new_entrance not in queue:
670682
queue.append(new_entrance)
671683

672684
def copy(self) -> CollectionState:

Fill.py

+20-21
Original file line numberDiff line numberDiff line change
@@ -550,36 +550,36 @@ def flood_items(world: MultiWorld) -> None:
550550
break
551551

552552

553-
def balance_multiworld_progression(world: MultiWorld) -> None:
553+
def balance_multiworld_progression(multiworld: MultiWorld) -> None:
554554
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
555555
# Overall progression balancing algorithm:
556556
# Gather up all locations in a sphere.
557557
# Define a threshold value based on the player with the most available locations.
558558
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
559559
# which gives more locations available by this sphere.
560560
balanceable_players: typing.Dict[int, float] = {
561-
player: world.worlds[player].options.progression_balancing / 100
562-
for player in world.player_ids
563-
if world.worlds[player].options.progression_balancing > 0
561+
player: multiworld.worlds[player].options.progression_balancing / 100
562+
for player in multiworld.player_ids
563+
if multiworld.worlds[player].options.progression_balancing > 0
564564
}
565565
if not balanceable_players:
566566
logging.info('Skipping multiworld progression balancing.')
567567
else:
568568
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
569569
logging.debug(balanceable_players)
570-
state: CollectionState = CollectionState(world)
570+
state: CollectionState = CollectionState(multiworld)
571571
checked_locations: typing.Set[Location] = set()
572-
unchecked_locations: typing.Set[Location] = set(world.get_locations())
572+
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
573573

574574
total_locations_count: typing.Counter[int] = Counter(
575575
location.player
576-
for location in world.get_locations()
576+
for location in multiworld.get_locations()
577577
if not location.locked
578578
)
579579
reachable_locations_count: typing.Dict[int, int] = {
580580
player: 0
581-
for player in world.player_ids
582-
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
581+
for player in multiworld.player_ids
582+
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
583583
}
584584
balanceable_players = {
585585
player: balanceable_players[player]
@@ -658,7 +658,7 @@ def item_percentage(player: int, num: int) -> float:
658658
balancing_unchecked_locations.remove(location)
659659
if not location.locked:
660660
balancing_reachables[location.player] += 1
661-
if world.has_beaten_game(balancing_state) or all(
661+
if multiworld.has_beaten_game(balancing_state) or all(
662662
item_percentage(player, reachables) >= threshold_percentages[player]
663663
for player, reachables in balancing_reachables.items()
664664
if player in threshold_percentages):
@@ -675,7 +675,7 @@ def item_percentage(player: int, num: int) -> float:
675675
locations_to_test = unlocked_locations[player]
676676
items_to_test = list(candidate_items[player])
677677
items_to_test.sort()
678-
world.random.shuffle(items_to_test)
678+
multiworld.random.shuffle(items_to_test)
679679
while items_to_test:
680680
testing = items_to_test.pop()
681681
reducing_state = state.copy()
@@ -687,42 +687,41 @@ def item_percentage(player: int, num: int) -> float:
687687

688688
reducing_state.sweep_for_events(locations=locations_to_test)
689689

690-
if world.has_beaten_game(balancing_state):
691-
if not world.has_beaten_game(reducing_state):
690+
if multiworld.has_beaten_game(balancing_state):
691+
if not multiworld.has_beaten_game(reducing_state):
692692
items_to_replace.append(testing)
693693
else:
694694
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
695695
p = item_percentage(player, reachable_locations_count[player] + len(reduced_sphere))
696696
if p < threshold_percentages[player]:
697697
items_to_replace.append(testing)
698698

699-
replaced_items = False
699+
old_moved_item_count = moved_item_count
700700

701701
# sort then shuffle to maintain deterministic behaviour,
702702
# while allowing use of set for better algorithm growth behaviour elsewhere
703703
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
704-
world.random.shuffle(replacement_locations)
704+
multiworld.random.shuffle(replacement_locations)
705705
items_to_replace.sort()
706-
world.random.shuffle(items_to_replace)
706+
multiworld.random.shuffle(items_to_replace)
707707

708708
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
709709
while replacement_locations and items_to_replace:
710710
old_location = items_to_replace.pop()
711-
for new_location in replacement_locations:
711+
for i, new_location in enumerate(replacement_locations):
712712
if new_location.can_fill(state, old_location.item, False) and \
713713
old_location.can_fill(state, new_location.item, False):
714-
replacement_locations.remove(new_location)
714+
replacement_locations.pop(i)
715715
swap_location_item(old_location, new_location)
716716
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
717717
f"displacing {old_location.item} into {old_location}")
718718
moved_item_count += 1
719719
state.collect(new_location.item, True, new_location)
720-
replaced_items = True
721720
break
722721
else:
723722
logging.warning(f"Could not Progression Balance {old_location.item}")
724723

725-
if replaced_items:
724+
if old_moved_item_count < moved_item_count:
726725
logging.debug(f"Moved {moved_item_count} items so far\n")
727726
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
728727
for location in get_sphere_locations(state, unlocked):
@@ -736,7 +735,7 @@ def item_percentage(player: int, num: int) -> float:
736735
state.collect(location.item, True, location)
737736
checked_locations |= sphere_locations
738737

739-
if world.has_beaten_game(state):
738+
if multiworld.has_beaten_game(state):
740739
break
741740
elif not sphere_locations:
742741
logging.warning("Progression Balancing ran out of paths.")

Main.py

+11
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
117117
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
118118
for _ in range(count):
119119
world.push_precollected(world.create_item(item_name, player))
120+
# remove from_pool items also from early items handling, as starting is plenty early.
121+
early = world.early_items[player].get(item_name, 0)
122+
if early:
123+
world.early_items[player][item_name] = max(0, early-count)
124+
remaining_count = count-early
125+
if remaining_count > 0:
126+
local_early = world.early_local_items[player].get(item_name, 0)
127+
if local_early:
128+
world.early_items[player][item_name] = max(0, local_early - remaining_count)
129+
del local_early
130+
del early
120131

121132
logger.info('Creating World.')
122133
AutoWorld.call_all(world, "create_regions")

Utils.py

+19
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,25 @@ def deprecate(message: str):
779779
import warnings
780780
warnings.warn(message)
781781

782+
783+
class DeprecateDict(dict):
784+
log_message: str
785+
should_error: bool
786+
787+
def __init__(self, message, error: bool = False) -> None:
788+
self.log_message = message
789+
self.should_error = error
790+
super().__init__()
791+
792+
def __getitem__(self, item: Any) -> Any:
793+
if self.should_error:
794+
deprecate(self.log_message)
795+
elif __debug__:
796+
import warnings
797+
warnings.warn(self.log_message)
798+
return super().__getitem__(item)
799+
800+
782801
def _extend_freeze_support() -> None:
783802
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
784803
# upstream issue: https://github.com/python/cpython/issues/76327

0 commit comments

Comments
 (0)