mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 15:45:04 -07:00
Merge remote-tracking branch 'Main/main' into rework_accessibility
# Conflicts: # test/general/test_fill.py
This commit is contained in:
5
.coveragerc
Normal file
5
.coveragerc
Normal file
@@ -0,0 +1,5 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
if typing.TYPE_CHECKING:
|
||||
30
.github/labeler.yml
vendored
Normal file
30
.github/labeler.yml
vendored
Normal file
@@ -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'
|
||||
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -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
|
||||
|
||||
44
.github/workflows/label-pull-requests.yml
vendored
Normal file
44
.github/workflows/label-pull-requests.yml
vendored
Normal file
@@ -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 }}
|
||||
18
.run/Archipelago Unittests.run.xml
Normal file
18
.run/Archipelago Unittests.run.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Archipelago Unittests" type="tests" factoryName="Unittests">
|
||||
<module name="Archipelago" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="_new_pattern" value="""" />
|
||||
<option name="_new_additionalArguments" value="""" />
|
||||
<option name="_new_target" value=""$PROJECT_DIR$/test"" />
|
||||
<option name="_new_targetType" value=""PATH"" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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':
|
||||
|
||||
186
Fill.py
186
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
286
Main.py
286
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
|
||||
|
||||
@@ -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")):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
23
Utils.py
23
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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<select name="collect_mode" id="collect_mode">
|
||||
<option value="goal">Allow !collect after goal completion</option>
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !collect after goal completion</option>
|
||||
<option value="auto-enabled">
|
||||
Automatic on goal completion and manual !collect
|
||||
</option>
|
||||
@@ -93,9 +93,9 @@
|
||||
{% if race -%}
|
||||
<option value="disabled">Disabled in Race mode</option>
|
||||
{%- else -%}
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="goal">Allow !remaining after goal completion</option>
|
||||
<option value="enabled">Manual !remaining</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
{%- endif -%}
|
||||
</select>
|
||||
</td>
|
||||
@@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items">
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
|
||||
<label for="plando_bosses">Bosses</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
|
||||
<label for="plando_connections">Connections</label><br>
|
||||
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
{% block head %}
|
||||
<title>Multiworld {{ room.id|suuid }}</title>
|
||||
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
|
||||
<meta name="og:site_name" content="Archipelago">
|
||||
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
|
||||
<meta property="og:type" content="website" />
|
||||
{% if room.seed.slots|length < 2 %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% else %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% endif %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2023 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2024 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import collections
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
from uuid import UUID
|
||||
@@ -8,7 +9,7 @@ from werkzeug.exceptions import abort
|
||||
|
||||
from MultiServer import Context, get_saving_second
|
||||
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
|
||||
from Utils import restricted_loads
|
||||
from Utils import restricted_loads, KeyedDefaultDict
|
||||
from . import app, cache
|
||||
from .models import GameDataPackage, Room
|
||||
|
||||
@@ -62,12 +63,18 @@ class TrackerData:
|
||||
self.location_name_to_id: Dict[str, Dict[str, int]] = {}
|
||||
|
||||
# Generate inverse lookup tables from data package, useful for trackers.
|
||||
self.item_id_to_name: Dict[str, Dict[int, str]] = {}
|
||||
self.location_id_to_name: Dict[str, Dict[int, str]] = {}
|
||||
self.item_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
|
||||
game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Item (ID: {code})")
|
||||
})
|
||||
self.location_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
|
||||
game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Location (ID: {code})")
|
||||
})
|
||||
for game, game_package in self._multidata["datapackage"].items():
|
||||
game_package = restricted_loads(GameDataPackage.get(checksum=game_package["checksum"]).data)
|
||||
self.item_id_to_name[game] = {id: name for name, id in game_package["item_name_to_id"].items()}
|
||||
self.location_id_to_name[game] = {id: name for name, id in game_package["location_name_to_id"].items()}
|
||||
self.item_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Item (ID: {code})", {
|
||||
id: name for name, id in game_package["item_name_to_id"].items()})
|
||||
self.location_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Location (ID: {code})", {
|
||||
id: name for name, id in game_package["location_name_to_id"].items()})
|
||||
|
||||
# Normal lookup tables as well.
|
||||
self.item_name_to_id[game] = game_package["item_name_to_id"]
|
||||
@@ -115,10 +122,10 @@ class TrackerData:
|
||||
return self._multisave.get("received_items", {}).get((team, player, True), [])
|
||||
|
||||
@_cache_results
|
||||
def get_player_inventory_counts(self, team: int, player: int) -> Dict[int, int]:
|
||||
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
|
||||
"""Retrieves a dictionary of all items received by their id and their received count."""
|
||||
items = self.get_player_received_items(team, player)
|
||||
inventory = {item: 0 for item in self.item_id_to_name[self.get_player_game(team, player)]}
|
||||
inventory = collections.Counter()
|
||||
for item in items:
|
||||
inventory[item.item] += 1
|
||||
|
||||
@@ -149,16 +156,15 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of number of completed worlds per team."""
|
||||
return {
|
||||
team: sum(
|
||||
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL
|
||||
for player in players if self.get_slot_info(team, player).type == SlotType.player
|
||||
) for team, players in self.get_team_players().items()
|
||||
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL for player in players
|
||||
) for team, players in self.get_all_players().items()
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
def get_team_hints(self) -> Dict[int, Set[Hint]]:
|
||||
"""Retrieves a dictionary of all hints per team."""
|
||||
hints = {}
|
||||
for team, players in self.get_team_players().items():
|
||||
for team, players in self.get_all_slots().items():
|
||||
hints[team] = set()
|
||||
for player in players:
|
||||
hints[team] |= self.get_player_hints(team, player)
|
||||
@@ -170,7 +176,7 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of total player locations each team has."""
|
||||
return {
|
||||
team: sum(len(self.get_player_locations(team, player)) for player in players)
|
||||
for team, players in self.get_team_players().items()
|
||||
for team, players in self.get_all_players().items()
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -178,16 +184,30 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of checked player locations each team has."""
|
||||
return {
|
||||
team: sum(len(self.get_player_checked_locations(team, player)) for player in players)
|
||||
for team, players in self.get_team_players().items()
|
||||
for team, players in self.get_all_players().items()
|
||||
}
|
||||
|
||||
# TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
|
||||
# currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
|
||||
@_cache_results
|
||||
def get_team_players(self) -> Dict[int, List[int]]:
|
||||
def get_all_slots(self) -> Dict[int, List[int]]:
|
||||
"""Retrieves a dictionary of all players ids on each team."""
|
||||
return {
|
||||
0: [player for player, slot_info in self._multidata["slot_info"].items()]
|
||||
0: [
|
||||
player for player, slot_info in self._multidata["slot_info"].items()
|
||||
]
|
||||
}
|
||||
|
||||
# TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
|
||||
# currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
|
||||
@_cache_results
|
||||
def get_all_players(self) -> Dict[int, List[int]]:
|
||||
"""Retrieves a dictionary of all player slot-type players ids on each team."""
|
||||
return {
|
||||
0: [
|
||||
player for player, slot_info in self._multidata["slot_info"].items()
|
||||
if self.get_slot_info(0, player).type == SlotType.player
|
||||
]
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -203,7 +223,7 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of all locations and their associated item metadata per player."""
|
||||
return {
|
||||
(team, player): self.get_player_locations(team, player)
|
||||
for team, players in self.get_team_players().items() for player in players
|
||||
for team, players in self.get_all_players().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -211,7 +231,7 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of games for each player."""
|
||||
return {
|
||||
(team, player): self.get_player_game(team, player)
|
||||
for team, players in self.get_team_players().items() for player in players
|
||||
for team, players in self.get_all_slots().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -219,7 +239,7 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of all locations complete per player."""
|
||||
return {
|
||||
(team, player): len(self.get_player_checked_locations(team, player))
|
||||
for team, players in self.get_team_players().items() for player in players
|
||||
for team, players in self.get_all_players().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -227,14 +247,14 @@ class TrackerData:
|
||||
"""Retrieves a dictionary of all ClientStatus values per player."""
|
||||
return {
|
||||
(team, player): self.get_player_client_status(team, player)
|
||||
for team, players in self.get_team_players().items() for player in players
|
||||
for team, players in self.get_all_players().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
def get_room_long_player_names(self) -> Dict[TeamPlayer, str]:
|
||||
"""Retrieves a dictionary of names with aliases for each player."""
|
||||
long_player_names = {}
|
||||
for team, players in self.get_team_players().items():
|
||||
for team, players in self.get_all_slots().items():
|
||||
for player in players:
|
||||
alias = self.get_player_alias(team, player)
|
||||
if alias:
|
||||
@@ -370,7 +390,8 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Generic",
|
||||
room=tracker_data.room,
|
||||
room_players=tracker_data.get_team_players(),
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
locations_complete=tracker_data.get_room_locations_complete(),
|
||||
total_team_locations=tracker_data.get_team_locations_total_count(),
|
||||
@@ -389,7 +410,6 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
||||
|
||||
# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to
|
||||
# live in their respective world folders.
|
||||
import collections
|
||||
|
||||
from worlds import network_data_package
|
||||
|
||||
@@ -400,7 +420,7 @@ if "Factorio" in network_data_package["games"]:
|
||||
(team, player): {
|
||||
tracker_data.item_id_to_name["Factorio"][item_id]: count
|
||||
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||
} for team, players in tracker_data.get_team_players().items() for player in players
|
||||
} for team, players in tracker_data.get_all_slots().items() for player in players
|
||||
if tracker_data.get_player_game(team, player) == "Factorio"
|
||||
}
|
||||
|
||||
@@ -409,7 +429,8 @@ if "Factorio" in network_data_package["games"]:
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Factorio",
|
||||
room=tracker_data.room,
|
||||
room_players=tracker_data.get_team_players(),
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
locations_complete=tracker_data.get_room_locations_complete(),
|
||||
total_team_locations=tracker_data.get_team_locations_total_count(),
|
||||
@@ -547,7 +568,7 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
if area_name != "Total" else tracker_data._multidata["checks_in_area"][player]["Total"]
|
||||
for area_name in ordered_areas
|
||||
}
|
||||
for team, players in tracker_data.get_team_players().items()
|
||||
for team, players in tracker_data.get_all_slots().items()
|
||||
for player in players
|
||||
if tracker_data.get_slot_info(team, player).type != SlotType.group and
|
||||
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
@@ -585,7 +606,7 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
|
||||
player_location_to_area = {
|
||||
(team, player): _get_location_table(tracker_data._multidata["checks_in_area"][player])
|
||||
for team, players in tracker_data.get_team_players().items()
|
||||
for team, players in tracker_data.get_all_slots().items()
|
||||
for player in players
|
||||
if tracker_data.get_slot_info(team, player).type != SlotType.group and
|
||||
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
@@ -593,15 +614,15 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
|
||||
checks_done: Dict[TeamPlayer, Dict[str: int]] = {
|
||||
(team, player): {location_name: 0 for location_name in default_locations}
|
||||
for team, players in tracker_data.get_team_players().items()
|
||||
for team, players in tracker_data.get_all_slots().items()
|
||||
for player in players
|
||||
if tracker_data.get_slot_info(team, player).type != SlotType.group and
|
||||
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
|
||||
}
|
||||
|
||||
inventories: Dict[TeamPlayer, Dict[int, int]] = {}
|
||||
player_big_key_locations = {(player): set() for player in tracker_data.get_team_players()[0]}
|
||||
player_small_key_locations = {player: set() for player in tracker_data.get_team_players()[0]}
|
||||
player_big_key_locations = {(player): set() for player in tracker_data.get_all_slots()[0]}
|
||||
player_small_key_locations = {player: set() for player in tracker_data.get_all_slots()[0]}
|
||||
group_big_key_locations = set()
|
||||
group_key_locations = set()
|
||||
|
||||
@@ -639,7 +660,8 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="A Link to the Past",
|
||||
room=tracker_data.room,
|
||||
room_players=tracker_data.get_team_players(),
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
locations_complete=tracker_data.get_room_locations_complete(),
|
||||
total_team_locations=tracker_data.get_team_locations_total_count(),
|
||||
|
||||
@@ -11,11 +11,14 @@ from flask import request, flash, redirect, url_for, session, render_template
|
||||
from markupsafe import Markup
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
import schema
|
||||
|
||||
import MultiServer
|
||||
from NetUtils import SlotType
|
||||
from Utils import VersionException, __version__
|
||||
from worlds import GamesPackage
|
||||
from worlds.Files import AutoPatchRegister
|
||||
from worlds.AutoWorld import data_package_checksum
|
||||
from . import app
|
||||
from .models import Seed, Room, Slot, GameDataPackage
|
||||
|
||||
@@ -23,6 +26,15 @@ banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gb
|
||||
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
|
||||
allowed_generation_extensions = (".archipelago", ".zip")
|
||||
|
||||
games_package_schema = schema.Schema({
|
||||
"item_name_groups": {str: [str]},
|
||||
"item_name_to_id": {str: int},
|
||||
"location_name_groups": {str: [str]},
|
||||
"location_name_to_id": {str: int},
|
||||
schema.Optional("checksum"): str,
|
||||
schema.Optional("version"): int,
|
||||
})
|
||||
|
||||
|
||||
def allowed_options(filename: str) -> bool:
|
||||
return filename.endswith(allowed_options_extensions)
|
||||
@@ -37,6 +49,8 @@ def banned_file(filename: str) -> bool:
|
||||
|
||||
|
||||
def process_multidata(compressed_multidata, files={}):
|
||||
game_data: GamesPackage
|
||||
|
||||
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
|
||||
|
||||
slots: typing.Set[Slot] = set()
|
||||
@@ -45,11 +59,19 @@ def process_multidata(compressed_multidata, files={}):
|
||||
game_data_packages: typing.List[GameDataPackage] = []
|
||||
for game, game_data in decompressed_multidata["datapackage"].items():
|
||||
if game_data.get("checksum"):
|
||||
original_checksum = game_data.pop("checksum")
|
||||
game_data = games_package_schema.validate(game_data)
|
||||
game_data = {key: value for key, value in sorted(game_data.items())}
|
||||
game_data["checksum"] = data_package_checksum(game_data)
|
||||
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||
data=pickle.dumps(game_data))
|
||||
if original_checksum != game_data["checksum"]:
|
||||
raise Exception(f"Original checksum {original_checksum} != "
|
||||
f"calculated checksum {game_data['checksum']} "
|
||||
f"for game {game}.")
|
||||
decompressed_multidata["datapackage"][game] = {
|
||||
"version": game_data.get("version", 0),
|
||||
"checksum": game_data["checksum"]
|
||||
"checksum": game_data["checksum"],
|
||||
}
|
||||
try:
|
||||
commit() # commit game data package
|
||||
@@ -64,14 +86,15 @@ def process_multidata(compressed_multidata, files={}):
|
||||
if slot_info.type == SlotType.group:
|
||||
continue
|
||||
slots.add(Slot(data=files.get(slot, None),
|
||||
player_name=slot_info.name,
|
||||
player_id=slot,
|
||||
game=slot_info.game))
|
||||
player_name=slot_info.name,
|
||||
player_id=slot,
|
||||
game=slot_info.game))
|
||||
flush() # commit slots
|
||||
|
||||
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
|
||||
return slots, compressed_multidata
|
||||
|
||||
|
||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||
if not owner:
|
||||
owner = session["_id"]
|
||||
|
||||
505
ZillionClient.py
505
ZillionClient.py
@@ -1,505 +1,10 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import platform
|
||||
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
# CommonClient import first to trigger ModuleUpdater
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
ClientCommandProcessor, logger, get_base_parser
|
||||
from NetUtils import ClientStatus
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
|
||||
import colorama
|
||||
|
||||
from zilliandomizer.zri.memory import Memory
|
||||
from zilliandomizer.zri import events
|
||||
from zilliandomizer.utils.loc_name_maps import id_to_loc
|
||||
from zilliandomizer.options import Chars
|
||||
from zilliandomizer.patch import RescueInfo
|
||||
|
||||
from worlds.zillion.id_maps import make_id_to_others
|
||||
from worlds.zillion.config import base_id, zillion_map
|
||||
|
||||
|
||||
class ZillionCommandProcessor(ClientCommandProcessor):
|
||||
ctx: "ZillionContext"
|
||||
|
||||
def _cmd_sms(self) -> None:
|
||||
""" Tell the client that Zillion is running in RetroArch. """
|
||||
logger.info("ready to look for game")
|
||||
self.ctx.look_for_retroarch.set()
|
||||
|
||||
def _cmd_map(self) -> None:
|
||||
""" Toggle view of the map tracker. """
|
||||
self.ctx.ui_toggle_map()
|
||||
|
||||
|
||||
class ToggleCallback(Protocol):
|
||||
def __call__(self) -> None: ...
|
||||
|
||||
|
||||
class SetRoomCallback(Protocol):
|
||||
def __call__(self, rooms: List[List[int]]) -> None: ...
|
||||
|
||||
|
||||
class ZillionContext(CommonContext):
|
||||
game = "Zillion"
|
||||
command_processor = ZillionCommandProcessor
|
||||
items_handling = 1 # receive items from other players
|
||||
|
||||
known_name: Optional[str]
|
||||
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
|
||||
|
||||
from_game: "asyncio.Queue[events.EventFromGame]"
|
||||
to_game: "asyncio.Queue[events.EventToGame]"
|
||||
ap_local_count: int
|
||||
""" local checks watched by server """
|
||||
next_item: int
|
||||
""" index in `items_received` """
|
||||
ap_id_to_name: Dict[int, str]
|
||||
ap_id_to_zz_id: Dict[int, int]
|
||||
start_char: Chars = "JJ"
|
||||
rescues: Dict[int, RescueInfo] = {}
|
||||
loc_mem_to_id: Dict[int, int] = {}
|
||||
got_room_info: asyncio.Event
|
||||
""" flag for connected to server """
|
||||
got_slot_data: asyncio.Event
|
||||
""" serves as a flag for whether I am logged in to the server """
|
||||
|
||||
look_for_retroarch: asyncio.Event
|
||||
"""
|
||||
There is a bug in Python in Windows
|
||||
https://github.com/python/cpython/issues/91227
|
||||
that makes it so if I look for RetroArch before it's ready,
|
||||
it breaks the asyncio udp transport system.
|
||||
|
||||
As a workaround, we don't look for RetroArch until this event is set.
|
||||
"""
|
||||
|
||||
ui_toggle_map: ToggleCallback
|
||||
ui_set_rooms: SetRoomCallback
|
||||
""" parameter is y 16 x 8 numbers to show in each room """
|
||||
|
||||
def __init__(self,
|
||||
server_address: str,
|
||||
password: str) -> None:
|
||||
super().__init__(server_address, password)
|
||||
self.known_name = None
|
||||
self.from_game = asyncio.Queue()
|
||||
self.to_game = asyncio.Queue()
|
||||
self.got_room_info = asyncio.Event()
|
||||
self.got_slot_data = asyncio.Event()
|
||||
self.ui_toggle_map = lambda: None
|
||||
self.ui_set_rooms = lambda rooms: None
|
||||
|
||||
self.look_for_retroarch = asyncio.Event()
|
||||
if platform.system() != "Windows":
|
||||
# asyncio udp bug is only on Windows
|
||||
self.look_for_retroarch.set()
|
||||
|
||||
self.reset_game_state()
|
||||
|
||||
def reset_game_state(self) -> None:
|
||||
for _ in range(self.from_game.qsize()):
|
||||
self.from_game.get_nowait()
|
||||
for _ in range(self.to_game.qsize()):
|
||||
self.to_game.get_nowait()
|
||||
self.got_slot_data.clear()
|
||||
|
||||
self.ap_local_count = 0
|
||||
self.next_item = 0
|
||||
self.ap_id_to_name = {}
|
||||
self.ap_id_to_zz_id = {}
|
||||
self.rescues = {}
|
||||
self.loc_mem_to_id = {}
|
||||
|
||||
self.locations_checked.clear()
|
||||
self.missing_locations.clear()
|
||||
self.checked_locations.clear()
|
||||
self.finished_game = False
|
||||
self.items_received.clear()
|
||||
|
||||
# override
|
||||
def on_deathlink(self, data: Dict[str, Any]) -> None:
|
||||
self.to_game.put_nowait(events.DeathEventToGame())
|
||||
return super().on_deathlink(data)
|
||||
|
||||
# override
|
||||
async def server_auth(self, password_requested: bool = False) -> None:
|
||||
if password_requested and not self.password:
|
||||
await super().server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('waiting for connection to game...')
|
||||
return
|
||||
logger.info("logging in to server...")
|
||||
await self.send_connect()
|
||||
|
||||
# override
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
from kivy.core.text import Label as CoreLabel
|
||||
from kivy.graphics import Ellipse, Color, Rectangle
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
class ZillionManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Zillion Client"
|
||||
|
||||
class MapPanel(Widget):
|
||||
MAP_WIDTH: ClassVar[int] = 281
|
||||
|
||||
_number_textures: List[Any] = []
|
||||
rooms: List[List[int]] = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.rooms = [[0 for _ in range(8)] for _ in range(16)]
|
||||
|
||||
self._make_numbers()
|
||||
self.update_map()
|
||||
|
||||
self.bind(pos=self.update_map)
|
||||
# self.bind(size=self.update_bg)
|
||||
|
||||
def _make_numbers(self) -> None:
|
||||
self._number_textures = []
|
||||
for n in range(10):
|
||||
label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1))
|
||||
label.refresh()
|
||||
self._number_textures.append(label.texture)
|
||||
|
||||
def update_map(self, *args: Any) -> None:
|
||||
self.canvas.clear()
|
||||
|
||||
with self.canvas:
|
||||
Color(1, 1, 1, 1)
|
||||
Rectangle(source=zillion_map,
|
||||
pos=self.pos,
|
||||
size=(ZillionManager.MapPanel.MAP_WIDTH,
|
||||
int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image
|
||||
for y in range(16):
|
||||
for x in range(8):
|
||||
num = self.rooms[15 - y][x]
|
||||
if num > 0:
|
||||
Color(0, 0, 0, 0.4)
|
||||
pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24]
|
||||
Ellipse(size=[22, 22], pos=pos)
|
||||
Color(1, 1, 1, 1)
|
||||
pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24]
|
||||
num_texture = self._number_textures[num]
|
||||
Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
|
||||
|
||||
def build(self) -> Layout:
|
||||
container = super().build()
|
||||
self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0)
|
||||
self.main_area_container.add_widget(self.map_widget)
|
||||
return container
|
||||
|
||||
def toggle_map_width(self) -> None:
|
||||
if self.map_widget.width == 0:
|
||||
self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH
|
||||
else:
|
||||
self.map_widget.width = 0
|
||||
self.container.do_layout()
|
||||
|
||||
def set_rooms(self, rooms: List[List[int]]) -> None:
|
||||
self.map_widget.rooms = rooms
|
||||
self.map_widget.update_map()
|
||||
|
||||
self.ui = ZillionManager(self)
|
||||
self.ui_toggle_map = lambda: self.ui.toggle_map_width()
|
||||
self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
|
||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run()
|
||||
self.ui_task = asyncio.create_task(run_co, name="UI")
|
||||
|
||||
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
||||
self.room_item_numbers_to_ui()
|
||||
if cmd == "Connected":
|
||||
logger.info("logged in to Archipelago server")
|
||||
if "slot_data" not in args:
|
||||
logger.warn("`Connected` packet missing `slot_data`")
|
||||
return
|
||||
slot_data = args["slot_data"]
|
||||
|
||||
if "start_char" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
|
||||
return
|
||||
self.start_char = slot_data['start_char']
|
||||
if self.start_char not in {"Apple", "Champ", "JJ"}:
|
||||
logger.warn("invalid Zillion `Connected` packet, "
|
||||
f"`slot_data` `start_char` has invalid value: {self.start_char}")
|
||||
|
||||
if "rescues" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
|
||||
return
|
||||
rescues = slot_data["rescues"]
|
||||
self.rescues = {}
|
||||
for rescue_id, json_info in rescues.items():
|
||||
assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
|
||||
# TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
|
||||
assert json_info["start_char"] == self.start_char, \
|
||||
f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
|
||||
ri = RescueInfo(json_info["start_char"],
|
||||
json_info["room_code"],
|
||||
json_info["mask"])
|
||||
self.rescues[0 if rescue_id == "0" else 1] = ri
|
||||
|
||||
if "loc_mem_to_id" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
|
||||
return
|
||||
loc_mem_to_id = slot_data["loc_mem_to_id"]
|
||||
self.loc_mem_to_id = {}
|
||||
for mem_str, id_str in loc_mem_to_id.items():
|
||||
mem = int(mem_str)
|
||||
id_ = int(id_str)
|
||||
room_i = mem // 256
|
||||
assert 0 <= room_i < 74
|
||||
assert id_ in id_to_loc
|
||||
self.loc_mem_to_id[mem] = id_
|
||||
|
||||
if len(self.loc_mem_to_id) != 394:
|
||||
logger.warn("invalid Zillion `Connected` packet, "
|
||||
f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
|
||||
|
||||
self.got_slot_data.set()
|
||||
|
||||
payload = {
|
||||
"cmd": "Get",
|
||||
"keys": [f"zillion-{self.auth}-doors"]
|
||||
}
|
||||
async_start(self.send_msgs([payload]))
|
||||
elif cmd == "Retrieved":
|
||||
if "keys" not in args:
|
||||
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
|
||||
return
|
||||
keys = cast(Dict[str, Optional[str]], args["keys"])
|
||||
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
|
||||
if doors_b64:
|
||||
logger.info("received door data from server")
|
||||
doors = base64.b64decode(doors_b64)
|
||||
self.to_game.put_nowait(events.DoorEventToGame(doors))
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args["seed_name"]
|
||||
self.got_room_info.set()
|
||||
|
||||
def room_item_numbers_to_ui(self) -> None:
|
||||
rooms = [[0 for _ in range(8)] for _ in range(16)]
|
||||
for loc_id in self.missing_locations:
|
||||
loc_id_small = loc_id - base_id
|
||||
loc_name = id_to_loc[loc_id_small]
|
||||
y = ord(loc_name[0]) - 65
|
||||
x = ord(loc_name[2]) - 49
|
||||
if y == 9 and x == 5:
|
||||
# don't show main computer in numbers
|
||||
continue
|
||||
assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}"
|
||||
rooms[y][x] += 1
|
||||
# TODO: also add locations with locals lost from loading save state or reset
|
||||
self.ui_set_rooms(rooms)
|
||||
|
||||
def process_from_game_queue(self) -> None:
|
||||
if self.from_game.qsize():
|
||||
event_from_game = self.from_game.get_nowait()
|
||||
if isinstance(event_from_game, events.AcquireLocationEventFromGame):
|
||||
server_id = event_from_game.id + base_id
|
||||
loc_name = id_to_loc[event_from_game.id]
|
||||
self.locations_checked.add(server_id)
|
||||
if server_id in self.missing_locations:
|
||||
self.ap_local_count += 1
|
||||
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
|
||||
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
|
||||
async_start(self.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [server_id]}
|
||||
]))
|
||||
else:
|
||||
# This will happen a lot in Zillion,
|
||||
# because all the key words are local and unwatched by the server.
|
||||
logger.debug(f"DEBUG: {loc_name} not in missing")
|
||||
elif isinstance(event_from_game, events.DeathEventFromGame):
|
||||
async_start(self.send_death())
|
||||
elif isinstance(event_from_game, events.WinEventFromGame):
|
||||
if not self.finished_game:
|
||||
async_start(self.send_msgs([
|
||||
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
|
||||
]))
|
||||
self.finished_game = True
|
||||
elif isinstance(event_from_game, events.DoorEventFromGame):
|
||||
if self.auth:
|
||||
doors_b64 = base64.b64encode(event_from_game.doors).decode()
|
||||
payload = {
|
||||
"cmd": "Set",
|
||||
"key": f"zillion-{self.auth}-doors",
|
||||
"operations": [{"operation": "replace", "value": doors_b64}]
|
||||
}
|
||||
async_start(self.send_msgs([payload]))
|
||||
else:
|
||||
logger.warning(f"WARNING: unhandled event from game {event_from_game}")
|
||||
|
||||
def process_items_received(self) -> None:
|
||||
if len(self.items_received) > self.next_item:
|
||||
zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
|
||||
for index in range(self.next_item, len(self.items_received)):
|
||||
ap_id = self.items_received[index].item
|
||||
from_name = self.player_names[self.items_received[index].player]
|
||||
# TODO: colors in this text, like sni client?
|
||||
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
|
||||
self.to_game.put_nowait(
|
||||
events.ItemEventToGame(zz_item_ids)
|
||||
)
|
||||
self.next_item = len(self.items_received)
|
||||
|
||||
|
||||
def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
|
||||
""" returns player name, and end of seed string """
|
||||
if len(data) == 0:
|
||||
# no connection to game
|
||||
return "", "xxx"
|
||||
null_index = data.find(b'\x00')
|
||||
if null_index == -1:
|
||||
logger.warning(f"invalid game id in rom {repr(data)}")
|
||||
null_index = len(data)
|
||||
name = data[:null_index].decode()
|
||||
null_index_2 = data.find(b'\x00', null_index + 1)
|
||||
if null_index_2 == -1:
|
||||
null_index_2 = len(data)
|
||||
seed_name = data[null_index + 1:null_index_2].decode()
|
||||
|
||||
return name, seed_name
|
||||
|
||||
|
||||
async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
logger.info("started zillion sync task")
|
||||
|
||||
# to work around the Python bug where we can't check for RetroArch
|
||||
if not ctx.look_for_retroarch.is_set():
|
||||
logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.look_for_retroarch.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait())
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
last_log = ""
|
||||
|
||||
def log_no_spam(msg: str) -> None:
|
||||
nonlocal last_log
|
||||
if msg != last_log:
|
||||
last_log = msg
|
||||
logger.info(msg)
|
||||
|
||||
# to only show this message once per client run
|
||||
help_message_shown = False
|
||||
|
||||
with Memory(ctx.from_game, ctx.to_game) as memory:
|
||||
while not ctx.exit_event.is_set():
|
||||
ram = await memory.read()
|
||||
game_id = memory.get_rom_to_ram_data(ram)
|
||||
name, seed_end = name_seed_from_ram(game_id)
|
||||
if len(name):
|
||||
if name == ctx.known_name:
|
||||
ctx.auth = name
|
||||
# this is the name we know
|
||||
if ctx.server and ctx.server.socket: # type: ignore
|
||||
if ctx.got_room_info.is_set():
|
||||
if ctx.seed_name and ctx.seed_name.endswith(seed_end):
|
||||
# correct seed
|
||||
if memory.have_generation_info():
|
||||
log_no_spam("everything connected")
|
||||
await memory.process_ram(ram)
|
||||
ctx.process_from_game_queue()
|
||||
ctx.process_items_received()
|
||||
else: # no generation info
|
||||
if ctx.got_slot_data.is_set():
|
||||
memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
|
||||
ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
|
||||
make_id_to_others(ctx.start_char)
|
||||
ctx.next_item = 0
|
||||
ctx.ap_local_count = len(ctx.checked_locations)
|
||||
else: # no slot data yet
|
||||
async_start(ctx.send_connect())
|
||||
log_no_spam("logging in to server...")
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.got_slot_data.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait()),
|
||||
asyncio.create_task(asyncio.sleep(6))
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
else: # not correct seed name
|
||||
log_no_spam("incorrect seed - did you mix up roms?")
|
||||
else: # no room info
|
||||
# If we get here, it looks like `RoomInfo` packet got lost
|
||||
log_no_spam("waiting for room info from server...")
|
||||
else: # server not connected
|
||||
log_no_spam("waiting for server connection...")
|
||||
else: # new game
|
||||
log_no_spam("connected to new game")
|
||||
await ctx.disconnect()
|
||||
ctx.reset_server_state()
|
||||
ctx.seed_name = None
|
||||
ctx.got_room_info.clear()
|
||||
ctx.reset_game_state()
|
||||
memory.reset_game_state()
|
||||
|
||||
ctx.auth = name
|
||||
ctx.known_name = name
|
||||
async_start(ctx.connect())
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.got_room_info.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait()),
|
||||
asyncio.create_task(asyncio.sleep(6))
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
else: # no name found in game
|
||||
if not help_message_shown:
|
||||
logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')
|
||||
help_message_shown = True
|
||||
log_no_spam("looking for connection to game...")
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
await asyncio.sleep(0.09375)
|
||||
logger.info("zillion sync task ending")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apzl Archipelago Binary Patch file')
|
||||
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating sms rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.connect = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
|
||||
ctx = ZillionContext(args.connect, args.password)
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
sync_task = asyncio.create_task(zillion_sync_task(ctx))
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
logger.debug("waiting for sync task to end")
|
||||
await sync_task
|
||||
logger.debug("sync task ended")
|
||||
await ctx.shutdown()
|
||||
import Utils # noqa: E402
|
||||
|
||||
from worlds.zillion.client import launch # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("ZillionClient", exception_logger="Client")
|
||||
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
launch()
|
||||
|
||||
@@ -456,6 +456,7 @@ function send_receive ()
|
||||
failed_guard_response = response
|
||||
end
|
||||
else
|
||||
if type(response) ~= "string" then response = "Unknown error" end
|
||||
res[i] = {type = "ERROR", err = response}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
# The Legend of Zelda (1)
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ There are also a number of community-supported libraries available that implemen
|
||||
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
|
||||
| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | |
|
||||
| Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | |
|
||||
| Game Maker + Studio 1.x | [gm-apclientpp](https://github.com/black-sliver/gm-apclientpp) | For GM7, GM8 and GMS1.x, maybe older |
|
||||
| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | |
|
||||
|
||||
## Synchronizing Items
|
||||
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
|
||||
@@ -380,12 +382,13 @@ Additional arguments sent in this package will also be added to the [Retrieved](
|
||||
|
||||
Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`.
|
||||
|
||||
| Name | Type | Notes |
|
||||
|------------------------------|-------------------------------|---------------------------------------------------|
|
||||
| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
|
||||
| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
|
||||
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
||||
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
|
||||
| Name | Type | Notes |
|
||||
|----------------------------------|-------------------------------|-------------------------------------------------------|
|
||||
| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
|
||||
| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
|
||||
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
||||
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
|
||||
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
|
||||
|
||||
### Set
|
||||
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
||||
@@ -674,8 +677,8 @@ Tags are represented as a list of strings, the common Client tags follow:
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ---- |
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
|
||||
@@ -27,14 +27,15 @@ Choice, and defining `alias_true = option_full`.
|
||||
- All options support `random` as a generic option. `random` chooses from any of the available values for that option,
|
||||
and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||
|
||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
|
||||
create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
|
||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option
|
||||
to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our
|
||||
option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass:
|
||||
|
||||
```python
|
||||
# options.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Toggle, PerGameCommonOptions
|
||||
from Options import Toggle, Range, Choice, PerGameCommonOptions
|
||||
|
||||
|
||||
class StartingSword(Toggle):
|
||||
@@ -42,13 +43,33 @@ class StartingSword(Toggle):
|
||||
display_name = "Start With Sword"
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Sets overall game difficulty."""
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options
|
||||
alias_expert = 2 # same as hard
|
||||
default = 1 # default to normal
|
||||
|
||||
|
||||
class FinalBossHP(Range):
|
||||
"""Sets the HP of the final boss"""
|
||||
display_name = "Final Boss HP"
|
||||
range_start = 100
|
||||
range_end = 10000
|
||||
default = 2000
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExampleGameOptions(PerGameCommonOptions):
|
||||
starting_sword: StartingSword
|
||||
difficulty: Difficulty
|
||||
final_boss_health: FinalBossHP
|
||||
```
|
||||
|
||||
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
|
||||
to our world's `__init__.py`:
|
||||
To then submit this to the multiworld, we add it to our world's `__init__.py`:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 120 character per line for all source files.
|
||||
* Avoid white space errors like trailing spaces.
|
||||
|
||||
|
||||
## Python Code
|
||||
|
||||
* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences.
|
||||
@@ -18,9 +17,10 @@
|
||||
* Use type annotations where possible for function signatures and class members.
|
||||
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
|
||||
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
|
||||
* New classes, attributes, and methods in core code should have docstrings that follow
|
||||
[reST style](https://peps.python.org/pep-0287/).
|
||||
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
|
||||
|
||||
|
||||
## Markdown
|
||||
|
||||
* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html).
|
||||
@@ -30,20 +30,17 @@
|
||||
* One space between bullet/number and text.
|
||||
* No lazy numbering.
|
||||
|
||||
|
||||
## HTML
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* kebab-case for ids and classes.
|
||||
|
||||
|
||||
## CSS
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* `{` on the same line as the selector.
|
||||
* No space between selector and `{`.
|
||||
|
||||
|
||||
## JS
|
||||
|
||||
* Indent with 2 spaces.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -197,7 +197,7 @@ begin
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.38.33130') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
||||
@@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
|
||||
game: # Pick a game to play
|
||||
A Link to the Past: 1
|
||||
requires:
|
||||
version: 0.4.3 # Version of Archipelago required for this yaml to work as expected.
|
||||
version: 0.4.4 # Version of Archipelago required for this yaml to work as expected.
|
||||
A Link to the Past:
|
||||
progression_balancing:
|
||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
|
||||
@@ -4,7 +4,7 @@ PyYAML>=6.0.1
|
||||
jellyfish>=1.0.3
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.2.1
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.0.0
|
||||
certifi>=2023.11.17
|
||||
|
||||
@@ -597,8 +597,8 @@ class ServerOptions(Group):
|
||||
disable_item_cheat: Union[DisableItemCheat, bool] = False
|
||||
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
|
||||
hint_cost: HintCost = HintCost(10)
|
||||
release_mode: ReleaseMode = ReleaseMode("goal")
|
||||
collect_mode: CollectMode = CollectMode("goal")
|
||||
release_mode: ReleaseMode = ReleaseMode("auto")
|
||||
collect_mode: CollectMode = CollectMode("auto")
|
||||
remaining_mode: RemainingMode = RemainingMode("goal")
|
||||
auto_shutdown: AutoShutdown = AutoShutdown(0)
|
||||
compatibility: Compatibility = Compatibility(2)
|
||||
@@ -673,7 +673,7 @@ class GeneratorOptions(Group):
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses")
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
|
||||
15
setup.py
15
setup.py
@@ -54,7 +54,6 @@ if __name__ == "__main__":
|
||||
# TODO: move stuff to not require this
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
||||
ModuleUpdate.update_ran = False # restore for later
|
||||
|
||||
from worlds.LauncherComponents import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
@@ -76,7 +75,6 @@ non_apworlds: set = {
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Raft",
|
||||
"Secret of Evermore",
|
||||
"Slay the Spire",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
@@ -305,7 +303,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
print(f"Outputting to: {self.buildfolder}")
|
||||
os.makedirs(self.buildfolder, exist_ok=True)
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
|
||||
ModuleUpdate.update(yes=self.yes)
|
||||
|
||||
# auto-build cython modules
|
||||
@@ -352,6 +349,18 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
||||
print(f"copying {folder} -> {self.libfolder}")
|
||||
# windows needs Visual Studio C++ Redistributable
|
||||
# Installer works for x64 and arm64
|
||||
print("Downloading VC Redist")
|
||||
import certifi
|
||||
import ssl
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
||||
with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe",
|
||||
context=context) as download:
|
||||
vc_redist = download.read()
|
||||
print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", )
|
||||
with open("VC_redist.x64.exe", "wb") as vc_file:
|
||||
vc_file.write(vc_redist)
|
||||
|
||||
for data in self.extra_data:
|
||||
self.installfile(Path(data))
|
||||
|
||||
@@ -285,7 +285,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
excluded = self.multiworld.exclude_locations[1].value
|
||||
excluded = self.multiworld.worlds[1].options.exclude_locations.value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
|
||||
7
test/benchmark/__init__.py
Normal file
7
test/benchmark/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
if __name__ == "__main__":
|
||||
import path_change
|
||||
path_change.change_home()
|
||||
import load_worlds
|
||||
load_worlds.run_load_worlds_benchmark()
|
||||
import locations
|
||||
locations.run_locations_benchmark()
|
||||
27
test/benchmark/load_worlds.py
Normal file
27
test/benchmark/load_worlds.py
Normal file
@@ -0,0 +1,27 @@
|
||||
def run_load_worlds_benchmark():
|
||||
"""List worlds and their load time.
|
||||
Note that any first-time imports will be attributed to that world, as it is cached afterwards.
|
||||
Likely best used with isolated worlds to measure their time alone."""
|
||||
import logging
|
||||
|
||||
from Utils import init_logging
|
||||
|
||||
# get some general imports cached, to prevent it from being attributed to one world.
|
||||
import orjson
|
||||
orjson.loads("{}") # orjson runs initialization on first use
|
||||
|
||||
import BaseClasses, Launcher, Fill
|
||||
|
||||
from worlds import world_sources
|
||||
|
||||
init_logging("Benchmark Runner")
|
||||
logger = logging.getLogger("Benchmark")
|
||||
|
||||
for module in world_sources:
|
||||
logger.info(f"{module} took {module.time_taken:.4f} seconds.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from path_change import change_home
|
||||
change_home()
|
||||
run_load_worlds_benchmark()
|
||||
101
test/benchmark/locations.py
Normal file
101
test/benchmark/locations.py
Normal file
@@ -0,0 +1,101 @@
|
||||
def run_locations_benchmark():
|
||||
import argparse
|
||||
import logging
|
||||
import gc
|
||||
import collections
|
||||
import typing
|
||||
import sys
|
||||
|
||||
from time_it import TimeIt
|
||||
|
||||
from Utils import init_logging
|
||||
from BaseClasses import MultiWorld, CollectionState, Location
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
init_logging("Benchmark Runner")
|
||||
logger = logging.getLogger("Benchmark")
|
||||
|
||||
class BenchmarkRunner:
|
||||
gen_steps: typing.Tuple[str, ...] = (
|
||||
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
rule_iterations: int = 100_000
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
else:
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
|
||||
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
|
||||
with TimeIt(f"{test_location.game} {self.rule_iterations} "
|
||||
f"runs of {test_location}.access_rule({state_name})", logger) as t:
|
||||
for _ in range(self.rule_iterations):
|
||||
test_location.access_rule(state)
|
||||
# if time is taken to disentangle complex ref chains,
|
||||
# this time should be attributed to the rule.
|
||||
gc.collect()
|
||||
return t.dif
|
||||
|
||||
def main(self):
|
||||
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
|
||||
summary_data: typing.Dict[str, collections.Counter[str]] = {
|
||||
"empty_state": collections.Counter(),
|
||||
"all_state": collections.Counter(),
|
||||
}
|
||||
try:
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
multiworld.set_seed(0)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = argparse.Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(getattr(option, "default"))
|
||||
})
|
||||
multiworld.set_options(args)
|
||||
|
||||
gc.collect()
|
||||
for step in self.gen_steps:
|
||||
with TimeIt(f"{game} step {step}", logger):
|
||||
call_all(multiworld, step)
|
||||
gc.collect()
|
||||
|
||||
locations = sorted(multiworld.get_unfilled_locations())
|
||||
if not locations:
|
||||
continue
|
||||
|
||||
all_state = multiworld.get_all_state(False)
|
||||
for location in locations:
|
||||
time_taken = self.location_test(location, multiworld.state, "empty_state")
|
||||
summary_data["empty_state"][location.name] = time_taken
|
||||
|
||||
time_taken = self.location_test(location, all_state, "all_state")
|
||||
summary_data["all_state"][location.name] = time_taken
|
||||
|
||||
total_empty_state = sum(summary_data["empty_state"].values())
|
||||
total_all_state = sum(summary_data["all_state"].values())
|
||||
|
||||
logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
|
||||
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
|
||||
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
|
||||
logger.info(f"Top times in empty_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['empty_state'])}")
|
||||
logger.info(f"Top times in all_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['all_state'])}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
runner = BenchmarkRunner()
|
||||
runner.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from path_change import change_home
|
||||
change_home()
|
||||
run_locations_benchmark()
|
||||
16
test/benchmark/path_change.py
Normal file
16
test/benchmark/path_change.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def change_home():
|
||||
"""Allow scripts to run from "this" folder."""
|
||||
old_home = os.path.dirname(__file__)
|
||||
sys.path.remove(old_home)
|
||||
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
||||
os.chdir(new_home)
|
||||
sys.path.append(new_home)
|
||||
# fallback to local import
|
||||
sys.path.append(old_home)
|
||||
|
||||
from Utils import local_path
|
||||
local_path.cached_path = new_home
|
||||
23
test/benchmark/time_it.py
Normal file
23
test/benchmark/time_it.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import time
|
||||
|
||||
|
||||
class TimeIt:
|
||||
def __init__(self, name: str, time_logger=None):
|
||||
self.name = name
|
||||
self.logger = time_logger
|
||||
self.timer = None
|
||||
self.end_timer = None
|
||||
|
||||
def __enter__(self):
|
||||
self.timer = time.perf_counter()
|
||||
return self
|
||||
|
||||
@property
|
||||
def dif(self):
|
||||
return self.end_timer - self.timer
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if not self.end_timer:
|
||||
self.end_timer = time.perf_counter()
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
|
||||
@@ -11,30 +11,30 @@ from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item
|
||||
from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
|
||||
|
||||
|
||||
def generate_multi_world(players: int = 1) -> MultiWorld:
|
||||
multi_world = MultiWorld(players)
|
||||
multi_world.player_name = {}
|
||||
multi_world.state = CollectionState(multi_world)
|
||||
def generate_multiworld(players: int = 1) -> MultiWorld:
|
||||
multiworld = MultiWorld(players)
|
||||
multiworld.player_name = {}
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
for i in range(players):
|
||||
player_id = i+1
|
||||
world = World(multi_world, player_id)
|
||||
multi_world.game[player_id] = f"Game {player_id}"
|
||||
multi_world.worlds[player_id] = world
|
||||
multi_world.player_name[player_id] = "Test Player " + str(player_id)
|
||||
region = Region("Menu", player_id, multi_world, "Menu Region Hint")
|
||||
multi_world.regions.append(region)
|
||||
world = World(multiworld, player_id)
|
||||
multiworld.game[player_id] = f"Game {player_id}"
|
||||
multiworld.worlds[player_id] = world
|
||||
multiworld.player_name[player_id] = "Test Player " + str(player_id)
|
||||
region = Region("Menu", player_id, multiworld, "Menu Region Hint")
|
||||
multiworld.regions.append(region)
|
||||
for option_key, option in Options.PerGameCommonOptions.type_hints.items():
|
||||
if hasattr(multi_world, option_key):
|
||||
getattr(multi_world, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
|
||||
if hasattr(multiworld, option_key):
|
||||
getattr(multiworld, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
|
||||
else:
|
||||
setattr(multi_world, option_key, {player_id: option.from_any(getattr(option, "default"))})
|
||||
setattr(multiworld, option_key, {player_id: option.from_any(getattr(option, "default"))})
|
||||
# TODO - remove this loop once all worlds use options dataclasses
|
||||
world.options = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id]
|
||||
world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id]
|
||||
for option_key in world.options_dataclass.type_hints})
|
||||
|
||||
multi_world.set_seed(0)
|
||||
multiworld.set_seed(0)
|
||||
|
||||
return multi_world
|
||||
return multiworld
|
||||
|
||||
|
||||
class PlayerDefinition(object):
|
||||
@@ -46,8 +46,8 @@ class PlayerDefinition(object):
|
||||
basic_items: List[Item]
|
||||
regions: List[Region]
|
||||
|
||||
def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
|
||||
self.multiworld = world
|
||||
def __init__(self, multiworld: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
|
||||
self.multiworld = multiworld
|
||||
self.id = id
|
||||
self.menu = menu
|
||||
self.locations = locations
|
||||
@@ -72,7 +72,7 @@ class PlayerDefinition(object):
|
||||
return region
|
||||
|
||||
|
||||
def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||
def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||
items = items.copy()
|
||||
while len(items) > 0:
|
||||
location = region.locations.pop(0)
|
||||
@@ -80,7 +80,7 @@ def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[It
|
||||
if location.item:
|
||||
return items
|
||||
item = items.pop(0)
|
||||
world.push_item(location, item, False)
|
||||
multiworld.push_item(location, item, False)
|
||||
location.event = item.advancement
|
||||
|
||||
return items
|
||||
@@ -94,15 +94,15 @@ def region_contains(region: Region, item: Item) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
|
||||
menu = multi_world.get_region("Menu", player_id)
|
||||
def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
|
||||
menu = multiworld.get_region("Menu", player_id)
|
||||
locations = generate_locations(location_count, player_id, None, menu)
|
||||
prog_items = generate_items(prog_item_count, player_id, True)
|
||||
multi_world.itempool += prog_items
|
||||
multiworld.itempool += prog_items
|
||||
basic_items = generate_items(basic_item_count, player_id, False)
|
||||
multi_world.itempool += basic_items
|
||||
multiworld.itempool += basic_items
|
||||
|
||||
return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items)
|
||||
return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items)
|
||||
|
||||
|
||||
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
|
||||
@@ -134,15 +134,15 @@ def names(objs: list) -> Iterable[str]:
|
||||
class TestFillRestrictive(unittest.TestCase):
|
||||
def test_basic_fill(self):
|
||||
"""Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item1)
|
||||
@@ -152,16 +152,16 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_ordered_fill(self):
|
||||
"""Tests `fill_restrictive` fulfills set rules"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[0].name, player1.id) and state.has(items[1].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[0])
|
||||
@@ -169,8 +169,8 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_partial_fill(self):
|
||||
"""Tests that `fill_restrictive` returns unfilled locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 3, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
@@ -178,14 +178,14 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
loc1 = player1.locations[1]
|
||||
loc2 = player1.locations[2]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item1.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(
|
||||
item0.name, player1.id))
|
||||
# forces a swap
|
||||
set_rule(loc2, lambda state: state.has(
|
||||
item0.name, player1.id))
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item0)
|
||||
@@ -195,19 +195,19 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_minimal_fill(self):
|
||||
"""Test that fill for minimal player can have unreachable items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[1].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[1])
|
||||
@@ -220,15 +220,15 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
the non-minimal player get all items.
|
||||
"""
|
||||
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||
player2 = generate_player_data(multi_world, 2, 3, 3)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||
player2 = generate_player_data(multiworld, 2, 3, 3)
|
||||
|
||||
multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal
|
||||
multi_world.accessibility[player2.id].value = multi_world.accessibility[player2.id].option_full
|
||||
multiworld.worlds[player1.id].options.accessibility.value = Accessibility.option_minimal
|
||||
multiworld.worlds[player2.id].options.accessibility.value = Accessibility.option_locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: True
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
||||
multiworld.completion_condition[player1.id] = lambda state: True
|
||||
multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
||||
|
||||
set_rule(player1.locations[1], lambda state: state.has(player1.prog_items[0].name, player1.id))
|
||||
set_rule(player1.locations[2], lambda state: state.has(player1.prog_items[1].name, player1.id))
|
||||
@@ -241,28 +241,28 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
# fill remaining locations with remaining items
|
||||
location_pool = player1.locations[1:] + player2.locations
|
||||
item_pool = player1.prog_items[:-1] + player2.prog_items
|
||||
fill_restrictive(multi_world, multi_world.state, location_pool, item_pool)
|
||||
multi_world.state.sweep_for_events() # collect everything
|
||||
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
|
||||
multiworld.state.sweep_for_events() # collect everything
|
||||
|
||||
# all of player2's locations and items should be accessible (not all of player1's)
|
||||
for item in player2.prog_items:
|
||||
self.assertTrue(multi_world.state.has(item.name, player2.id),
|
||||
self.assertTrue(multiworld.state.has(item.name, player2.id),
|
||||
f'{item} is unreachable in {item.location}')
|
||||
|
||||
def test_reversed_fill(self):
|
||||
"""Test a different set of rules can be satisfied"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item1.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(item1.name, player1.id))
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item1)
|
||||
@@ -270,13 +270,13 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multi_step_fill(self):
|
||||
"""Test that fill is able to satisfy multiple spheres"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 4, 4)
|
||||
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[2].name, player1.id) and state.has(items[3].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
@@ -285,7 +285,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
set_rule(locations[3], lambda state: state.has(
|
||||
items[1].name, player1.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[1])
|
||||
@@ -295,25 +295,25 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_impossible_fill(self):
|
||||
"""Test that fill raises an error when it can't place any items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[0].name, player1.id) and state.has(items[1].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[1].name, player1.id))
|
||||
set_rule(locations[0], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_circular_fill(self):
|
||||
"""Test that fill raises an error when it can't place all items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
@@ -322,46 +322,46 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
loc1 = player1.locations[1]
|
||||
loc2 = player1.locations[2]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item1.name, player1.id) and state.has(item2.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(item0.name, player1.id))
|
||||
set_rule(loc2, lambda state: state.has(item1.name, player1.id))
|
||||
set_rule(loc0, lambda state: state.has(item2.name, player1.id))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_competing_fill(self):
|
||||
"""Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(item0.name, player1.id)
|
||||
and state.has(item1.name, player1.id))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_multiplayer_fill(self):
|
||||
"""Test that items can be placed across worlds"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
player2 = generate_player_data(multiworld, 2, 2, 2)
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
player1.prog_items[0].name, player1.id) and state.has(
|
||||
player1.prog_items[1].name, player1.id)
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player2.id] = lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id) and state.has(
|
||||
player2.prog_items[1].name, player2.id)
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations +
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations +
|
||||
player2.locations, player1.prog_items + player2.prog_items)
|
||||
|
||||
self.assertEqual(player1.locations[0].item, player1.prog_items[1])
|
||||
@@ -371,21 +371,21 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multiplayer_rules_fill(self):
|
||||
"""Test that fill across worlds satisfies the rules"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
player2 = generate_player_data(multiworld, 2, 2, 2)
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
player1.prog_items[0].name, player1.id) and state.has(
|
||||
player1.prog_items[1].name, player1.id)
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player2.id] = lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id) and state.has(
|
||||
player2.prog_items[1].name, player2.id)
|
||||
|
||||
set_rule(player2.locations[1], lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations +
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations +
|
||||
player2.locations, player1.prog_items + player2.prog_items)
|
||||
|
||||
self.assertEqual(player1.locations[0].item, player2.prog_items[0])
|
||||
@@ -395,10 +395,10 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_restrictive_progress(self):
|
||||
"""Test that various spheres with different requirements can be filled"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, prog_item_count=25)
|
||||
items = player1.prog_items.copy()
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
names(player1.prog_items), player1.id)
|
||||
|
||||
player1.generate_region(player1.menu, 5)
|
||||
@@ -411,16 +411,16 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
names(items[17:22]), player1.id))
|
||||
|
||||
locations = multi_world.get_unfilled_locations()
|
||||
locations = multiworld.get_unfilled_locations()
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
locations, player1.prog_items)
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule(self):
|
||||
"""Test that item swap happens and works as intended"""
|
||||
# test for PR#1109
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
multiworld = generate_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 4, 4)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
# for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last
|
||||
@@ -437,15 +437,15 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed")
|
||||
self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed")
|
||||
# fill has to place items[1] in locations[0] which will result in a swap because of placement order
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items)
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items)
|
||||
# assert swap happened
|
||||
self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1")
|
||||
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule2(self):
|
||||
"""Test that swap works before all items are placed"""
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 5, 5)
|
||||
multiworld = generate_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 5, 5)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
# Two items provide access to sphere 2.
|
||||
@@ -477,7 +477,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
# Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap,
|
||||
# which it will attempt before two_to_three and three_to_four are placed, testing the behavior.
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items)
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items)
|
||||
# assert swap happened
|
||||
self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1")
|
||||
self.assertTrue(sphere1_loc1.item.name == one_to_two1 or
|
||||
@@ -486,29 +486,29 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
def test_double_sweep(self):
|
||||
"""Test that sweep doesn't duplicate Event items when sweeping"""
|
||||
# test for PR1114
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 1, 1)
|
||||
multiworld = generate_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 1, 1)
|
||||
location = player1.locations[0]
|
||||
location.address = None
|
||||
location.event = True
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
multi_world.state.sweep_for_events()
|
||||
multi_world.state.sweep_for_events()
|
||||
self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_events()
|
||||
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
|
||||
def test_correct_item_instance_removed_from_pool(self):
|
||||
"""Test that a placed item gets removed from the submitted pool"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
|
||||
player1.prog_items[1].name = "Different_item_instance_but_same_item_name"
|
||||
loc0 = player1.locations[0]
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
[loc0], player1.prog_items)
|
||||
|
||||
self.assertEqual(1, len(player1.prog_items))
|
||||
@@ -518,14 +518,14 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
def test_basic_distribute(self):
|
||||
"""Test that distribute_items_restrictive is deterministic"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
prog_items = player1.prog_items
|
||||
basic_items = player1.basic_items
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
@@ -538,52 +538,52 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[1].progress_type = LocationProgressType.EXCLUDED
|
||||
locations[2].progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertFalse(locations[1].item.advancement)
|
||||
self.assertFalse(locations[2].item.advancement)
|
||||
|
||||
def test_non_excluded_item_distribute(self):
|
||||
"""Test that useful items aren't placed on excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
basic_items = player1.basic_items
|
||||
|
||||
locations[1].progress_type = LocationProgressType.EXCLUDED
|
||||
basic_items[1].classification = ItemClassification.useful
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[1].item, basic_items[0])
|
||||
|
||||
def test_too_many_excluded_distribute(self):
|
||||
"""Test that fill fails if it can't place all progression items due to too many excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[0].progress_type = LocationProgressType.EXCLUDED
|
||||
locations[1].progress_type = LocationProgressType.EXCLUDED
|
||||
locations[2].progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multiworld)
|
||||
|
||||
def test_non_excluded_item_must_distribute(self):
|
||||
"""Test that fill fails if it can't place useful items due to too many excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
basic_items = player1.basic_items
|
||||
|
||||
@@ -592,47 +592,47 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
basic_items[0].classification = ItemClassification.useful
|
||||
basic_items[1].classification = ItemClassification.useful
|
||||
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multiworld)
|
||||
|
||||
def test_priority_distribute(self):
|
||||
"""Test that priority locations receive advancement items"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[0].progress_type = LocationProgressType.PRIORITY
|
||||
locations[3].progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertTrue(locations[0].item.advancement)
|
||||
self.assertTrue(locations[3].item.advancement)
|
||||
|
||||
def test_excess_priority_distribute(self):
|
||||
"""Test that if there's more priority locations than advancement items, they can still fill"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[0].progress_type = LocationProgressType.PRIORITY
|
||||
locations[1].progress_type = LocationProgressType.PRIORITY
|
||||
locations[2].progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertFalse(locations[3].item.advancement)
|
||||
|
||||
def test_multiple_world_priority_distribute(self):
|
||||
"""Test that priority fill can be satisfied for multiple worlds"""
|
||||
multi_world = generate_multi_world(3)
|
||||
multiworld = generate_multiworld(3)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, 4, prog_item_count=1, basic_item_count=3)
|
||||
multiworld, 2, 4, prog_item_count=1, basic_item_count=3)
|
||||
player3 = generate_player_data(
|
||||
multi_world, 3, 6, prog_item_count=4, basic_item_count=2)
|
||||
multiworld, 3, 6, prog_item_count=4, basic_item_count=2)
|
||||
|
||||
player1.locations[2].progress_type = LocationProgressType.PRIORITY
|
||||
player1.locations[3].progress_type = LocationProgressType.PRIORITY
|
||||
@@ -644,7 +644,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
player3.locations[2].progress_type = LocationProgressType.PRIORITY
|
||||
player3.locations[3].progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertTrue(player1.locations[2].item.advancement)
|
||||
self.assertTrue(player1.locations[3].item.advancement)
|
||||
@@ -656,9 +656,9 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_can_remove_locations_in_fill_hook(self):
|
||||
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
|
||||
removed_item: list[Item] = []
|
||||
removed_location: list[Location] = []
|
||||
@@ -667,21 +667,21 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
removed_item.append(filleritempool.pop(0))
|
||||
removed_location.append(fill_locations.pop(0))
|
||||
|
||||
multi_world.worlds[player1.id].fill_hook = fill_hook
|
||||
multiworld.worlds[player1.id].fill_hook = fill_hook
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertIsNone(removed_item[0].location)
|
||||
self.assertIsNone(removed_location[0].item)
|
||||
|
||||
def test_seed_robust_to_item_order(self):
|
||||
"""Test deterministic fill"""
|
||||
mw1 = generate_multi_world()
|
||||
mw1 = generate_multiworld()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
distribute_items_restrictive(mw1)
|
||||
|
||||
mw2 = generate_multi_world()
|
||||
mw2 = generate_multiworld()
|
||||
gen2 = generate_player_data(
|
||||
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
mw2.itempool.append(mw2.itempool.pop(0))
|
||||
@@ -694,12 +694,12 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_seed_robust_to_location_order(self):
|
||||
"""Test deterministic fill even if locations in a region are reordered"""
|
||||
mw1 = generate_multi_world()
|
||||
mw1 = generate_multiworld()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
distribute_items_restrictive(mw1)
|
||||
|
||||
mw2 = generate_multi_world()
|
||||
mw2 = generate_multiworld()
|
||||
gen2 = generate_player_data(
|
||||
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
reg = mw2.get_region("Menu", gen2.id)
|
||||
@@ -713,45 +713,45 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_can_reserve_advancement_items_for_general_fill(self):
|
||||
"""Test that priority locations fill still satisfies item rules"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, prog_item_count=5)
|
||||
multiworld, 1, location_count=5, prog_item_count=5)
|
||||
items = player1.prog_items
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
names(items), player1.id)
|
||||
|
||||
location = player1.locations[0]
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
location.item_rule = lambda item: item not in items[:4]
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(location.item, items[4])
|
||||
|
||||
def test_non_excluded_local_items(self):
|
||||
"""Test that local items get placed locally in a multiworld"""
|
||||
multi_world = generate_multi_world(2)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, basic_item_count=5)
|
||||
multiworld, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, location_count=5, basic_item_count=5)
|
||||
multiworld, 2, location_count=5, basic_item_count=5)
|
||||
|
||||
for item in multi_world.get_items():
|
||||
for item in multiworld.get_items():
|
||||
item.classification = ItemClassification.useful
|
||||
|
||||
multi_world.local_items[player1.id].value = set(names(player1.basic_items))
|
||||
multi_world.local_items[player2.id].value = set(names(player2.basic_items))
|
||||
locality_rules(multi_world)
|
||||
multiworld.local_items[player1.id].value = set(names(player1.basic_items))
|
||||
multiworld.local_items[player2.id].value = set(names(player2.basic_items))
|
||||
locality_rules(multiworld)
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
for item in multi_world.get_items():
|
||||
for item in multiworld.get_items():
|
||||
self.assertEqual(item.player, item.location.player)
|
||||
self.assertFalse(item.location.event, False)
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
mw = generate_multi_world(2)
|
||||
mw = generate_multiworld(2)
|
||||
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
|
||||
mw.early_items[1][player1.basic_items[0].name] = 1
|
||||
@@ -810,19 +810,19 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
"\n Contains" + str(list(map(lambda location: location.item, region.locations))))
|
||||
|
||||
def setUp(self) -> None:
|
||||
multi_world = generate_multi_world(2)
|
||||
self.multi_world = multi_world
|
||||
multiworld = generate_multiworld(2)
|
||||
self.multiworld = multiworld
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, prog_item_count=2, basic_item_count=40)
|
||||
multiworld, 1, prog_item_count=2, basic_item_count=40)
|
||||
self.player1 = player1
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, prog_item_count=2, basic_item_count=40)
|
||||
multiworld, 2, prog_item_count=2, basic_item_count=40)
|
||||
self.player2 = player2
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
player1.prog_items[0].name, player1.id) and state.has(
|
||||
player1.prog_items[1].name, player1.id)
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player2.id] = lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id) and state.has(
|
||||
player2.prog_items[1].name, player2.id)
|
||||
|
||||
@@ -830,42 +830,42 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
# Sphere 1
|
||||
region = player1.generate_region(player1.menu, 20)
|
||||
items = fill_region(multi_world, region, [
|
||||
items = fill_region(multiworld, region, [
|
||||
player1.prog_items[0]] + items)
|
||||
|
||||
# Sphere 2
|
||||
region = player1.generate_region(
|
||||
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
|
||||
items = fill_region(
|
||||
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
||||
multiworld, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
||||
|
||||
# Sphere 3
|
||||
region = player2.generate_region(
|
||||
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
|
||||
fill_region(multi_world, region, [player2.prog_items[1]] + items)
|
||||
fill_region(multiworld, region, [player2.prog_items[1]] + items)
|
||||
|
||||
def test_balances_progression(self) -> None:
|
||||
"""Tests that progression balancing moves progression items earlier"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 50
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[1], self.player2.prog_items[0])
|
||||
|
||||
def test_balances_progression_light(self) -> None:
|
||||
"""Test that progression balancing still moves items earlier on minimum value"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 1
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 1
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 1
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 1
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
# TODO: arrange for a result that's different from the default
|
||||
self.assertRegionContains(
|
||||
@@ -873,13 +873,13 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
def test_balances_progression_heavy(self) -> None:
|
||||
"""Test that progression balancing moves items earlier on maximum value"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 99
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 99
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 99
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 99
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
# TODO: arrange for a result that's different from the default
|
||||
self.assertRegionContains(
|
||||
@@ -887,25 +887,25 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
def test_skips_balancing_progression(self) -> None:
|
||||
"""Test that progression balancing is skipped when players have it disabled"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 0
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 0
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 0
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 0
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
def test_ignores_priority_locations(self) -> None:
|
||||
"""Test that progression items on priority locations don't get moved by balancing"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 50
|
||||
|
||||
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
27
test/general/test_groups.py
Normal file
27
test/general/test_groups.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestNameGroups(TestCase):
|
||||
def test_item_name_groups_not_empty(self) -> None:
|
||||
"""
|
||||
Test that there are no empty item name groups, which is likely a bug.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.item_id_to_name:
|
||||
continue # ignore worlds without items
|
||||
with self.subTest(game=game_name):
|
||||
for name, group in world_type.item_name_groups.items():
|
||||
self.assertTrue(group, f"Item name group \"{name}\" of \"{game_name}\" is empty")
|
||||
|
||||
def test_location_name_groups_not_empty(self) -> None:
|
||||
"""
|
||||
Test that there are no empty location name groups, which is likely a bug.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.location_id_to_name:
|
||||
continue # ignore worlds without locations
|
||||
with self.subTest(game=game_name):
|
||||
for name, group in world_type.location_name_groups.items():
|
||||
self.assertTrue(group, f"Location name group \"{name}\" of \"{game_name}\" is empty")
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
@@ -42,18 +43,18 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest(group_name, group_name=group_name):
|
||||
self.assertNotIn(group_name, world_type.item_name_to_id)
|
||||
|
||||
def test_item_count_greater_equal_locations(self):
|
||||
"""Test that by the pre_fill step under default settings, each game submits items >= locations"""
|
||||
def test_item_count_equal_locations(self):
|
||||
"""Test that by the pre_fill step under default settings, each game submits items == locations"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
self.assertGreaterEqual(
|
||||
self.assertEqual(
|
||||
len(multiworld.itempool),
|
||||
len(multiworld.get_unfilled_locations()),
|
||||
f"{game_name} Item count MUST meet or exceed the number of locations",
|
||||
f"{game_name} Item count MUST match the number of locations",
|
||||
)
|
||||
|
||||
def testItemsInDatapackage(self):
|
||||
def test_items_in_datapackage(self):
|
||||
"""Test that any created items in the itempool are in the datapackage"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
@@ -69,3 +70,20 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest("Name should be valid", game=game_name, item=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All item descriptions must match defined item names")
|
||||
|
||||
def test_itempool_not_modified(self):
|
||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
||||
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
|
||||
worlds_to_test = {game: world
|
||||
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
|
||||
for game_name, world_type in worlds_to_test.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||
created_items = multiworld.itempool.copy()
|
||||
for step in additional_steps:
|
||||
with self.subTest("step", step=step):
|
||||
call_all(multiworld, step)
|
||||
self.assertEqual(created_items, multiworld.itempool,
|
||||
f"{game_name} modified the itempool during {step}")
|
||||
|
||||
@@ -11,14 +11,14 @@ class TestBase(unittest.TestCase):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
locations = Counter(location.name for location in multiworld.get_locations())
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
|
||||
self.assertEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
|
||||
|
||||
locations = Counter(location.address for location in multiworld.get_locations()
|
||||
if type(location.address) is int)
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||
self.assertEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||
|
||||
def test_locations_in_datapackage(self):
|
||||
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
||||
|
||||
@@ -10,3 +10,10 @@ class TestOptions(unittest.TestCase):
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
with self.subTest(game=gamename, option=option_key):
|
||||
self.assertTrue(option.__doc__)
|
||||
|
||||
def test_options_are_not_set_by_world(self):
|
||||
"""Test that options attribute is not already set"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertFalse(hasattr(world_type, "options"),
|
||||
f"Unexpected assignment to {world_type.__name__}.options!")
|
||||
|
||||
@@ -36,15 +36,15 @@ class TestBase(unittest.TestCase):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_solo_multiworld(world_type)
|
||||
excluded = world.exclude_locations[1].value
|
||||
state = world.get_all_state(False)
|
||||
for location in world.get_locations():
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
excluded = multiworld.worlds[1].options.exclude_locations.value
|
||||
state = multiworld.get_all_state(False)
|
||||
for location in multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
|
||||
for region in world.get_regions():
|
||||
for region in multiworld.get_regions():
|
||||
if region.name in unreachable_regions:
|
||||
with self.subTest("Region should be unreachable", region=region):
|
||||
self.assertFalse(region.can_reach(state))
|
||||
@@ -53,15 +53,15 @@ class TestBase(unittest.TestCase):
|
||||
self.assertTrue(region.can_reach(state))
|
||||
|
||||
with self.subTest("Completion Condition"):
|
||||
self.assertTrue(world.can_beat_game(state))
|
||||
self.assertTrue(multiworld.can_beat_game(state))
|
||||
|
||||
def test_default_empty_state_can_reach_something(self):
|
||||
"""Ensure empty state can reach at least one location with the defined options"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_solo_multiworld(world_type)
|
||||
state = CollectionState(world)
|
||||
all_locations = world.get_locations()
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
state = CollectionState(multiworld)
|
||||
all_locations = multiworld.get_locations()
|
||||
if all_locations:
|
||||
locations = set()
|
||||
for location in all_locations:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import io
|
||||
import unittest
|
||||
import json
|
||||
import yaml
|
||||
|
||||
|
||||
class TestDocs(unittest.TestCase):
|
||||
@@ -23,7 +25,7 @@ class TestDocs(unittest.TestCase):
|
||||
response = self.client.post("/api/generate")
|
||||
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||
|
||||
def test_generation_queued(self):
|
||||
def test_generation_queued_weights(self):
|
||||
options = {
|
||||
"Tester1":
|
||||
{
|
||||
@@ -40,3 +42,19 @@ class TestDocs(unittest.TestCase):
|
||||
json_data = response.get_json()
|
||||
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||
|
||||
def test_generation_queued_file(self):
|
||||
options = {
|
||||
"game": "Archipelago",
|
||||
"name": "Tester",
|
||||
"Archipelago": {}
|
||||
}
|
||||
response = self.client.post(
|
||||
"/api/generate",
|
||||
data={
|
||||
'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml")
|
||||
},
|
||||
)
|
||||
json_data = response.get_json()
|
||||
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
FillType_Vec = Sequence[int]
|
||||
|
||||
|
||||
class FillType_Drawable:
|
||||
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
|
||||
class FillType_Texture(FillType_Drawable):
|
||||
pass
|
||||
from .texture import FillType_Drawable, FillType_Vec, Texture
|
||||
|
||||
|
||||
class FillType_Shape(FillType_Drawable):
|
||||
texture: FillType_Texture
|
||||
texture: Texture
|
||||
|
||||
def __init__(self,
|
||||
*,
|
||||
texture: FillType_Texture = ...,
|
||||
texture: Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
@@ -35,6 +23,6 @@ class Rectangle(FillType_Shape):
|
||||
def __init__(self,
|
||||
*,
|
||||
source: str = ...,
|
||||
texture: FillType_Texture = ...,
|
||||
texture: Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
13
typings/kivy/graphics/texture.pyi
Normal file
13
typings/kivy/graphics/texture.pyi
Normal file
@@ -0,0 +1,13 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
FillType_Vec = Sequence[int]
|
||||
|
||||
|
||||
class FillType_Drawable:
|
||||
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
|
||||
class Texture:
|
||||
pass
|
||||
9
typings/kivy/uix/image.pyi
Normal file
9
typings/kivy/uix/image.pyi
Normal file
@@ -0,0 +1,9 @@
|
||||
import io
|
||||
|
||||
from kivy.graphics.texture import Texture
|
||||
|
||||
|
||||
class CoreImage:
|
||||
texture: Texture
|
||||
|
||||
def __init__(self, data: io.BytesIO, ext: str) -> None: ...
|
||||
@@ -77,6 +77,10 @@ class AutoWorldRegister(type):
|
||||
# create missing options_dataclass from legacy option_definitions
|
||||
# TODO - remove this once all worlds use options dataclasses
|
||||
if "options_dataclass" not in dct and "option_definitions" in dct:
|
||||
# TODO - switch to deprecate after a version
|
||||
if __debug__:
|
||||
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
|
||||
"Please use options_dataclass instead.")
|
||||
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
|
||||
bases=(PerGameCommonOptions,))
|
||||
|
||||
@@ -324,7 +328,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
|
||||
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,9 @@ import os
|
||||
import sys
|
||||
import warnings
|
||||
import zipimport
|
||||
from typing import Dict, List, NamedTuple, TypedDict
|
||||
import time
|
||||
import dataclasses
|
||||
from typing import Dict, List, TypedDict, Optional
|
||||
|
||||
from Utils import local_path, user_path
|
||||
|
||||
@@ -34,10 +36,12 @@ class DataPackage(TypedDict):
|
||||
games: Dict[str, GamesPackage]
|
||||
|
||||
|
||||
class WorldSource(NamedTuple):
|
||||
@dataclasses.dataclass(order=True)
|
||||
class WorldSource:
|
||||
path: str # typically relative path from this module
|
||||
is_zip: bool = False
|
||||
relative: bool = True # relative to regular world import folder
|
||||
time_taken: Optional[float] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
|
||||
@@ -50,6 +54,7 @@ class WorldSource(NamedTuple):
|
||||
|
||||
def load(self) -> bool:
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||
@@ -69,6 +74,7 @@ class WorldSource(NamedTuple):
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{self.path}", "worlds")
|
||||
self.time_taken = time.perf_counter()-start
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
|
||||
@@ -271,7 +271,7 @@ class AdventureWorld(World):
|
||||
overworld_locations_copy = overworld.locations.copy()
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
|
||||
locations_copy = all_locations.copy()
|
||||
locations_copy = list(all_locations)
|
||||
for loc in all_locations:
|
||||
if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT:
|
||||
locations_copy.remove(loc)
|
||||
|
||||
@@ -10,8 +10,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
||||
- Detailed installation instructions for BizHawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(select `Adventure Client` during installation).
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
|
||||
|
||||
## Configuring BizHawk
|
||||
|
||||
@@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
|
||||
if loc in all_state_base.events:
|
||||
all_state_base.events.remove(loc)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
|
||||
name="LttP Dungeon Items")
|
||||
|
||||
|
||||
|
||||
@@ -969,6 +969,9 @@ def standard_rules(world, player):
|
||||
|
||||
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3))
|
||||
else:
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has('Big Key (Hyrule Castle)', player))
|
||||
|
||||
def toss_junk_item(world, player):
|
||||
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
|
||||
|
||||
@@ -26,6 +26,13 @@ class ALttPLocation(Location):
|
||||
self.player_address = player_address
|
||||
self._hint_text = hint_text
|
||||
|
||||
@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("-", " ")
|
||||
|
||||
|
||||
class ALttPItem(Item):
|
||||
game: str = "A Link to the Past"
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for
|
||||
`SNI Client - A Link to the Past Patch Setup`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
|
||||
- SNI is not compatible with (Q)Usb2Snes.
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
@@ -18,11 +17,12 @@ but it is not supported.**
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Download and install SNIClient from the link above, making sure to install the most recent version.
|
||||
**The installer file is located in the assets section at the bottom of the version information**.
|
||||
- During setup, you will be asked to locate your base ROM file. This is your Japanese Link to the Past ROM file.
|
||||
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
||||
file is located in the assets section at the bottom of the version information.**
|
||||
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
|
||||
This is your Japanese Link to the Past ROM file. This only needs to be done once.
|
||||
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
|
||||
@@ -7,16 +7,25 @@ from ..AutoWorld import WebWorld, World
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing BK Sudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='setup/en',
|
||||
authors=['Jarno']
|
||||
)
|
||||
]
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing BK Sudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='setup/en',
|
||||
authors=['Jarno']
|
||||
)
|
||||
setup_de = Tutorial(
|
||||
tutorial_name='Setup Anleitung',
|
||||
description='Eine Anleitung um BK-Sudoku zu spielen',
|
||||
language='Deutsch',
|
||||
file_name='setup_de.md',
|
||||
link='setup/de',
|
||||
authors=['Held_der_Zeit']
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
|
||||
|
||||
class Bk_SudokuWorld(World):
|
||||
|
||||
21
worlds/bk_sudoku/docs/de_Sudoku.md
Normal file
21
worlds/bk_sudoku/docs/de_Sudoku.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# BK-Sudoku
|
||||
|
||||
## Was ist das für ein Spiel?
|
||||
|
||||
BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder
|
||||
beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis
|
||||
für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein
|
||||
weitere „Checks” zu erreichen.
|
||||
(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu
|
||||
spielen/generieren.)
|
||||
|
||||
## Wie werden Hinweise freigeschalten?
|
||||
|
||||
Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen
|
||||
Gegenstand der noch nicht gefunden wurde.
|
||||
|
||||
## Wo ist die Seite für die Einstellungen?
|
||||
|
||||
Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen
|
||||
kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der
|
||||
Schwierigkeitsgrad des Sudoku ausgewählt werden.
|
||||
27
worlds/bk_sudoku/docs/setup_de.md
Normal file
27
worlds/bk_sudoku/docs/setup_de.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# BK-Sudoku Setup Anleitung
|
||||
|
||||
## Benötigte Software
|
||||
- [Bk-Sudoku](https://github.com/Jarno458/sudoku)
|
||||
- Windows 8 oder höher
|
||||
|
||||
## Generelles Konzept
|
||||
|
||||
Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku
|
||||
spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten.
|
||||
|
||||
Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig
|
||||
eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt.
|
||||
|
||||
## Installationsprozess
|
||||
|
||||
Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases).
|
||||
Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei.
|
||||
|
||||
## Verbinden mit einer Multiworld
|
||||
|
||||
1. Starte `Bk_Sudoku.exe`
|
||||
2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest
|
||||
3. Trage die Server-URL und den Port ein
|
||||
4. Drücke auf Verbinden (connect)
|
||||
5. Wähle deinen Schwierigkeitsgrad
|
||||
6. Versuche das Sudoku zu Lösen
|
||||
@@ -5,7 +5,6 @@
|
||||
- ChecksFinder from
|
||||
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
|
||||
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- (select `ChecksFinder Client` during installation.)
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
|
||||
@@ -11,16 +11,26 @@ from .Rules import get_button_rule
|
||||
|
||||
class CliqueWebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
]
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
|
||||
setup_de = Tutorial(
|
||||
tutorial_name="Anleitung zum Anfangen",
|
||||
description="Eine Anleitung um Clique zu spielen.",
|
||||
language="Deutsch",
|
||||
file_name="guide_de.md",
|
||||
link="guide/de",
|
||||
authors=["Held_der_Zeit"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
|
||||
18
worlds/clique/docs/de_Clique.md
Normal file
18
worlds/clique/docs/de_Clique.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Clique
|
||||
|
||||
## Was ist das für ein Spiel?
|
||||
|
||||
~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~
|
||||
~~(rote) Knöpfe zu drücken.~~
|
||||
|
||||
Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach
|
||||
es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten
|
||||
Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand
|
||||
anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann.
|
||||
|
||||
Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden.
|
||||
|
||||
## Wo ist die Seite für die Einstellungen?
|
||||
|
||||
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
|
||||
eine YAML-Datei zu konfigurieren und zu exportieren.
|
||||
25
worlds/clique/docs/guide_de.md
Normal file
25
worlds/clique/docs/guide_de.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Clique Anleitung
|
||||
|
||||
Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib
|
||||
Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden).
|
||||
|
||||
Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten.
|
||||
|
||||
Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst.
|
||||
Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf
|
||||
deinem Handy starten und produktiv sein während du wartest!
|
||||
|
||||
Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche
|
||||
(mindestens) eins der Folgenden:
|
||||
|
||||
- Dein Zimmer aufräumen.
|
||||
- Die Wäsche machen.
|
||||
- Etwas Essen von einem X-Belieben Fast Food Restaruant holen.
|
||||
- Das tägliche Wordle machen.
|
||||
- ~~Deine Seele an **Phar** verkaufen.~~
|
||||
- Deine Hausaufgaben erledigen.
|
||||
- Deine Post abholen.
|
||||
|
||||
|
||||
~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~
|
||||
~~Discord kontaktieren. *zwinker* *zwinker*~~
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
@@ -23,9 +23,10 @@
|
||||
|
||||
### Windows Setup
|
||||
|
||||
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
|
||||
or you are on an older version, you may run the installer again to install the SNI Client.
|
||||
2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file.
|
||||
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
||||
file is located in the assets section at the bottom of the version information.**
|
||||
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
|
||||
This is your Donkey Kong Country 3 ROM file. This only needs to be done once.
|
||||
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
|
||||
@@ -13,14 +13,23 @@ client_version = 0
|
||||
|
||||
|
||||
class DLCqwebworld(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"A guide to setting up the Archipelago DLCQuest game on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["axe_y"]
|
||||
)]
|
||||
)
|
||||
setup_fr = Tutorial(
|
||||
"Guide de configuration MultiWorld",
|
||||
"Un guide pour configurer DLCQuest sur votre PC.",
|
||||
"Français",
|
||||
"setup_fr.md",
|
||||
"setup/fr",
|
||||
["Deoxis"]
|
||||
)
|
||||
tutorials = [setup_en, setup_fr]
|
||||
|
||||
|
||||
class DLCqworld(World):
|
||||
|
||||
49
worlds/dlcquest/docs/fr_DLCQuest.md
Normal file
49
worlds/dlcquest/docs/fr_DLCQuest.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# DLC Quest
|
||||
|
||||
## Où se trouve la page des paramètres ?
|
||||
|
||||
La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
|
||||
|
||||
|
||||
## Quel est l'effet de la randomisation sur ce jeu ?
|
||||
|
||||
Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest.
|
||||
|
||||
## Quel est le but de DLC Quest ?
|
||||
|
||||
DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie.
|
||||
Il peut également choisir de faire les deux campagnes.
|
||||
|
||||
|
||||
## Quels sont les emplacements dans DLC quest ?
|
||||
|
||||
Les emplacements dans DLC Quest comprennent toujours
|
||||
- les achats de DLC auprès du commerçant
|
||||
- Les objectifs liés aux récompenses
|
||||
- Tuer des moutons dans DLC Quest
|
||||
- Objectifs spécifiques de l'attribution dans Live Freemium or Die
|
||||
|
||||
Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection :
|
||||
- Objets que votre personnage peut obtenir de différentes manières
|
||||
- Swords
|
||||
- Gun
|
||||
- Box of Various Supplies
|
||||
- Humble Indie Bindle
|
||||
- Pickaxe
|
||||
- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés
|
||||
|
||||
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
|
||||
|
||||
Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock
|
||||
|
||||
Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille.
|
||||
- Zombie Sheep
|
||||
- Loading Screens
|
||||
- Temporary Spikes
|
||||
|
||||
## Que se passe-t-il lorsque le joueur reçoit un objet ?
|
||||
|
||||
Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
|
||||
Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
|
||||
|
||||
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.
|
||||
55
worlds/dlcquest/docs/setup_fr.md
Normal file
55
worlds/dlcquest/docs/setup_fr.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# # Guide de configuration MultiWorld de DLCQuest
|
||||
|
||||
## Logiciels requis
|
||||
|
||||
- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/))
|
||||
- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases)
|
||||
- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète)
|
||||
|
||||
## Logiciels optionnels
|
||||
- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- (Uniquement pour le TextClient)
|
||||
|
||||
## Créer un fichier de configuration (.yaml)
|
||||
|
||||
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
|
||||
|
||||
Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings).
|
||||
|
||||
## Rejoindre une partie multi-monde
|
||||
|
||||
### Installer le mod
|
||||
|
||||
- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous
|
||||
|
||||
|
||||
- Extraire l'archive .zip à l'emplacement de votre choix
|
||||
|
||||
|
||||
- Exécutez "DLCQuestipelagoInstaller.exe".
|
||||
|
||||

|
||||
- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire.
|
||||
- Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut
|
||||
- Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès.
|
||||
- Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé.
|
||||
|
||||
### Se connecter au MultiServer
|
||||
|
||||
- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés.
|
||||
|
||||
- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable.
|
||||

|
||||
|
||||
- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes.
|
||||
- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez.
|
||||
|
||||
### Interagir avec le MultiWorld depuis le jeu
|
||||
|
||||
Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
|
||||
Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
|
||||
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.
|
||||
@@ -177,7 +177,7 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
else:
|
||||
basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
|
||||
for dirpath, dirnames, filenames in os.walk(basepath):
|
||||
base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)
|
||||
base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\")
|
||||
for filename in filenames:
|
||||
mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
|
||||
file_path=os.path.join(dirpath, filename):
|
||||
|
||||
@@ -210,7 +210,7 @@ class RecipeIngredientsOffset(Range):
|
||||
class FactorioStartItems(OptionDict):
|
||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||
display_name = "Starting Items"
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
|
||||
|
||||
|
||||
class FactorioFreeSampleBlacklist(OptionSet):
|
||||
|
||||
@@ -246,7 +246,8 @@ class Factorio(World):
|
||||
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
|
||||
(ingredient not in technology_table or state.has(ingredient, player)) and \
|
||||
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
|
||||
for technology in required_technologies[sub_ingredient])
|
||||
for technology in required_technologies[sub_ingredient]) and \
|
||||
all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine])
|
||||
else:
|
||||
location.access_rule = lambda state, ingredient=ingredient: \
|
||||
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
|
||||
|
||||
@@ -74,6 +74,7 @@ class FF1World(World):
|
||||
items = get_options(self.multiworld, 'items', self.player)
|
||||
goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]],
|
||||
self.player)
|
||||
terminated_event.access_rule = goal_rule
|
||||
if "Shard" in items.keys():
|
||||
def goal_rule_and_shards(state):
|
||||
return goal_rule(state) and state.has("Shard", self.player, 32)
|
||||
|
||||
@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
|
||||
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
|
||||
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
|
||||
check_2 = await snes_read(ctx, 0xF53749, 1)
|
||||
if check_1 == b'\x00' or check_2 == b'\x00':
|
||||
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
|
||||
return
|
||||
|
||||
def get_range(data_range):
|
||||
|
||||
@@ -223,11 +223,6 @@ for item, data in item_table.items():
|
||||
def create_items(self) -> None:
|
||||
items = []
|
||||
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
|
||||
if self.multiworld.progressive_gear[self.player]:
|
||||
for item_group in prog_map:
|
||||
if starting_weapon in self.item_name_groups[item_group]:
|
||||
starting_weapon = prog_map[item_group]
|
||||
break
|
||||
self.multiworld.push_precollected(self.create_item(starting_weapon))
|
||||
self.multiworld.push_precollected(self.create_item("Steel Armor"))
|
||||
if self.multiworld.sky_coin_mode[self.player] == "start_with":
|
||||
|
||||
@@ -267,11 +267,11 @@ class CompanionLevelingType(Choice):
|
||||
class CompanionSpellbookType(Choice):
|
||||
"""Update companions' spellbook.
|
||||
Standard: Original game spellbooks.
|
||||
Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
|
||||
Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
|
||||
Random Balanced: Randomize the spellbooks with an appropriate mix of spells.
|
||||
Random Chaos: Randomize the spellbooks in total free-for-all."""
|
||||
option_standard = 0
|
||||
option_standard_extended = 1
|
||||
option_extended = 1
|
||||
option_random_balanced = 2
|
||||
option_random_chaos = 3
|
||||
default = 0
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import zipfile
|
||||
from copy import deepcopy
|
||||
from .Regions import object_id_table
|
||||
from Main import __version__
|
||||
from Utils import __version__
|
||||
from worlds.Files import APContainer
|
||||
import pkgutil
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ Final Fantasy Mystic Quest:
|
||||
Normal: 0
|
||||
OneAndHalf: 0
|
||||
Double: 0
|
||||
DoubleHalf: 0
|
||||
DoubleAndHalf: 0
|
||||
Triple: 0
|
||||
Quadruple: 0
|
||||
companion_leveling_type:
|
||||
@@ -98,7 +98,7 @@ Final Fantasy Mystic Quest:
|
||||
BenPlus10: 0
|
||||
companion_spellbook_type:
|
||||
Standard: 0
|
||||
StandardExtended: 0
|
||||
Extended: 0
|
||||
RandomBalanced: 0
|
||||
RandomChaos: 0
|
||||
starting_companion:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
@@ -19,8 +19,8 @@ The Archipelago community cannot supply you with this.
|
||||
|
||||
### Windows Setup
|
||||
|
||||
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
|
||||
or you are on an older version, you may run the installer again to install the SNI Client.
|
||||
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
||||
file is located in the assets section at the bottom of the version information.**
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import collections
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
|
||||
@@ -81,15 +82,18 @@ def locality_rules(world: MultiWorld):
|
||||
i.name not in sending_blockers[i.player] and old_rule(i)
|
||||
|
||||
|
||||
def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
|
||||
def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
|
||||
for loc_name in exclude_locations:
|
||||
try:
|
||||
location = world.get_location(loc_name, player)
|
||||
location = multiworld.get_location(loc_name, player)
|
||||
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||
if loc_name not in world.worlds[player].location_name_to_id:
|
||||
if loc_name not in multiworld.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||
else:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
if not location.event:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
|
||||
|
||||
|
||||
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):
|
||||
|
||||
@@ -17,19 +17,22 @@ The most recent public release of Archipelago can be found on the GitHub Release
|
||||
Run the exe file, and after accepting the license agreement you will be asked which components you would like to
|
||||
install.
|
||||
|
||||
The generator allows you to generate multiworld games on your computer. The ROM setups are required if anyone in the
|
||||
game that you generate wants to play any of those games as they are needed to generate the relevant patch files. If you
|
||||
do not own the game, uncheck the relevant box. If you gain the game later, the installer can be run again to install and
|
||||
set up new components.
|
||||
Archipelago installations are automatically bundled with some programs. These include a launcher, a generator, a
|
||||
server and some clients.
|
||||
|
||||
The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
|
||||
- The launcher lets you quickly access Archipelago's different components and programs. It is found under the name
|
||||
`ArchipelagoLauncher` and can be found in the main directory of your Archipelago installation.
|
||||
|
||||
- The generator allows you to generate multiworld games on your computer. Please refer to the 'Generating a game'
|
||||
section of this guide for more information about it.
|
||||
|
||||
- The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
|
||||
you are hosting on. The default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of
|
||||
other guides on the internet that will be more suited to your hardware.
|
||||
|
||||
The `Clients` are what are used to connect your game to the multiworld. If the game you plan to play is available
|
||||
here, go ahead and install its client as well. If the game you choose to play is supported by Archipelago but not listed
|
||||
in the installation, check the setup guide for that game. Installing a client for a ROM based game requires you to have
|
||||
a legally obtained ROM for that game as well.
|
||||
- The clients are what are used to connect your game to the multiworld. Some games use a client that is automatically
|
||||
installed with an Archipelago installation. You can access those clients via the launcher or by navigating
|
||||
to your Archipelago installation.
|
||||
|
||||
## Generating a game
|
||||
|
||||
@@ -72,14 +75,18 @@ If you have downloaded the settings, or have created a settings file manually, t
|
||||
|
||||
#### On your local installation
|
||||
|
||||
To generate a game on your local machine, make sure to install the Archipelago software, and ensure to select the
|
||||
`Generator` component, as well as the `ROM setup` for any games you will want to play. Navigate to your Archipelago
|
||||
To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago
|
||||
installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded
|
||||
from the website in the `Players` folder.
|
||||
|
||||
Run `ArchipelagoGenerate.exe`, and it will inform you whether the generation was successful or not. If successful, there
|
||||
will be an output zip in the `output` folder (usually named something like `AP_XXXXX.zip`). This will contain all
|
||||
relevant information to the session, including the spoiler log, if one was generated.
|
||||
Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation
|
||||
was successful or not. If successful, there will be an output zip in the `output` folder
|
||||
(usually named something like `AP_XXXXX.zip`). This will contain all relevant information to the session, including the
|
||||
spoiler log, if one was generated.
|
||||
|
||||
Please note that some games require you to own their ROM files to generate with them as they are needed to generate the
|
||||
relevant patch files. When you generate with a ROM game for the first time, you will be asked to locate its base ROM file.
|
||||
This step only needs to be done once.
|
||||
|
||||
### Generating a multiplayer game
|
||||
|
||||
@@ -97,12 +104,9 @@ player name.
|
||||
|
||||
#### On the website
|
||||
|
||||
Gather all player YAML files into a single place, and compress them into a zip file. This can be done by pressing
|
||||
ctrl/cmd + clicking on each file until all are selected, right-clicking one of the files, and clicking
|
||||
`compress to ZIP file` or `send to > compressed folder`.
|
||||
|
||||
Navigate to the [Generate Page](/generate), select the host settings you would like, click on `Upload File`, and
|
||||
select the newly created zip from the opened window.
|
||||
Gather all player YAML files into a single place, then navigate to the [Generate Page](/generate). Select the host settings
|
||||
you would like, click on `Upload File(s)`, and select all player YAML files. The site also accepts `zip` archives containing YAML
|
||||
files.
|
||||
|
||||
After some time, you will be redirected to a seed info page that will display the generated seed, the time it was
|
||||
created, the number of players, the spoiler (if one was created) and all rooms created from this seed.
|
||||
@@ -114,8 +118,11 @@ It is possible to generate the multiworld locally, using a local Archipelago ins
|
||||
Archipelago installation folder (usually C:\ProgramData\Archipelago) and placing each YAML file in the `Players` folder.
|
||||
If the folder does not exist then it must be created manually. The files here should not be compressed.
|
||||
|
||||
After filling the `Players` folder, the `ArchipelagoGenerate.exe` program should be run in order to generate a
|
||||
multiworld. The output of this process is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
|
||||
After filling the `Players` folder, run`ArchipelagoGenerate.exe` or click `Generate` in the launcher. The output of
|
||||
the generation is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
|
||||
|
||||
Please note that if any player in the game you want to generate plays a game that needs a ROM file to generate, you will
|
||||
need the corresponding ROM files.
|
||||
|
||||
##### Changing local host settings for generation
|
||||
|
||||
@@ -123,10 +130,12 @@ Sometimes there are various settings that you may want to change before rolling
|
||||
auto-release, plando support, or setting a password.
|
||||
|
||||
All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago
|
||||
installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other
|
||||
files after generation, so if you are rolling locally, ensure this file is edited to your liking **before** rolling the
|
||||
seed. This file is overwritten when running the Archipelago Installation software. If you have changed settings in this
|
||||
file, and would like to retain them, you may rename the file to `options.yaml`.
|
||||
installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings
|
||||
chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you
|
||||
are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten
|
||||
when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
|
||||
them, you may rename the file to `options.yaml`.
|
||||
|
||||
|
||||
## Hosting an Archipelago Server
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@ import typing
|
||||
from .ExtractedData import logic_options, starts, pool_options
|
||||
from .Rules import cost_terms
|
||||
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink
|
||||
from .Charms import vanilla_costs, names as charm_names
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -402,22 +402,34 @@ class WhitePalace(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class DeathLink(Choice):
|
||||
class ExtraPlatforms(DefaultOnToggle):
|
||||
"""Places additional platforms to make traveling throughout Hallownest more convenient."""
|
||||
|
||||
|
||||
class DeathLinkShade(Choice):
|
||||
"""Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any.
|
||||
|
||||
vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any.
|
||||
shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched.
|
||||
shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless.
|
||||
|
||||
* This option has no effect if DeathLink is disabled.
|
||||
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
|
||||
your existing shade, if any.
|
||||
"""
|
||||
When you die, everyone dies. Of course the reverse is true too.
|
||||
When enabled, choose how incoming deathlinks are handled:
|
||||
vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
|
||||
shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
|
||||
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
|
||||
"""
|
||||
option_off = 0
|
||||
alias_no = 0
|
||||
alias_true = 1
|
||||
alias_on = 1
|
||||
alias_yes = 1
|
||||
option_vanilla = 0
|
||||
option_shadeless = 1
|
||||
option_vanilla = 2
|
||||
option_shade = 3
|
||||
option_shade = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class DeathLinkBreaksFragileCharms(Toggle):
|
||||
"""Sets if fragile charms break when you are killed by a DeathLink.
|
||||
|
||||
* This option has no effect if DeathLink is disabled.
|
||||
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
|
||||
will continue to do so.
|
||||
"""
|
||||
|
||||
|
||||
class StartingGeo(Range):
|
||||
@@ -476,7 +488,8 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
**{
|
||||
option.__name__: option
|
||||
for option in (
|
||||
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
|
||||
StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo,
|
||||
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
|
||||
MinimumGeoPrice, MaximumGeoPrice,
|
||||
MinimumGrubPrice, MaximumGrubPrice,
|
||||
MinimumEssencePrice, MaximumEssencePrice,
|
||||
@@ -488,7 +501,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
LegEaterShopSlots, GrubfatherRewardSlots,
|
||||
SeerRewardSlots, ExtraShopSlots,
|
||||
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
|
||||
CostSanity, CostSanityHybridChance,
|
||||
CostSanity, CostSanityHybridChance
|
||||
)
|
||||
},
|
||||
**cost_sanity_weights
|
||||
|
||||
@@ -444,6 +444,8 @@ def set_rules(hylics2world):
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Lava Medallion", player),
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
|
||||
lambda state: bridge_key(state, player))
|
||||
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
|
||||
|
||||
@@ -80,11 +80,6 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
},
|
||||
}
|
||||
self.front_of_inventory = {
|
||||
"Sora": 0x2546,
|
||||
"Donald": 0x2658,
|
||||
"Goofy": 0x276C,
|
||||
}
|
||||
self.kh2seedname = None
|
||||
self.kh2slotdata = None
|
||||
self.itemamount = {}
|
||||
@@ -169,6 +164,14 @@ class KH2Context(CommonContext):
|
||||
self.ability_code_list = None
|
||||
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
|
||||
|
||||
self.base_hp = 20
|
||||
self.base_mp = 100
|
||||
self.base_drive = 5
|
||||
self.base_accessory_slots = 1
|
||||
self.base_armor_slots = 1
|
||||
self.base_item_slots = 3
|
||||
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(KH2Context, self).server_auth(password_requested)
|
||||
@@ -219,6 +222,12 @@ class KH2Context(CommonContext):
|
||||
def kh2_read_byte(self, address):
|
||||
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big")
|
||||
|
||||
def kh2_read_int(self, address):
|
||||
return self.kh2.read_int(self.kh2.base_address + address)
|
||||
|
||||
def kh2_write_int(self, address, value):
|
||||
self.kh2.write_int(self.kh2.base_address + address, value)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"RoomInfo"}:
|
||||
self.kh2seedname = args['seed_name']
|
||||
@@ -476,7 +485,7 @@ class KH2Context(CommonContext):
|
||||
|
||||
async def give_item(self, item, location):
|
||||
try:
|
||||
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
|
||||
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
|
||||
itemname = self.lookup_id_to_item[item]
|
||||
itemdata = self.item_name_to_data[itemname]
|
||||
# itemcode = self.kh2_item_name_to_id[itemname]
|
||||
@@ -507,6 +516,8 @@ class KH2Context(CommonContext):
|
||||
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1]
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
|
||||
self.kh2_seed_save_cache["GoofyInvo"][1] -= 2
|
||||
if ability_slot in self.front_ability_slots:
|
||||
self.front_ability_slots.remove(ability_slot)
|
||||
|
||||
elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
|
||||
self.AbilityQuantityDict[itemname]:
|
||||
@@ -518,11 +529,14 @@ class KH2Context(CommonContext):
|
||||
ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0]
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
|
||||
self.kh2_seed_save_cache["DonaldInvo"][0] -= 2
|
||||
elif itemname in self.goofy_ability_set:
|
||||
else:
|
||||
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0]
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
|
||||
self.kh2_seed_save_cache["GoofyInvo"][0] -= 2
|
||||
|
||||
if ability_slot in self.front_ability_slots:
|
||||
self.front_ability_slots.remove(ability_slot)
|
||||
|
||||
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
|
||||
# if memaddr is in a bitmask location in memory
|
||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
|
||||
@@ -615,7 +629,7 @@ class KH2Context(CommonContext):
|
||||
master_sell = master_equipment | master_staff | master_shield
|
||||
|
||||
await asyncio.create_task(self.IsInShop(master_sell))
|
||||
|
||||
# print(self.kh2_seed_save_cache["AmountInvo"]["Ability"])
|
||||
for item_name in master_amount:
|
||||
item_data = self.item_name_to_data[item_name]
|
||||
amount_of_items = 0
|
||||
@@ -673,10 +687,10 @@ class KH2Context(CommonContext):
|
||||
self.kh2_write_short(self.Save + slot, item_data.memaddr)
|
||||
# removes the duped ability if client gave faster than the game.
|
||||
|
||||
for charInvo in {"Sora", "Donald", "Goofy"}:
|
||||
if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0:
|
||||
print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}")
|
||||
self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0)
|
||||
for ability in self.front_ability_slots:
|
||||
if self.kh2_read_short(self.Save + ability) != 0:
|
||||
print(f"removed {self.Save + ability} from {ability}")
|
||||
self.kh2_write_short(self.Save + ability, 0)
|
||||
|
||||
# remove the dummy level 1 growths if they are in these invo slots.
|
||||
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
|
||||
@@ -740,15 +754,60 @@ class KH2Context(CommonContext):
|
||||
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
|
||||
|
||||
for item_name in master_stat:
|
||||
item_data = self.item_name_to_data[item_name]
|
||||
amount_of_items = 0
|
||||
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name]
|
||||
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5:
|
||||
if item_name == ItemName.MaxHPUp:
|
||||
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
|
||||
Bonus = 5
|
||||
else: # Critical
|
||||
Bonus = 2
|
||||
if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items):
|
||||
self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items))
|
||||
|
||||
elif item_name == ItemName.MaxMPUp:
|
||||
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
|
||||
Bonus = 10
|
||||
else: # Critical
|
||||
Bonus = 5
|
||||
if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items):
|
||||
self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items))
|
||||
|
||||
elif item_name == ItemName.DriveGaugeUp:
|
||||
current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2)
|
||||
# change when max drive is changed from 6 to 4
|
||||
if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items:
|
||||
self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items)
|
||||
|
||||
elif item_name == ItemName.AccessorySlotUp:
|
||||
current_accessory = self.kh2_read_byte(self.Save + 0x2501)
|
||||
if current_accessory != self.base_accessory_slots + amount_of_items:
|
||||
if 4 > current_accessory < self.base_accessory_slots + amount_of_items:
|
||||
self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1)
|
||||
elif self.base_accessory_slots + amount_of_items < 4:
|
||||
self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items)
|
||||
|
||||
elif item_name == ItemName.ArmorSlotUp:
|
||||
current_armor_slots = self.kh2_read_byte(self.Save + 0x2500)
|
||||
if current_armor_slots != self.base_armor_slots + amount_of_items:
|
||||
if 4 > current_armor_slots < self.base_armor_slots + amount_of_items:
|
||||
self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1)
|
||||
elif self.base_armor_slots + amount_of_items < 4:
|
||||
self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items)
|
||||
|
||||
elif item_name == ItemName.ItemSlotUp:
|
||||
current_item_slots = self.kh2_read_byte(self.Save + 0x2502)
|
||||
if current_item_slots != self.base_item_slots + amount_of_items:
|
||||
if 8 > current_item_slots < self.base_item_slots + amount_of_items:
|
||||
self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1)
|
||||
elif self.base_item_slots + amount_of_items < 8:
|
||||
self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items)
|
||||
|
||||
# if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \
|
||||
# and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \
|
||||
# self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}:
|
||||
# self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
|
||||
|
||||
# if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \
|
||||
and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \
|
||||
self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}:
|
||||
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
|
||||
if "PoptrackerVersionCheck" in self.kh2slotdata:
|
||||
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
|
||||
self.kh2_write_byte(self.Save + 0x3607, 1)
|
||||
@@ -762,7 +821,8 @@ class KH2Context(CommonContext):
|
||||
|
||||
def finishedGame(ctx: KH2Context, message):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked:
|
||||
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
|
||||
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
|
||||
ctx.final_xemnas = True
|
||||
# three proofs
|
||||
if ctx.kh2slotdata['Goal'] == 0:
|
||||
|
||||
@@ -2,22 +2,7 @@ import typing
|
||||
|
||||
from BaseClasses import Item
|
||||
from .Names import ItemName
|
||||
|
||||
|
||||
class KH2Item(Item):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
quantity: int = 0
|
||||
kh2id: int = 0
|
||||
# Save+ mem addr
|
||||
memaddr: int = 0
|
||||
# some items have bitmasks. if bitmask>0 bitor to give item else
|
||||
bitmask: int = 0
|
||||
# if ability then
|
||||
ability: bool = False
|
||||
|
||||
from .Subclasses import ItemData
|
||||
|
||||
# 0x130000
|
||||
Reports_Table = {
|
||||
@@ -209,7 +194,7 @@ Armor_Table = {
|
||||
ItemName.GrandRibbon: ItemData(1, 157, 0x35D4),
|
||||
}
|
||||
Usefull_Table = {
|
||||
ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
|
||||
ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
|
||||
ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per
|
||||
ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696),
|
||||
ItemName.UnknownDisk: ItemData(1, 462, 0x365F),
|
||||
@@ -349,7 +334,7 @@ GoofyAbility_Table = {
|
||||
|
||||
Wincon_Table = {
|
||||
ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item
|
||||
ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
|
||||
# ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
|
||||
ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14
|
||||
# ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster
|
||||
}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Location
|
||||
from .Names import LocationName, ItemName
|
||||
|
||||
|
||||
class KH2Location(Location):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class LocationData(typing.NamedTuple):
|
||||
locid: int
|
||||
yml: str
|
||||
charName: str = "Sora"
|
||||
charNumber: int = 1
|
||||
|
||||
from .Names import LocationName, ItemName, RegionName
|
||||
from .Subclasses import LocationData
|
||||
from .Regions import KH2REGIONS
|
||||
|
||||
# data's addrcheck sys3 addr obtained roomid bit index is eventid
|
||||
LoD_Checks = {
|
||||
@@ -541,7 +531,7 @@ TWTNW_Checks = {
|
||||
LocationName.Xemnas1: LocationData(26, "Double Get Bonus"),
|
||||
LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"),
|
||||
LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"),
|
||||
LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
|
||||
# LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
|
||||
LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"),
|
||||
}
|
||||
|
||||
@@ -806,74 +796,75 @@ Atlantica_Checks = {
|
||||
}
|
||||
|
||||
event_location_to_item = {
|
||||
LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
|
||||
LocationName.McpEventLocation: ItemName.McpEvent,
|
||||
LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
|
||||
LocationName.McpEventLocation: ItemName.McpEvent,
|
||||
# LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent,
|
||||
LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
|
||||
LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
|
||||
LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
|
||||
LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
|
||||
LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
|
||||
LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
|
||||
LocationName.CerberusEventLocation: ItemName.CerberusEvent,
|
||||
LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
|
||||
LocationName.HydraEventLocation: ItemName.HydraEvent,
|
||||
LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
|
||||
LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
|
||||
LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
|
||||
LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
|
||||
LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
|
||||
LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
|
||||
LocationName.CerberusEventLocation: ItemName.CerberusEvent,
|
||||
LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
|
||||
LocationName.HydraEventLocation: ItemName.HydraEvent,
|
||||
LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent,
|
||||
LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
|
||||
LocationName.HadesEventLocation: ItemName.HadesEvent,
|
||||
LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
|
||||
LocationName.HadesEventLocation: ItemName.HadesEvent,
|
||||
# LocationName.ASZexionEventLocation: ItemName.ASZexionEvent,
|
||||
LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
|
||||
LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
|
||||
LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
|
||||
LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
|
||||
LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
|
||||
LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
|
||||
# LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation,
|
||||
LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
|
||||
LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
|
||||
LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
|
||||
LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
|
||||
LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
|
||||
LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
|
||||
LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
|
||||
LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
|
||||
# LocationName.ASVexenEventLocation: ItemName.ASVexenEvent,
|
||||
LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
|
||||
LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
|
||||
LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
|
||||
LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
|
||||
LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
|
||||
LocationName.RoxasEventLocation: ItemName.RoxasEvent,
|
||||
LocationName.XigbarEventLocation: ItemName.XigbarEvent,
|
||||
LocationName.LuxordEventLocation: ItemName.LuxordEvent,
|
||||
LocationName.SaixEventLocation: ItemName.SaixEvent,
|
||||
LocationName.XemnasEventLocation: ItemName.XemnasEvent,
|
||||
LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
|
||||
LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
|
||||
LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
|
||||
LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
|
||||
LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
|
||||
LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
|
||||
LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
|
||||
LocationName.RoxasEventLocation: ItemName.RoxasEvent,
|
||||
LocationName.XigbarEventLocation: ItemName.XigbarEvent,
|
||||
LocationName.LuxordEventLocation: ItemName.LuxordEvent,
|
||||
LocationName.SaixEventLocation: ItemName.SaixEvent,
|
||||
LocationName.XemnasEventLocation: ItemName.XemnasEvent,
|
||||
LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
|
||||
LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
|
||||
# LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent,
|
||||
LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
|
||||
LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
|
||||
LocationName.BeastEventLocation: ItemName.BeastEvent,
|
||||
LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
|
||||
LocationName.XaldinEventLocation: ItemName.XaldinEvent,
|
||||
LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
|
||||
LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
|
||||
LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
|
||||
LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
|
||||
LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
|
||||
LocationName.BeastEventLocation: ItemName.BeastEvent,
|
||||
LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
|
||||
LocationName.XaldinEventLocation: ItemName.XaldinEvent,
|
||||
LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
|
||||
LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
|
||||
LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
|
||||
# LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent,
|
||||
LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
|
||||
LocationName.ScarEventLocation: ItemName.ScarEvent,
|
||||
LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
|
||||
LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
|
||||
LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
|
||||
LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
|
||||
LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
|
||||
LocationName.SephiEventLocation: ItemName.SephiEvent,
|
||||
LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
|
||||
LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
|
||||
LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
|
||||
LocationName.TransportEventLocation: ItemName.TransportEvent,
|
||||
LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
|
||||
LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
|
||||
LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
|
||||
LocationName.ScarEventLocation: ItemName.ScarEvent,
|
||||
LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
|
||||
LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
|
||||
LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
|
||||
LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
|
||||
LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
|
||||
LocationName.SephiEventLocation: ItemName.SephiEvent,
|
||||
LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
|
||||
LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
|
||||
LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
|
||||
LocationName.TransportEventLocation: ItemName.TransportEvent,
|
||||
LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
|
||||
LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
|
||||
# LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent,
|
||||
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
|
||||
LocationName.TerraEventLocation: ItemName.TerraEvent,
|
||||
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
|
||||
LocationName.Axel1EventLocation: ItemName.Axel1Event,
|
||||
LocationName.Axel2EventLocation: ItemName.Axel2Event,
|
||||
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
|
||||
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
|
||||
LocationName.TerraEventLocation: ItemName.TerraEvent,
|
||||
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
|
||||
LocationName.Axel1EventLocation: ItemName.Axel1Event,
|
||||
LocationName.Axel2EventLocation: ItemName.Axel2Event,
|
||||
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
|
||||
LocationName.FinalXemnasEventLocation: ItemName.Victory,
|
||||
}
|
||||
all_weapon_slot = {
|
||||
LocationName.FAKESlot,
|
||||
@@ -1361,3 +1352,9 @@ exclusion_table = {
|
||||
location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest"
|
||||
}
|
||||
}
|
||||
|
||||
location_groups: typing.Dict[str, list]
|
||||
location_groups = {
|
||||
Region_Name: [loc for loc in Region_Locs if "Event" not in loc]
|
||||
for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs and "Event" not in Region_Locs[0]
|
||||
}
|
||||
|
||||
@@ -606,11 +606,11 @@ hard_data_xemnas = {
|
||||
ItemName.LimitForm: 1,
|
||||
}
|
||||
final_leveling_access = {
|
||||
LocationName.MemorysSkyscaperMythrilCrystal,
|
||||
LocationName.RoxasEventLocation,
|
||||
LocationName.GrimReaper2,
|
||||
LocationName.Xaldin,
|
||||
LocationName.StormRider,
|
||||
LocationName.SunsetTerraceAbilityRing
|
||||
LocationName.UndergroundConcourseMythrilGem
|
||||
}
|
||||
|
||||
multi_form_region_access = {
|
||||
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool
|
||||
|
||||
from worlds.kh2 import default_itempool_option
|
||||
from . import default_itempool_option
|
||||
|
||||
|
||||
class SoraEXP(Range):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, Region
|
||||
from . import Locations
|
||||
|
||||
from .Locations import KH2Location, event_location_to_item
|
||||
from . import LocationName, RegionName, Events_Table
|
||||
from .Subclasses import KH2Location
|
||||
from .Names import LocationName, RegionName
|
||||
from .Items import Events_Table
|
||||
|
||||
KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
"Menu": [],
|
||||
@@ -788,7 +790,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
LocationName.ArmoredXemnas2EventLocation
|
||||
],
|
||||
RegionName.FinalXemnas: [
|
||||
LocationName.FinalXemnas
|
||||
LocationName.FinalXemnasEventLocation
|
||||
],
|
||||
RegionName.DataXemnas: [
|
||||
LocationName.XemnasDataPowerBoost,
|
||||
@@ -1020,7 +1022,8 @@ def create_regions(self):
|
||||
multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in
|
||||
KH2REGIONS.items()]
|
||||
# fill the event locations with events
|
||||
for location, item in event_location_to_item.items():
|
||||
|
||||
for location, item in Locations.event_location_to_item.items():
|
||||
multiworld.get_location(location, player).place_locked_item(
|
||||
multiworld.worlds[player].create_event_item(item))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, Callable, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table
|
||||
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table
|
||||
from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks
|
||||
from .Names import LocationName, ItemName, RegionName
|
||||
from worlds.generic.Rules import add_rule, forbid_items, add_item_rule
|
||||
@@ -83,6 +83,8 @@ class KH2Rules:
|
||||
return state.has(ItemName.TornPages, self.player, amount)
|
||||
|
||||
def level_locking_unlock(self, state: CollectionState, amount):
|
||||
if self.world.options.Promise_Charm and state.has(ItemName.PromiseCharm, self.player):
|
||||
return True
|
||||
return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]])
|
||||
|
||||
def summon_levels_unlocked(self, state: CollectionState, amount) -> bool:
|
||||
@@ -224,7 +226,7 @@ class KH2WorldRules(KH2Rules):
|
||||
RegionName.Pl2: lambda state: self.pl_unlocked(state, 2),
|
||||
|
||||
RegionName.Ag: lambda state: self.ag_unlocked(state, 1),
|
||||
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state),
|
||||
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement, ItemName.BlizzardElement, ItemName.ThunderElement], state),
|
||||
|
||||
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
|
||||
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
|
||||
@@ -266,10 +268,11 @@ class KH2WorldRules(KH2Rules):
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
|
||||
elif location.name in Donald_Checks:
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
|
||||
else:
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys())
|
||||
|
||||
def set_kh2_goal(self):
|
||||
|
||||
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player)
|
||||
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, self.player)
|
||||
if self.multiworld.Goal[self.player] == "three_proofs":
|
||||
final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state)
|
||||
if self.multiworld.FinalXemnas[self.player]:
|
||||
@@ -291,8 +294,8 @@ class KH2WorldRules(KH2Rules):
|
||||
else:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value)
|
||||
else:
|
||||
final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\
|
||||
state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value)
|
||||
final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \
|
||||
state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value)
|
||||
if self.multiworld.FinalXemnas[self.player]:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
|
||||
else:
|
||||
@@ -418,7 +421,7 @@ class KH2FightRules(KH2Rules):
|
||||
RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state),
|
||||
RegionName.OldPete: lambda state: self.get_old_pete_rules(),
|
||||
RegionName.FuturePete: lambda state: self.get_future_pete_rules(state),
|
||||
RegionName.Terra: lambda state: self.get_terra_rules(state),
|
||||
RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player),
|
||||
RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state),
|
||||
RegionName.Barbosa: lambda state: self.get_barbosa_rules(state),
|
||||
RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(),
|
||||
|
||||
29
worlds/kh2/Subclasses.py
Normal file
29
worlds/kh2/Subclasses.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Location, Item
|
||||
|
||||
|
||||
class KH2Location(Location):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class LocationData(typing.NamedTuple):
|
||||
locid: int
|
||||
yml: str
|
||||
charName: str = "Sora"
|
||||
charNumber: int = 1
|
||||
|
||||
|
||||
class KH2Item(Item):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
quantity: int = 0
|
||||
kh2id: int = 0
|
||||
# Save+ mem addr
|
||||
memaddr: int = 0
|
||||
# some items have bitmasks. if bitmask>0 bitor to give item else
|
||||
bitmask: int = 0
|
||||
# if ability then
|
||||
ability: bool = False
|
||||
@@ -12,6 +12,7 @@ from .OpenKH import patch_kh2
|
||||
from .Options import KingdomHearts2Options
|
||||
from .Regions import create_regions, connect_regions
|
||||
from .Rules import *
|
||||
from .Subclasses import KH2Item
|
||||
|
||||
|
||||
def launch_client():
|
||||
@@ -49,7 +50,9 @@ class KH2World(World):
|
||||
for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)}
|
||||
location_name_to_id = {item: location
|
||||
for location, item in enumerate(all_locations.keys(), 0x130000)}
|
||||
|
||||
item_name_groups = item_groups
|
||||
location_name_groups = location_groups
|
||||
|
||||
visitlocking_dict: Dict[str, int]
|
||||
plando_locations: Dict[str, str]
|
||||
@@ -253,11 +256,8 @@ class KH2World(World):
|
||||
self.goofy_gen_early()
|
||||
self.keyblade_gen_early()
|
||||
|
||||
if self.multiworld.FinalXemnas[self.player]:
|
||||
self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory
|
||||
else:
|
||||
self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name
|
||||
self.total_locations -= 1
|
||||
# final xemnas isn't a location anymore
|
||||
# self.total_locations -= 1
|
||||
|
||||
if self.options.WeaponSlotStartHint:
|
||||
for location in all_weapon_slot:
|
||||
|
||||
@@ -16,6 +16,7 @@ The [player settings page for this game](../player-settings) contains all the op
|
||||
- Popups
|
||||
- Get Bonuses
|
||||
- Form Levels
|
||||
- Summon Levels
|
||||
- Sora's Levels
|
||||
- Keyblade Stats
|
||||
- Keyblade Abilities
|
||||
@@ -23,7 +24,7 @@ The [player settings page for this game](../player-settings) contains all the op
|
||||
<h2 style="text-transform:none";>What Kingdom Hearts 2 items can appear in other players' worlds?</h2>
|
||||
|
||||
|
||||
Every item in the game except for party members' abilities.
|
||||
Every item in the game except for abilities on weapons.
|
||||
|
||||
<h2 style="text-transform:none";>What is The Garden of Assemblage "GoA"?</h2>
|
||||
|
||||
@@ -73,6 +74,8 @@ The list of possible locations that can contain a bounty:
|
||||
|
||||
- Each of the 13 Data Fights
|
||||
- Max level (7) for each Drive Form
|
||||
- Max level (7) of Summons
|
||||
- Last song of Atlantica
|
||||
- Sephiroth
|
||||
- Lingering Will
|
||||
- Starry Hill
|
||||
@@ -87,6 +90,7 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a
|
||||
|
||||
- Faster Wardrobe.
|
||||
- Faster Water Jafar Chase.
|
||||
- Faster Bulky Vendors
|
||||
- Carpet Skip.
|
||||
- Start with Lion Dash.
|
||||
- Faster Urns.
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<h2 style="text-transform:none";>Required Software:</h2>
|
||||
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts)
|
||||
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)<br>
|
||||
1. `3.0.0 OpenKH Mod Manager with Panacea`<br>
|
||||
2. `Install mod from KH2FM-Mods-Num/GoA-ROM-Edition`<br>
|
||||
3. `Setup Lua Backend From the 3.0.0 KH2Randomizer.exe per the setup guide linked above`<br>
|
||||
1. `3.2.0 OpenKH Mod Manager with Panacea`<br>
|
||||
2. `Lua Backend from the OpenKH Mod Manager`
|
||||
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`<br>
|
||||
|
||||
- Needed for Archipelago
|
||||
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)<br>
|
||||
2. `Install mod from JaredWeakStrike/APCompanion`<br>
|
||||
3. `Install mod from KH2FM-Mods-equations19/auto-save`<br>
|
||||
2. `Install the mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`<br>
|
||||
3. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`<br>
|
||||
4. `AP Randomizer Seed`
|
||||
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
|
||||
|
||||
@@ -68,8 +68,8 @@ Enter `The room's port number` into the top box <b> where the x's are</b> and pr
|
||||
- Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out.
|
||||
- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed
|
||||
|
||||
<h2 style="text-transform:none";>Requirement/logic sheet</h2>
|
||||
Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing)
|
||||
<h2 style="text-transform:none";>Logic Sheet</h2>
|
||||
Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing)
|
||||
<h2 style="text-transform:none";>F.A.Q.</h2>
|
||||
|
||||
- Why is my HP/MP continuously increasing without stopping?
|
||||
|
||||
@@ -146,7 +146,7 @@ def setReplacementName(key: str, value: str) -> None:
|
||||
|
||||
def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes:
|
||||
instr = instr.format(**_NAMES)
|
||||
s = instr.encode("ascii")
|
||||
s = instr.encode("ascii", errors="replace")
|
||||
s = s.replace(b"'", b"^")
|
||||
|
||||
def padLine(line: bytes) -> bytes:
|
||||
@@ -169,7 +169,7 @@ def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -
|
||||
if result_line:
|
||||
result += padLine(result_line)
|
||||
if ask is not None:
|
||||
askbytes = ask.encode("ascii")
|
||||
askbytes = ask.encode("ascii", errors="replace")
|
||||
result = result.rstrip()
|
||||
while len(result) % 32 != 16:
|
||||
result += b' '
|
||||
|
||||
@@ -399,6 +399,26 @@ class Palette(Choice):
|
||||
option_pink = 4
|
||||
option_inverted = 5
|
||||
|
||||
class Music(Choice, LADXROption):
|
||||
"""
|
||||
[Vanilla] Regular Music
|
||||
[Shuffled] Shuffled Music
|
||||
[Off] No music
|
||||
"""
|
||||
ladxr_name = "music"
|
||||
option_vanilla = 0
|
||||
option_shuffled = 1
|
||||
option_off = 2
|
||||
|
||||
|
||||
def to_ladxr_option(self, all_options):
|
||||
s = ""
|
||||
if self.value == self.option_shuffled:
|
||||
s = "random"
|
||||
elif self.value == self.option_off:
|
||||
s = "off"
|
||||
return self.ladxr_name, s
|
||||
|
||||
class WarpImprovements(DefaultOffToggle):
|
||||
"""
|
||||
[On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
|
||||
@@ -444,6 +464,7 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
|
||||
'shuffle_maps': ShuffleMaps,
|
||||
'shuffle_compasses': ShuffleCompasses,
|
||||
'shuffle_stone_beaks': ShuffleStoneBeaks,
|
||||
'music': Music,
|
||||
'music_change_condition': MusicChangeCondition,
|
||||
'nag_messages': NagMessages,
|
||||
'ap_title_screen': APTitleScreen,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- Software capable of loading and playing GBC ROM files
|
||||
- [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer.
|
||||
@@ -10,11 +10,12 @@
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version.
|
||||
**The installer file is located in the assets section at the bottom of the version information**.
|
||||
- During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file.
|
||||
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
||||
file is located in the assets section at the bottom of the version information.**
|
||||
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
|
||||
This is your Links Awakening DX ROM file. This only needs to be done once..
|
||||
|
||||
2. You should assign your emulator as your default program for launching ROM
|
||||
3. You should assign your emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
|
||||
@@ -30,8 +30,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings
|
||||
and export a config file from them.
|
||||
The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows
|
||||
you to easily configure your personal settings
|
||||
|
||||
## How-to-play
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user