Skip to content

Commit

Permalink
Merge remote-tracking branch 'ER/generic-entrance-rando' into oos-er
Browse files Browse the repository at this point in the history
  • Loading branch information
Ishigh1 committed Aug 21, 2024
2 parents b79444d + be912e6 commit e3921ff
Show file tree
Hide file tree
Showing 251 changed files with 18,078 additions and 11,596 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
205 changes: 162 additions & 43 deletions BaseClasses.py

Large diffs are not rendered by default.

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
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
112 changes: 24 additions & 88 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
flood_items
from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple, get_settings
from settings import get_settings
Expand Down Expand Up @@ -100,7 +101,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multiworld.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
local_early = multiworld.early_local_items[player].get(item_name, 0)
local_early = multiworld.local_early_items[player].get(item_name, 0)
if local_early:
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
Expand All @@ -124,14 +125,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player in multiworld.player_ids:
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
world_excluded_locations = set()
for location_name in multiworld.worlds[player].options.priority_locations.value:
try:
location = multiworld.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if location_name not in multiworld.worlds[player].location_name_to_id:
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
else:
except KeyError:
continue

if location.progress_type != LocationProgressType.EXCLUDED:
location.progress_type = LocationProgressType.PRIORITY
else:
logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.")
world_excluded_locations.add(location_name)
multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations

# Set local and non-local item rules.
if multiworld.players > 1:
Expand All @@ -146,6 +152,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
new_items: List[Item] = []
old_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
Expand All @@ -164,97 +171,26 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
new_items.extend(multiworld.itempool[i+1:])
old_items.extend(multiworld.itempool[i+1:])
break
else:
new_items.append(item)
old_items.append(item)

# leftovers?
if target:
for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items:
raise Exception(f"{multiworld.get_player_name(player)}"
logger.warning(f"{multiworld.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items

# temporary home for item links, should be moved out of Main
for group_id, group in multiworld.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 multiworld.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, multiworld, "ItemLink")
multiworld.regions.append(region)
locations = region.locations
for item in multiworld.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.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(multiworld.itempool)
multiworld.itempool = new_itempool
# find all filler we generated for the current player and remove until it matches
removables = [item for item in new_items if item.player == player]
for _ in range(sum(remaining_items.values())):
new_items.remove(removables.pop())
assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items + old_items

while itemcount > len(multiworld.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(multiworld, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player))
multiworld.random.shuffle(items_to_add)
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)])
multiworld.link_items()

if any(multiworld.item_links.values()):
multiworld._all_state = None
Expand Down Expand Up @@ -411,7 +347,7 @@ def precollect_hint(location):
output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result():
if not multiworld.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld)
else:
logger.warning("Location Accessibility requirements not fulfilled.")

Expand Down
18 changes: 9 additions & 9 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
collect_player(ctx, team, group, True)


def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]:
return ctx.locations.get_remaining(ctx.location_checks, team, slot)


Expand Down Expand Up @@ -1350,10 +1350,10 @@ def _cmd_collect(self) -> bool:
def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled":
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
for item_id in remaining_item_ids))
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
if rest_locations:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
for slot, item_id in rest_locations))
else:
self.output("No remaining items found.")
return True
Expand All @@ -1363,10 +1363,10 @@ def _cmd_remaining(self) -> bool:
return False
else: # is goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
for item_id in remaining_item_ids))
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
if rest_locations:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
for slot, item_id in rest_locations))
else:
self.output("No remaining items found.")
return True
Expand Down
8 changes: 4 additions & 4 deletions NetUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,12 +397,12 @@ def get_missing(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]
location_id not in checked]

def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[int]:
) -> typing.List[typing.Tuple[int, int]]:
checked = state[team, slot]
player_locations = self[slot]
return sorted([player_locations[location_id][0] for
location_id in player_locations if
location_id not in checked])
return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for
location_id in player_locations if
location_id not in checked])


if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
Expand Down
Loading

0 comments on commit e3921ff

Please sign in to comment.