Skip to content

Commit

Permalink
Merge branch 'ArchipelagoMW:main' into kh2-regionrework
Browse files Browse the repository at this point in the history
  • Loading branch information
JaredWeakStrike authored Nov 22, 2023
2 parents 7dc781e + 0f98cf5 commit 28c77f9
Show file tree
Hide file tree
Showing 315 changed files with 40,023 additions and 7,204 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
*.apmc
*.apz5
*.aptloz
*.apemerald
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.nes
*.smc
*.sms
*.gb
*.gbc
Expand Down
238 changes: 152 additions & 86 deletions BaseClasses.py

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
elif 'InvalidGame' in errors:
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible')
raise Exception('Server reported your client version as incompatible. '
'This probably means you have to update.')
elif 'InvalidItemsHandling' in errors:
raise Exception('The item handling flags requested by the client are not supported')
# last to check, recoverable problem
Expand All @@ -758,6 +759,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
ctx.hint_points = args.get("hint_points", 0)
ctx.consume_players_package(args["players"])
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
msgs = []
if ctx.locations_checked:
msgs.append({"cmd": "LocationChecks",
Expand Down Expand Up @@ -836,10 +838,14 @@ async def process_server_cmd(ctx: CommonContext, args: dict):

elif cmd == "Retrieved":
ctx.stored_data.update(args["keys"])
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]:
ctx.ui.update_hints()

elif cmd == "SetReply":
ctx.stored_data[args["key"]] = args["value"]
if args["key"].startswith("EnergyLink"):
if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]:
ctx.ui.update_hints()
elif args["key"].startswith("EnergyLink"):
ctx.current_energy_link_value = args["value"]
if ctx.ui:
ctx.ui.set_new_energy_link_value()
Expand Down
47 changes: 36 additions & 11 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class FillError(RuntimeError):
pass


def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")


def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
new_state = base_state.copy()
for item in itempool:
Expand All @@ -26,7 +30,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
allow_partial: bool = False, allow_excluded: bool = False) -> None:
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
"""
:param world: Multiworld to be filled.
:param base_state: State assumed before fill.
Expand All @@ -38,16 +42,20 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
:param on_place: callback that is called when a placement happens
:param allow_partial: only place what is possible. Remaining items will be in the item_pool list.
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
:param name: name of this fill step for progress logging purposes
"""
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
cleanup_required = False

swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
for item in item_pool:
reachable_items.setdefault(item.player, deque()).append(item)

# for progress logging
total = min(len(item_pool), len(locations))
placed = 0

while any(reachable_items.values()) and locations:
# grab one item per player
items_to_place = [items.pop()
Expand Down Expand Up @@ -104,7 +112,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:

location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item] if unsafe else [])
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool)
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
Expand Down Expand Up @@ -152,9 +160,15 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
spot_to_fill.locked = lock
placements.append(spot_to_fill)
spot_to_fill.event = item_to_place.advancement
placed += 1
if not placed % 1000:
_log_fill_progress(name, placed, total)
if on_place:
on_place(spot_to_fill)

if total > 1000:
_log_fill_progress(name, placed, total)

if cleanup_required:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
Expand Down Expand Up @@ -198,6 +212,8 @@ def remaining_fill(world: MultiWorld,
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
total = min(len(itempool), len(locations))
placed = 0
while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
Expand Down Expand Up @@ -247,6 +263,12 @@ def remaining_fill(world: MultiWorld,

world.push_item(spot_to_fill, item_to_place, False)
placements.append(spot_to_fill)
placed += 1
if not placed % 1000:
_log_fill_progress("Remaining", placed, total)

if total > 1000:
_log_fill_progress("Remaining", placed, total)

if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
Expand Down Expand Up @@ -282,7 +304,7 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio
locations.append(location)
if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
fill_restrictive(world, state, locations, pool)
fill_restrictive(world, state, locations, pool, name="Accessibility Corrections")


def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
Expand Down Expand Up @@ -352,23 +374,25 @@ def distribute_early_items(world: MultiWorld,
player_local = early_local_rest_items[player]
fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}")
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_rest_items.extend(early_local_rest_items[player])
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True)
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
name="Early Items")
early_locations += early_priority_locations
for player in world.player_ids:
player_local = early_local_prog_items[player]
fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}")
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_prog_items.extend(player_local)
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True)
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
name="Early Progression")
unplaced_early_items = early_rest_items + early_prog_items
if unplaced_early_items:
logging.warning("Ran out of early locations for early items. Failed to place "
Expand Down Expand Up @@ -422,13 +446,14 @@ def mark_for_locking(location: Location):

if prioritylocations:
# "priority fill"
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking)
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
name="Priority")
accessibility_corrections(world, world.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

if progitempool:
# "progression fill"
fill_restrictive(world, world.state, defaultlocations, progitempool)
# "advancement/progression fill"
fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
if progitempool:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
Expand Down
19 changes: 14 additions & 5 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import string
import urllib.parse
import urllib.request
from collections import ChainMap, Counter
from typing import Any, Callable, Dict, Tuple, Union
from collections import Counter
from typing import Any, Dict, Tuple, Union

import ModuleUpdate

Expand Down Expand Up @@ -169,7 +169,7 @@ def main(args=None, callback=ERmain):
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
name_counter = Counter()
erargs.player_settings = {}
erargs.player_options = {}

player = 1
while player <= args.multi:
Expand Down Expand Up @@ -225,7 +225,7 @@ def main(args=None, callback=ERmain):
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)

