Skip to content

Commit

Permalink
Merge branch 'main' into more_door_panels
Browse files Browse the repository at this point in the history
  • Loading branch information
NewSoupVi authored Aug 19, 2024
2 parents 1342e3d + c4e7b6c commit 782a68f
Show file tree
Hide file tree
Showing 461 changed files with 33,578 additions and 16,415 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ venv/
ENV/
env.bak/
venv.bak/
.code-workspace
*.code-workspace
shell.nix

# Spyder project settings
Expand Down
146 changes: 107 additions & 39 deletions BaseClasses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

import copy
import collections
import itertools
import functools
import logging
Expand Down Expand Up @@ -63,7 +63,6 @@ class MultiWorld():
state: CollectionState

plando_options: PlandoOptions
accessibility: Dict[int, Options.Accessibility]
early_items: Dict[int, Dict[str, int]]
local_early_items: Dict[int, Dict[str, int]]
local_items: Dict[int, Options.LocalItems]
Expand Down Expand Up @@ -288,6 +287,86 @@ def set_item_links(self):
group["non_local_items"] = item_link["non_local_items"]
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]

def link_items(self) -> None:
"""Called to link together items in the itempool related to the registered item link groups."""
from worlds import AutoWorld

for group_id, group in self.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
]:
classifications: Dict[str, int] = collections.defaultdict(int)
counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in self.itempool:
if item.player in counters and item.name in shared_pool:
counters[item.player][item.name] += 1
classifications[item.name] |= item.classification

for player in players.copy():
if all([counters[player][item] == 0 for item in shared_pool]):
players.remove(player)
del (counters[player])

if not players:
return None, None

for item in shared_pool:
count = min(counters[player][item] for player in players)
if count:
for player in players:
counters[player][item] = count
else:
for player in players:
del (counters[player][item])
return counters, classifications

common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
if not common_item_count:
continue

new_itempool: List[Item] = []
for item_name, item_count in next(iter(common_item_count.values())).items():
for _ in range(item_count):
new_item = group["world"].create_item(item_name)
# mangle together all original classification bits
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)

region = Region("Menu", group_id, self, "ItemLink")
self.regions.append(region)
locations = region.locations
for item in self.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}",
None, region)
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
state.has(item_name, group_id_, count_)

locations.append(loc)
loc.place_locked_item(item)
common_item_count[item.player][item.name] -= 1
else:
new_itempool.append(item)

itemcount = len(self.itempool)
self.itempool = new_itempool

while itemcount > len(self.itempool):
items_to_add = []
for player in group["players"]:
if group["link_replacement"]:
item_player = group_id
else:
item_player = player
if group["replacement_items"][player]:
items_to_add.append(AutoWorld.call_single(self, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player))
self.random.shuffle(items_to_add)
self.itempool.extend(items_to_add[:itemcount - len(self.itempool)])

def secure(self):
self.random = ThreadBarrierProxy(secrets.SystemRandom())
self.is_race = True
Expand Down Expand Up @@ -462,9 +541,9 @@ def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> boo
return True
state = starting_state.copy()
else:
if self.has_beaten_game(self.state):
return True
state = CollectionState(self)
if self.has_beaten_game(state):
return True
prog_locations = {location for location in self.get_locations() if location.item
and location.item.advancement and location not in state.locations_checked}

Expand Down Expand Up @@ -523,26 +602,21 @@ def fulfills_accessibility(self, state: Optional[CollectionState] = None):
players: Dict[str, Set[int]] = {
"minimal": set(),
"items": set(),
"locations": set()
"full": set()
}
for player, access in self.accessibility.items():
players[access.current_key].add(player)
for player, world in self.worlds.items():
players[world.options.accessibility.current_key].add(player)

beatable_fulfilled = False

def location_condition(location: Location):
def location_condition(location: Location) -> bool:
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
if location.player in players["locations"] or (location.item and location.item.player not in
players["minimal"]):
return True
return False
return location.player in players["full"] or \
(location.item and location.item.player not in players["minimal"])

def location_relevant(location: Location):
def location_relevant(location: Location) -> bool:
"""Determine if this location is relevant to sweep."""
if location.progress_type != LocationProgressType.EXCLUDED \
and (location.player in players["locations"] or location.advancement):
return True
return False
return location.player in players["full"] or location.advancement

def all_done() -> bool:
"""Check if all access rules are fulfilled"""
Expand Down Expand Up @@ -643,14 +717,14 @@ def update_reachable_regions(self, player: int):

def copy(self) -> CollectionState:
ret = CollectionState(self.multiworld)
ret.prog_items = copy.deepcopy(self.prog_items)
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
self.reachable_regions}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
self.blocked_connections}
ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
ret.reachable_regions = {player: region_set.copy() for player, region_set in
self.reachable_regions.items()}
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
self.blocked_connections.items()}
ret.events = self.events.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
Expand Down Expand Up @@ -680,13 +754,13 @@ def can_reach_entrance(self, spot: str, player: int) -> bool:
def can_reach_region(self, spot: str, player: int) -> bool:
return self.multiworld.get_region(spot, player).can_reach(self)

