diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000..17a60ad125
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,5 @@
+[report]
+exclude_lines =
+ pragma: no cover
+ if TYPE_CHECKING:
+ if typing.TYPE_CHECKING:
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000000..c582902836
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,30 @@
+'is: documentation':
+- changed-files:
+ - all-globs-to-all-files: '{**/docs/**,**/README.md}'
+
+'affects: webhost':
+- changed-files:
+ - all-globs-to-any-file: 'WebHost.py'
+ - all-globs-to-any-file: 'WebHostLib/**/*'
+
+'affects: core':
+- changed-files:
+ - all-globs-to-any-file:
+ - '!*Client.py'
+ - '!README.md'
+ - '!LICENSE'
+ - '!*.yml'
+ - '!.gitignore'
+ - '!**/docs/**'
+ - '!typings/kivy/**'
+ - '!test/**'
+ - '!data/**'
+ - '!.run/**'
+ - '!.github/**'
+ - '!worlds_disabled/**'
+ - '!worlds/**'
+ - '!WebHost.py'
+ - '!WebHostLib/**'
+ - any-glob-to-any-file: # exceptions to the above rules of "stuff that isn't core"
+ - 'worlds/generic/**/*.py'
+ - 'CommonClient.py'
diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml
index ba2660809a..d01365745c 100644
--- a/.github/workflows/analyze-modified-files.yml
+++ b/.github/workflows/analyze-modified-files.yml
@@ -71,7 +71,7 @@ jobs:
continue-on-error: true
if: env.diff != '' && matrix.task == 'flake8'
run: |
- flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
+ flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
- name: "mypy: Type check modified files"
continue-on-error: true
diff --git a/.github/workflows/label-pull-requests.yml b/.github/workflows/label-pull-requests.yml
new file mode 100644
index 0000000000..42881aa49d
--- /dev/null
+++ b/.github/workflows/label-pull-requests.yml
@@ -0,0 +1,44 @@
+name: Label Pull Request
+on:
+ pull_request_target:
+ types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed']
+ branches: ['main']
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ labeler:
+ name: 'Apply content-based labels'
+ if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ sync-labels: true
+ peer_review:
+ name: 'Apply peer review label'
+ if: >-
+ (github.event.action == 'opened' || github.event.action == 'reopened' ||
+ github.event.action == 'ready_for_review') && !github.event.pull_request.draft
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Add label'
+ run: "gh pr edit \"$PR_URL\" --add-label 'waiting-on: peer-review'"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ unblock_draft_prs:
+ name: 'Remove waiting-on labels'
+ if: github.event.action == 'converted_to_draft' || github.event.action == 'closed'
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Remove labels'
+ run: |-
+ gh pr edit "$PR_URL" --remove-label 'waiting-on: peer-review' \
+ --remove-label 'waiting-on: core-review' \
+ --remove-label 'waiting-on: world-maintainer' \
+ --remove-label 'waiting-on: author'
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.run/Archipelago Unittests.run.xml b/.run/Archipelago Unittests.run.xml
new file mode 100644
index 0000000000..24fea0f73f
--- /dev/null
+++ b/.run/Archipelago Unittests.run.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AdventureClient.py b/AdventureClient.py
index d2f4e734ac..06e4d60dad 100644
--- a/AdventureClient.py
+++ b/AdventureClient.py
@@ -115,11 +115,12 @@ class AdventureContext(CommonContext):
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
- self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
- if self.freeincarnates_used is None:
- self.freeincarnates_used = 0
- self.freeincarnates_used += self.freeincarnate_pending
- self.send_pending_freeincarnates()
+ if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
+ self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
+ if self.freeincarnates_used is None:
+ self.freeincarnates_used = 0
+ self.freeincarnates_used += self.freeincarnate_pending
+ self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]
diff --git a/BaseClasses.py b/BaseClasses.py
index 4ff55291c3..2a7a5ab615 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -252,15 +252,20 @@ class MultiWorld():
range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None:
+ # TODO - remove this section once all worlds use options dataclasses
+ all_keys: Set[str] = {key for player in self.player_ids for key in
+ AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
+ for option_key in all_keys:
+ option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
+ f"Please use `self.options.{option_key}` instead.")
+ option.update(getattr(args, option_key, {}))
+ setattr(self, option_key, option)
+
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
- for option_key in world_type.options_dataclass.type_hints:
- option_values = getattr(args, option_key, {})
- setattr(self, option_key, option_values)
- # TODO - remove this loop once all worlds use options dataclasses
- options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
+ options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
@@ -567,9 +572,10 @@ class MultiWorld():
def location_condition(location: Location):
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
- if location.player in players["minimal"]:
- return False
- return True
+ if location.player in players["locations"] or (location.item and location.item.player not in
+ players["minimal"]):
+ return True
+ return False
def location_relevant(location: Location):
"""Determine if this location is relevant to sweep."""
@@ -647,34 +653,34 @@ class CollectionState():
def update_reachable_regions(self, player: int):
self.stale[player] = False
- rrp = self.reachable_regions[player]
- bc = self.blocked_connections[player]
+ reachable_regions = self.reachable_regions[player]
+ blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player])
- start = self.multiworld.get_region('Menu', player)
+ start = self.multiworld.get_region("Menu", player)
# init on first call - this can't be done on construction since the regions don't exist yet
- if start not in rrp:
- rrp.add(start)
- bc.update(start.exits)
+ if start not in reachable_regions:
+ reachable_regions.add(start)
+ blocked_connections.update(start.exits)
queue.extend(start.exits)
# run BFS on all connections, and keep track of those blocked by missing items
while queue:
connection = queue.popleft()
new_region = connection.connected_region
- if new_region in rrp:
- bc.remove(connection)
+ if new_region in reachable_regions:
+ blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
- rrp.add(new_region)
- bc.remove(connection)
- bc.update(new_region.exits)
+ reachable_regions.add(new_region)
+ blocked_connections.remove(connection)
+ blocked_connections.update(new_region.exits)
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
- if new_entrance in bc and new_entrance not in queue:
+ if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance)
def copy(self) -> CollectionState:
@@ -819,8 +825,8 @@ class Entrance:
return self.__str__()
def __str__(self):
- world = self.parent_region.multiworld if self.parent_region else None
- return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
+ multiworld = self.parent_region.multiworld if self.parent_region else None
+ return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
class Region:
@@ -1036,8 +1042,8 @@ class Location:
return self.__str__()
def __str__(self):
- world = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
- return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
+ multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
+ return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
def __hash__(self):
return hash((self.name, self.player))
@@ -1052,9 +1058,6 @@ class Location:
@property
def hint_text(self) -> str:
- hint_text = getattr(self, "_hint_text", None)
- if hint_text:
- return hint_text
return "at " + self.name.replace("_", " ").replace("-", " ")
@@ -1174,7 +1177,7 @@ class Spoiler:
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
def create_playthrough(self, create_paths: bool = True) -> None:
- """Destructive to the world while it is run, damage gets repaired afterwards."""
+ """Destructive to the multiworld while it is run, damage gets repaired afterwards."""
from itertools import chain
# get locations containing progress items
multiworld = self.multiworld
diff --git a/CommonClient.py b/CommonClient.py
index c4d80f3416..736cf4922f 100644
--- a/CommonClient.py
+++ b/CommonClient.py
@@ -460,7 +460,7 @@ class CommonContext:
else:
self.update_game(cached_game)
if needed_updates:
- await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
+ await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
def update_game(self, game_package: dict):
for item_name, item_id in game_package["item_name_to_id"].items():
@@ -477,6 +477,7 @@ class CommonContext:
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
+ logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
@@ -727,7 +728,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
- logger.info("Got new ID/Name DataPackage")
ctx.consume_network_data_package(args['data'])
elif cmd == 'ConnectionRefused':
diff --git a/Fill.py b/Fill.py
index 525d27d338..ae44710469 100644
--- a/Fill.py
+++ b/Fill.py
@@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
return new_state
-def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
+def fill_restrictive(multiworld: 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, name: str = "Unknown") -> None:
"""
- :param world: Multiworld to be filled.
+ :param multiworld: Multiworld to be filled.
:param base_state: State assumed before fill.
:param locations: Locations to be filled with item_pool
:param item_pool: Items to fill into the locations
@@ -68,7 +68,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
- has_beaten_game = world.has_beaten_game(maximum_exploration_state)
+ has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)
while items_to_place:
# if we have run out of locations to fill,break out of this loop
@@ -80,8 +80,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable
- if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
- perform_access_check = not world.has_beaten_game(maximum_exploration_state,
+ if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
+ perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
if single_player_placement else not has_beaten_game
else:
@@ -122,11 +122,11 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
# Verify placing this item won't reduce available locations, which would be a useless swap.
prev_state = swap_state.copy()
prev_loc_count = len(
- world.get_reachable_locations(prev_state))
+ multiworld.get_reachable_locations(prev_state))
swap_state.collect(item_to_place, True)
new_loc_count = len(
- world.get_reachable_locations(swap_state))
+ multiworld.get_reachable_locations(swap_state))
if new_loc_count >= prev_loc_count:
# Add this item to the existing placement, and
@@ -156,7 +156,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
else:
unplaced_items.append(item_to_place)
continue
- world.push_item(spot_to_fill, item_to_place, False)
+ multiworld.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock
placements.append(spot_to_fill)
spot_to_fill.event = item_to_place.advancement
@@ -173,7 +173,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
for placement in placements:
- if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
+ if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
placement.item.location = None
unplaced_items.append(placement.item)
placement.item = None
@@ -188,7 +188,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
if excluded_locations:
for location in excluded_locations:
location.progress_type = location.progress_type.DEFAULT
- fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
+ fill_restrictive(multiworld, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
swap, on_place, allow_partial, False)
for location in excluded_locations:
if not location.item:
@@ -196,7 +196,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
# There are leftover unplaceable items and locations that won't accept them
- if world.can_beat_game():
+ if multiworld.can_beat_game():
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
else:
@@ -206,7 +206,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
item_pool.extend(unplaced_items)
-def remaining_fill(world: MultiWorld,
+def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item]) -> None:
unplaced_items: typing.List[Item] = []
@@ -261,7 +261,7 @@ def remaining_fill(world: MultiWorld,
unplaced_items.append(item_to_place)
continue
- world.push_item(spot_to_fill, item_to_place, False)
+ multiworld.push_item(spot_to_fill, item_to_place, False)
placements.append(spot_to_fill)
placed += 1
if not placed % 1000:
@@ -278,19 +278,19 @@ def remaining_fill(world: MultiWorld,
itempool.extend(unplaced_items)
-def fast_fill(world: MultiWorld,
+def fast_fill(multiworld: MultiWorld,
item_pool: typing.List[Item],
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations):
- world.push_item(location, item, False)
+ multiworld.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:]
-def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
+def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
maximum_exploration_state = sweep_from_pool(state, pool)
- minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"}
- unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
+ minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"}
+ unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and
not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations:
if (location.item is not None and location.item.advancement and location.address is not None and not
@@ -304,36 +304,36 @@ 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, name="Accessibility Corrections")
+ fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
-def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
+def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations):
maximum_exploration_state = sweep_from_pool(state)
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations:
def forbid_important_item_rule(item: Item):
- return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal')
+ return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule)
-def distribute_early_items(world: MultiWorld,
+def distribute_early_items(multiworld: MultiWorld,
fill_locations: typing.List[Location],
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
""" returns new fill_locations and itempool """
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
- for player in world.player_ids:
- items = itertools.chain(world.early_items[player], world.local_early_items[player])
+ for player in multiworld.player_ids:
+ items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
for item in items:
- early_items_count[item, player] = [world.early_items[player].get(item, 0),
- world.local_early_items[player].get(item, 0)]
+ early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
+ multiworld.local_early_items[player].get(item, 0)]
if early_items_count:
early_locations: typing.List[Location] = []
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
- base_state = world.state.copy()
- base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
+ base_state = multiworld.state.copy()
+ base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
@@ -345,8 +345,8 @@ def distribute_early_items(world: MultiWorld,
early_prog_items: typing.List[Item] = []
early_rest_items: typing.List[Item] = []
- early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
- early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
+ early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
+ early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
item_indexes_to_remove: typing.Set[int] = set()
for i, item in enumerate(itempool):
if (item.name, item.player) in early_items_count:
@@ -370,28 +370,28 @@ def distribute_early_items(world: MultiWorld,
if len(early_items_count) == 0:
break
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
- for player in world.player_ids:
+ for player in multiworld.player_ids:
player_local = early_local_rest_items[player]
- fill_restrictive(world, base_state,
+ fill_restrictive(multiworld, base_state,
[loc for loc in early_locations if loc.player == player],
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(multiworld, 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:
+ for player in multiworld.player_ids:
player_local = early_local_prog_items[player]
- fill_restrictive(world, base_state,
+ fill_restrictive(multiworld, base_state,
[loc for loc in early_locations if loc.player == player],
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(multiworld, 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:
@@ -400,18 +400,18 @@ def distribute_early_items(world: MultiWorld,
itempool += unplaced_early_items
fill_locations.extend(early_locations)
- world.random.shuffle(fill_locations)
+ multiworld.random.shuffle(fill_locations)
return fill_locations, itempool
-def distribute_items_restrictive(world: MultiWorld) -> None:
- fill_locations = sorted(world.get_unfilled_locations())
- world.random.shuffle(fill_locations)
+def distribute_items_restrictive(multiworld: MultiWorld) -> None:
+ fill_locations = sorted(multiworld.get_unfilled_locations())
+ multiworld.random.shuffle(fill_locations)
# get items to distribute
- itempool = sorted(world.itempool)
- world.random.shuffle(itempool)
+ itempool = sorted(multiworld.itempool)
+ multiworld.random.shuffle(itempool)
- fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
+ fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
progitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = []
@@ -425,7 +425,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
else:
filleritempool.append(item)
- call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
+ call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType}
@@ -446,34 +446,34 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
if prioritylocations:
# "priority fill"
- fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
+ fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
name="Priority")
- accessibility_corrections(world, world.state, prioritylocations, progitempool)
+ accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
if progitempool:
# "advancement/progression fill"
- fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
+ fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
if progitempool:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
- accessibility_corrections(world, world.state, defaultlocations)
+ accessibility_corrections(multiworld, multiworld.state, defaultlocations)
for location in lock_later:
if location.item:
location.locked = True
del mark_for_locking, lock_later
- inaccessible_location_rules(world, world.state, defaultlocations)
+ inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
- remaining_fill(world, excludedlocations, filleritempool)
+ remaining_fill(multiworld, excludedlocations, filleritempool)
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
restitempool = filleritempool + usefulitempool
- remaining_fill(world, defaultlocations, restitempool)
+ remaining_fill(multiworld, defaultlocations, restitempool)
unplaced = restitempool
unfilled = defaultlocations
@@ -481,40 +481,40 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
if unplaced or unfilled:
logging.warning(
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
- items_counter = Counter(location.item.player for location in world.get_locations() if location.item)
- locations_counter = Counter(location.player for location in world.get_locations())
+ items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
+ locations_counter = Counter(location.player for location in multiworld.get_locations())
items_counter.update(item.player for item in unplaced)
locations_counter.update(location.player for location in unfilled)
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f'Per-Player counts: {print_data})')
-def flood_items(world: MultiWorld) -> None:
+def flood_items(multiworld: MultiWorld) -> None:
# get items to distribute
- world.random.shuffle(world.itempool)
- itempool = world.itempool
+ multiworld.random.shuffle(multiworld.itempool)
+ itempool = multiworld.itempool
progress_done = False
# sweep once to pick up preplaced items
- world.state.sweep_for_events()
+ multiworld.state.sweep_for_events()
- # fill world from top of itempool while we can
+ # fill multiworld from top of itempool while we can
while not progress_done:
- location_list = world.get_unfilled_locations()
- world.random.shuffle(location_list)
+ location_list = multiworld.get_unfilled_locations()
+ multiworld.random.shuffle(location_list)
spot_to_fill = None
for location in location_list:
- if location.can_fill(world.state, itempool[0]):
+ if location.can_fill(multiworld.state, itempool[0]):
spot_to_fill = location
break
if spot_to_fill:
item = itempool.pop(0)
- world.push_item(spot_to_fill, item, True)
+ multiworld.push_item(spot_to_fill, item, True)
continue
# ran out of spots, check if we need to step in and correct things
- if len(world.get_reachable_locations()) == len(world.get_locations()):
+ if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()):
progress_done = True
continue
@@ -524,7 +524,7 @@ def flood_items(world: MultiWorld) -> None:
for item in itempool:
if item.advancement:
candidate_item_to_place = item
- if world.unlocks_new_location(item):
+ if multiworld.unlocks_new_location(item):
item_to_place = item
break
@@ -537,15 +537,15 @@ def flood_items(world: MultiWorld) -> None:
raise FillError('No more progress items left to place.')
# find item to replace with progress item
- location_list = world.get_reachable_locations()
- world.random.shuffle(location_list)
+ location_list = multiworld.get_reachable_locations()
+ multiworld.random.shuffle(location_list)
for location in location_list:
if location.item is not None and not location.item.advancement:
# safe to replace
replace_item = location.item
replace_item.location = None
itempool.append(replace_item)
- world.push_item(location, item_to_place, True)
+ multiworld.push_item(location, item_to_place, True)
itempool.remove(item_to_place)
break
@@ -755,7 +755,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
location_1.event, location_2.event = location_2.event, location_1.event
-def distribute_planned(world: MultiWorld) -> None:
+def distribute_planned(multiworld: MultiWorld) -> None:
def warn(warning: str, force: typing.Union[bool, str]) -> None:
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
logging.warning(f'{warning}')
@@ -768,24 +768,24 @@ def distribute_planned(world: MultiWorld) -> None:
else:
warn(warning, force)
- swept_state = world.state.copy()
+ swept_state = multiworld.state.copy()
swept_state.sweep_for_events()
- reachable = frozenset(world.get_reachable_locations(swept_state))
+ reachable = frozenset(multiworld.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
- for loc in world.get_unfilled_locations():
+ for loc in multiworld.get_unfilled_locations():
if loc in reachable:
early_locations[loc.player].append(loc.name)
else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name)
- world_name_lookup = world.world_name_lookup
+ world_name_lookup = multiworld.world_name_lookup
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
- player_ids = set(world.player_ids)
+ player_ids = set(multiworld.player_ids)
for player in player_ids:
- for block in world.plando_items[player]:
+ for block in multiworld.plando_items[player]:
block['player'] = player
if 'force' not in block:
block['force'] = 'silent'
@@ -799,12 +799,12 @@ def distribute_planned(world: MultiWorld) -> None:
else:
target_world = block['world']
- if target_world is False or world.players == 1: # target own world
+ if target_world is False or multiworld.players == 1: # target own world
worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own
- worlds = set(world.player_ids) - {player}
+ worlds = set(multiworld.player_ids) - {player}
elif target_world is None: # target all worlds
- worlds = set(world.player_ids)
+ worlds = set(multiworld.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = set()
for listed_world in target_world:
@@ -814,9 +814,9 @@ def distribute_planned(world: MultiWorld) -> None:
continue
worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number
- if target_world not in range(1, world.players + 1):
+ if target_world not in range(1, multiworld.players + 1):
failed(
- f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
+ f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
block['force'])
continue
worlds = {target_world}
@@ -844,7 +844,7 @@ def distribute_planned(world: MultiWorld) -> None:
item_list: typing.List[str] = []
for key, value in items.items():
if value is True:
- value = world.itempool.count(world.worlds[player].create_item(key))
+ value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
item_list += [key] * value
items = item_list
if isinstance(items, str):
@@ -894,17 +894,17 @@ def distribute_planned(world: MultiWorld) -> None:
count = block['count']
failed(f"Plando count {count} greater than locations specified", block['force'])
block['count'] = len(block['locations'])
- block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
+ block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max'])
if block['count']['target'] > 0:
plando_blocks.append(block)
# shuffle, but then sort blocks by number of locations minus number of items,
# so less-flexible blocks get priority
- world.random.shuffle(plando_blocks)
+ multiworld.random.shuffle(plando_blocks)
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
if len(block['locations']) > 0
- else len(world.get_unfilled_locations(player)) - block['count']['target']))
+ else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
for placement in plando_blocks:
player = placement['player']
@@ -915,19 +915,19 @@ def distribute_planned(world: MultiWorld) -> None:
maxcount = placement['count']['target']
from_pool = placement['from_pool']
- candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
- world.random.shuffle(candidates)
- world.random.shuffle(items)
+ candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
+ multiworld.random.shuffle(candidates)
+ multiworld.random.shuffle(items)
count = 0
err: typing.List[str] = []
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
for item_name in items:
- item = world.worlds[player].create_item(item_name)
+ item = multiworld.worlds[player].create_item(item_name)
for location in reversed(candidates):
if (location.address is None) == (item.code is None): # either both None or both not None
if not location.item:
if location.item_rule(item):
- if location.can_fill(world.state, item, False):
+ if location.can_fill(multiworld.state, item, False):
successful_pairs.append((item, location))
candidates.remove(location)
count = count + 1
@@ -945,21 +945,21 @@ def distribute_planned(world: MultiWorld) -> None:
if count < placement['count']['min']:
m = placement['count']['min']
failed(
- f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}",
+ f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
placement['force'])
for (item, location) in successful_pairs:
- world.push_item(location, item, collect=False)
+ multiworld.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
if from_pool:
try:
- world.itempool.remove(item)
+ multiworld.itempool.remove(item)
except ValueError:
warn(
- f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
+ f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
placement['force'])
except Exception as e:
raise Exception(
- f"Error running plando for player {player} ({world.player_name[player]})") from e
+ f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py
index f3fc9d2cdb..a51645feac 100644
--- a/LinksAwakeningClient.py
+++ b/LinksAwakeningClient.py
@@ -348,7 +348,8 @@ class LinksAwakeningClient():
await asyncio.sleep(1.0)
continue
self.stop_bizhawk_spam = False
- logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
+ logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} "
+ f"running {rom_name.decode('ascii', errors='replace')}")
return
except (BlockingIOError, TimeoutError, ConnectionResetError):
await asyncio.sleep(1.0)
diff --git a/Main.py b/Main.py
index 8dac8f7d20..f1d2f63692 100644
--- a/Main.py
+++ b/Main.py
@@ -30,49 +30,49 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_path.cached_path = args.outputpath
start = time.perf_counter()
- # initialize the world
- world = MultiWorld(args.multi)
+ # initialize the multiworld
+ multiworld = MultiWorld(args.multi)
logger = logging.getLogger()
- world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
- world.plando_options = args.plando_options
+ multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
+ multiworld.plando_options = args.plando_options
- world.shuffle = args.shuffle.copy()
- world.logic = args.logic.copy()
- world.mode = args.mode.copy()
- world.difficulty = args.difficulty.copy()
- world.item_functionality = args.item_functionality.copy()
- world.timer = args.timer.copy()
- world.goal = args.goal.copy()
- world.boss_shuffle = args.shufflebosses.copy()
- world.enemy_health = args.enemy_health.copy()
- world.enemy_damage = args.enemy_damage.copy()
- world.beemizer_total_chance = args.beemizer_total_chance.copy()
- world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
- world.countdown_start_time = args.countdown_start_time.copy()
- world.red_clock_time = args.red_clock_time.copy()
- world.blue_clock_time = args.blue_clock_time.copy()
- world.green_clock_time = args.green_clock_time.copy()
- world.dungeon_counters = args.dungeon_counters.copy()
- world.triforce_pieces_available = args.triforce_pieces_available.copy()
- world.triforce_pieces_required = args.triforce_pieces_required.copy()
- world.shop_shuffle = args.shop_shuffle.copy()
- world.shuffle_prizes = args.shuffle_prizes.copy()
- world.sprite_pool = args.sprite_pool.copy()
- world.dark_room_logic = args.dark_room_logic.copy()
- world.plando_items = args.plando_items.copy()
- world.plando_texts = args.plando_texts.copy()
- world.plando_connections = args.plando_connections.copy()
- world.required_medallions = args.required_medallions.copy()
- world.game = args.game.copy()
- world.player_name = args.name.copy()
- world.sprite = args.sprite.copy()
- world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
+ multiworld.shuffle = args.shuffle.copy()
+ multiworld.logic = args.logic.copy()
+ multiworld.mode = args.mode.copy()
+ multiworld.difficulty = args.difficulty.copy()
+ multiworld.item_functionality = args.item_functionality.copy()
+ multiworld.timer = args.timer.copy()
+ multiworld.goal = args.goal.copy()
+ multiworld.boss_shuffle = args.shufflebosses.copy()
+ multiworld.enemy_health = args.enemy_health.copy()
+ multiworld.enemy_damage = args.enemy_damage.copy()
+ multiworld.beemizer_total_chance = args.beemizer_total_chance.copy()
+ multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy()
+ multiworld.countdown_start_time = args.countdown_start_time.copy()
+ multiworld.red_clock_time = args.red_clock_time.copy()
+ multiworld.blue_clock_time = args.blue_clock_time.copy()
+ multiworld.green_clock_time = args.green_clock_time.copy()
+ multiworld.dungeon_counters = args.dungeon_counters.copy()
+ multiworld.triforce_pieces_available = args.triforce_pieces_available.copy()
+ multiworld.triforce_pieces_required = args.triforce_pieces_required.copy()
+ multiworld.shop_shuffle = args.shop_shuffle.copy()
+ multiworld.shuffle_prizes = args.shuffle_prizes.copy()
+ multiworld.sprite_pool = args.sprite_pool.copy()
+ multiworld.dark_room_logic = args.dark_room_logic.copy()
+ multiworld.plando_items = args.plando_items.copy()
+ multiworld.plando_texts = args.plando_texts.copy()
+ multiworld.plando_connections = args.plando_connections.copy()
+ multiworld.required_medallions = args.required_medallions.copy()
+ multiworld.game = args.game.copy()
+ multiworld.player_name = args.name.copy()
+ multiworld.sprite = args.sprite.copy()
+ multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
- world.set_options(args)
- world.set_item_links()
- world.state = CollectionState(world)
- logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
+ multiworld.set_options(args)
+ multiworld.set_item_links()
+ multiworld.state = CollectionState(multiworld)
+ logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
@@ -103,87 +103,93 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output:
- AutoWorld.call_stage(world, "assert_generate")
+ AutoWorld.call_stage(multiworld, "assert_generate")
- AutoWorld.call_all(world, "generate_early")
+ AutoWorld.call_all(multiworld, "generate_early")
logger.info('')
- for player in world.player_ids:
- for item_name, count in world.worlds[player].options.start_inventory.value.items():
+ for player in multiworld.player_ids:
+ for item_name, count in multiworld.worlds[player].options.start_inventory.value.items():
for _ in range(count):
- world.push_precollected(world.create_item(item_name, player))
+ multiworld.push_precollected(multiworld.create_item(item_name, player))
- for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
+ for item_name, count in getattr(multiworld.worlds[player].options,
+ "start_inventory_from_pool",
+ StartInventoryPool({})).value.items():
for _ in range(count):
- world.push_precollected(world.create_item(item_name, player))
+ multiworld.push_precollected(multiworld.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
- early = world.early_items[player].get(item_name, 0)
+ early = multiworld.early_items[player].get(item_name, 0)
if early:
- world.early_items[player][item_name] = max(0, early-count)
+ multiworld.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
- local_early = world.early_local_items[player].get(item_name, 0)
+ local_early = multiworld.early_local_items[player].get(item_name, 0)
if local_early:
- world.early_items[player][item_name] = max(0, local_early - remaining_count)
+ multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
del early
- logger.info('Creating World.')
- AutoWorld.call_all(world, "create_regions")
+ logger.info('Creating MultiWorld.')
+ AutoWorld.call_all(multiworld, "create_regions")
logger.info('Creating Items.')
- AutoWorld.call_all(world, "create_items")
+ AutoWorld.call_all(multiworld, "create_items")
logger.info('Calculating Access Rules.')
- for player in world.player_ids:
+ for player in multiworld.player_ids:
# items can't be both local and non-local, prefer local
- world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
- world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player])
+ multiworld.worlds[player].options.non_local_items.value -= multiworld.worlds[player].options.local_items.value
+ multiworld.worlds[player].options.non_local_items.value -= set(multiworld.local_early_items[player])
- AutoWorld.call_all(world, "set_rules")
+ AutoWorld.call_all(multiworld, "set_rules")
- for player in world.player_ids:
- exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
- world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
- for location_name in world.worlds[player].options.priority_locations.value:
+ 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
+ for location_name in multiworld.worlds[player].options.priority_locations.value:
try:
- location = world.get_location(location_name, player)
+ 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 world.worlds[player].location_name_to_id:
+ 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:
location.progress_type = LocationProgressType.PRIORITY
# Set local and non-local item rules.
- if world.players > 1:
- locality_rules(world)
+ if multiworld.players > 1:
+ locality_rules(multiworld)
else:
- world.worlds[1].options.non_local_items.value = set()
- world.worlds[1].options.local_items.value = set()
+ multiworld.worlds[1].options.non_local_items.value = set()
+ multiworld.worlds[1].options.local_items.value = set()
- AutoWorld.call_all(world, "generate_basic")
+ AutoWorld.call_all(multiworld, "generate_basic")
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
- if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
+ if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
- player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
+ player: getattr(multiworld.worlds[player].options,
+ "start_inventory_from_pool",
+ StartInventoryPool({})).value.copy()
+ for player in multiworld.player_ids
+ }
for player, items in depletion_pool.items():
- player_world: AutoWorld.World = world.worlds[player]
+ player_world: AutoWorld.World = multiworld.worlds[player]
for count in items.values():
for _ in range(count):
new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values())
- for i, item in enumerate(world.itempool):
+ for i, item in enumerate(multiworld.itempool):
if depletion_pool[item.player].get(item.name, 0):
target -= 1
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
- new_items.extend(world.itempool[i+1:])
+ new_items.extend(multiworld.itempool[i+1:])
break
else:
new_items.append(item)
@@ -193,19 +199,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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"{world.get_player_name(player)}"
+ raise Exception(f"{multiworld.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
- assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
- world.itempool[:] = new_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 world.groups.items():
+ 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 world.itempool:
+ 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
@@ -240,13 +246,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)
- region = Region("Menu", group_id, world, "ItemLink")
- world.regions.append(region)
+ region = Region("Menu", group_id, multiworld, "ItemLink")
+ multiworld.regions.append(region)
locations = region.locations
- for item in world.itempool:
+ 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} -> {world.player_name[item.player]} {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_)
@@ -257,10 +263,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
new_itempool.append(item)
- itemcount = len(world.itempool)
- world.itempool = new_itempool
+ itemcount = len(multiworld.itempool)
+ multiworld.itempool = new_itempool
- while itemcount > len(world.itempool):
+ while itemcount > len(multiworld.itempool):
items_to_add = []
for player in group["players"]:
if group["link_replacement"]:
@@ -268,64 +274,64 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
item_player = player
if group["replacement_items"][player]:
- items_to_add.append(AutoWorld.call_single(world, "create_item", item_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(world, "create_filler", item_player))
- world.random.shuffle(items_to_add)
- world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
+ 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)])
- if any(world.item_links.values()):
- world._all_state = None
+ if any(multiworld.item_links.values()):
+ multiworld._all_state = None
logger.info("Running Item Plando.")
- distribute_planned(world)
+ distribute_planned(multiworld)
logger.info('Running Pre Main Fill.')
- AutoWorld.call_all(world, "pre_fill")
+ AutoWorld.call_all(multiworld, "pre_fill")
- logger.info(f'Filling the world with {len(world.itempool)} items.')
+ logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.')
- if world.algorithm == 'flood':
- flood_items(world) # different algo, biased towards early game progress items
- elif world.algorithm == 'balanced':
- distribute_items_restrictive(world)
+ if multiworld.algorithm == 'flood':
+ flood_items(multiworld) # different algo, biased towards early game progress items
+ elif multiworld.algorithm == 'balanced':
+ distribute_items_restrictive(multiworld)
- AutoWorld.call_all(world, 'post_fill')
+ AutoWorld.call_all(multiworld, 'post_fill')
- if world.players > 1 and not args.skip_prog_balancing:
- balance_multiworld_progression(world)
+ if multiworld.players > 1 and not args.skip_prog_balancing:
+ balance_multiworld_progression(multiworld)
else:
logger.info("Progression balancing skipped.")
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
- world.random.passthrough = False
+ multiworld.random.passthrough = False
if args.skip_output:
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
- return world
+ return multiworld
logger.info(f'Beginning output...')
- outfilebase = 'AP_' + world.seed_name
+ outfilebase = 'AP_' + multiworld.seed_name
output = tempfile.TemporaryDirectory()
with output as temp_dir:
- output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
- is not world.worlds[player].generate_output.__code__]
+ output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
+ is not multiworld.worlds[player].generate_output.__code__]
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
- check_accessibility_task = pool.submit(world.fulfills_accessibility)
+ check_accessibility_task = pool.submit(multiworld.fulfills_accessibility)
- output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
+ output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)]
for player in output_players:
# skip starting a thread for methods that say "pass".
output_file_futures.append(
- pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
+ pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
# collect ER hint info
er_hint_data: Dict[int, Dict[int, str]] = {}
- AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
+ AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
def write_multidata():
import NetUtils
@@ -334,38 +340,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
games = {}
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
slot_info = {}
- names = [[name for player, name in sorted(world.player_name.items())]]
- for slot in world.player_ids:
- player_world: AutoWorld.World = world.worlds[slot]
+ names = [[name for player, name in sorted(multiworld.player_name.items())]]
+ for slot in multiworld.player_ids:
+ player_world: AutoWorld.World = multiworld.worlds[slot]
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
client_versions[slot] = player_world.required_client_version
- games[slot] = world.game[slot]
- slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
- world.player_types[slot])
- for slot, group in world.groups.items():
- games[slot] = world.game[slot]
- slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
+ games[slot] = multiworld.game[slot]
+ slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], multiworld.game[slot],
+ multiworld.player_types[slot])
+ for slot, group in multiworld.groups.items():
+ games[slot] = multiworld.game[slot]
+ slot_info[slot] = NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot],
group_members=sorted(group["players"]))
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
- for player, world_precollected in world.precollected_items.items()}
- precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
+ for player, world_precollected in multiworld.precollected_items.items()}
+ precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))}
- for slot in world.player_ids:
- slot_data[slot] = world.worlds[slot].fill_slot_data()
+ for slot in multiworld.player_ids:
+ slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
def precollect_hint(location):
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False, entrance, location.item.flags)
precollected_hints[location.player].add(hint)
- if location.item.player not in world.groups:
+ if location.item.player not in multiworld.groups:
precollected_hints[location.item.player].add(hint)
else:
- for player in world.groups[location.item.player]["players"]:
+ for player in multiworld.groups[location.item.player]["players"]:
precollected_hints[player].add(hint)
- locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
- for location in world.get_filled_locations():
+ locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
+ for location in multiworld.get_filled_locations():
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
@@ -375,18 +381,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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:
+ if location.name in multiworld.worlds[location.player].options.start_location_hints:
precollect_hint(location)
- elif location.item.name in world.worlds[location.item.player].options.start_hints:
+ elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
precollect_hint(location)
- elif any([location.item.name in world.worlds[player].options.start_hints
- for player in world.groups.get(location.item.player, {}).get("players", [])]):
+ elif any([location.item.name in multiworld.worlds[player].options.start_hints
+ for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location)
# embedded data package
data_package = {
game_world.game: worlds.network_data_package["games"][game_world.game]
- for game_world in world.worlds.values()
+ for game_world in multiworld.worlds.values()
}
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
@@ -394,7 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
- "connect_names": {name: (0, player) for player, name in world.player_name.items()},
+ "connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": baked_server_options,
@@ -404,10 +410,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
- "seed_name": world.seed_name,
+ "seed_name": multiworld.seed_name,
"datapackage": data_package,
}
- AutoWorld.call_all(world, "modify_multidata", multidata)
+ AutoWorld.call_all(multiworld, "modify_multidata", multidata)
multidata = zlib.compress(pickle.dumps(multidata), 9)
@@ -417,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result():
- if not world.can_beat_game():
+ if not multiworld.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
@@ -430,12 +436,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if args.spoiler > 1:
logger.info('Calculating playthrough.')
- world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
+ multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
if args.spoiler:
- world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
+ multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
- zipfilename = output_path(f"AP_{world.seed_name}.zip")
+ zipfilename = output_path(f"AP_{multiworld.seed_name}.zip")
logger.info(f"Creating final archive at {zipfilename}")
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
@@ -443,4 +449,4 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
- return world
+ return multiworld
diff --git a/ModuleUpdate.py b/ModuleUpdate.py
index c33e894e8b..c3dc8c8a87 100644
--- a/ModuleUpdate.py
+++ b/ModuleUpdate.py
@@ -4,14 +4,29 @@ import subprocess
import multiprocessing
import warnings
-local_dir = os.path.dirname(__file__)
-requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
-update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
+_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
+update_ran = _skip_update
+
+
+class RequirementsSet(set):
+ def add(self, e):
+ global update_ran
+ update_ran &= _skip_update
+ super().add(e)
+
+ def update(self, *s):
+ global update_ran
+ update_ran &= _skip_update
+ super().update(*s)
+
+
+local_dir = os.path.dirname(__file__)
+requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):
diff --git a/MultiServer.py b/MultiServer.py
index 9d2e9b564e..15ed22d715 100644
--- a/MultiServer.py
+++ b/MultiServer.py
@@ -2210,25 +2210,24 @@ def parse_args() -> argparse.Namespace:
async def auto_shutdown(ctx, to_cancel=None):
await asyncio.sleep(ctx.auto_shutdown)
+
+ def inactivity_shutdown():
+ ctx.server.ws_server.close()
+ ctx.exit_event.set()
+ if to_cancel:
+ for task in to_cancel:
+ task.cancel()
+ logging.info("Shutting down due to inactivity.")
+
while not ctx.exit_event.is_set():
if not ctx.client_activity_timers.values():
- ctx.server.ws_server.close()
- ctx.exit_event.set()
- if to_cancel:
- for task in to_cancel:
- task.cancel()
- logging.info("Shutting down due to inactivity.")
+ inactivity_shutdown()
else:
newest_activity = max(ctx.client_activity_timers.values())
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
seconds = ctx.auto_shutdown - delta.total_seconds()
if seconds < 0:
- ctx.server.ws_server.close()
- ctx.exit_event.set()
- if to_cancel:
- for task in to_cancel:
- task.cancel()
- logging.info("Shutting down due to inactivity.")
+ inactivity_shutdown()
else:
await asyncio.sleep(seconds)
diff --git a/OoTAdjuster.py b/OoTAdjuster.py
index 38ebe62e2a..9519b191e7 100644
--- a/OoTAdjuster.py
+++ b/OoTAdjuster.py
@@ -195,10 +195,10 @@ def set_icon(window):
window.tk.call('wm', 'iconphoto', window._w, logo)
def adjust(args):
- # Create a fake world and OOTWorld to use as a base
- world = MultiWorld(1)
- world.per_slot_randoms = {1: random}
- ootworld = OOTWorld(world, 1)
+ # Create a fake multiworld and OOTWorld to use as a base
+ multiworld = MultiWorld(1)
+ multiworld.per_slot_randoms = {1: random}
+ ootworld = OOTWorld(multiworld, 1)
# Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
result = getattr(args, name, None)
diff --git a/README.md b/README.md
index a1e03293d5..ce2130ce8e 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ Currently, the following games are supported:
* Heretic
* Landstalker: The Treasures of King Nole
* Final Fantasy Mystic Quest
+* TUNIC
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
diff --git a/Utils.py b/Utils.py
index 5955e92432..8b91226bed 100644
--- a/Utils.py
+++ b/Utils.py
@@ -779,6 +779,25 @@ def deprecate(message: str):
import warnings
warnings.warn(message)
+
+class DeprecateDict(dict):
+ log_message: str
+ should_error: bool
+
+ def __init__(self, message, error: bool = False) -> None:
+ self.log_message = message
+ self.should_error = error
+ super().__init__()
+
+ def __getitem__(self, item: Any) -> Any:
+ if self.should_error:
+ deprecate(self.log_message)
+ elif __debug__:
+ import warnings
+ warnings.warn(self.log_message)
+ return super().__getitem__(item)
+
+
def _extend_freeze_support() -> None:
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
# upstream issue: https://github.com/python/cpython/issues/76327
@@ -852,8 +871,8 @@ def visualize_regions(root_region: Region, file_name: str, *,
Example usage in Main code:
from Utils import visualize_regions
- for player in world.player_ids:
- visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
+ for player in multiworld.player_ids:
+ visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
"""
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py
index 61e9164e26..5a66d1e693 100644
--- a/WebHostLib/api/generate.py
+++ b/WebHostLib/api/generate.py
@@ -20,8 +20,8 @@ def generate_api():
race = False
meta_options_source = {}
if 'file' in request.files:
- file = request.files['file']
- options = get_yaml_data(file)
+ files = request.files.getlist('file')
+ options = get_yaml_data(files)
if isinstance(options, Markup):
return {"text": options.striptags()}, 400
if isinstance(options, str):
diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html
index 33f8dbc09e..53d98dfae6 100644
--- a/WebHostLib/templates/generate.html
+++ b/WebHostLib/templates/generate.html
@@ -69,8 +69,8 @@
|
@@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld
+
+
+
-
-
-
diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html
index ba15d64aca..2981c41452 100644
--- a/WebHostLib/templates/hostRoom.html
+++ b/WebHostLib/templates/hostRoom.html
@@ -3,6 +3,16 @@
{% block head %}
Multiworld {{ room.id|suuid }}
{% if should_refresh %}{% endif %}
+
+
+
+ {% if room.seed.slots|length < 2 %}
+
+ {% else %}
+
+ {% endif %}
{% endblock %}
diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html
index 7b89c4a9e0..08cf227990 100644
--- a/WebHostLib/templates/islandFooter.html
+++ b/WebHostLib/templates/islandFooter.html
@@ -1,6 +1,6 @@
{% block footer %}
|