callback(erargs, seed)
return callback(erargs, seed)


def read_weights_yamls(path) -> Tuple[Any, ...]:
Expand Down Expand Up @@ -639,6 +639,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if __name__ == '__main__':
import atexit
confirmation = atexit.register(input, "Press enter to close.")
main()
multiworld = main()
if __debug__:
import gc
import sys
import weakref
weak = weakref.ref(multiworld)
del multiworld
gc.collect() # need to collect to deref all hard references
assert not weak(), f"MultiWorld object was not de-allocated, it's referenced {sys.getrefcount(weak())} times." \
" This would be a memory leak."
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)
12 changes: 5 additions & 7 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")

# All worlds should have finished creating all regions, locations, and entrances.
# Recache to ensure that they are all visible for locality rules.
world._recache()

logger.info('Calculating Access Rules.')

for player in world.player_ids:
Expand Down Expand Up @@ -233,7 +229,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[

region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region)
locations = region.locations = []
locations = region.locations
for item in world.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
Expand Down Expand Up @@ -267,10 +263,9 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])

if any(world.item_links.values()):
world._recache()
world._all_state = None

logger.info("Running Item Plando")
logger.info("Running Item Plando.")

distribute_planned(world)

Expand Down Expand Up @@ -359,6 +354,9 @@ def precollect_hint(location):
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}"
assert location.address not in locations_data[location.player], (
f"Locations with duplicate address. {location} and "
f"{locations_data[location.player][location.address]}")
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.worlds[location.player].options.start_location_hints:
Expand Down
13 changes: 11 additions & 2 deletions ModuleUpdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,23 @@ def update(yes=False, force=False):
install_pkg_resources(yes=yes)
import pkg_resources

prev = "" # if a line ends in \ we store here and merge later
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
for line in requirementsfile:
if not line or line[0] == "#":
continue # ignore comments
if not line or line.lstrip(" \t")[0] == "#":
if not prev:
continue # ignore comments
line = ""
elif line.rstrip("\r\n").endswith("\\"):
prev = prev + line.rstrip("\r\n")[:-1] + " " # continue on next line
continue
line = prev + line
line = line.split("--hash=")[0] # remove hashes from requirement for version checking
prev = ""
if line.startswith(("https://", "git+https://")):
# extract name and version for url
rest = line.split('/')[-1]
Expand Down
5 changes: 4 additions & 1 deletion Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,10 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str,
else:
raise ValueError(f"{casing} is invalid casing for as_dict. "
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
option_results[display_name] = getattr(self, option_name).value
value = getattr(self, option_name).value
if isinstance(value, set):
value = sorted(value)
option_results[display_name] = value
else:
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
return option_results
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Currently, the following games are supported:
* Muse Dash
* DOOM 1993
* Terraria
* Lingo
* Pokémon Emerald

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
Expand Down
4 changes: 2 additions & 2 deletions SNIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,12 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
super(SNIContext, self).on_deathlink(data)

async def handle_deathlink_state(self, currently_dead: bool) -> None:
async def handle_deathlink_state(self, currently_dead: bool, death_text: str = "") -> None:
# in this state we only care about triggering a death send
if self.death_state == DeathState.alive:
if currently_dead:
self.death_state = DeathState.dead
await self.send_death()
await self.send_death(death_text)
# in this state we care about confirming a kill, to move state to dead
elif self.death_state == DeathState.killing_player:
# this is being handled in deathlink_kill_player(ctx) already
Expand Down
6 changes: 3 additions & 3 deletions UndertaleClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ def _cmd_resync(self):
self.ctx.syncing = True

def _cmd_patch(self):
"""Patch the game."""
"""Patch the game. Only use this command if /auto_patch fails."""
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
self.ctx.patch_game()
self.output("Patched.")

def _cmd_savepath(self, directory: str):
"""Redirect to proper save data folder. (Use before connecting!)"""
"""Redirect to proper save data folder. This is necessary for Linux users to use before connecting."""
if isinstance(self.ctx, UndertaleContext):
self.ctx.save_game_folder = directory
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
Expand Down Expand Up @@ -67,7 +67,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
self.output("Patching successful!")

def _cmd_online(self):
"""Makes you no longer able to see other Undertale players."""
"""Toggles seeing other Undertale players."""
if isinstance(self.ctx, UndertaleContext):
self.ctx.update_online_mode(not ("Online" in self.ctx.tags))
if "Online" in self.ctx.tags:
Expand Down
Loading

0 comments on commit 28c77f9

Please sign in to comment.