def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.advancement and location not in self.events and
not key_only or getattr(location.item, "locked_dungeon_item", False)}
locations = {location for location in locations if location.advancement and location not in self.events}

while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
locations -= reachable_events
Expand Down Expand Up @@ -788,19 +862,15 @@ def count_group_unique(self, item_name_group: str, player: int) -> int:
)

# Item related
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool:
if location:
self.locations_checked.add(location)

changed = self.multiworld.worlds[item.player].collect(self, item)

if not changed and event:
self.prog_items[item.player][item.name] += 1
changed = True

self.stale[item.player] = True

if changed and not event:
if changed and not prevent_sweep:
self.sweep_for_events()

return changed
Expand Down Expand Up @@ -1052,9 +1122,9 @@ def can_fill(self, state: CollectionState, item: Item, check_access=True) -> boo
and (not check_access or self.can_reach(state))))

def can_reach(self, state: CollectionState) -> bool:
# self.access_rule computes faster on average, so placing it first for faster abort
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
assert self.parent_region, "Can't reach location without region"
return self.access_rule(state) and self.parent_region.can_reach(state)
return self.parent_region.can_reach(state) and self.access_rule(state)

def place_locked_item(self, item: Item):
if self.item:
Expand Down Expand Up @@ -1291,8 +1361,6 @@ def create_playthrough(self, create_paths: bool = True) -> None:
state = CollectionState(multiworld)
collection_spheres = []
while required_locations:
state.sweep_for_events(key_only=True)

sphere = set(filter(state.can_reach, required_locations))

for location in sphere:
Expand Down Expand Up @@ -1354,7 +1422,7 @@ def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, st
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
for (_, exit_path) in path):
if multiworld.mode[player] != 'inverted':
if multiworld.worlds[player].options.mode != 'inverted':
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Big Bomb Shop', player))
else:
Expand Down
4 changes: 3 additions & 1 deletion CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def _cmd_connect(self, address: str = "") -> bool:
if address:
self.ctx.server_address = None
self.ctx.username = None
self.ctx.password = None
elif not self.ctx.server_address:
self.output("Please specify an address.")
return False
Expand Down Expand Up @@ -251,7 +252,7 @@ def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int])
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui = None
ui: typing.Optional["kvui.GameManager"] = None
ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
Expand Down Expand Up @@ -514,6 +515,7 @@ def update_permissions(self, permissions: typing.Dict[str, int]):
async def shutdown(self):
self.server_address = ""
self.username = None
self.password = None
self.cancel_autoreconnect()
if self.server and not self.server.socket.closed:
await self.server.socket.close()
Expand Down
20 changes: 13 additions & 7 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@


class FillError(RuntimeError):
pass
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
if "multiworld" in kwargs and isinstance(args[0], str):
placements = (args[0] + f"\nAll Placements:\n" +
f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}")
args = (placements, *args[1:])
super().__init__(*args)


def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
Expand Down Expand Up @@ -212,7 +217,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)

item_pool.extend(unplaced_items)

Expand Down Expand Up @@ -299,7 +304,7 @@ def remaining_fill(multiworld: MultiWorld,
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)

itempool.extend(unplaced_items)

Expand Down Expand Up @@ -506,7 +511,8 @@ def mark_for_locking(location: Location):
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations."
f"There are {len(progitempool)} more progression items than there are available locations.",
multiworld=multiworld,
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)

Expand All @@ -523,7 +529,8 @@ def mark_for_locking(location: Location):
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
multiworld=multiworld,
)

restitempool = filleritempool + usefulitempool
Expand Down Expand Up @@ -589,7 +596,7 @@ def flood_items(multiworld: MultiWorld) -> None:
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
else:
raise FillError('No more progress items left to place.')
raise FillError('No more progress items left to place.', multiworld=multiworld)

# find item to replace with progress item
location_list = multiworld.get_reachable_locations()
Expand Down Expand Up @@ -646,7 +653,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:

def get_sphere_locations(sphere_state: CollectionState,
locations: typing.Set[Location]) -> typing.Set[Location]:
sphere_state.sweep_for_events(key_only=True, locations=locations)
return {loc for loc in locations if sphere_state.can_reach(loc)}

def item_percentage(player: int, num: int) -> float:
Expand Down
9 changes: 9 additions & 0 deletions KH1Client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
if __name__ == '__main__':
import ModuleUpdate
ModuleUpdate.update()

import Utils
Utils.init_logging("KH1Client", exception_logger="Client")

from worlds.kh1.Client import launch
launch()
2 changes: 1 addition & 1 deletion Launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None
if file and component:
run_component(component, file)
else:
logging.warning(f"unable to identify component for {filename}")
logging.warning(f"unable to identify component for {file}")

def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
Expand Down
Loading

0 comments on commit 782a68f

Please sign in to comment.