mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 15:45:04 -07:00
Compare commits
4 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bf3ce2da4 | ||
|
|
8fae75f577 | ||
|
|
8b8a95089c | ||
|
|
1e99625f3b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -150,7 +150,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
*.code-workspace
|
.code-workspace
|
||||||
shell.nix
|
shell.nix
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
|
|||||||
112
BaseClasses.py
112
BaseClasses.py
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
|
||||||
import copy
|
import copy
|
||||||
import itertools
|
import itertools
|
||||||
import functools
|
import functools
|
||||||
@@ -64,6 +63,7 @@ class MultiWorld():
|
|||||||
state: CollectionState
|
state: CollectionState
|
||||||
|
|
||||||
plando_options: PlandoOptions
|
plando_options: PlandoOptions
|
||||||
|
accessibility: Dict[int, Options.Accessibility]
|
||||||
early_items: Dict[int, Dict[str, int]]
|
early_items: Dict[int, Dict[str, int]]
|
||||||
local_early_items: Dict[int, Dict[str, int]]
|
local_early_items: Dict[int, Dict[str, int]]
|
||||||
local_items: Dict[int, Options.LocalItems]
|
local_items: Dict[int, Options.LocalItems]
|
||||||
@@ -288,86 +288,6 @@ class MultiWorld():
|
|||||||
group["non_local_items"] = item_link["non_local_items"]
|
group["non_local_items"] = item_link["non_local_items"]
|
||||||
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
||||||
|
|
||||||
def link_items(self) -> None:
|
|
||||||
"""Called to link together items in the itempool related to the registered item link groups."""
|
|
||||||
from worlds import AutoWorld
|
|
||||||
|
|
||||||
for group_id, group in self.groups.items():
|
|
||||||
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
|
||||||
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
|
|
||||||
]:
|
|
||||||
classifications: Dict[str, int] = collections.defaultdict(int)
|
|
||||||
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
|
||||||
for item in self.itempool:
|
|
||||||
if item.player in counters and item.name in shared_pool:
|
|
||||||
counters[item.player][item.name] += 1
|
|
||||||
classifications[item.name] |= item.classification
|
|
||||||
|
|
||||||
for player in players.copy():
|
|
||||||
if all([counters[player][item] == 0 for item in shared_pool]):
|
|
||||||
players.remove(player)
|
|
||||||
del (counters[player])
|
|
||||||
|
|
||||||
if not players:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
for item in shared_pool:
|
|
||||||
count = min(counters[player][item] for player in players)
|
|
||||||
if count:
|
|
||||||
for player in players:
|
|
||||||
counters[player][item] = count
|
|
||||||
else:
|
|
||||||
for player in players:
|
|
||||||
del (counters[player][item])
|
|
||||||
return counters, classifications
|
|
||||||
|
|
||||||
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
|
|
||||||
if not common_item_count:
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_itempool: List[Item] = []
|
|
||||||
for item_name, item_count in next(iter(common_item_count.values())).items():
|
|
||||||
for _ in range(item_count):
|
|
||||||
new_item = group["world"].create_item(item_name)
|
|
||||||
# mangle together all original classification bits
|
|
||||||
new_item.classification |= classifications[item_name]
|
|
||||||
new_itempool.append(new_item)
|
|
||||||
|
|
||||||
region = Region("Menu", group_id, self, "ItemLink")
|
|
||||||
self.regions.append(region)
|
|
||||||
locations = region.locations
|
|
||||||
for item in self.itempool:
|
|
||||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
|
||||||
if count:
|
|
||||||
loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}",
|
|
||||||
None, region)
|
|
||||||
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
|
|
||||||
state.has(item_name, group_id_, count_)
|
|
||||||
|
|
||||||
locations.append(loc)
|
|
||||||
loc.place_locked_item(item)
|
|
||||||
common_item_count[item.player][item.name] -= 1
|
|
||||||
else:
|
|
||||||
new_itempool.append(item)
|
|
||||||
|
|
||||||
itemcount = len(self.itempool)
|
|
||||||
self.itempool = new_itempool
|
|
||||||
|
|
||||||
while itemcount > len(self.itempool):
|
|
||||||
items_to_add = []
|
|
||||||
for player in group["players"]:
|
|
||||||
if group["link_replacement"]:
|
|
||||||
item_player = group_id
|
|
||||||
else:
|
|
||||||
item_player = player
|
|
||||||
if group["replacement_items"][player]:
|
|
||||||
items_to_add.append(AutoWorld.call_single(self, "create_item", item_player,
|
|
||||||
group["replacement_items"][player]))
|
|
||||||
else:
|
|
||||||
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player))
|
|
||||||
self.random.shuffle(items_to_add)
|
|
||||||
self.itempool.extend(items_to_add[:itemcount - len(self.itempool)])
|
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
||||||
self.is_race = True
|
self.is_race = True
|
||||||
@@ -603,22 +523,26 @@ class MultiWorld():
|
|||||||
players: Dict[str, Set[int]] = {
|
players: Dict[str, Set[int]] = {
|
||||||
"minimal": set(),
|
"minimal": set(),
|
||||||
"items": set(),
|
"items": set(),
|
||||||
"full": set()
|
"locations": set()
|
||||||
}
|
}
|
||||||
for player, world in self.worlds.items():
|
for player, access in self.accessibility.items():
|
||||||
players[world.options.accessibility.current_key].add(player)
|
players[access.current_key].add(player)
|
||||||
|
|
||||||
beatable_fulfilled = False
|
beatable_fulfilled = False
|
||||||
|
|
||||||
def location_condition(location: Location) -> bool:
|
def location_condition(location: Location):
|
||||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||||
return location.player in players["full"] or \
|
if location.player in players["locations"] or (location.item and location.item.player not in
|
||||||
(location.item and location.item.player not in players["minimal"])
|
players["minimal"]):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def location_relevant(location: Location) -> bool:
|
def location_relevant(location: Location):
|
||||||
"""Determine if this location is relevant to sweep."""
|
"""Determine if this location is relevant to sweep."""
|
||||||
return location.progress_type != LocationProgressType.EXCLUDED \
|
if location.progress_type != LocationProgressType.EXCLUDED \
|
||||||
and (location.player in players["full"] or location.advancement)
|
and (location.player in players["locations"] or location.advancement):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def all_done() -> bool:
|
def all_done() -> bool:
|
||||||
"""Check if all access rules are fulfilled"""
|
"""Check if all access rules are fulfilled"""
|
||||||
@@ -756,13 +680,13 @@ class CollectionState():
|
|||||||
def can_reach_region(self, spot: str, player: int) -> bool:
|
def can_reach_region(self, spot: str, player: int) -> bool:
|
||||||
return self.multiworld.get_region(spot, player).can_reach(self)
|
return self.multiworld.get_region(spot, player).can_reach(self)
|
||||||
|
|
||||||
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
|
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
|
||||||
if locations is None:
|
if locations is None:
|
||||||
locations = self.multiworld.get_filled_locations()
|
locations = self.multiworld.get_filled_locations()
|
||||||
reachable_events = True
|
reachable_events = True
|
||||||
# since the loop has a good chance to run more than once, only filter the events once
|
# since the loop has a good chance to run more than once, only filter the events once
|
||||||
locations = {location for location in locations if location.advancement and location not in self.events}
|
locations = {location for location in locations if location.advancement and location not in self.events and
|
||||||
|
not key_only or getattr(location.item, "locked_dungeon_item", False)}
|
||||||
while reachable_events:
|
while reachable_events:
|
||||||
reachable_events = {location for location in locations if location.can_reach(self)}
|
reachable_events = {location for location in locations if location.can_reach(self)}
|
||||||
locations -= reachable_events
|
locations -= reachable_events
|
||||||
@@ -1367,6 +1291,8 @@ class Spoiler:
|
|||||||
state = CollectionState(multiworld)
|
state = CollectionState(multiworld)
|
||||||
collection_spheres = []
|
collection_spheres = []
|
||||||
while required_locations:
|
while required_locations:
|
||||||
|
state.sweep_for_events(key_only=True)
|
||||||
|
|
||||||
sphere = set(filter(state.can_reach, required_locations))
|
sphere = set(filter(state.can_reach, required_locations))
|
||||||
|
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
|
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes)
|
||||||
from Utils import Version, stream_input, async_start
|
from Utils import Version, stream_input, async_start
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
import os
|
import os
|
||||||
@@ -61,7 +61,6 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
if address:
|
if address:
|
||||||
self.ctx.server_address = None
|
self.ctx.server_address = None
|
||||||
self.ctx.username = None
|
self.ctx.username = None
|
||||||
self.ctx.password = None
|
|
||||||
elif not self.ctx.server_address:
|
elif not self.ctx.server_address:
|
||||||
self.output("Please specify an address.")
|
self.output("Please specify an address.")
|
||||||
return False
|
return False
|
||||||
@@ -515,7 +514,6 @@ class CommonContext:
|
|||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
self.server_address = ""
|
self.server_address = ""
|
||||||
self.username = None
|
self.username = None
|
||||||
self.password = None
|
|
||||||
self.cancel_autoreconnect()
|
self.cancel_autoreconnect()
|
||||||
if self.server and not self.server.socket.closed:
|
if self.server and not self.server.socket.closed:
|
||||||
await self.server.socket.close()
|
await self.server.socket.close()
|
||||||
@@ -864,8 +862,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
ctx.team = args["team"]
|
ctx.team = args["team"]
|
||||||
ctx.slot = args["slot"]
|
ctx.slot = args["slot"]
|
||||||
# int keys get lost in JSON transfer
|
# int keys get lost in JSON transfer
|
||||||
ctx.slot_info = {0: NetworkSlot("Archipelago", "Archipelago", SlotType.player)}
|
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
|
||||||
ctx.slot_info.update({int(pid): data for pid, data in args["slot_info"].items()})
|
|
||||||
ctx.hint_points = args.get("hint_points", 0)
|
ctx.hint_points = args.get("hint_points", 0)
|
||||||
ctx.consume_players_package(args["players"])
|
ctx.consume_players_package(args["players"])
|
||||||
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
|
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
|
||||||
|
|||||||
8
Fill.py
8
Fill.py
@@ -227,15 +227,12 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||||
total = min(len(itempool), len(locations))
|
total = min(len(itempool), len(locations))
|
||||||
placed = 0
|
placed = 0
|
||||||
|
|
||||||
state = CollectionState(multiworld)
|
|
||||||
|
|
||||||
while locations and itempool:
|
while locations and itempool:
|
||||||
item_to_place = itempool.pop()
|
item_to_place = itempool.pop()
|
||||||
spot_to_fill: typing.Optional[Location] = None
|
spot_to_fill: typing.Optional[Location] = None
|
||||||
|
|
||||||
for i, location in enumerate(locations):
|
for i, location in enumerate(locations):
|
||||||
if location.can_fill(state, item_to_place, check_access=False):
|
if location.item_rule(item_to_place):
|
||||||
# popping by index is faster than removing by content,
|
# popping by index is faster than removing by content,
|
||||||
spot_to_fill = locations.pop(i)
|
spot_to_fill = locations.pop(i)
|
||||||
# skipping a scan for the element
|
# skipping a scan for the element
|
||||||
@@ -256,7 +253,7 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
|
|
||||||
location.item = None
|
location.item = None
|
||||||
placed_item.location = None
|
placed_item.location = None
|
||||||
if location.can_fill(state, item_to_place, check_access=False):
|
if location.item_rule(item_to_place):
|
||||||
# Add this item to the existing placement, and
|
# Add this item to the existing placement, and
|
||||||
# add the old item to the back of the queue
|
# add the old item to the back of the queue
|
||||||
spot_to_fill = placements.pop(i)
|
spot_to_fill = placements.pop(i)
|
||||||
@@ -649,6 +646,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
|||||||
|
|
||||||
def get_sphere_locations(sphere_state: CollectionState,
|
def get_sphere_locations(sphere_state: CollectionState,
|
||||||
locations: typing.Set[Location]) -> typing.Set[Location]:
|
locations: typing.Set[Location]) -> typing.Set[Location]:
|
||||||
|
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||||
return {loc for loc in locations if sphere_state.can_reach(loc)}
|
return {loc for loc in locations if sphere_state.can_reach(loc)}
|
||||||
|
|
||||||
def item_percentage(player: int, num: int) -> float:
|
def item_percentage(player: int, num: int) -> float:
|
||||||
|
|||||||
@@ -446,6 +446,14 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
|||||||
|
|
||||||
|
|
||||||
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
|
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
|
||||||
|
"""
|
||||||
|
Roll options from specified weights, usually originating from a .yaml options file.
|
||||||
|
|
||||||
|
Important note:
|
||||||
|
The same weights dict is shared between all slots using the same yaml (e.g. generic weights file for filler slots).
|
||||||
|
This means it should never be modified without making a deepcopy first.
|
||||||
|
"""
|
||||||
|
|
||||||
from worlds import AutoWorldRegister
|
from worlds import AutoWorldRegister
|
||||||
|
|
||||||
if "linked_options" in weights:
|
if "linked_options" in weights:
|
||||||
|
|||||||
90
Main.py
90
Main.py
@@ -124,19 +124,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player in multiworld.player_ids:
|
for player in multiworld.player_ids:
|
||||||
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
|
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
|
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
|
||||||
world_excluded_locations = set()
|
|
||||||
for location_name in multiworld.worlds[player].options.priority_locations.value:
|
for location_name in multiworld.worlds[player].options.priority_locations.value:
|
||||||
try:
|
try:
|
||||||
location = multiworld.get_location(location_name, player)
|
location = multiworld.get_location(location_name, player)
|
||||||
except KeyError:
|
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||||
continue
|
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
|
||||||
if location.progress_type != LocationProgressType.EXCLUDED:
|
|
||||||
location.progress_type = LocationProgressType.PRIORITY
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.")
|
location.progress_type = LocationProgressType.PRIORITY
|
||||||
world_excluded_locations.add(location_name)
|
|
||||||
multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations
|
|
||||||
|
|
||||||
# Set local and non-local item rules.
|
# Set local and non-local item rules.
|
||||||
if multiworld.players > 1:
|
if multiworld.players > 1:
|
||||||
@@ -184,7 +179,82 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||||
multiworld.itempool[:] = new_items
|
multiworld.itempool[:] = new_items
|
||||||
|
|
||||||
multiworld.link_items()
|
# temporary home for item links, should be moved out of Main
|
||||||
|
for group_id, group in multiworld.groups.items():
|
||||||
|
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
||||||
|
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
|
||||||
|
]:
|
||||||
|
classifications: Dict[str, int] = collections.defaultdict(int)
|
||||||
|
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
||||||
|
for item in multiworld.itempool:
|
||||||
|
if item.player in counters and item.name in shared_pool:
|
||||||
|
counters[item.player][item.name] += 1
|
||||||
|
classifications[item.name] |= item.classification
|
||||||
|
|
||||||
|
for player in players.copy():
|
||||||
|
if all([counters[player][item] == 0 for item in shared_pool]):
|
||||||
|
players.remove(player)
|
||||||
|
del (counters[player])
|
||||||
|
|
||||||
|
if not players:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
for item in shared_pool:
|
||||||
|
count = min(counters[player][item] for player in players)
|
||||||
|
if count:
|
||||||
|
for player in players:
|
||||||
|
counters[player][item] = count
|
||||||
|
else:
|
||||||
|
for player in players:
|
||||||
|
del (counters[player][item])
|
||||||
|
return counters, classifications
|
||||||
|
|
||||||
|
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
|
||||||
|
if not common_item_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_itempool: List[Item] = []
|
||||||
|
for item_name, item_count in next(iter(common_item_count.values())).items():
|
||||||
|
for _ in range(item_count):
|
||||||
|
new_item = group["world"].create_item(item_name)
|
||||||
|
# mangle together all original classification bits
|
||||||
|
new_item.classification |= classifications[item_name]
|
||||||
|
new_itempool.append(new_item)
|
||||||
|
|
||||||
|
region = Region("Menu", group_id, multiworld, "ItemLink")
|
||||||
|
multiworld.regions.append(region)
|
||||||
|
locations = region.locations
|
||||||
|
for item in multiworld.itempool:
|
||||||
|
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||||
|
if count:
|
||||||
|
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}",
|
||||||
|
None, region)
|
||||||
|
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
|
||||||
|
state.has(item_name, group_id_, count_)
|
||||||
|
|
||||||
|
locations.append(loc)
|
||||||
|
loc.place_locked_item(item)
|
||||||
|
common_item_count[item.player][item.name] -= 1
|
||||||
|
else:
|
||||||
|
new_itempool.append(item)
|
||||||
|
|
||||||
|
itemcount = len(multiworld.itempool)
|
||||||
|
multiworld.itempool = new_itempool
|
||||||
|
|
||||||
|
while itemcount > len(multiworld.itempool):
|
||||||
|
items_to_add = []
|
||||||
|
for player in group["players"]:
|
||||||
|
if group["link_replacement"]:
|
||||||
|
item_player = group_id
|
||||||
|
else:
|
||||||
|
item_player = player
|
||||||
|
if group["replacement_items"][player]:
|
||||||
|
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player,
|
||||||
|
group["replacement_items"][player]))
|
||||||
|
else:
|
||||||
|
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player))
|
||||||
|
multiworld.random.shuffle(items_to_add)
|
||||||
|
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)])
|
||||||
|
|
||||||
if any(multiworld.item_links.values()):
|
if any(multiworld.item_links.values()):
|
||||||
multiworld._all_state = None
|
multiworld._all_state = None
|
||||||
|
|||||||
@@ -1352,7 +1352,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if self.ctx.remaining_mode == "enabled":
|
if self.ctx.remaining_mode == "enabled":
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id]
|
||||||
for item_id in remaining_item_ids))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
@@ -1365,7 +1365,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id]
|
||||||
for item_id in remaining_item_ids))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
|
|||||||
67
Options.py
67
Options.py
@@ -786,22 +786,17 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
|||||||
verify_location_name: bool = False
|
verify_location_name: bool = False
|
||||||
value: typing.Any
|
value: typing.Any
|
||||||
|
|
||||||
def verify_keys(self) -> None:
|
@classmethod
|
||||||
if self.valid_keys:
|
def verify_keys(cls, data: typing.Iterable[str]) -> None:
|
||||||
data = set(self.value)
|
if cls.valid_keys:
|
||||||
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
|
data = set(data)
|
||||||
extra = dataset - self._valid_keys
|
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
||||||
|
extra = dataset - cls._valid_keys
|
||||||
if extra:
|
if extra:
|
||||||
raise OptionError(
|
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||||
f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
|
f"Allowed keys: {cls._valid_keys}.")
|
||||||
f"Allowed keys: {self._valid_keys}."
|
|
||||||
)
|
|
||||||
|
|
||||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||||
try:
|
|
||||||
self.verify_keys()
|
|
||||||
except OptionError as validation_error:
|
|
||||||
raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}")
|
|
||||||
if self.convert_name_groups and self.verify_item_name:
|
if self.convert_name_groups and self.verify_item_name:
|
||||||
new_value = type(self.value)() # empty container of whatever value is
|
new_value = type(self.value)() # empty container of whatever value is
|
||||||
for item_name in self.value:
|
for item_name in self.value:
|
||||||
@@ -838,6 +833,7 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||||
if type(data) == dict:
|
if type(data) == dict:
|
||||||
|
cls.verify_keys(data)
|
||||||
return cls(data)
|
return cls(data)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||||
@@ -883,6 +879,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
if is_iterable_except_str(data):
|
if is_iterable_except_str(data):
|
||||||
|
cls.verify_keys(data)
|
||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
@@ -908,6 +905,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
if is_iterable_except_str(data):
|
if is_iterable_except_str(data):
|
||||||
|
cls.verify_keys(data)
|
||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
@@ -950,19 +948,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
self.value = []
|
self.value = []
|
||||||
logging.warning(f"The plando texts module is turned off, "
|
logging.warning(f"The plando texts module is turned off, "
|
||||||
f"so text for {player_name} will be ignored.")
|
f"so text for {player_name} will be ignored.")
|
||||||
else:
|
|
||||||
super().verify(world, player_name, plando_options)
|
|
||||||
|
|
||||||
def verify_keys(self) -> None:
|
|
||||||
if self.valid_keys:
|
|
||||||
data = set(text.at for text in self)
|
|
||||||
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
|
|
||||||
extra = dataset - self._valid_keys
|
|
||||||
if extra:
|
|
||||||
raise OptionError(
|
|
||||||
f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
|
|
||||||
f"Allowed placements: {self._valid_keys}."
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
|
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
|
||||||
@@ -986,6 +971,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
texts.append(text)
|
texts.append(text)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
||||||
|
cls.verify_keys([text.at for text in texts])
|
||||||
return cls(texts)
|
return cls(texts)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
|
||||||
@@ -1158,35 +1144,18 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
|||||||
|
|
||||||
|
|
||||||
class Accessibility(Choice):
|
class Accessibility(Choice):
|
||||||
"""
|
"""Set rules for reachability of your items/locations.
|
||||||
Set rules for reachability of your items/locations.
|
|
||||||
|
|
||||||
**Full:** ensure everything can be reached and acquired.
|
|
||||||
|
|
||||||
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
- **Locations:** ensure everything can be reached and acquired.
|
||||||
|
- **Items:** ensure all logically relevant items can be acquired.
|
||||||
|
- **Minimal:** ensure what is needed to reach your goal can be acquired.
|
||||||
"""
|
"""
|
||||||
display_name = "Accessibility"
|
display_name = "Accessibility"
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
option_full = 0
|
option_locations = 0
|
||||||
|
option_items = 1
|
||||||
option_minimal = 2
|
option_minimal = 2
|
||||||
alias_none = 2
|
alias_none = 2
|
||||||
alias_locations = 0
|
|
||||||
alias_items = 0
|
|
||||||
default = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ItemsAccessibility(Accessibility):
|
|
||||||
"""
|
|
||||||
Set rules for reachability of your items/locations.
|
|
||||||
|
|
||||||
**Full:** ensure everything can be reached and acquired.
|
|
||||||
|
|
||||||
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
|
||||||
|
|
||||||
**Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and
|
|
||||||
some locations may be inaccessible.
|
|
||||||
"""
|
|
||||||
option_items = 1
|
|
||||||
default = 1
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_patch(self):
|
def _cmd_patch(self):
|
||||||
"""Patch the game. Only use this command if /auto_patch fails."""
|
"""Patch the game. Only use this command if /auto_patch fails."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patched.")
|
self.output("Patched.")
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||||
"""Patch the game automatically."""
|
"""Patch the game automatically."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||||
tempInstall = steaminstall
|
tempInstall = steaminstall
|
||||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
tempInstall = None
|
tempInstall = None
|
||||||
@@ -62,7 +62,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
for file_name in os.listdir(tempInstall):
|
for file_name in os.listdir(tempInstall):
|
||||||
if file_name != "steam_api.dll":
|
if file_name != "steam_api.dll":
|
||||||
shutil.copy(os.path.join(tempInstall, file_name),
|
shutil.copy(os.path.join(tempInstall, file_name),
|
||||||
Utils.user_path("Undertale", file_name))
|
os.path.join(os.getcwd(), "Undertale", file_name))
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patching successful!")
|
self.output("Patching successful!")
|
||||||
|
|
||||||
@@ -111,12 +111,12 @@ class UndertaleContext(CommonContext):
|
|||||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
|
||||||
def patch_game(self):
|
def patch_game(self):
|
||||||
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
|
||||||
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
with open(Utils.user_path("Undertale", "data.win"), "wb") as f:
|
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
|
||||||
f.write(patchedFile)
|
f.write(patchedFile)
|
||||||
os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
|
||||||
with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites",
|
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites",
|
||||||
"Which Character.txt")), "w") as f:
|
"Which Character.txt")), "w") as f:
|
||||||
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
||||||
"line other than this one.\n", "frisk"])
|
"line other than this one.\n", "frisk"])
|
||||||
|
|||||||
@@ -325,12 +325,10 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
def run(self):
|
def run(self):
|
||||||
while 1:
|
while 1:
|
||||||
next_room = rooms_to_run.get(block=True, timeout=None)
|
next_room = rooms_to_run.get(block=True, timeout=None)
|
||||||
gc.collect(0)
|
|
||||||
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
||||||
self._tasks.append(task)
|
self._tasks.append(task)
|
||||||
task.add_done_callback(self._done)
|
task.add_done_callback(self._done)
|
||||||
logging.info(f"Starting room {next_room} on {name}.")
|
logging.info(f"Starting room {next_room} on {name}.")
|
||||||
del task # delete reference to task object
|
|
||||||
|
|
||||||
starter = Starter()
|
starter = Starter()
|
||||||
starter.daemon = True
|
starter.daemon = True
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from typing import Any, IO, Dict, Iterator, List, Tuple, Union
|
from typing import List, Dict, Union
|
||||||
|
|
||||||
import jinja2.exceptions
|
import jinja2.exceptions
|
||||||
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
@@ -97,37 +97,25 @@ def new_room(seed: UUID):
|
|||||||
return redirect(url_for("host_room", room=room.id))
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
def _read_log(log: IO[Any], offset: int = 0) -> Iterator[bytes]:
|
def _read_log(path: str):
|
||||||
marker = log.read(3) # skip optional BOM
|
if os.path.exists(path):
|
||||||
if marker != b'\xEF\xBB\xBF':
|
with open(path, encoding="utf-8-sig") as log:
|
||||||
log.seek(0, os.SEEK_SET)
|
yield from log
|
||||||
log.seek(offset, os.SEEK_CUR)
|
else:
|
||||||
yield from log
|
yield f"Logfile {path} does not exist. " \
|
||||||
log.close() # free file handle as soon as possible
|
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log/<suuid:room>')
|
@app.route('/log/<suuid:room>')
|
||||||
def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
|
def display_log(room: UUID):
|
||||||
room = Room.get(id=room)
|
room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
if room.owner == session["_id"]:
|
if room.owner == session["_id"]:
|
||||||
file_path = os.path.join("logs", str(room.id) + ".txt")
|
file_path = os.path.join("logs", str(room.id) + ".txt")
|
||||||
try:
|
if os.path.exists(file_path):
|
||||||
log = open(file_path, "rb")
|
return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8")
|
||||||
range_header = request.headers.get("Range")
|
return "Log File does not exist."
|
||||||
if range_header:
|
|
||||||
range_type, range_values = range_header.split('=')
|
|
||||||
start, end = map(str.strip, range_values.split('-', 1))
|
|
||||||
if range_type != "bytes" or end != "":
|
|
||||||
return "Unsupported range", 500
|
|
||||||
# NOTE: we skip Content-Range in the response here, which isn't great but works for our JS
|
|
||||||
return Response(_read_log(log, int(start)), mimetype="text/plain", status=206)
|
|
||||||
return Response(_read_log(log), mimetype="text/plain")
|
|
||||||
except FileNotFoundError:
|
|
||||||
return Response(f"Logfile {file_path} does not exist. "
|
|
||||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up.",
|
|
||||||
mimetype="text/plain")
|
|
||||||
|
|
||||||
return "Access Denied", 403
|
return "Access Denied", 403
|
||||||
|
|
||||||
@@ -151,22 +139,7 @@ def host_room(room: UUID):
|
|||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = now # will trigger a spinup, if it's not already running
|
room.last_activity = now # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
def get_log(max_size: int = 1024000) -> str:
|
return render_template("hostRoom.html", room=room, should_refresh=should_refresh)
|
||||||
try:
|
|
||||||
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
|
||||||
raw_size = 0
|
|
||||||
fragments: List[str] = []
|
|
||||||
for block in _read_log(log):
|
|
||||||
if raw_size + len(block) > max_size:
|
|
||||||
fragments.append("…")
|
|
||||||
break
|
|
||||||
raw_size += len(block)
|
|
||||||
fragments.append(block.decode("utf-8"))
|
|
||||||
return "".join(fragments)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
|
|||||||
@@ -231,13 +231,6 @@ def generate_yaml(game: str):
|
|||||||
|
|
||||||
del options[key]
|
del options[key]
|
||||||
|
|
||||||
# Detect keys which end with -range, indicating a NamedRange with a possible custom value
|
|
||||||
elif key_parts[-1].endswith("-range"):
|
|
||||||
if options[key_parts[-1][:-6]] == "custom":
|
|
||||||
options[key_parts[-1][:-6]] = val
|
|
||||||
|
|
||||||
del options[key]
|
|
||||||
|
|
||||||
# Detect random-* keys and set their options accordingly
|
# Detect random-* keys and set their options accordingly
|
||||||
for key, val in options.copy().items():
|
for key, val in options.copy().items():
|
||||||
if key.startswith("random-"):
|
if key.startswith("random-"):
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from . import cache
|
|||||||
def robots():
|
def robots():
|
||||||
# If this host is not official, do not allow search engine crawling
|
# If this host is not official, do not allow search engine crawling
|
||||||
if not app.config["ASSET_RIGHTS"]:
|
if not app.config["ASSET_RIGHTS"]:
|
||||||
# filename changed in case the path is intercepted and served by an outside service
|
return app.send_static_file('robots.txt')
|
||||||
return app.send_static_file('robots_file.txt')
|
|
||||||
|
|
||||||
# Send 404 if the host has affirmed this to be the official WebHost
|
# Send 404 if the host has affirmed this to be the official WebHost
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<form method="post" id="command-form" style="flex-grow: 1; margin-right: 1em;">
|
<form method=post style="flex-grow: 1; margin-right: 1em;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cmd"></label>
|
<label for="cmd"></label>
|
||||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||||
@@ -55,89 +55,24 @@
|
|||||||
Open Log File...
|
Open Log File...
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% set log = get_log() -%}
|
<div id="logger"></div>
|
||||||
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
|
<script type="application/ecmascript">
|
||||||
<div id="logger" style="white-space: pre">{{ log }}</div>
|
let xmlhttp = new XMLHttpRequest();
|
||||||
<script>
|
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
|
||||||
let bytesReceived = {{ log_len }};
|
|
||||||
let updateLogTimeout;
|
|
||||||
let awaitingCommandResponse = false;
|
|
||||||
let logger = document.getElementById("logger");
|
|
||||||
|
|
||||||
function scrollToBottom(el) {
|
xmlhttp.onreadystatechange = function () {
|
||||||
let bot = el.scrollHeight - el.clientHeight;
|
if (this.readyState === 4 && this.status === 200) {
|
||||||
el.scrollTop += Math.ceil((bot - el.scrollTop)/10);
|
document.getElementById("logger").innerText = this.responseText;
|
||||||
if (bot - el.scrollTop >= 1) {
|
|
||||||
window.clearTimeout(el.scrollTimer);
|
|
||||||
el.scrollTimer = window.setTimeout(() => {
|
|
||||||
scrollToBottom(el)
|
|
||||||
}, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateLog() {
|
|
||||||
try {
|
|
||||||
let res = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
'Range': `bytes=${bytesReceived}-`,
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
if (res.ok) {
|
|
||||||
let text = await res.text();
|
|
||||||
if (text.length > 0) {
|
|
||||||
awaitingCommandResponse = false;
|
|
||||||
if (bytesReceived === 0 || res.status !== 206) {
|
|
||||||
logger.innerHTML = '';
|
|
||||||
}
|
|
||||||
if (res.status !== 206) {
|
|
||||||
bytesReceived = 0;
|
|
||||||
} else {
|
|
||||||
bytesReceived += new Blob([text]).size;
|
|
||||||
}
|
|
||||||
if (logger.innerHTML.endsWith('…')) {
|
|
||||||
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
|
||||||
}
|
|
||||||
logger.appendChild(document.createTextNode(text));
|
|
||||||
scrollToBottom(logger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
window.clearTimeout(updateLogTimeout);
|
|
||||||
updateLogTimeout = window.setTimeout(updateLog, awaitingCommandResponse ? 500 : 10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postForm(ev) {
|
function request_new() {
|
||||||
/** @type {HTMLInputElement} */
|
xmlhttp.open("GET", url, true);
|
||||||
let cmd = document.getElementById("cmd");
|
xmlhttp.send();
|
||||||
if (cmd.value === "") {
|
|
||||||
ev.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
/** @type {HTMLFormElement} */
|
|
||||||
let form = document.getElementById("command-form");
|
|
||||||
let req = fetch(form.action || window.location.href, {
|
|
||||||
method: form.method,
|
|
||||||
body: new FormData(form),
|
|
||||||
redirect: "manual",
|
|
||||||
});
|
|
||||||
ev.preventDefault(); // has to happen before first await
|
|
||||||
form.reset();
|
|
||||||
let res = await req;
|
|
||||||
if (res.ok || res.type === 'opaqueredirect') {
|
|
||||||
awaitingCommandResponse = true;
|
|
||||||
window.clearTimeout(updateLogTimeout);
|
|
||||||
updateLogTimeout = window.setTimeout(updateLog, 100);
|
|
||||||
} else {
|
|
||||||
window.alert(res.statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("command-form").addEventListener("submit", postForm);
|
window.setTimeout(request_new, 1000);
|
||||||
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
window.setInterval(request_new, 10000);
|
||||||
logger.scrollTop = logger.scrollHeight;
|
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
{% macro NamedRange(option_name, option) %}
|
{% macro NamedRange(option_name, option) %}
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="named-range-container">
|
<div class="named-range-container">
|
||||||
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||||
{% for key, val in option.special_range_names.items() %}
|
{% for key, val in option.special_range_names.items() %}
|
||||||
{% if option.default == val %}
|
{% if option.default == val %}
|
||||||
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||||
@@ -64,17 +64,17 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<option value="custom" hidden>Custom</option>
|
<option value="custom" hidden>Custom</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="named-range-wrapper js-required">
|
<div class="named-range-wrapper">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
id="{{ option_name }}"
|
id="{{ option_name }}"
|
||||||
name="{{ option_name }}-range"
|
name="{{ option_name }}"
|
||||||
min="{{ option.range_start }}"
|
min="{{ option.range_start }}"
|
||||||
max="{{ option.range_end }}"
|
max="{{ option.range_end }}"
|
||||||
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
|
||||||
{{ "disabled" if option.default == "random" }}
|
{{ "disabled" if option.default == "random" }}
|
||||||
/>
|
/>
|
||||||
<span id="{{ option_name }}-value" class="range-value">
|
<span id="{{ option_name }}-value" class="range-value js-required">
|
||||||
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
|
||||||
</span>
|
</span>
|
||||||
{{ RandomizeButton(option_name, option) }}
|
{{ RandomizeButton(option_name, option) }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<noscript>
|
<noscript>
|
||||||
<style>
|
<style>
|
||||||
.js-required{
|
.js-required{
|
||||||
display: none !important;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class TrackerData:
|
|||||||
|
|
||||||
# Normal lookup tables as well.
|
# Normal lookup tables as well.
|
||||||
self.item_name_to_id[game] = game_package["item_name_to_id"]
|
self.item_name_to_id[game] = game_package["item_name_to_id"]
|
||||||
self.location_name_to_id[game] = game_package["location_name_to_id"]
|
self.location_name_to_id[game] = game_package["item_name_to_id"]
|
||||||
|
|
||||||
def get_seed_name(self) -> str:
|
def get_seed_name(self) -> str:
|
||||||
"""Retrieves the seed name."""
|
"""Retrieves the seed name."""
|
||||||
@@ -1366,28 +1366,28 @@ if "Starcraft 2" in network_data_package["games"]:
|
|||||||
organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/"
|
organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/"
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Starting Minerals": github_icon_base_url + "blizzard/icon-mineral-nobg.png",
|
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
|
||||||
"Starting Vespene": github_icon_base_url + "blizzard/icon-gas-terran-nobg.png",
|
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
|
||||||
"Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png",
|
"Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png",
|
||||||
|
|
||||||
"Terran Infantry Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel1.png",
|
"Terran Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png",
|
||||||
"Terran Infantry Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel2.png",
|
"Terran Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png",
|
||||||
"Terran Infantry Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel3.png",
|
"Terran Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png",
|
||||||
"Terran Infantry Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel1.png",
|
"Terran Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png",
|
||||||
"Terran Infantry Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel2.png",
|
"Terran Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png",
|
||||||
"Terran Infantry Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel3.png",
|
"Terran Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png",
|
||||||
"Terran Vehicle Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel1.png",
|
"Terran Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png",
|
||||||
"Terran Vehicle Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel2.png",
|
"Terran Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png",
|
||||||
"Terran Vehicle Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel3.png",
|
"Terran Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png",
|
||||||
"Terran Vehicle Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel1.png",
|
"Terran Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png",
|
||||||
"Terran Vehicle Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel2.png",
|
"Terran Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png",
|
||||||
"Terran Vehicle Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel3.png",
|
"Terran Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png",
|
||||||
"Terran Ship Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel1.png",
|
"Terran Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png",
|
||||||
"Terran Ship Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel2.png",
|
"Terran Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png",
|
||||||
"Terran Ship Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel3.png",
|
"Terran Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png",
|
||||||
"Terran Ship Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel1.png",
|
"Terran Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png",
|
||||||
"Terran Ship Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel2.png",
|
"Terran Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png",
|
||||||
"Terran Ship Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel3.png",
|
"Terran Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png",
|
||||||
|
|
||||||
"Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
|
"Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
|
||||||
"Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
|
"Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Archipelago World Code Owners / Maintainers Document
|
# Archipelago World Code Owners / Maintainers Document
|
||||||
#
|
#
|
||||||
# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as
|
# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull
|
||||||
# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in
|
# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to
|
||||||
# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly.
|
# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer.
|
||||||
#
|
#
|
||||||
# All usernames must be GitHub usernames (and are case sensitive).
|
# All usernames must be GitHub usernames (and are case sensitive).
|
||||||
|
|
||||||
@@ -226,11 +226,3 @@
|
|||||||
|
|
||||||
# Ori and the Blind Forest
|
# Ori and the Blind Forest
|
||||||
# /worlds_disabled/oribf/
|
# /worlds_disabled/oribf/
|
||||||
|
|
||||||
###################
|
|
||||||
## Documentation ##
|
|
||||||
###################
|
|
||||||
|
|
||||||
# Apworld Dev Faq
|
|
||||||
/docs/apworld_dev_faq.md @qwint @ScipioWright
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# APWorld Dev FAQ
|
|
||||||
|
|
||||||
This document is meant as a reference tool to show solutions to common problems when developing an apworld.
|
|
||||||
It is not intended to answer every question about Archipelago and it assumes you have read the other docs,
|
|
||||||
including [Contributing](contributing.md), [Adding Games](<adding games.md>), and [World API](<world api.md>).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### My game has a restrictive start that leads to fill errors
|
|
||||||
|
|
||||||
Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
|
|
||||||
```py
|
|
||||||
early_item_name = "Sword"
|
|
||||||
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Some alternative ways to try to fix this problem are:
|
|
||||||
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
|
||||||
* Pre-place items yourself, such as during `create_items`
|
|
||||||
* Put items into the player's starting inventory using `push_precollected`
|
|
||||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### I have multiple settings that change the item/location pool counts and need to balance them out
|
|
||||||
|
|
||||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
|
||||||
|
|
||||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
|
||||||
|
|
||||||
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
|
||||||
```py
|
|
||||||
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
|
||||||
item_pool = self.create_non_filler_items()
|
|
||||||
|
|
||||||
for _ in range(total_locations - len(item_pool)):
|
|
||||||
item_pool.append(self.create_filler())
|
|
||||||
|
|
||||||
self.multiworld.itempool += item_pool
|
|
||||||
```
|
|
||||||
|
|
||||||
A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
|
||||||
```py
|
|
||||||
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
|
||||||
```
|
|
||||||
@@ -456,9 +456,8 @@ In addition, the following methods can be implemented and are called in this ord
|
|||||||
called to place player's regions and their locations into the MultiWorld's regions list.
|
called to place player's regions and their locations into the MultiWorld's regions list.
|
||||||
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||||
* `create_items(self)`
|
* `create_items(self)`
|
||||||
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
|
called to place player's items into the MultiWorld's itempool. After this step all regions
|
||||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
|
and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward.
|
||||||
after this step. Locations cannot be moved to different regions after this step.
|
|
||||||
* `set_rules(self)`
|
* `set_rules(self)`
|
||||||
called to set access and item rules on locations and entrances.
|
called to set access and item rules on locations and entrances.
|
||||||
* `generate_basic(self)`
|
* `generate_basic(self)`
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{
|
|||||||
Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
||||||
|
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||||
|
|||||||
7
kvui.py
7
kvui.py
@@ -595,9 +595,8 @@ class GameManager(App):
|
|||||||
"!help for server commands.")
|
"!help for server commands.")
|
||||||
|
|
||||||
def connect_button_action(self, button):
|
def connect_button_action(self, button):
|
||||||
self.ctx.username = None
|
|
||||||
self.ctx.password = None
|
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
|
self.ctx.username = None
|
||||||
async_start(self.ctx.disconnect())
|
async_start(self.ctx.disconnect())
|
||||||
else:
|
else:
|
||||||
async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
|
async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
|
||||||
@@ -837,10 +836,6 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
|||||||
return self._handle_text(node)
|
return self._handle_text(node)
|
||||||
|
|
||||||
def _handle_text(self, node: JSONMessagePart):
|
def _handle_text(self, node: JSONMessagePart):
|
||||||
# All other text goes through _handle_color, and we don't want to escape markup twice,
|
|
||||||
# or mess up text that already has intentional markup applied to it
|
|
||||||
if node.get("type", "text") == "text":
|
|
||||||
node["text"] = escape_markup(node["text"])
|
|
||||||
for ref in node.get("refs", []):
|
for ref in node.get("refs", []):
|
||||||
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
|
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
|
||||||
self.ref_count += 1
|
self.ref_count += 1
|
||||||
|
|||||||
18
settings.py
18
settings.py
@@ -3,7 +3,6 @@ Application settings / host.yaml interface using type hints.
|
|||||||
This is different from player options.
|
This is different from player options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@@ -12,6 +11,7 @@ import warnings
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
|
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
|
||||||
|
import os
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"get_settings", "fmt_doc", "no_gui",
|
"get_settings", "fmt_doc", "no_gui",
|
||||||
@@ -798,7 +798,6 @@ class Settings(Group):
|
|||||||
atexit.register(autosave)
|
atexit.register(autosave)
|
||||||
|
|
||||||
def save(self, location: Optional[str] = None) -> None: # as above
|
def save(self, location: Optional[str] = None) -> None: # as above
|
||||||
from Utils import parse_yaml
|
|
||||||
location = location or self._filename
|
location = location or self._filename
|
||||||
assert location, "No file specified"
|
assert location, "No file specified"
|
||||||
temp_location = location + ".tmp" # not using tempfile to test expected file access
|
temp_location = location + ".tmp" # not using tempfile to test expected file access
|
||||||
@@ -808,18 +807,10 @@ class Settings(Group):
|
|||||||
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
|
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
|
||||||
with open(temp_location, "w", encoding="utf-8") as f:
|
with open(temp_location, "w", encoding="utf-8") as f:
|
||||||
self.dump(f)
|
self.dump(f)
|
||||||
f.flush()
|
# replace old with new
|
||||||
if hasattr(os, "fsync"):
|
if os.path.exists(location):
|
||||||
os.fsync(f.fileno())
|
|
||||||
# validate new file is valid yaml
|
|
||||||
with open(temp_location, encoding="utf-8") as f:
|
|
||||||
parse_yaml(f.read())
|
|
||||||
# replace old with new, try atomic operation first
|
|
||||||
try:
|
|
||||||
os.rename(temp_location, location)
|
|
||||||
except (OSError, FileExistsError):
|
|
||||||
os.unlink(location)
|
os.unlink(location)
|
||||||
os.rename(temp_location, location)
|
os.rename(temp_location, location)
|
||||||
self._filename = location
|
self._filename = location
|
||||||
|
|
||||||
def dump(self, f: TextIO, level: int = 0) -> None:
|
def dump(self, f: TextIO, level: int = 0) -> None:
|
||||||
@@ -841,6 +832,7 @@ def get_settings() -> Settings:
|
|||||||
with _lock: # make sure we only have one instance
|
with _lock: # make sure we only have one instance
|
||||||
res = getattr(get_settings, "_cache", None)
|
res = getattr(get_settings, "_cache", None)
|
||||||
if not res:
|
if not res:
|
||||||
|
import os
|
||||||
from Utils import user_path, local_path
|
from Utils import user_path, local_path
|
||||||
filenames = ("options.yaml", "host.yaml")
|
filenames = ("options.yaml", "host.yaml")
|
||||||
locations: List[str] = []
|
locations: List[str] = []
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -21,7 +21,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||||
try:
|
try:
|
||||||
requirement = 'cx-Freeze==7.2.0'
|
requirement = 'cx-Freeze==7.0.0'
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
try:
|
try:
|
||||||
pkg_resources.require(requirement)
|
pkg_resources.require(requirement)
|
||||||
@@ -66,6 +66,7 @@ non_apworlds: set = {
|
|||||||
"Adventure",
|
"Adventure",
|
||||||
"ArchipIDLE",
|
"ArchipIDLE",
|
||||||
"Archipelago",
|
"Archipelago",
|
||||||
|
"ChecksFinder",
|
||||||
"Clique",
|
"Clique",
|
||||||
"Final Fantasy",
|
"Final Fantasy",
|
||||||
"Lufia II Ancient Cave",
|
"Lufia II Ancient Cave",
|
||||||
|
|||||||
@@ -292,12 +292,12 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||||
if not (self.run_default_tests and self.constructed):
|
if not (self.run_default_tests and self.constructed):
|
||||||
return
|
return
|
||||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
with self.subTest("Game", game=self.game):
|
||||||
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
|
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
|
||||||
state = self.multiworld.get_all_state(False)
|
state = self.multiworld.get_all_state(False)
|
||||||
for location in self.multiworld.get_locations():
|
for location in self.multiworld.get_locations():
|
||||||
if location.name not in excluded:
|
if location.name not in excluded:
|
||||||
with self.subTest("Location should be reached", location=location.name):
|
with self.subTest("Location should be reached", location=location):
|
||||||
reachable = location.can_reach(state)
|
reachable = location.can_reach(state)
|
||||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||||
with self.subTest("Beatable"):
|
with self.subTest("Beatable"):
|
||||||
@@ -308,7 +308,7 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
"""Ensure empty state can reach at least one location with the defined options"""
|
"""Ensure empty state can reach at least one location with the defined options"""
|
||||||
if not (self.run_default_tests and self.constructed):
|
if not (self.run_default_tests and self.constructed):
|
||||||
return
|
return
|
||||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
with self.subTest("Game", game=self.game):
|
||||||
state = CollectionState(self.multiworld)
|
state = CollectionState(self.multiworld)
|
||||||
locations = self.multiworld.get_reachable_locations(state, self.player)
|
locations = self.multiworld.get_reachable_locations(state, self.player)
|
||||||
self.assertGreater(len(locations), 0,
|
self.assertGreater(len(locations), 0,
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ class TestFillRestrictive(unittest.TestCase):
|
|||||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||||
player2 = generate_player_data(multiworld, 2, 3, 3)
|
player2 = generate_player_data(multiworld, 2, 3, 3)
|
||||||
|
|
||||||
multiworld.worlds[player1.id].options.accessibility.value = Accessibility.option_minimal
|
multiworld.accessibility[player1.id].value = multiworld.accessibility[player1.id].option_minimal
|
||||||
multiworld.worlds[player2.id].options.accessibility.value = Accessibility.option_full
|
multiworld.accessibility[player2.id].value = multiworld.accessibility[player2.id].option_locations
|
||||||
|
|
||||||
multiworld.completion_condition[player1.id] = lambda state: True
|
multiworld.completion_condition[player1.id] = lambda state: True
|
||||||
multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import os.path
|
|
||||||
import unittest
|
import unittest
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from tempfile import TemporaryDirectory, TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
from typing import Any, Dict, List, cast
|
from typing import Any, Dict, List, cast
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from settings import Group, Settings, ServerOptions
|
from settings import Settings, Group
|
||||||
|
|
||||||
|
|
||||||
class TestIDs(unittest.TestCase):
|
class TestIDs(unittest.TestCase):
|
||||||
@@ -81,27 +80,3 @@ class TestSettingsDumper(unittest.TestCase):
|
|||||||
self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list
|
self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list
|
||||||
self.assertGreater(value_spaces[3], value_spaces[0],
|
self.assertGreater(value_spaces[3], value_spaces[0],
|
||||||
f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}")
|
f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}")
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsSave(unittest.TestCase):
|
|
||||||
def test_save(self) -> None:
|
|
||||||
"""Test that saving and updating works"""
|
|
||||||
with TemporaryDirectory() as d:
|
|
||||||
filename = os.path.join(d, "host.yaml")
|
|
||||||
new_release_mode = ServerOptions.ReleaseMode("enabled")
|
|
||||||
# create default host.yaml
|
|
||||||
settings = Settings(None)
|
|
||||||
settings.save(filename)
|
|
||||||
self.assertTrue(os.path.exists(filename),
|
|
||||||
"Default settings could not be saved")
|
|
||||||
self.assertNotEqual(settings.server_options.release_mode, new_release_mode,
|
|
||||||
"Unexpected default release mode")
|
|
||||||
# update host.yaml
|
|
||||||
settings.server_options.release_mode = new_release_mode
|
|
||||||
settings.save(filename)
|
|
||||||
self.assertFalse(os.path.exists(filename + ".tmp"),
|
|
||||||
"Temp file was not removed during save")
|
|
||||||
# read back host.yaml
|
|
||||||
settings = Settings(filename)
|
|
||||||
self.assertEqual(settings.server_options.release_mode, new_release_mode,
|
|
||||||
"Settings were not overwritten")
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from Options import ItemLinks
|
from Options import ItemLinks
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
@@ -47,15 +47,3 @@ class TestOptions(unittest.TestCase):
|
|||||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
self.assertIn("Bow", link.value[0]["item_pool"])
|
||||||
|
|
||||||
# TODO test that the group created using these options has the items
|
# TODO test that the group created using these options has the items
|
||||||
|
|
||||||
def test_item_links_resolve(self):
|
|
||||||
"""Test item link option resolves correctly."""
|
|
||||||
item_link_group = [{
|
|
||||||
"name": "ItemLinkTest",
|
|
||||||
"item_pool": ["Everything"],
|
|
||||||
"link_replacement": False,
|
|
||||||
"replacement_item": None,
|
|
||||||
}]
|
|
||||||
item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)}
|
|
||||||
for link in item_links.values():
|
|
||||||
self.assertEqual(link.value[0], item_link_group[0])
|
|
||||||
|
|||||||
@@ -41,15 +41,15 @@ class TestBase(unittest.TestCase):
|
|||||||
state = multiworld.get_all_state(False)
|
state = multiworld.get_all_state(False)
|
||||||
for location in multiworld.get_locations():
|
for location in multiworld.get_locations():
|
||||||
if location.name not in excluded:
|
if location.name not in excluded:
|
||||||
with self.subTest("Location should be reached", location=location.name):
|
with self.subTest("Location should be reached", location=location):
|
||||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||||
|
|
||||||
for region in multiworld.get_regions():
|
for region in multiworld.get_regions():
|
||||||
if region.name in unreachable_regions:
|
if region.name in unreachable_regions:
|
||||||
with self.subTest("Region should be unreachable", region=region.name):
|
with self.subTest("Region should be unreachable", region=region):
|
||||||
self.assertFalse(region.can_reach(state))
|
self.assertFalse(region.can_reach(state))
|
||||||
else:
|
else:
|
||||||
with self.subTest("Region should be reached", region=region.name):
|
with self.subTest("Region should be reached", region=region):
|
||||||
self.assertTrue(region.can_reach(state))
|
self.assertTrue(region.can_reach(state))
|
||||||
|
|
||||||
with self.subTest("Completion Condition"):
|
with self.subTest("Completion Condition"):
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class TestTwoPlayerMulti(MultiworldTestBase):
|
|||||||
for world in AutoWorldRegister.world_types.values():
|
for world in AutoWorldRegister.world_types.values():
|
||||||
self.multiworld = setup_multiworld([world, world], ())
|
self.multiworld = setup_multiworld([world, world], ())
|
||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_full
|
world.options.accessibility.value = Accessibility.option_locations
|
||||||
self.assertSteps(gen_steps)
|
self.assertSteps(gen_steps)
|
||||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import typing
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
|
|
||||||
|
|
||||||
class TestBase(unittest.TestCase):
|
|
||||||
app: typing.ClassVar[Flask]
|
|
||||||
client: FlaskClient
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
from WebHostLib import app as raw_app
|
|
||||||
from WebHost import get_app
|
|
||||||
|
|
||||||
raw_app.config["PONY"] = {
|
|
||||||
"provider": "sqlite",
|
|
||||||
"filename": ":memory:",
|
|
||||||
"create_db": True,
|
|
||||||
}
|
|
||||||
raw_app.config.update({
|
|
||||||
"TESTING": True,
|
|
||||||
"DEBUG": True,
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
cls.app = get_app()
|
|
||||||
except AssertionError as e:
|
|
||||||
# since we only have 1 global app object, this might fail, but luckily all tests use the same config
|
|
||||||
if "register_blueprint" not in e.args[0]:
|
|
||||||
raise
|
|
||||||
cls.app = raw_app
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.client = self.app.test_client()
|
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import io
|
import io
|
||||||
|
import unittest
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from . import TestBase
|
|
||||||
|
|
||||||
|
class TestDocs(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
from WebHostLib import app as raw_app
|
||||||
|
from WebHost import get_app
|
||||||
|
raw_app.config["PONY"] = {
|
||||||
|
"provider": "sqlite",
|
||||||
|
"filename": ":memory:",
|
||||||
|
"create_db": True,
|
||||||
|
}
|
||||||
|
raw_app.config.update({
|
||||||
|
"TESTING": True,
|
||||||
|
})
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
class TestAPIGenerate(TestBase):
|
cls.client = app.test_client()
|
||||||
def test_correct_error_empty_request(self) -> None:
|
|
||||||
|
def test_correct_error_empty_request(self):
|
||||||
response = self.client.post("/api/generate")
|
response = self.client.post("/api/generate")
|
||||||
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||||
|
|
||||||
def test_generation_queued_weights(self) -> None:
|
def test_generation_queued_weights(self):
|
||||||
options = {
|
options = {
|
||||||
"Tester1":
|
"Tester1":
|
||||||
{
|
{
|
||||||
@@ -28,7 +43,7 @@ class TestAPIGenerate(TestBase):
|
|||||||
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||||
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||||
|
|
||||||
def test_generation_queued_file(self) -> None:
|
def test_generation_queued_file(self):
|
||||||
options = {
|
options = {
|
||||||
"game": "Archipelago",
|
"game": "Archipelago",
|
||||||
"name": "Tester",
|
"name": "Tester",
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
import os
|
|
||||||
from uuid import UUID, uuid4, uuid5
|
|
||||||
|
|
||||||
from flask import url_for
|
|
||||||
|
|
||||||
from . import TestBase
|
|
||||||
|
|
||||||
|
|
||||||
class TestHostFakeRoom(TestBase):
|
|
||||||
room_id: UUID
|
|
||||||
log_filename: str
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
from pony.orm import db_session
|
|
||||||
from Utils import user_path
|
|
||||||
from WebHostLib.models import Room, Seed
|
|
||||||
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
with self.client.session_transaction() as session:
|
|
||||||
session["_id"] = uuid4()
|
|
||||||
with db_session:
|
|
||||||
# create an empty seed and a room from it
|
|
||||||
seed = Seed(multidata=b"", owner=session["_id"])
|
|
||||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
|
||||||
self.room_id = room.id
|
|
||||||
self.log_filename = user_path("logs", f"{self.room_id}.txt")
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
from pony.orm import db_session, select
|
|
||||||
from WebHostLib.models import Command, Room
|
|
||||||
|
|
||||||
with db_session:
|
|
||||||
for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore
|
|
||||||
command.delete()
|
|
||||||
room: Room = Room.get(id=self.room_id)
|
|
||||||
room.seed.delete()
|
|
||||||
room.delete()
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.unlink(self.log_filename)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_display_log_missing_full(self) -> None:
|
|
||||||
"""
|
|
||||||
Verify that we get a 200 response even if log is missing.
|
|
||||||
This is required to not get an error for fetch.
|
|
||||||
"""
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("display_log", room=self.room_id))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_display_log_missing_range(self) -> None:
|
|
||||||
"""
|
|
||||||
Verify that we get a full response for missing log even if we asked for range.
|
|
||||||
This is required for the JS logic to differentiate between log update and log error message.
|
|
||||||
"""
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("display_log", room=self.room_id), headers={
|
|
||||||
"Range": "bytes=100-"
|
|
||||||
})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_display_log_denied(self) -> None:
|
|
||||||
"""Verify that only the owner can see the log."""
|
|
||||||
other_client = self.app.test_client()
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = other_client.get(url_for("display_log", room=self.room_id))
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
def test_display_log_missing_room(self) -> None:
|
|
||||||
"""Verify log for missing room gives an error as opposed to missing log for existing room."""
|
|
||||||
missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist
|
|
||||||
other_client = self.app.test_client()
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = other_client.get(url_for("display_log", room=missing_room_id))
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_display_log_full(self) -> None:
|
|
||||||
"""Verify full log response."""
|
|
||||||
with open(self.log_filename, "w", encoding="utf-8") as f:
|
|
||||||
text = "x" * 200
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("display_log", room=self.room_id))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.get_data(True), text)
|
|
||||||
|
|
||||||
def test_display_log_range(self) -> None:
|
|
||||||
"""Verify that Range header in request gives a range in response."""
|
|
||||||
with open(self.log_filename, "w", encoding="utf-8") as f:
|
|
||||||
f.write(" " * 100)
|
|
||||||
text = "x" * 100
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("display_log", room=self.room_id), headers={
|
|
||||||
"Range": "bytes=100-"
|
|
||||||
})
|
|
||||||
self.assertEqual(response.status_code, 206)
|
|
||||||
self.assertEqual(response.get_data(True), text)
|
|
||||||
|
|
||||||
def test_display_log_range_bom(self) -> None:
|
|
||||||
"""Verify that a BOM in the log file is skipped for range."""
|
|
||||||
with open(self.log_filename, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(" " * 100)
|
|
||||||
text = "x" * 100
|
|
||||||
f.write(text)
|
|
||||||
self.assertEqual(f.tell(), 203) # including BOM
|
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("display_log", room=self.room_id), headers={
|
|
||||||
"Range": "bytes=100-"
|
|
||||||
})
|
|
||||||
self.assertEqual(response.status_code, 206)
|
|
||||||
self.assertEqual(response.get_data(True), text)
|
|
||||||
|
|
||||||
def test_host_room_missing(self) -> None:
|
|
||||||
"""Verify that missing room gives a 404 response."""
|
|
||||||
missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("host_room", room=missing_room_id))
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_host_room_own(self) -> None:
|
|
||||||
"""Verify that own room gives the full output."""
|
|
||||||
with open(self.log_filename, "w", encoding="utf-8-sig") as f:
|
|
||||||
text = "* should be visible *"
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.get(url_for("host_room", room=self.room_id))
|
|
||||||
response_text = response.get_data(True)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertIn("href=\"/seed/", response_text)
|
|
||||||
self.assertIn(text, response_text)
|
|
||||||
|
|
||||||
def test_host_room_other(self) -> None:
|
|
||||||
"""Verify that non-own room gives the reduced output."""
|
|
||||||
from pony.orm import db_session
|
|
||||||
from WebHostLib.models import Room
|
|
||||||
|
|
||||||
with db_session:
|
|
||||||
room: Room = Room.get(id=self.room_id)
|
|
||||||
room.last_port = 12345
|
|
||||||
|
|
||||||
with open(self.log_filename, "w", encoding="utf-8-sig") as f:
|
|
||||||
text = "* should not be visible *"
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
other_client = self.app.test_client()
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = other_client.get(url_for("host_room", room=self.room_id))
|
|
||||||
response_text = response.get_data(True)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertNotIn("href=\"/seed/", response_text)
|
|
||||||
self.assertNotIn(text, response_text)
|
|
||||||
self.assertIn("/connect ", response_text)
|
|
||||||
self.assertIn(":12345", response_text)
|
|
||||||
|
|
||||||
def test_host_room_own_post(self) -> None:
|
|
||||||
"""Verify command from owner gets queued for the server and response is redirect."""
|
|
||||||
from pony.orm import db_session, select
|
|
||||||
from WebHostLib.models import Command
|
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = self.client.post(url_for("host_room", room=self.room_id), data={
|
|
||||||
"cmd": "/help"
|
|
||||||
})
|
|
||||||
self.assertEqual(response.status_code, 302, response.text)\
|
|
||||||
|
|
||||||
with db_session:
|
|
||||||
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
|
||||||
self.assertIn("/help", (command.commandtext for command in commands))
|
|
||||||
|
|
||||||
def test_host_room_other_post(self) -> None:
|
|
||||||
"""Verify command from non-owner does not get queued for the server."""
|
|
||||||
from pony.orm import db_session, select
|
|
||||||
from WebHostLib.models import Command
|
|
||||||
|
|
||||||
other_client = self.app.test_client()
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
|
||||||
response = other_client.post(url_for("host_room", room=self.room_id), data={
|
|
||||||
"cmd": "/help"
|
|
||||||
})
|
|
||||||
self.assertLess(response.status_code, 500)
|
|
||||||
|
|
||||||
with db_session:
|
|
||||||
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
|
||||||
self.assertNotIn("/help", (command.commandtext for command in commands))
|
|
||||||
@@ -280,7 +280,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required_server_version: Tuple[int, int, int] = (0, 5, 0)
|
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
||||||
"""update this if the resulting multidata breaks forward-compatibility of the server"""
|
"""update this if the resulting multidata breaks forward-compatibility of the server"""
|
||||||
|
|
||||||
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
|
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from .Options import BatLogic, DifficultySwitchB
|
from worlds.adventure import location_table
|
||||||
|
from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA
|
||||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||||
|
from BaseClasses import LocationProgressType
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]:
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if name == "Scooter Badge":
|
if name == "Scooter Badge":
|
||||||
if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
|
if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
|
||||||
item_type = ItemClassification.progression
|
item_type = ItemClassification.progression
|
||||||
elif name == "No Bonk Badge" and world.is_dw():
|
elif name == "No Bonk Badge" and world.is_dw():
|
||||||
item_type = ItemClassification.progression
|
item_type = ItemClassification.progression
|
||||||
|
|||||||
@@ -292,9 +292,6 @@ blacklisted_combos = {
|
|||||||
# See above comment
|
# See above comment
|
||||||
"Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations",
|
"Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations",
|
||||||
"Murder on the Owl Express"],
|
"Murder on the Owl Express"],
|
||||||
|
|
||||||
# was causing test failures
|
|
||||||
"Time Rift - Balcony": ["Alpine Free Roam"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -659,10 +656,6 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region,
|
|||||||
if exit_act.name not in chapter_finales:
|
if exit_act.name not in chapter_finales:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
exit_chapter: str = act_chapters.get(exit_act.name)
|
|
||||||
# make sure that certain time rift combinations never happen
|
|
||||||
always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest"
|
|
||||||
if not ignore_certain_rules or always_block:
|
|
||||||
if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
|
if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -688,12 +681,9 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
|
|||||||
if act.name not in guaranteed_first_acts:
|
if act.name not in guaranteed_first_acts:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
|
# If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
|
||||||
start_chapter = world.options.StartingChapter
|
start_chapter = world.options.StartingChapter
|
||||||
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
|
if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON:
|
||||||
if "Time Rift" in act.name:
|
if "Time Rift" in act.name:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -730,8 +720,7 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
|
|||||||
elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
|
elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \
|
if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest":
|
||||||
and act_chapters.get(act.name, "") == "Subcon Forest":
|
|
||||||
# Only allow Subcon levels if painting skips are allowed
|
# Only allow Subcon levels if painting skips are allowed
|
||||||
if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
|
if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from worlds.AutoWorld import CollectionState
|
from worlds.AutoWorld import CollectionState
|
||||||
from worlds.generic.Rules import add_rule, set_rule
|
from worlds.generic.Rules import add_rule, set_rule
|
||||||
from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs
|
from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \
|
||||||
|
shop_locations, event_locs
|
||||||
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
|
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
|
||||||
from BaseClasses import Location, Entrance, Region
|
from BaseClasses import Location, Entrance, Region
|
||||||
from typing import TYPE_CHECKING, List, Callable, Union, Dict
|
from typing import TYPE_CHECKING, List, Callable, Union, Dict
|
||||||
@@ -147,14 +148,14 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
chapter_list.append(ChapterIndex.CRUISE)
|
chapter_list.append(ChapterIndex.CRUISE)
|
||||||
|
|
||||||
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
|
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
|
||||||
chapter_list.append(ChapterIndex.METRO)
|
chapter_list.append(ChapterIndex.METRO)
|
||||||
|
|
||||||
chapter_list.remove(starting_chapter)
|
chapter_list.remove(starting_chapter)
|
||||||
world.random.shuffle(chapter_list)
|
world.random.shuffle(chapter_list)
|
||||||
|
|
||||||
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
|
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
|
||||||
if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
|
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
|
||||||
index1 = 69
|
index1 = 69
|
||||||
index2 = 69
|
index2 = 69
|
||||||
pos: int
|
pos: int
|
||||||
@@ -164,7 +165,7 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
index1 = chapter_list.index(ChapterIndex.CRUISE)
|
index1 = chapter_list.index(ChapterIndex.CRUISE)
|
||||||
|
|
||||||
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
|
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
|
||||||
index2 = chapter_list.index(ChapterIndex.METRO)
|
index2 = chapter_list.index(ChapterIndex.METRO)
|
||||||
|
|
||||||
lowest_index = min(index1, index2)
|
lowest_index = min(index1, index2)
|
||||||
@@ -241,6 +242,9 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if not is_location_valid(world, key):
|
if not is_location_valid(world, key):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if key in contract_locations.keys():
|
||||||
|
continue
|
||||||
|
|
||||||
loc = world.multiworld.get_location(key, world.player)
|
loc = world.multiworld.get_location(key, world.player)
|
||||||
|
|
||||||
for hat in data.required_hats:
|
for hat in data.required_hats:
|
||||||
@@ -252,7 +256,7 @@ def set_rules(world: "HatInTimeWorld"):
|
|||||||
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
|
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
|
||||||
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||||
|
|
||||||
if data.hit_type != HitType.none and world.options.UmbrellaLogic:
|
if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
|
||||||
if data.hit_type == HitType.umbrella:
|
if data.hit_type == HitType.umbrella:
|
||||||
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
||||||
|
|
||||||
@@ -514,7 +518,7 @@ def set_hard_rules(world: "HatInTimeWorld"):
|
|||||||
lambda state: can_use_hat(state, world, HatType.ICE))
|
lambda state: can_use_hat(state, world, HatType.ICE))
|
||||||
|
|
||||||
# Hard: clear Rush Hour with Brewing Hat only
|
# Hard: clear Rush Hour with Brewing Hat only
|
||||||
if world.options.NoTicketSkips != NoTicketSkips.option_true:
|
if world.options.NoTicketSkips is not NoTicketSkips.option_true:
|
||||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||||
lambda state: can_use_hat(state, world, HatType.BREWING))
|
lambda state: can_use_hat(state, world, HatType.BREWING))
|
||||||
else:
|
else:
|
||||||
@@ -859,8 +863,6 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
|
|||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
for entrance in regions["Time Rift - Balcony"].entrances:
|
for entrance in regions["Time Rift - Balcony"].entrances:
|
||||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
||||||
reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale",
|
|
||||||
world.player).connected_region, entrance)
|
|
||||||
|
|
||||||
for entrance in regions["Time Rift - Deep Sea"].entrances:
|
for entrance in regions["Time Rift - Deep Sea"].entrances:
|
||||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
||||||
@@ -937,7 +939,6 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
|
|||||||
if world.is_dlc1():
|
if world.is_dlc1():
|
||||||
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
||||||
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
|
||||||
reg_act_connection(world, "Rock the Boat", entrance.name)
|
|
||||||
|
|
||||||
for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
|
for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
|
||||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
||||||
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
||||||
calculate_yarn_costs, alps_hooks
|
calculate_yarn_costs
|
||||||
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||||
get_total_locations
|
get_total_locations
|
||||||
from .Rules import set_rules, has_paintings
|
from .Rules import set_rules
|
||||||
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
|
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
|
||||||
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty
|
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item
|
||||||
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
|
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
|
||||||
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
|
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
|
||||||
from worlds.AutoWorld import World, WebWorld, CollectionState
|
from worlds.AutoWorld import World, WebWorld, CollectionState
|
||||||
from worlds.generic.Rules import add_rule
|
|
||||||
from typing import List, Dict, TextIO
|
from typing import List, Dict, TextIO
|
||||||
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
|
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
|
||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
@@ -87,27 +86,19 @@ class HatInTimeWorld(World):
|
|||||||
if self.is_dw_only():
|
if self.is_dw_only():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Take care of some extremely restrictive starts in other chapters with act shuffle off
|
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
|
||||||
if not self.options.ActRandomizer:
|
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
|
||||||
start_chapter = self.options.StartingChapter
|
start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter)
|
||||||
if start_chapter == ChapterIndex.ALPINE:
|
|
||||||
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
|
|
||||||
if self.options.UmbrellaLogic:
|
|
||||||
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
|
||||||
|
|
||||||
if self.options.ShuffleAlpineZiplines:
|
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
|
||||||
ziplines = list(alps_hooks.keys())
|
if not self.options.ActRandomizer:
|
||||||
ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one
|
if start_chapter == ChapterIndex.ALPINE:
|
||||||
self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines)))
|
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
|
||||||
elif start_chapter == ChapterIndex.SUBCON:
|
if self.options.UmbrellaLogic:
|
||||||
if self.options.ShuffleSubconPaintings:
|
|
||||||
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
|
|
||||||
elif start_chapter == ChapterIndex.BIRDS:
|
|
||||||
if self.options.UmbrellaLogic:
|
|
||||||
if self.options.LogicDifficulty < Difficulty.EXPERT:
|
|
||||||
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
||||||
elif self.options.LogicDifficulty < Difficulty.MODERATE:
|
|
||||||
self.multiworld.push_precollected(self.create_item("Umbrella"))
|
if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings:
|
||||||
|
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
@@ -128,10 +119,7 @@ class HatInTimeWorld(World):
|
|||||||
# place vanilla contract locations if contract shuffle is off
|
# place vanilla contract locations if contract shuffle is off
|
||||||
if not self.options.ShuffleActContracts:
|
if not self.options.ShuffleActContracts:
|
||||||
for name in contract_locations.keys():
|
for name in contract_locations.keys():
|
||||||
loc = self.get_location(name)
|
self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name))
|
||||||
loc.place_locked_item(create_item(self, name))
|
|
||||||
if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well":
|
|
||||||
add_rule(loc, lambda state: has_paintings(state, self, 1))
|
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
if self.has_yarn():
|
if self.has_yarn():
|
||||||
@@ -329,7 +317,7 @@ class HatInTimeWorld(World):
|
|||||||
|
|
||||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||||
old_count: int = state.count(item.name, self.player)
|
old_count: int = state.count(item.name, self.player)
|
||||||
change = super().remove(state, item)
|
change = super().collect(state, item)
|
||||||
if change and old_count == 1:
|
if change and old_count == 1:
|
||||||
if "Stamp" in item.name:
|
if "Stamp" in item.name:
|
||||||
if "2 Stamp" in item.name:
|
if "2 Stamp" in item.name:
|
||||||
|
|||||||
@@ -12,29 +12,41 @@
|
|||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!**
|
1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console)
|
||||||
Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place.
|
This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R,
|
||||||
**This is important! Changing the game version CAN and WILL break your existing save files!!!**
|
paste the link into the box, and hit Enter.
|
||||||
|
|
||||||
|
|
||||||
2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**.
|
2. In the Steam console, enter the following command:
|
||||||
|
`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!***
|
||||||
|
This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally,
|
||||||
|
**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,**
|
||||||
|
or else the download may potentially become corrupted (see first FAQ issue below).
|
||||||
|
|
||||||
|
|
||||||
3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
|
3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder.
|
||||||
While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
|
|
||||||
|
|
||||||
|
|
||||||
4. Once the game finishes downloading, start it up.
|
4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder.
|
||||||
In Game Settings, make sure **Enable Developer Console** is checked.
|
|
||||||
|
|
||||||
|
|
||||||
5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game.
|
5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`.
|
||||||
|
In this new text file, input the number **253230** on the first line.
|
||||||
|
|
||||||
|
|
||||||
|
6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like.
|
||||||
|
You will use this shortcut to open the Archipelago-compatible version of A Hat in Time.
|
||||||
|
|
||||||
|
|
||||||
|
7. Start up the game using your new shortcut. To confirm if you are on the correct version,
|
||||||
|
go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running
|
||||||
|
the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked.
|
||||||
|
|
||||||
|
|
||||||
## Connecting to the Archipelago server
|
## Connecting to the Archipelago server
|
||||||
|
|
||||||
To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher
|
To connect to the multiworld server, simply run the **ArchipelagoAHITClient**
|
||||||
and connect it to the Archipelago server.
|
(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server.
|
||||||
The game will connect to the client automatically when you create a new save file.
|
The game will connect to the client automatically when you create a new save file.
|
||||||
|
|
||||||
|
|
||||||
@@ -49,8 +61,33 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
|
|||||||
|
|
||||||
|
|
||||||
## FAQ/Common Issues
|
## FAQ/Common Issues
|
||||||
|
### I followed the setup, but I receive an odd error message upon starting the game or creating a save file!
|
||||||
|
If you receive an error message such as
|
||||||
|
**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or
|
||||||
|
**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot
|
||||||
|
download was likely corrupted. The only way to fix this is to start the entire download all over again.
|
||||||
|
Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this
|
||||||
|
from happening is to ensure that your connection is not interrupted or slowed while downloading.
|
||||||
|
|
||||||
### The game is not connecting when starting a new save!
|
### The game keeps crashing on startup after the splash screen!
|
||||||
|
This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however,
|
||||||
|
try the following:
|
||||||
|
|
||||||
|
- Close Steam **entirely**.
|
||||||
|
- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen.
|
||||||
|
- Close the game, and then open Steam again.
|
||||||
|
- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does.
|
||||||
|
|
||||||
|
### I followed the setup, but "Live Game Events" still shows up in the options menu!
|
||||||
|
The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by
|
||||||
|
default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file
|
||||||
|
extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect.
|
||||||
|
To show file extensions in Windows 10, open any folder, click the View tab at the top, and check
|
||||||
|
"File name extensions". Then you can correct the name of the file. If the name of the file is correct,
|
||||||
|
and you're still running into the issue, re-read the setup guide again in case you missed a step.
|
||||||
|
If you still can't get it to work, ask for help in the Discord thread.
|
||||||
|
|
||||||
|
### The game is running on the older version, but it's not connecting when starting a new save!
|
||||||
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
||||||
(rocket icon) in-game, and re-enable the mod.
|
(rocket icon) in-game, and re-enable the mod.
|
||||||
|
|
||||||
|
|||||||
@@ -682,7 +682,7 @@ def get_alttp_settings(romfile: str):
|
|||||||
|
|
||||||
if 'yes' in choice:
|
if 'yes' in choice:
|
||||||
import LttPAdjuster
|
import LttPAdjuster
|
||||||
from .Rom import get_base_rom_path
|
from worlds.alttp.Rom import get_base_rom_path
|
||||||
last_settings.rom = romfile
|
last_settings.rom = romfile
|
||||||
last_settings.baserom = get_base_rom_path()
|
last_settings.baserom = get_base_rom_path()
|
||||||
last_settings.world = None
|
last_settings.world = None
|
||||||
|
|||||||
@@ -1437,7 +1437,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
|
|||||||
invalid_cave_connections = defaultdict(set)
|
invalid_cave_connections = defaultdict(set)
|
||||||
|
|
||||||
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
from . import OverworldGlitchRules
|
from worlds.alttp import OverworldGlitchRules
|
||||||
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
|
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
|
||||||
invalid_connections[entrance] = set()
|
invalid_connections[entrance] = set()
|
||||||
if entrance in must_be_exits:
|
if entrance in must_be_exits:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \
|
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \
|
||||||
PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle
|
StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed
|
||||||
from .EntranceShuffle import default_connections, default_dungeon_connections, \
|
from .EntranceShuffle import default_connections, default_dungeon_connections, \
|
||||||
inverted_default_connections, inverted_default_dungeon_connections
|
inverted_default_connections, inverted_default_dungeon_connections
|
||||||
from .Text import TextTable
|
from .Text import TextTable
|
||||||
@@ -486,7 +486,7 @@ class LTTPBosses(PlandoBosses):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_place_boss(cls, boss: str, location: str) -> bool:
|
def can_place_boss(cls, boss: str, location: str) -> bool:
|
||||||
from .Bosses import can_place_boss
|
from worlds.alttp.Bosses import can_place_boss
|
||||||
level = ''
|
level = ''
|
||||||
words = location.split(" ")
|
words = location.split(" ")
|
||||||
if words[-1] in ("top", "middle", "bottom"):
|
if words[-1] in ("top", "middle", "bottom"):
|
||||||
@@ -743,7 +743,6 @@ class ALttPPlandoTexts(PlandoTexts):
|
|||||||
|
|
||||||
|
|
||||||
alttp_options: typing.Dict[str, type(Option)] = {
|
alttp_options: typing.Dict[str, type(Option)] = {
|
||||||
"accessibility": ItemsAccessibility,
|
|
||||||
"plando_connections": ALttPPlandoConnections,
|
"plando_connections": ALttPPlandoConnections,
|
||||||
"plando_texts": ALttPPlandoTexts,
|
"plando_texts": ALttPPlandoTexts,
|
||||||
"start_inventory_from_pool": StartInventoryPool,
|
"start_inventory_from_pool": StartInventoryPool,
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str,
|
|||||||
|
|
||||||
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
||||||
exits=None):
|
exits=None):
|
||||||
from .SubClasses import ALttPLocation
|
from worlds.alttp.SubClasses import ALttPLocation
|
||||||
ret = LTTPRegion(name, type, hint, player, world)
|
ret = LTTPRegion(name, type, hint, player, world)
|
||||||
if exits:
|
if exits:
|
||||||
for exit in exits:
|
for exit in exits:
|
||||||
@@ -760,7 +760,7 @@ location_table: typing.Dict[str,
|
|||||||
'Turtle Rock - Prize': (
|
'Turtle Rock - Prize': (
|
||||||
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||||
|
|
||||||
from .Shops import shop_table_by_location_id, shop_table_by_location
|
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
|
||||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
|
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
|
||||||
lookup_id_to_name.update(shop_table_by_location_id)
|
lookup_id_to_name.update(shop_table_by_location_id)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import collections
|
|||||||
import logging
|
import logging
|
||||||
from typing import Iterator, Set
|
from typing import Iterator, Set
|
||||||
|
|
||||||
from Options import ItemsAccessibility
|
|
||||||
from BaseClasses import Entrance, MultiWorld
|
from BaseClasses import Entrance, MultiWorld
|
||||||
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
||||||
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
||||||
@@ -40,7 +39,7 @@ def set_rules(world):
|
|||||||
else:
|
else:
|
||||||
# Set access rules according to max glitches for multiworld progression.
|
# Set access rules according to max glitches for multiworld progression.
|
||||||
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
||||||
world.accessibility[player].value = ItemsAccessibility.option_minimal
|
world.accessibility[player] = world.accessibility[player].from_text("minimal")
|
||||||
world.progression_balancing[player].value = 0
|
world.progression_balancing[player].value = 0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -378,7 +377,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
or state.has("Cane of Somaria", player)))
|
or state.has("Cane of Somaria", player)))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if multiworld.accessibility[player] != 'locations':
|
||||||
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||||
@@ -394,7 +393,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
if state.has('Hookshot', player)
|
if state.has('Hookshot', player)
|
||||||
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
||||||
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if multiworld.accessibility[player] != 'locations':
|
||||||
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
||||||
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
||||||
@@ -406,14 +405,16 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||||
|
|
||||||
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||||
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
||||||
|
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
||||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
|
||||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
|
||||||
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
||||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||||
|
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||||
@@ -424,7 +425,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if multiworld.accessibility[player] != 'locations':
|
||||||
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
||||||
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
||||||
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
@@ -489,7 +490,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||||
set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player))
|
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
|
||||||
set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||||
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
||||||
@@ -523,12 +524,12 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||||
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if multiworld.accessibility[player] != 'locations':
|
||||||
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||||
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if multiworld.accessibility[player] != 'locations':
|
||||||
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||||
@@ -1201,7 +1202,7 @@ def set_trock_key_rules(world, player):
|
|||||||
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
||||||
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||||
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
||||||
if world.accessibility[player] == 'full':
|
if world.accessibility[player] == 'locations':
|
||||||
if world.big_key_shuffle[player] and can_reach_big_chest:
|
if world.big_key_shuffle[player] and can_reach_big_chest:
|
||||||
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
||||||
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
||||||
@@ -1215,7 +1216,7 @@ def set_trock_key_rules(world, player):
|
|||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
toss_junk_item(world, player)
|
toss_junk_item(world, player)
|
||||||
|
|
||||||
if world.accessibility[player] != 'full':
|
if world.accessibility[player] != 'locations':
|
||||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||||
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class ALttPItem(Item):
|
|||||||
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||||
return self.type
|
return self.type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locked_dungeon_item(self):
|
||||||
|
return self.location.locked and self.dungeon_item
|
||||||
|
|
||||||
|
|
||||||
class LTTPRegionType(IntEnum):
|
class LTTPRegionType(IntEnum):
|
||||||
LightWorld = 1
|
LightWorld = 1
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ class TestThievesTown(TestDungeon):
|
|||||||
|
|
||||||
["Thieves' Town - Blind's Cell", False, []],
|
["Thieves' Town - Blind's Cell", False, []],
|
||||||
["Thieves' Town - Blind's Cell", False, [], ['Big Key (Thieves Town)']],
|
["Thieves' Town - Blind's Cell", False, [], ['Big Key (Thieves Town)']],
|
||||||
["Thieves' Town - Blind's Cell", False, [], ['Small Key (Thieves Town)']],
|
["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)']],
|
||||||
["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
|
|
||||||
|
|
||||||
["Thieves' Town - Boss", False, []],
|
["Thieves' Town - Boss", False, []],
|
||||||
["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']],
|
["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']],
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||||
from worlds.alttp.InvertedRegions import create_inverted_regions
|
from worlds.alttp.InvertedRegions import create_inverted_regions
|
||||||
from worlds.alttp.ItemPool import difficulties
|
from worlds.alttp.ItemPool import difficulties
|
||||||
from worlds.alttp.Items import item_factory
|
from worlds.alttp.Items import item_factory
|
||||||
from worlds.alttp.Regions import mark_light_world_regions
|
from worlds.alttp.Regions import mark_light_world_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from test.bases import TestBase
|
from test.TestBase import TestBase
|
||||||
|
|
||||||
from worlds.alttp.test import LTTPTestBase
|
from worlds.alttp.test import LTTPTestBase
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from worlds.alttp.Items import item_factory
|
|||||||
from worlds.alttp.Options import GlitchesRequired
|
from worlds.alttp.Options import GlitchesRequired
|
||||||
from worlds.alttp.Regions import mark_light_world_regions
|
from worlds.alttp.Regions import mark_light_world_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from test.bases import TestBase
|
from test.TestBase import TestBase
|
||||||
|
|
||||||
from worlds.alttp.test import LTTPTestBase
|
from worlds.alttp.test import LTTPTestBase
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from worlds.alttp.Items import item_factory
|
|||||||
from worlds.alttp.Options import GlitchesRequired
|
from worlds.alttp.Options import GlitchesRequired
|
||||||
from worlds.alttp.Regions import mark_light_world_regions
|
from worlds.alttp.Regions import mark_light_world_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from test.bases import TestBase
|
from test.TestBase import TestBase
|
||||||
|
|
||||||
from worlds.alttp.test import LTTPTestBase
|
from worlds.alttp.test import LTTPTestBase
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_verse_cave_r = {
|
locations_verse_cave_r = {
|
||||||
"Verse Cave, bulb in the skeleton room": 698107,
|
"Verse Cave, bulb in the skeleton room": 698107,
|
||||||
"Verse Cave, bulb in the path right of the skeleton room": 698108,
|
"Verse Cave, bulb in the path left of the skeleton room": 698108,
|
||||||
"Verse Cave right area, Big Seed": 698175,
|
"Verse Cave right area, Big Seed": 698175,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,6 @@ class AquariaLocations:
|
|||||||
"Open Water top right area, second urn in the Mithalas exit": 698149,
|
"Open Water top right area, second urn in the Mithalas exit": 698149,
|
||||||
"Open Water top right area, third urn in the Mithalas exit": 698150,
|
"Open Water top right area, third urn in the Mithalas exit": 698150,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_openwater_tr_turtle = {
|
locations_openwater_tr_turtle = {
|
||||||
"Open Water top right area, bulb in the turtle room": 698009,
|
"Open Water top right area, bulb in the turtle room": 698009,
|
||||||
"Open Water top right area, Transturtle": 698211,
|
"Open Water top right area, Transturtle": 698211,
|
||||||
@@ -196,7 +195,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_cathedral_l = {
|
locations_cathedral_l = {
|
||||||
"Mithalas City Castle, bulb in the flesh hole": 698042,
|
"Mithalas City Castle, bulb in the flesh hole": 698042,
|
||||||
"Mithalas City Castle, Blue Banner": 698165,
|
"Mithalas City Castle, Blue banner": 698165,
|
||||||
"Mithalas City Castle, urn in the bedroom": 698130,
|
"Mithalas City Castle, urn in the bedroom": 698130,
|
||||||
"Mithalas City Castle, first urn of the single lamp path": 698131,
|
"Mithalas City Castle, first urn of the single lamp path": 698131,
|
||||||
"Mithalas City Castle, second urn of the single lamp path": 698132,
|
"Mithalas City Castle, second urn of the single lamp path": 698132,
|
||||||
@@ -227,7 +226,7 @@ class AquariaLocations:
|
|||||||
"Mithalas Cathedral, third urn in the path behind the flesh vein": 698146,
|
"Mithalas Cathedral, third urn in the path behind the flesh vein": 698146,
|
||||||
"Mithalas Cathedral, fourth urn in the top right room": 698147,
|
"Mithalas Cathedral, fourth urn in the top right room": 698147,
|
||||||
"Mithalas Cathedral, Mithalan Dress": 698189,
|
"Mithalas Cathedral, Mithalan Dress": 698189,
|
||||||
"Mithalas Cathedral, urn below the left entrance": 698198,
|
"Mithalas Cathedral right area, urn below the left entrance": 698198,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_cathedral_underground = {
|
locations_cathedral_underground = {
|
||||||
@@ -240,7 +239,7 @@ class AquariaLocations:
|
|||||||
}
|
}
|
||||||
|
|
||||||
locations_cathedral_boss = {
|
locations_cathedral_boss = {
|
||||||
"Mithalas boss area, beating Mithalan God": 698202,
|
"Cathedral boss area, beating Mithalan God": 698202,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_forest_tl = {
|
locations_forest_tl = {
|
||||||
@@ -270,7 +269,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_forest_bl = {
|
locations_forest_bl = {
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
|
"Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
|
||||||
"Kelp Forest bottom left area, Walker Baby": 698186,
|
"Kelp Forest bottom left area, Walker baby": 698186,
|
||||||
"Kelp Forest bottom left area, Transturtle": 698212,
|
"Kelp Forest bottom left area, Transturtle": 698212,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +451,7 @@ class AquariaLocations:
|
|||||||
|
|
||||||
locations_body_c = {
|
locations_body_c = {
|
||||||
"The Body center area, breaking Li's cage": 698201,
|
"The Body center area, breaking Li's cage": 698201,
|
||||||
"The Body center area, bulb on the main path blocking tube": 698097,
|
"The Body main area, bulb on the main path blocking tube": 698097,
|
||||||
}
|
}
|
||||||
|
|
||||||
locations_body_l = {
|
locations_body_l = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Manage options in the Aquaria game multiworld randomizer
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from Options import Toggle, Choice, Range, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
|
from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
|
||||||
|
|
||||||
|
|
||||||
class IngredientRandomizer(Choice):
|
class IngredientRandomizer(Choice):
|
||||||
@@ -111,14 +111,6 @@ class BindSongNeededToGetUnderRockBulb(Toggle):
|
|||||||
display_name = "Bind song needed to get sing bulbs under rocks"
|
display_name = "Bind song needed to get sing bulbs under rocks"
|
||||||
|
|
||||||
|
|
||||||
class BlindGoal(Toggle):
|
|
||||||
"""
|
|
||||||
Hide the goal's requirements from the help page so that you have to go to the last boss door to know
|
|
||||||
what is needed to access the boss.
|
|
||||||
"""
|
|
||||||
display_name = "Hide the goal's requirements"
|
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWater(Choice):
|
class UnconfineHomeWater(Choice):
|
||||||
"""
|
"""
|
||||||
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
|
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
|
||||||
@@ -150,4 +142,4 @@ class AquariaOptions(PerGameCommonOptions):
|
|||||||
dish_randomizer: DishRandomizer
|
dish_randomizer: DishRandomizer
|
||||||
aquarian_translation: AquarianTranslation
|
aquarian_translation: AquarianTranslation
|
||||||
skip_first_vision: SkipFirstVision
|
skip_first_vision: SkipFirstVision
|
||||||
blind_goal: BlindGoal
|
death_link: DeathLink
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ class AquariaRegions:
|
|||||||
AquariaLocations.locations_cathedral_l_sc)
|
AquariaLocations.locations_cathedral_l_sc)
|
||||||
self.cathedral_r = self.__add_region("Mithalas Cathedral",
|
self.cathedral_r = self.__add_region("Mithalas Cathedral",
|
||||||
AquariaLocations.locations_cathedral_r)
|
AquariaLocations.locations_cathedral_r)
|
||||||
self.cathedral_underground = self.__add_region("Mithalas Cathedral underground",
|
self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area",
|
||||||
AquariaLocations.locations_cathedral_underground)
|
AquariaLocations.locations_cathedral_underground)
|
||||||
self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room",
|
self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room",
|
||||||
AquariaLocations.locations_cathedral_boss)
|
AquariaLocations.locations_cathedral_boss)
|
||||||
@@ -597,22 +597,22 @@ class AquariaRegions:
|
|||||||
lambda state: _has_beast_form(state, self.player) and
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
_has_energy_form(state, self.player) and
|
_has_energy_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player))
|
_has_bind_song(state, self.player))
|
||||||
self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground",
|
self.__connect_regions("Mithalas castle", "Cathedral underground",
|
||||||
self.cathedral_l, self.cathedral_underground,
|
self.cathedral_l, self.cathedral_underground,
|
||||||
lambda state: _has_beast_form(state, self.player) and
|
lambda state: _has_beast_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player))
|
_has_bind_song(state, self.player))
|
||||||
self.__connect_regions("Mithalas castle", "Mithalas Cathedral",
|
self.__connect_regions("Mithalas castle", "Cathedral right area",
|
||||||
self.cathedral_l, self.cathedral_r,
|
self.cathedral_l, self.cathedral_r,
|
||||||
lambda state: _has_bind_song(state, self.player) and
|
lambda state: _has_bind_song(state, self.player) and
|
||||||
_has_energy_form(state, self.player))
|
_has_energy_form(state, self.player))
|
||||||
self.__connect_regions("Mithalas Cathedral", "Mithalas Cathedral underground",
|
self.__connect_regions("Cathedral right area", "Cathedral underground",
|
||||||
self.cathedral_r, self.cathedral_underground,
|
self.cathedral_r, self.cathedral_underground,
|
||||||
lambda state: _has_energy_form(state, self.player))
|
lambda state: _has_energy_form(state, self.player))
|
||||||
self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss left area",
|
self.__connect_one_way_regions("Cathedral underground", "Cathedral boss left area",
|
||||||
self.cathedral_underground, self.cathedral_boss_r,
|
self.cathedral_underground, self.cathedral_boss_r,
|
||||||
lambda state: _has_energy_form(state, self.player) and
|
lambda state: _has_energy_form(state, self.player) and
|
||||||
_has_bind_song(state, self.player))
|
_has_bind_song(state, self.player))
|
||||||
self.__connect_one_way_regions("Cathedral boss left area", "Mithalas Cathedral underground",
|
self.__connect_one_way_regions("Cathedral boss left area", "Cathedral underground",
|
||||||
self.cathedral_boss_r, self.cathedral_underground,
|
self.cathedral_boss_r, self.cathedral_underground,
|
||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
self.__connect_regions("Cathedral boss right area", "Cathedral boss left area",
|
self.__connect_regions("Cathedral boss right area", "Cathedral boss left area",
|
||||||
@@ -1099,7 +1099,7 @@ class AquariaRegions:
|
|||||||
lambda state: _has_beast_form(state, self.player))
|
lambda state: _has_beast_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
|
||||||
lambda state: _has_fish_form(state, self.player))
|
lambda state: _has_fish_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player),
|
add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player),
|
||||||
lambda state: _has_spirit_form(state, self.player))
|
lambda state: _has_spirit_form(state, self.player))
|
||||||
add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
|
add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
|
||||||
lambda state: _has_bind_song(state, self.player))
|
lambda state: _has_bind_song(state, self.player))
|
||||||
@@ -1134,7 +1134,7 @@ class AquariaRegions:
|
|||||||
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
|
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
|
||||||
self.player).item_rule =\
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
|
self.multiworld.get_location("Cathedral boss area, beating Mithalan God",
|
||||||
self.player).item_rule =\
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
|
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
|
||||||
@@ -1191,7 +1191,7 @@ class AquariaRegions:
|
|||||||
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
|
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
self.player).item_rule =\
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
|
self.multiworld.get_location("Kelp Forest bottom left area, Walker baby",
|
||||||
self.player).item_rule =\
|
self.player).item_rule =\
|
||||||
lambda item: item.classification != ItemClassification.progression
|
lambda item: item.classification != ItemClassification.progression
|
||||||
self.multiworld.get_location("Sun Temple, Sun Key",
|
self.multiworld.get_location("Sun Temple, Sun Key",
|
||||||
|
|||||||
@@ -204,8 +204,7 @@ class AquariaWorld(World):
|
|||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
return {"ingredientReplacement": self.ingredients_substitution,
|
return {"ingredientReplacement": self.ingredients_substitution,
|
||||||
"aquarian_translate": bool(self.options.aquarian_translation.value),
|
"aquarianTranslate": bool(self.options.aquarian_translation.value),
|
||||||
"blind_goal": bool(self.options.blind_goal.value),
|
|
||||||
"secret_needed": self.options.objective.value > 0,
|
"secret_needed": self.options.objective.value > 0,
|
||||||
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
|
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
|
||||||
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
|
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ after_home_water_locations = [
|
|||||||
"Mithalas City, Doll",
|
"Mithalas City, Doll",
|
||||||
"Mithalas City, urn inside a home fish pass",
|
"Mithalas City, urn inside a home fish pass",
|
||||||
"Mithalas City Castle, bulb in the flesh hole",
|
"Mithalas City Castle, bulb in the flesh hole",
|
||||||
"Mithalas City Castle, Blue Banner",
|
"Mithalas City Castle, Blue banner",
|
||||||
"Mithalas City Castle, urn in the bedroom",
|
"Mithalas City Castle, urn in the bedroom",
|
||||||
"Mithalas City Castle, first urn of the single lamp path",
|
"Mithalas City Castle, first urn of the single lamp path",
|
||||||
"Mithalas City Castle, second urn of the single lamp path",
|
"Mithalas City Castle, second urn of the single lamp path",
|
||||||
@@ -82,14 +82,14 @@ after_home_water_locations = [
|
|||||||
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
||||||
"Mithalas Cathedral, fourth urn in the top right room",
|
"Mithalas Cathedral, fourth urn in the top right room",
|
||||||
"Mithalas Cathedral, Mithalan Dress",
|
"Mithalas Cathedral, Mithalan Dress",
|
||||||
"Mithalas Cathedral, urn below the left entrance",
|
"Mithalas Cathedral right area, urn below the left entrance",
|
||||||
"Cathedral Underground, bulb in the center part",
|
"Cathedral Underground, bulb in the center part",
|
||||||
"Cathedral Underground, first bulb in the top left part",
|
"Cathedral Underground, first bulb in the top left part",
|
||||||
"Cathedral Underground, second bulb in the top left part",
|
"Cathedral Underground, second bulb in the top left part",
|
||||||
"Cathedral Underground, third bulb in the top left part",
|
"Cathedral Underground, third bulb in the top left part",
|
||||||
"Cathedral Underground, bulb close to the save crystal",
|
"Cathedral Underground, bulb close to the save crystal",
|
||||||
"Cathedral Underground, bulb in the bottom right path",
|
"Cathedral Underground, bulb in the bottom right path",
|
||||||
"Mithalas boss area, beating Mithalan God",
|
"Cathedral boss area, beating Mithalan God",
|
||||||
"Kelp Forest top left area, bulb in the bottom left clearing",
|
"Kelp Forest top left area, bulb in the bottom left clearing",
|
||||||
"Kelp Forest top left area, bulb in the path down from the top left clearing",
|
"Kelp Forest top left area, bulb in the path down from the top left clearing",
|
||||||
"Kelp Forest top left area, bulb in the top left clearing",
|
"Kelp Forest top left area, bulb in the top left clearing",
|
||||||
@@ -104,7 +104,7 @@ after_home_water_locations = [
|
|||||||
"Kelp Forest top right area, Black Pearl",
|
"Kelp Forest top right area, Black Pearl",
|
||||||
"Kelp Forest top right area, bulb in the top fish pass",
|
"Kelp Forest top right area, bulb in the top fish pass",
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
"Kelp Forest bottom left area, Walker Baby",
|
"Kelp Forest bottom left area, Walker baby",
|
||||||
"Kelp Forest bottom left area, Transturtle",
|
"Kelp Forest bottom left area, Transturtle",
|
||||||
"Kelp Forest bottom right area, Odd Container",
|
"Kelp Forest bottom right area, Odd Container",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
@@ -175,7 +175,7 @@ after_home_water_locations = [
|
|||||||
"Sunken City left area, Girl Costume",
|
"Sunken City left area, Girl Costume",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
"The Body center area, breaking Li's cage",
|
"The Body center area, breaking Li's cage",
|
||||||
"The Body center area, bulb on the main path blocking tube",
|
"The Body main area, bulb on the main path blocking tube",
|
||||||
"The Body left area, first bulb in the top face room",
|
"The Body left area, first bulb in the top face room",
|
||||||
"The Body left area, second bulb in the top face room",
|
"The Body left area, second bulb in the top face room",
|
||||||
"The Body left area, bulb below the water stream",
|
"The Body left area, bulb below the water stream",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the beast form
|
Description: Unit test used to test accessibility of locations with and without the beast form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class BeastFormAccessTest(AquariaTestBase):
|
class BeastFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Unit test used to test accessibility of locations with and without
|
|||||||
under rock needing bind song option)
|
under rock needing bind song option)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase, after_home_water_locations
|
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
|
||||||
|
|
||||||
|
|
||||||
class BindSongAccessTest(AquariaTestBase):
|
class BindSongAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ Description: Unit test used to test accessibility of locations with and without
|
|||||||
under rock needing bind song option)
|
under rock needing bind song option)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
from .test_bind_song_access import after_home_water_locations
|
from worlds.aquaria.test.test_bind_song_access import after_home_water_locations
|
||||||
|
|
||||||
|
|
||||||
class BindSongOptionAccessTest(AquariaTestBase):
|
class BindSongOptionAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test accessibility of region with the home water confine via option
|
Description: Unit test used to test accessibility of region with the home water confine via option
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the dual song
|
Description: Unit test used to test accessibility of locations with and without the dual song
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class LiAccessTest(AquariaTestBase):
|
class LiAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Unit test used to test accessibility of locations with and without
|
|||||||
energy form option)
|
energy form option)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class EnergyFormAccessTest(AquariaTestBase):
|
class EnergyFormAccessTest(AquariaTestBase):
|
||||||
@@ -39,8 +39,8 @@ class EnergyFormAccessTest(AquariaTestBase):
|
|||||||
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
||||||
"Mithalas Cathedral, fourth urn in the top right room",
|
"Mithalas Cathedral, fourth urn in the top right room",
|
||||||
"Mithalas Cathedral, Mithalan Dress",
|
"Mithalas Cathedral, Mithalan Dress",
|
||||||
"Mithalas Cathedral, urn below the left entrance",
|
"Mithalas Cathedral right area, urn below the left entrance",
|
||||||
"Mithalas boss area, beating Mithalan God",
|
"Cathedral boss area, beating Mithalan God",
|
||||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||||
"Kelp Forest top left area, Verse Egg",
|
"Kelp Forest top left area, Verse Egg",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the fish form
|
Description: Unit test used to test accessibility of locations with and without the fish form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class FishFormAccessTest(AquariaTestBase):
|
class FishFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without Li
|
Description: Unit test used to test accessibility of locations with and without Li
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class LiAccessTest(AquariaTestBase):
|
class LiAccessTest(AquariaTestBase):
|
||||||
@@ -24,7 +24,7 @@ class LiAccessTest(AquariaTestBase):
|
|||||||
"Sunken City left area, Girl Costume",
|
"Sunken City left area, Girl Costume",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
"The Body center area, breaking Li's cage",
|
"The Body center area, breaking Li's cage",
|
||||||
"The Body center area, bulb on the main path blocking tube",
|
"The Body main area, bulb on the main path blocking tube",
|
||||||
"The Body left area, first bulb in the top face room",
|
"The Body left area, first bulb in the top face room",
|
||||||
"The Body left area, second bulb in the top face room",
|
"The Body left area, second bulb in the top face room",
|
||||||
"The Body left area, bulb below the water stream",
|
"The Body left area, bulb below the water stream",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
|
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class LightAccessTest(AquariaTestBase):
|
class LightAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the nature form
|
Description: Unit test used to test accessibility of locations with and without the nature form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class NatureFormAccessTest(AquariaTestBase):
|
class NatureFormAccessTest(AquariaTestBase):
|
||||||
@@ -38,7 +38,7 @@ class NatureFormAccessTest(AquariaTestBase):
|
|||||||
"Beating the Golem",
|
"Beating the Golem",
|
||||||
"Sunken City cleared",
|
"Sunken City cleared",
|
||||||
"The Body center area, breaking Li's cage",
|
"The Body center area, breaking Li's cage",
|
||||||
"The Body center area, bulb on the main path blocking tube",
|
"The Body main area, bulb on the main path blocking tube",
|
||||||
"The Body left area, first bulb in the top face room",
|
"The Body left area, first bulb in the top face room",
|
||||||
"The Body left area, second bulb in the top face room",
|
"The Body left area, second bulb in the top face room",
|
||||||
"The Body left area, bulb below the water stream",
|
"The Body left area, bulb below the water stream",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
from BaseClasses import ItemClassification
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
|
|
||||||
unfillable_locations = [
|
unfillable_locations = [
|
||||||
"Energy Temple boss area, Fallen God Tooth",
|
"Energy Temple boss area, Fallen God Tooth",
|
||||||
"Mithalas boss area, beating Mithalan God",
|
"Cathedral boss area, beating Mithalan God",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
"Sun Temple boss area, beating Sun God",
|
"Sun Temple boss area, beating Sun God",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
@@ -35,7 +35,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||||
"Bubble Cave, Verse Egg",
|
"Bubble Cave, Verse Egg",
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
"Kelp Forest bottom left area, Walker Baby",
|
"Kelp Forest bottom left area, Walker baby",
|
||||||
"Sun Temple, Sun Key",
|
"Sun Temple, Sun Key",
|
||||||
"The Body bottom area, Mutant Costume",
|
"The Body bottom area, Mutant Costume",
|
||||||
"Sun Temple, bulb in the hidden room of the right part",
|
"Sun Temple, bulb in the hidden room of the right part",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
|
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
|
|
||||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||||
@@ -15,7 +16,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
|
|
||||||
unfillable_locations = [
|
unfillable_locations = [
|
||||||
"Energy Temple boss area, Fallen God Tooth",
|
"Energy Temple boss area, Fallen God Tooth",
|
||||||
"Mithalas boss area, beating Mithalan God",
|
"Cathedral boss area, beating Mithalan God",
|
||||||
"Kelp Forest boss area, beating Drunian God",
|
"Kelp Forest boss area, beating Drunian God",
|
||||||
"Sun Temple boss area, beating Sun God",
|
"Sun Temple boss area, beating Sun God",
|
||||||
"Sunken City, bulb on top of the boss area",
|
"Sunken City, bulb on top of the boss area",
|
||||||
@@ -34,7 +35,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
|||||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||||
"Bubble Cave, Verse Egg",
|
"Bubble Cave, Verse Egg",
|
||||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||||
"Kelp Forest bottom left area, Walker Baby",
|
"Kelp Forest bottom left area, Walker baby",
|
||||||
"Sun Temple, Sun Key",
|
"Sun Temple, Sun Key",
|
||||||
"The Body bottom area, Mutant Costume",
|
"The Body bottom area, Mutant Costume",
|
||||||
"Sun Temple, bulb in the hidden room of the right part",
|
"Sun Temple, bulb in the hidden room of the right part",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the spirit form
|
Description: Unit test used to test accessibility of locations with and without the spirit form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class SpiritFormAccessTest(AquariaTestBase):
|
class SpiritFormAccessTest(AquariaTestBase):
|
||||||
@@ -16,7 +16,7 @@ class SpiritFormAccessTest(AquariaTestBase):
|
|||||||
"The Veil bottom area, bulb in the spirit path",
|
"The Veil bottom area, bulb in the spirit path",
|
||||||
"Mithalas City Castle, Trident Head",
|
"Mithalas City Castle, Trident Head",
|
||||||
"Open Water skeleton path, King Skull",
|
"Open Water skeleton path, King Skull",
|
||||||
"Kelp Forest bottom left area, Walker Baby",
|
"Kelp Forest bottom left area, Walker baby",
|
||||||
"Abyss right area, bulb behind the rock in the whale room",
|
"Abyss right area, bulb behind the rock in the whale room",
|
||||||
"The Whale, Verse Egg",
|
"The Whale, Verse Egg",
|
||||||
"Ice Cave, bulb in the room to the right",
|
"Ice Cave, bulb in the room to the right",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the sun form
|
Description: Unit test used to test accessibility of locations with and without the sun form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class SunFormAccessTest(AquariaTestBase):
|
class SunFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Unit test used to test accessibility of region with the unconfined
|
|||||||
turtle and energy door
|
turtle and energy door
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
|
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
|
Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
|
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
|
Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from . import AquariaTestBase
|
from worlds.aquaria.test import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
|
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -762,7 +762,7 @@ location_table: List[LocationDict] = [
|
|||||||
'game_id': "graf385"},
|
'game_id': "graf385"},
|
||||||
{'name': "Tagged 389 Graffiti Spots",
|
{'name': "Tagged 389 Graffiti Spots",
|
||||||
'stage': Stages.Misc,
|
'stage': Stages.Misc,
|
||||||
'game_id': "graf389"},
|
'game_id': "graf379"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1006,8 +1006,6 @@ def rules(brcworld):
|
|||||||
lambda state: mataan_challenge2(state, player, limit, glitched))
|
lambda state: mataan_challenge2(state, player, limit, glitched))
|
||||||
set_rule(multiworld.get_location("Mataan: Score challenge reward", player),
|
set_rule(multiworld.get_location("Mataan: Score challenge reward", player),
|
||||||
lambda state: mataan_challenge3(state, player))
|
lambda state: mataan_challenge3(state, player))
|
||||||
set_rule(multiworld.get_location("Mataan: Coil joins the crew", player),
|
|
||||||
lambda state: mataan_deepest(state, player, limit, glitched))
|
|
||||||
if photos:
|
if photos:
|
||||||
set_rule(multiworld.get_location("Mataan: Trash Polo", player),
|
set_rule(multiworld.get_location("Mataan: Trash Polo", player),
|
||||||
lambda state: camera(state, player))
|
lambda state: camera(state, player))
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import typing
|
|||||||
|
|
||||||
|
|
||||||
class ItemData(typing.NamedTuple):
|
class ItemData(typing.NamedTuple):
|
||||||
code: int
|
code: typing.Optional[int]
|
||||||
progression: bool = True
|
progression: bool
|
||||||
|
|
||||||
|
|
||||||
class ChecksFinderItem(Item):
|
class ChecksFinderItem(Item):
|
||||||
@@ -12,9 +12,16 @@ class ChecksFinderItem(Item):
|
|||||||
|
|
||||||
|
|
||||||
item_table = {
|
item_table = {
|
||||||
"Map Width": ItemData(80000),
|
"Map Width": ItemData(80000, True),
|
||||||
"Map Height": ItemData(80001),
|
"Map Height": ItemData(80001, True),
|
||||||
"Map Bombs": ItemData(80002),
|
"Map Bombs": ItemData(80002, True),
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items()}
|
required_items = {
|
||||||
|
}
|
||||||
|
|
||||||
|
item_frequencies = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||||
|
|||||||
@@ -3,14 +3,46 @@ import typing
|
|||||||
|
|
||||||
|
|
||||||
class AdvData(typing.NamedTuple):
|
class AdvData(typing.NamedTuple):
|
||||||
id: int
|
id: typing.Optional[int]
|
||||||
region: str = "Board"
|
region: str
|
||||||
|
|
||||||
|
|
||||||
class ChecksFinderLocation(Location):
|
class ChecksFinderAdvancement(Location):
|
||||||
game: str = "ChecksFinder"
|
game: str = "ChecksFinder"
|
||||||
|
|
||||||
|
|
||||||
base_id = 81000
|
advancement_table = {
|
||||||
advancement_table = {f"Tile {i+1}": AdvData(base_id+i) for i in range(25)}
|
"Tile 1": AdvData(81000, 'Board'),
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items()}
|
"Tile 2": AdvData(81001, 'Board'),
|
||||||
|
"Tile 3": AdvData(81002, 'Board'),
|
||||||
|
"Tile 4": AdvData(81003, 'Board'),
|
||||||
|
"Tile 5": AdvData(81004, 'Board'),
|
||||||
|
"Tile 6": AdvData(81005, 'Board'),
|
||||||
|
"Tile 7": AdvData(81006, 'Board'),
|
||||||
|
"Tile 8": AdvData(81007, 'Board'),
|
||||||
|
"Tile 9": AdvData(81008, 'Board'),
|
||||||
|
"Tile 10": AdvData(81009, 'Board'),
|
||||||
|
"Tile 11": AdvData(81010, 'Board'),
|
||||||
|
"Tile 12": AdvData(81011, 'Board'),
|
||||||
|
"Tile 13": AdvData(81012, 'Board'),
|
||||||
|
"Tile 14": AdvData(81013, 'Board'),
|
||||||
|
"Tile 15": AdvData(81014, 'Board'),
|
||||||
|
"Tile 16": AdvData(81015, 'Board'),
|
||||||
|
"Tile 17": AdvData(81016, 'Board'),
|
||||||
|
"Tile 18": AdvData(81017, 'Board'),
|
||||||
|
"Tile 19": AdvData(81018, 'Board'),
|
||||||
|
"Tile 20": AdvData(81019, 'Board'),
|
||||||
|
"Tile 21": AdvData(81020, 'Board'),
|
||||||
|
"Tile 22": AdvData(81021, 'Board'),
|
||||||
|
"Tile 23": AdvData(81022, 'Board'),
|
||||||
|
"Tile 24": AdvData(81023, 'Board'),
|
||||||
|
"Tile 25": AdvData(81024, 'Board'),
|
||||||
|
}
|
||||||
|
|
||||||
|
exclusion_table = {
|
||||||
|
}
|
||||||
|
|
||||||
|
events_table = {
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items() if data.id}
|
||||||
6
worlds/checksfinder/Options.py
Normal file
6
worlds/checksfinder/Options.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import typing
|
||||||
|
from Options import Option
|
||||||
|
|
||||||
|
|
||||||
|
checksfinder_options: typing.Dict[str, type(Option)] = {
|
||||||
|
}
|
||||||
@@ -1,24 +1,44 @@
|
|||||||
from worlds.generic.Rules import set_rule
|
from ..generic.Rules import set_rule
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld, CollectionState
|
||||||
|
|
||||||
|
|
||||||
items = ["Map Width", "Map Height", "Map Bombs"]
|
def _has_total(state: CollectionState, player: int, total: int):
|
||||||
|
return (state.count('Map Width', player) + state.count('Map Height', player) +
|
||||||
|
state.count('Map Bombs', player)) >= total
|
||||||
|
|
||||||
|
|
||||||
# Sets rules on entrances and advancements that are always applied
|
# Sets rules on entrances and advancements that are always applied
|
||||||
def set_rules(multiworld: MultiWorld, player: int):
|
def set_rules(world: MultiWorld, player: int):
|
||||||
for i in range(20):
|
set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1))
|
||||||
set_rule(multiworld.get_location(f"Tile {i+6}", player), lambda state, i=i: state.has_from_list(items, player, i+1))
|
set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2))
|
||||||
|
set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3))
|
||||||
|
set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4))
|
||||||
|
set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5))
|
||||||
|
set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6))
|
||||||
|
set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7))
|
||||||
|
set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8))
|
||||||
|
set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9))
|
||||||
|
set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10))
|
||||||
|
set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11))
|
||||||
|
set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12))
|
||||||
|
set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13))
|
||||||
|
set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14))
|
||||||
|
set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15))
|
||||||
|
set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16))
|
||||||
|
set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17))
|
||||||
|
set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18))
|
||||||
|
set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19))
|
||||||
|
set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20))
|
||||||
|
|
||||||
|
|
||||||
# Sets rules on completion condition
|
# Sets rules on completion condition
|
||||||
def set_completion_rules(multiworld: MultiWorld, player: int):
|
def set_completion_rules(world: MultiWorld, player: int):
|
||||||
width_req = 5 # 10 - 5
|
|
||||||
height_req = 5 # 10 - 5
|
width_req = 10-5
|
||||||
bomb_req = 15 # 20 - 5
|
height_req = 10-5
|
||||||
multiworld.completion_condition[player] = lambda state: state.has_all_counts(
|
bomb_req = 20-5
|
||||||
{
|
completion_requirements = lambda state: \
|
||||||
"Map Width": width_req,
|
state.has("Map Width", player, width_req) and \
|
||||||
"Map Height": height_req,
|
state.has("Map Height", player, height_req) and \
|
||||||
"Map Bombs": bomb_req,
|
state.has("Map Bombs", player, bomb_req)
|
||||||
}, player)
|
world.completion_condition[player] = lambda state: completion_requirements(state)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from BaseClasses import Region, Entrance, Tutorial, ItemClassification
|
from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification
|
||||||
from .Items import ChecksFinderItem, item_table
|
from .Items import ChecksFinderItem, item_table, required_items
|
||||||
from .Locations import ChecksFinderLocation, advancement_table
|
from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table
|
||||||
from Options import PerGameCommonOptions
|
from .Options import checksfinder_options
|
||||||
from .Rules import set_rules, set_completion_rules
|
from .Rules import set_rules, set_completion_rules
|
||||||
from worlds.AutoWorld import World, WebWorld
|
from ..AutoWorld import World, WebWorld
|
||||||
|
|
||||||
client_version = 7
|
client_version = 7
|
||||||
|
|
||||||
@@ -25,34 +25,38 @@ class ChecksFinderWorld(World):
|
|||||||
ChecksFinder is a game where you avoid mines and find checks inside the board
|
ChecksFinder is a game where you avoid mines and find checks inside the board
|
||||||
with the mines! You win when you get all your items and beat the board!
|
with the mines! You win when you get all your items and beat the board!
|
||||||
"""
|
"""
|
||||||
game = "ChecksFinder"
|
game: str = "ChecksFinder"
|
||||||
options_dataclass = PerGameCommonOptions
|
option_definitions = checksfinder_options
|
||||||
|
topology_present = True
|
||||||
web = ChecksFinderWeb()
|
web = ChecksFinderWeb()
|
||||||
|
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
|
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
|
||||||
|
|
||||||
def create_regions(self):
|
def _get_checksfinder_data(self):
|
||||||
menu = Region("Menu", self.player, self.multiworld)
|
return {
|
||||||
board = Region("Board", self.player, self.multiworld)
|
'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32),
|
||||||
board.locations += [ChecksFinderLocation(self.player, loc_name, loc_data.id, board)
|
'seed_name': self.multiworld.seed_name,
|
||||||
for loc_name, loc_data in advancement_table.items()]
|
'player_name': self.multiworld.get_player_name(self.player),
|
||||||
|
'player_id': self.player,
|
||||||
connection = Entrance(self.player, "New Board", menu)
|
'client_version': client_version,
|
||||||
menu.exits.append(connection)
|
'race': self.multiworld.is_race,
|
||||||
connection.connect(board)
|
}
|
||||||
self.multiworld.regions += [menu, board]
|
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
|
|
||||||
# Generate item pool
|
# Generate item pool
|
||||||
itempool = []
|
itempool = []
|
||||||
|
# Add all required progression items
|
||||||
|
for (name, num) in required_items.items():
|
||||||
|
itempool += [name] * num
|
||||||
# Add the map width and height stuff
|
# Add the map width and height stuff
|
||||||
itempool += ["Map Width"] * 5 # 10 - 5
|
itempool += ["Map Width"] * (10-5)
|
||||||
itempool += ["Map Height"] * 5 # 10 - 5
|
itempool += ["Map Height"] * (10-5)
|
||||||
# Add the map bombs
|
# Add the map bombs
|
||||||
itempool += ["Map Bombs"] * 15 # 20 - 5
|
itempool += ["Map Bombs"] * (20-5)
|
||||||
# Convert itempool into real items
|
# Convert itempool into real items
|
||||||
itempool = [self.create_item(item) for item in itempool]
|
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
|
||||||
|
|
||||||
self.multiworld.itempool += itempool
|
self.multiworld.itempool += itempool
|
||||||
|
|
||||||
@@ -60,16 +64,28 @@ class ChecksFinderWorld(World):
|
|||||||
set_rules(self.multiworld, self.player)
|
set_rules(self.multiworld, self.player)
|
||||||
set_completion_rules(self.multiworld, self.player)
|
set_completion_rules(self.multiworld, self.player)
|
||||||
|
|
||||||
def fill_slot_data(self):
|
def create_regions(self):
|
||||||
return {
|
menu = Region("Menu", self.player, self.multiworld)
|
||||||
"world_seed": self.random.getrandbits(32),
|
board = Region("Board", self.player, self.multiworld)
|
||||||
"seed_name": self.multiworld.seed_name,
|
board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
|
||||||
"player_name": self.player_name,
|
for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
|
||||||
"player_id": self.player,
|
|
||||||
"client_version": client_version,
|
|
||||||
"race": self.multiworld.is_race,
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> ChecksFinderItem:
|
connection = Entrance(self.player, "New Board", menu)
|
||||||
|
menu.exits.append(connection)
|
||||||
|
connection.connect(board)
|
||||||
|
self.multiworld.regions += [menu, board]
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
slot_data = self._get_checksfinder_data()
|
||||||
|
for option_name in checksfinder_options:
|
||||||
|
option = getattr(self.multiworld, option_name)[self.player]
|
||||||
|
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
|
||||||
|
slot_data[option_name] = int(option.value)
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def create_item(self, name: str) -> Item:
|
||||||
item_data = item_table[name]
|
item_data = item_table[name]
|
||||||
return ChecksFinderItem(name, ItemClassification.progression, item_data.code, self.player)
|
item = ChecksFinderItem(name,
|
||||||
|
ItemClassification.progression if item_data.progression else ItemClassification.filler,
|
||||||
|
item_data.code, self.player)
|
||||||
|
return item
|
||||||
|
|||||||
@@ -24,3 +24,8 @@ next to an icon, the number is how many you have gotten and the icon represents
|
|||||||
Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map
|
Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map
|
||||||
Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
|
Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
|
||||||
|
|
||||||
|
## Unique Local Commands
|
||||||
|
|
||||||
|
The following command is only available when using the ChecksFinderClient to play with Archipelago.
|
||||||
|
|
||||||
|
- `/resync` Manually trigger a resync.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- ChecksFinder from
|
- ChecksFinder from
|
||||||
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
|
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)
|
||||||
|
|
||||||
## Configuring your YAML file
|
## Configuring your YAML file
|
||||||
|
|
||||||
@@ -16,15 +17,28 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
|||||||
|
|
||||||
You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options)
|
You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options)
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
### Generating a ChecksFinder game
|
||||||
|
|
||||||
1. Start ChecksFinder
|
**ChecksFinder is meant to be played _alongside_ another game! You may not be playing it for long periods of time if
|
||||||
2. Enter the following information:
|
you play it by itself with another person!**
|
||||||
- Enter the server url (starting from `wss://` for https connection like archipelago.gg, and starting from `ws://` for http connection and local multiserver)
|
|
||||||
- Enter server port
|
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
|
||||||
- Enter the name of the slot you wish to connect to
|
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
|
||||||
- Enter the room password (optional)
|
files. You do not have a file inside that zip though!
|
||||||
- Press `Play Online` to connect
|
|
||||||
3. Start playing!
|
You need to start ChecksFinder client yourself, it is located within the Archipelago folder.
|
||||||
|
|
||||||
|
### Connect to the MultiServer
|
||||||
|
|
||||||
|
First start ChecksFinder.
|
||||||
|
|
||||||
|
Once both ChecksFinder and the client are started. In the client at the top type in the spot labeled `Server` type the
|
||||||
|
`Ip Address` and `Port` separated with a `:` symbol.
|
||||||
|
|
||||||
|
The client will then ask for the username you chose, input that in the text box at the bottom of the client.
|
||||||
|
|
||||||
|
### Play the game
|
||||||
|
|
||||||
|
When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
|
||||||
|
multiworld game!
|
||||||
|
|
||||||
Game options and controls are described in the readme on the github repository for the game
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from Options import (OptionGroup, Choice, DefaultOnToggle, ItemsAccessibility, PerGameCommonOptions, Range, Toggle,
|
from Options import OptionGroup, Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool
|
||||||
StartInventoryPool)
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterStages(Choice):
|
class CharacterStages(Choice):
|
||||||
@@ -522,7 +521,6 @@ class DeathLink(Choice):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CV64Options(PerGameCommonOptions):
|
class CV64Options(PerGameCommonOptions):
|
||||||
accessibility: ItemsAccessibility
|
|
||||||
start_inventory_from_pool: StartInventoryPool
|
start_inventory_from_pool: StartInventoryPool
|
||||||
character_stages: CharacterStages
|
character_stages: CharacterStages
|
||||||
stage_shuffle: StageShuffle
|
stage_shuffle: StageShuffle
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class DKC3SNIClient(SNIClient):
|
|||||||
return
|
return
|
||||||
|
|
||||||
new_checks = []
|
new_checks = []
|
||||||
from .Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
||||||
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
||||||
for loc_id, loc_data in location_rom_data.items():
|
for loc_id, loc_data in location_rom_data.items():
|
||||||
if loc_id not in ctx.locations_checked:
|
if loc_id not in ctx.locations_checked:
|
||||||
|
|||||||
@@ -8,15 +8,11 @@ from .Locations import DLCQuestLocation, location_table
|
|||||||
from .Options import DLCQuestOptions
|
from .Options import DLCQuestOptions
|
||||||
from .Regions import create_regions
|
from .Regions import create_regions
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
from .presets import dlcq_options_presets
|
|
||||||
from .option_groups import dlcq_option_groups
|
|
||||||
|
|
||||||
client_version = 0
|
client_version = 0
|
||||||
|
|
||||||
|
|
||||||
class DLCqwebworld(WebWorld):
|
class DLCqwebworld(WebWorld):
|
||||||
options_presets = dlcq_options_presets
|
|
||||||
option_groups = dlcq_option_groups
|
|
||||||
setup_en = Tutorial(
|
setup_en = Tutorial(
|
||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
"A guide to setting up the Archipelago DLCQuest game on your computer.",
|
"A guide to setting up the Archipelago DLCQuest game on your computer.",
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from Options import ProgressionBalancing, Accessibility, OptionGroup
|
|
||||||
from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity,
|
|
||||||
CoinSanityRange, DeathLink)
|
|
||||||
|
|
||||||
dlcq_option_groups: List[OptionGroup] = [
|
|
||||||
OptionGroup("General", [
|
|
||||||
Campaign,
|
|
||||||
ItemShuffle,
|
|
||||||
CoinSanity,
|
|
||||||
]),
|
|
||||||
OptionGroup("Customization", [
|
|
||||||
EndingChoice,
|
|
||||||
PermanentCoins,
|
|
||||||
CoinSanityRange,
|
|
||||||
]),
|
|
||||||
OptionGroup("Tedious and Grind", [
|
|
||||||
TimeIsMoney,
|
|
||||||
DoubleJumpGlitch,
|
|
||||||
]),
|
|
||||||
OptionGroup("Advanced Options", [
|
|
||||||
DeathLink,
|
|
||||||
ProgressionBalancing,
|
|
||||||
Accessibility,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle
|
|
||||||
|
|
||||||
all_random_settings = {
|
|
||||||
DoubleJumpGlitch.internal_name: "random",
|
|
||||||
CoinSanity.internal_name: "random",
|
|
||||||
CoinSanityRange.internal_name: "random",
|
|
||||||
PermanentCoins.internal_name: "random",
|
|
||||||
TimeIsMoney.internal_name: "random",
|
|
||||||
EndingChoice.internal_name: "random",
|
|
||||||
Campaign.internal_name: "random",
|
|
||||||
ItemShuffle.internal_name: "random",
|
|
||||||
"death_link": "random",
|
|
||||||
}
|
|
||||||
|
|
||||||
main_campaign_settings = {
|
|
||||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
|
|
||||||
CoinSanity.internal_name: CoinSanity.option_coin,
|
|
||||||
CoinSanityRange.internal_name: 30,
|
|
||||||
PermanentCoins.internal_name: PermanentCoins.option_false,
|
|
||||||
TimeIsMoney.internal_name: TimeIsMoney.option_required,
|
|
||||||
EndingChoice.internal_name: EndingChoice.option_true,
|
|
||||||
Campaign.internal_name: Campaign.option_basic,
|
|
||||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
|
||||||
}
|
|
||||||
|
|
||||||
lfod_campaign_settings = {
|
|
||||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
|
|
||||||
CoinSanity.internal_name: CoinSanity.option_coin,
|
|
||||||
CoinSanityRange.internal_name: 30,
|
|
||||||
PermanentCoins.internal_name: PermanentCoins.option_false,
|
|
||||||
TimeIsMoney.internal_name: TimeIsMoney.option_required,
|
|
||||||
EndingChoice.internal_name: EndingChoice.option_true,
|
|
||||||
Campaign.internal_name: Campaign.option_live_freemium_or_die,
|
|
||||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
|
||||||
}
|
|
||||||
|
|
||||||
easy_settings = {
|
|
||||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
|
|
||||||
CoinSanity.internal_name: CoinSanity.option_none,
|
|
||||||
CoinSanityRange.internal_name: 40,
|
|
||||||
PermanentCoins.internal_name: PermanentCoins.option_true,
|
|
||||||
TimeIsMoney.internal_name: TimeIsMoney.option_required,
|
|
||||||
EndingChoice.internal_name: EndingChoice.option_true,
|
|
||||||
Campaign.internal_name: Campaign.option_both,
|
|
||||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
|
||||||
}
|
|
||||||
|
|
||||||
hard_settings = {
|
|
||||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple,
|
|
||||||
CoinSanity.internal_name: CoinSanity.option_coin,
|
|
||||||
CoinSanityRange.internal_name: 30,
|
|
||||||
PermanentCoins.internal_name: PermanentCoins.option_false,
|
|
||||||
TimeIsMoney.internal_name: TimeIsMoney.option_optional,
|
|
||||||
EndingChoice.internal_name: EndingChoice.option_true,
|
|
||||||
Campaign.internal_name: Campaign.option_both,
|
|
||||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
dlcq_options_presets: Dict[str, Dict[str, Any]] = {
|
|
||||||
"All random": all_random_settings,
|
|
||||||
"Main campaign": main_campaign_settings,
|
|
||||||
"LFOD campaign": lfod_campaign_settings,
|
|
||||||
"Both easy": easy_settings,
|
|
||||||
"Both hard": hard_settings,
|
|
||||||
}
|
|
||||||
@@ -660,18 +660,11 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
|||||||
end
|
end
|
||||||
local tech
|
local tech
|
||||||
local force = game.forces["player"]
|
local force = game.forces["player"]
|
||||||
if call.parameter == nil then
|
|
||||||
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
chunks = split(call.parameter, "\t")
|
chunks = split(call.parameter, "\t")
|
||||||
local item_name = chunks[1]
|
local item_name = chunks[1]
|
||||||
local index = chunks[2]
|
local index = chunks[2]
|
||||||
local source = chunks[3] or "Archipelago"
|
local source = chunks[3] or "Archipelago"
|
||||||
if index == nil then
|
if index == -1 then -- for coop sync and restoring from an older savegame
|
||||||
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
|
|
||||||
return
|
|
||||||
elseif index == -1 then -- for coop sync and restoring from an older savegame
|
|
||||||
tech = force.technologies[item_name]
|
tech = force.technologies[item_name]
|
||||||
if tech.researched ~= true then
|
if tech.researched ~= true then
|
||||||
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
|
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
|
|||||||
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
|
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)
|
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
|
||||||
check_2 = await snes_read(ctx, 0xF53749, 1)
|
check_2 = await snes_read(ctx, 0xF53749, 1)
|
||||||
if check_1 != b'\x01' or check_2 != b'\x01':
|
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
|
||||||
return
|
return
|
||||||
|
|
||||||
def get_range(data_range):
|
def get_range(data_range):
|
||||||
|
|||||||
@@ -222,10 +222,10 @@ for item, data in item_table.items():
|
|||||||
|
|
||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
items = []
|
items = []
|
||||||
starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ")
|
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
|
||||||
self.multiworld.push_precollected(self.create_item(starting_weapon))
|
self.multiworld.push_precollected(self.create_item(starting_weapon))
|
||||||
self.multiworld.push_precollected(self.create_item("Steel Armor"))
|
self.multiworld.push_precollected(self.create_item("Steel Armor"))
|
||||||
if self.options.sky_coin_mode == "start_with":
|
if self.multiworld.sky_coin_mode[self.player] == "start_with":
|
||||||
self.multiworld.push_precollected(self.create_item("Sky Coin"))
|
self.multiworld.push_precollected(self.create_item("Sky Coin"))
|
||||||
|
|
||||||
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
|
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
|
||||||
@@ -233,28 +233,28 @@ def create_items(self) -> None:
|
|||||||
def add_item(item_name):
|
def add_item(item_name):
|
||||||
if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name:
|
if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name:
|
||||||
return
|
return
|
||||||
if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key:
|
if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key:
|
||||||
return
|
return
|
||||||
if self.options.progressive_gear:
|
if self.multiworld.progressive_gear[self.player]:
|
||||||
for item_group in prog_map:
|
for item_group in prog_map:
|
||||||
if item_name in self.item_name_groups[item_group]:
|
if item_name in self.item_name_groups[item_group]:
|
||||||
item_name = prog_map[item_group]
|
item_name = prog_map[item_group]
|
||||||
break
|
break
|
||||||
if item_name == "Sky Coin":
|
if item_name == "Sky Coin":
|
||||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||||
for _ in range(40):
|
for _ in range(40):
|
||||||
items.append(self.create_item("Sky Fragment"))
|
items.append(self.create_item("Sky Fragment"))
|
||||||
return
|
return
|
||||||
elif self.options.sky_coin_mode == "save_the_crystals":
|
elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals":
|
||||||
items.append(self.create_filler())
|
items.append(self.create_filler())
|
||||||
return
|
return
|
||||||
if item_name in precollected_item_names:
|
if item_name in precollected_item_names:
|
||||||
items.append(self.create_filler())
|
items.append(self.create_filler())
|
||||||
return
|
return
|
||||||
i = self.create_item(item_name)
|
i = self.create_item(item_name)
|
||||||
if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"):
|
if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"):
|
||||||
i.classification = ItemClassification.useful
|
i.classification = ItemClassification.useful
|
||||||
if (self.options.logic == "expert" and self.options.map_shuffle == "none" and
|
if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and
|
||||||
item_name == "Exit Book"):
|
item_name == "Exit Book"):
|
||||||
i.classification = ItemClassification.progression
|
i.classification = ItemClassification.progression
|
||||||
items.append(i)
|
items.append(i)
|
||||||
@@ -263,11 +263,11 @@ def create_items(self) -> None:
|
|||||||
for item in self.item_name_groups[item_group]:
|
for item in self.item_name_groups[item_group]:
|
||||||
add_item(item)
|
add_item(item)
|
||||||
|
|
||||||
if self.options.brown_boxes == "include":
|
if self.multiworld.brown_boxes[self.player] == "include":
|
||||||
filler_items = []
|
filler_items = []
|
||||||
for item, count in fillers.items():
|
for item, count in fillers.items():
|
||||||
filler_items += [self.create_item(item) for _ in range(count)]
|
filler_items += [self.create_item(item) for _ in range(count)]
|
||||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||||
self.multiworld.random.shuffle(filler_items)
|
self.multiworld.random.shuffle(filler_items)
|
||||||
filler_items = filler_items[39:]
|
filler_items = filler_items[39:]
|
||||||
items += filler_items
|
items += filler_items
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions
|
from Options import Choice, FreeText, Toggle, Range
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
class Logic(Choice):
|
class Logic(Choice):
|
||||||
@@ -322,36 +321,36 @@ class KaelisMomFightsMinotaur(Toggle):
|
|||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
option_definitions = {
|
||||||
class FFMQOptions(PerGameCommonOptions):
|
"logic": Logic,
|
||||||
logic: Logic
|
"brown_boxes": BrownBoxes,
|
||||||
brown_boxes: BrownBoxes
|
"sky_coin_mode": SkyCoinMode,
|
||||||
sky_coin_mode: SkyCoinMode
|
"shattered_sky_coin_quantity": ShatteredSkyCoinQuantity,
|
||||||
shattered_sky_coin_quantity: ShatteredSkyCoinQuantity
|
"starting_weapon": StartingWeapon,
|
||||||
starting_weapon: StartingWeapon
|
"progressive_gear": ProgressiveGear,
|
||||||
progressive_gear: ProgressiveGear
|
"leveling_curve": LevelingCurve,
|
||||||
leveling_curve: LevelingCurve
|
"starting_companion": StartingCompanion,
|
||||||
starting_companion: StartingCompanion
|
"available_companions": AvailableCompanions,
|
||||||
available_companions: AvailableCompanions
|
"companions_locations": CompanionsLocations,
|
||||||
companions_locations: CompanionsLocations
|
"kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur,
|
||||||
kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur
|
"companion_leveling_type": CompanionLevelingType,
|
||||||
companion_leveling_type: CompanionLevelingType
|
"companion_spellbook_type": CompanionSpellbookType,
|
||||||
companion_spellbook_type: CompanionSpellbookType
|
"enemies_density": EnemiesDensity,
|
||||||
enemies_density: EnemiesDensity
|
"enemies_scaling_lower": EnemiesScalingLower,
|
||||||
enemies_scaling_lower: EnemiesScalingLower
|
"enemies_scaling_upper": EnemiesScalingUpper,
|
||||||
enemies_scaling_upper: EnemiesScalingUpper
|
"bosses_scaling_lower": BossesScalingLower,
|
||||||
bosses_scaling_lower: BossesScalingLower
|
"bosses_scaling_upper": BossesScalingUpper,
|
||||||
bosses_scaling_upper: BossesScalingUpper
|
"enemizer_attacks": EnemizerAttacks,
|
||||||
enemizer_attacks: EnemizerAttacks
|
"enemizer_groups": EnemizerGroups,
|
||||||
enemizer_groups: EnemizerGroups
|
"shuffle_res_weak_types": ShuffleResWeakType,
|
||||||
shuffle_res_weak_types: ShuffleResWeakType
|
"shuffle_enemies_position": ShuffleEnemiesPositions,
|
||||||
shuffle_enemies_position: ShuffleEnemiesPositions
|
"progressive_formations": ProgressiveFormations,
|
||||||
progressive_formations: ProgressiveFormations
|
"doom_castle_mode": DoomCastle,
|
||||||
doom_castle_mode: DoomCastle
|
"doom_castle_shortcut": DoomCastleShortcut,
|
||||||
doom_castle_shortcut: DoomCastleShortcut
|
"tweak_frustrating_dungeons": TweakFrustratingDungeons,
|
||||||
tweak_frustrating_dungeons: TweakFrustratingDungeons
|
"map_shuffle": MapShuffle,
|
||||||
map_shuffle: MapShuffle
|
"crest_shuffle": CrestShuffle,
|
||||||
crest_shuffle: CrestShuffle
|
"shuffle_battlefield_rewards": ShuffleBattlefieldRewards,
|
||||||
shuffle_battlefield_rewards: ShuffleBattlefieldRewards
|
"map_shuffle_seed": MapShuffleSeed,
|
||||||
map_shuffle_seed: MapShuffleSeed
|
"battlefields_battles_quantities": BattlefieldsBattlesQuantities,
|
||||||
battlefields_battles_quantities: BattlefieldsBattlesQuantities
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import yaml
|
import yaml
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
import Utils
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from .Regions import object_id_table
|
from .Regions import object_id_table
|
||||||
|
from Utils import __version__
|
||||||
from worlds.Files import APPatch
|
from worlds.Files import APPatch
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml"))
|
settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader)
|
||||||
|
|
||||||
|
|
||||||
def generate_output(self, output_directory):
|
def generate_output(self, output_directory):
|
||||||
@@ -21,7 +21,7 @@ def generate_output(self, output_directory):
|
|||||||
item_name = "".join(item_name.split(" "))
|
item_name = "".join(item_name.split(" "))
|
||||||
else:
|
else:
|
||||||
if item.advancement or item.useful or (item.trap and
|
if item.advancement or item.useful or (item.trap and
|
||||||
self.random.randint(0, 1)):
|
self.multiworld.per_slot_randoms[self.player].randint(0, 1)):
|
||||||
item_name = "APItem"
|
item_name = "APItem"
|
||||||
else:
|
else:
|
||||||
item_name = "APItemFiller"
|
item_name = "APItemFiller"
|
||||||
@@ -46,60 +46,60 @@ def generate_output(self, output_directory):
|
|||||||
options = deepcopy(settings_template)
|
options = deepcopy(settings_template)
|
||||||
options["name"] = self.multiworld.player_name[self.player]
|
options["name"] = self.multiworld.player_name[self.player]
|
||||||
option_writes = {
|
option_writes = {
|
||||||
"enemies_density": cc(self.options.enemies_density),
|
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
|
||||||
"chests_shuffle": "Include",
|
"chests_shuffle": "Include",
|
||||||
"shuffle_boxes_content": self.options.brown_boxes == "shuffle",
|
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
|
||||||
"npcs_shuffle": "Include",
|
"npcs_shuffle": "Include",
|
||||||
"battlefields_shuffle": "Include",
|
"battlefields_shuffle": "Include",
|
||||||
"logic_options": cc(self.options.logic),
|
"logic_options": cc(self.multiworld.logic[self.player]),
|
||||||
"shuffle_enemies_position": tf(self.options.shuffle_enemies_position),
|
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
|
||||||
"enemies_scaling_lower": cc(self.options.enemies_scaling_lower),
|
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
|
||||||
"enemies_scaling_upper": cc(self.options.enemies_scaling_upper),
|
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
|
||||||
"bosses_scaling_lower": cc(self.options.bosses_scaling_lower),
|
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
|
||||||
"bosses_scaling_upper": cc(self.options.bosses_scaling_upper),
|
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
|
||||||
"enemizer_attacks": cc(self.options.enemizer_attacks),
|
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
|
||||||
"leveling_curve": cc(self.options.leveling_curve),
|
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
|
||||||
"battles_quantity": cc(self.options.battlefields_battles_quantities) if
|
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
|
||||||
self.options.battlefields_battles_quantities.value < 5 else
|
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
|
||||||
"RandomLow" if
|
"RandomLow" if
|
||||||
self.options.battlefields_battles_quantities.value == 5 else
|
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
|
||||||
"RandomHigh",
|
"RandomHigh",
|
||||||
"shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards),
|
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
|
||||||
"random_starting_weapon": True,
|
"random_starting_weapon": True,
|
||||||
"progressive_gear": tf(self.options.progressive_gear),
|
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
|
||||||
"tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons),
|
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
|
||||||
"doom_castle_mode": cc(self.options.doom_castle_mode),
|
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
|
||||||
"doom_castle_shortcut": tf(self.options.doom_castle_shortcut),
|
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
|
||||||
"sky_coin_mode": cc(self.options.sky_coin_mode),
|
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
|
||||||
"sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity),
|
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
|
||||||
"enable_spoilers": False,
|
"enable_spoilers": False,
|
||||||
"progressive_formations": cc(self.options.progressive_formations),
|
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
|
||||||
"map_shuffling": cc(self.options.map_shuffle),
|
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
|
||||||
"crest_shuffle": tf(self.options.crest_shuffle),
|
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
|
||||||
"enemizer_groups": cc(self.options.enemizer_groups),
|
"enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]),
|
||||||
"shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types),
|
"shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]),
|
||||||
"companion_leveling_type": cc(self.options.companion_leveling_type),
|
"companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]),
|
||||||
"companion_spellbook_type": cc(self.options.companion_spellbook_type),
|
"companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]),
|
||||||
"starting_companion": cc(self.options.starting_companion),
|
"starting_companion": cc(self.multiworld.starting_companion[self.player]),
|
||||||
"available_companions": ["Zero", "One", "Two",
|
"available_companions": ["Zero", "One", "Two",
|
||||||
"Three", "Four"][self.options.available_companions.value],
|
"Three", "Four"][self.multiworld.available_companions[self.player].value],
|
||||||
"companions_locations": cc(self.options.companions_locations),
|
"companions_locations": cc(self.multiworld.companions_locations[self.player]),
|
||||||
"kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur),
|
"kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]),
|
||||||
}
|
}
|
||||||
|
|
||||||
for option, data in option_writes.items():
|
for option, data in option_writes.items():
|
||||||
options["Final Fantasy Mystic Quest"][option][data] = 1
|
options["Final Fantasy Mystic Quest"][option][data] = 1
|
||||||
|
|
||||||
rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21]
|
rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21]
|
||||||
self.rom_name = bytearray(rom_name,
|
self.rom_name = bytearray(rom_name,
|
||||||
'utf8')
|
'utf8')
|
||||||
self.rom_name_available_event.set()
|
self.rom_name_available_event.set()
|
||||||
|
|
||||||
setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
|
setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
|
||||||
hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
|
hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
|
||||||
|
|
||||||
starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]
|
starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]
|
||||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||||
starting_items.append("SkyCoin")
|
starting_items.append("SkyCoin")
|
||||||
|
|
||||||
file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq")
|
file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq")
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification
|
from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification
|
||||||
from worlds.generic.Rules import add_rule
|
from worlds.generic.Rules import add_rule
|
||||||
from .data.rooms import rooms, entrances
|
|
||||||
from .Items import item_groups, yaml_item
|
from .Items import item_groups, yaml_item
|
||||||
|
import pkgutil
|
||||||
|
import yaml
|
||||||
|
|
||||||
entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances}
|
rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader)
|
||||||
|
entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)}
|
||||||
|
|
||||||
object_id_table = {}
|
object_id_table = {}
|
||||||
object_type_table = {}
|
object_type_table = {}
|
||||||
@@ -67,7 +69,7 @@ def create_regions(self):
|
|||||||
location_table else None, object["type"], object["access"],
|
location_table else None, object["type"], object["access"],
|
||||||
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
|
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
|
||||||
room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
|
room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
|
||||||
"BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and
|
"BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and
|
||||||
not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
|
not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
|
||||||
|
|
||||||
dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
|
dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
|
||||||
@@ -89,13 +91,15 @@ def create_regions(self):
|
|||||||
if "entrance" in link and link["entrance"] != -1:
|
if "entrance" in link and link["entrance"] != -1:
|
||||||
spoiler = False
|
spoiler = False
|
||||||
if link["entrance"] in crest_warps:
|
if link["entrance"] in crest_warps:
|
||||||
if self.options.crest_shuffle:
|
if self.multiworld.crest_shuffle[self.player]:
|
||||||
spoiler = True
|
spoiler = True
|
||||||
elif self.options.map_shuffle == "everything":
|
elif self.multiworld.map_shuffle[self.player] == "everything":
|
||||||
spoiler = True
|
spoiler = True
|
||||||
elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"):
|
elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons",
|
||||||
|
"none"):
|
||||||
spoiler = True
|
spoiler = True
|
||||||
elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"):
|
elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none",
|
||||||
|
"overworld"):
|
||||||
spoiler = True
|
spoiler = True
|
||||||
|
|
||||||
if spoiler:
|
if spoiler:
|
||||||
@@ -107,7 +111,6 @@ def create_regions(self):
|
|||||||
connection.connect(connect_room)
|
connection.connect(connect_room)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
non_dead_end_crest_rooms = [
|
non_dead_end_crest_rooms = [
|
||||||
'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room',
|
'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room',
|
||||||
'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge',
|
'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge',
|
||||||
@@ -137,7 +140,7 @@ def set_rules(self) -> None:
|
|||||||
add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic)
|
add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic)
|
||||||
add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic)
|
add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic)
|
||||||
|
|
||||||
if self.options.map_shuffle:
|
if self.multiworld.map_shuffle[self.player]:
|
||||||
for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"):
|
for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"):
|
||||||
loc = self.multiworld.get_location(boss, self.player)
|
loc = self.multiworld.get_location(boss, self.player)
|
||||||
checked_regions = {loc.parent_region}
|
checked_regions = {loc.parent_region}
|
||||||
@@ -155,12 +158,12 @@ def set_rules(self) -> None:
|
|||||||
return True
|
return True
|
||||||
check_foresta(loc.parent_region)
|
check_foresta(loc.parent_region)
|
||||||
|
|
||||||
if self.options.logic == "friendly":
|
if self.multiworld.logic[self.player] == "friendly":
|
||||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||||
["MagicMirror"])
|
["MagicMirror"])
|
||||||
process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player),
|
||||||
["Mask"])
|
["Mask"])
|
||||||
if self.options.map_shuffle in ("none", "overworld"):
|
if self.multiworld.map_shuffle[self.player] in ("none", "overworld"):
|
||||||
process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player),
|
||||||
["Bomb"])
|
["Bomb"])
|
||||||
process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player),
|
||||||
@@ -182,8 +185,8 @@ def set_rules(self) -> None:
|
|||||||
process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player),
|
||||||
["DragonClaw", "CaptainCap"])
|
["DragonClaw", "CaptainCap"])
|
||||||
|
|
||||||
if self.options.logic == "expert":
|
if self.multiworld.logic[self.player] == "expert":
|
||||||
if self.options.map_shuffle == "none" and not self.options.crest_shuffle:
|
if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]:
|
||||||
inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player)
|
inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player)
|
||||||
connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room)
|
connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room)
|
||||||
connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player))
|
connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player))
|
||||||
@@ -195,14 +198,14 @@ def set_rules(self) -> None:
|
|||||||
if entrance.connected_region.name in non_dead_end_crest_rooms:
|
if entrance.connected_region.name in non_dead_end_crest_rooms:
|
||||||
entrance.access_rule = lambda state: False
|
entrance.access_rule = lambda state: False
|
||||||
|
|
||||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||||
logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value]
|
logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value]
|
||||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||||
lambda state: state.has("Sky Fragment", self.player, logic_coins)
|
lambda state: state.has("Sky Fragment", self.player, logic_coins)
|
||||||
elif self.options.sky_coin_mode == "save_the_crystals":
|
elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals":
|
||||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||||
lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player)
|
lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player)
|
||||||
elif self.options.sky_coin_mode in ("standard", "start_with"):
|
elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"):
|
||||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||||
lambda state: state.has("Sky Coin", self.player)
|
lambda state: state.has("Sky Coin", self.player)
|
||||||
|
|
||||||
@@ -210,24 +213,26 @@ def set_rules(self) -> None:
|
|||||||
def stage_set_rules(multiworld):
|
def stage_set_rules(multiworld):
|
||||||
# If there's no enemies, there's no repeatable income sources
|
# If there's no enemies, there's no repeatable income sources
|
||||||
no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest")
|
no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest")
|
||||||
if multiworld.worlds[player].options.enemies_density == "none"]
|
if multiworld.enemies_density[player] == "none"]
|
||||||
if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler,
|
if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler,
|
||||||
ItemClassification.trap)]) > len([player for player in no_enemies_players if
|
ItemClassification.trap)]) > len([player for player in no_enemies_players if
|
||||||
multiworld.worlds[player].options.accessibility == "minimal"]) * 3):
|
multiworld.accessibility[player] == "minimal"]) * 3):
|
||||||
for player in no_enemies_players:
|
for player in no_enemies_players:
|
||||||
for location in vendor_locations:
|
for location in vendor_locations:
|
||||||
if multiworld.worlds[player].options.accessibility == "full":
|
if multiworld.accessibility[player] == "locations":
|
||||||
multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED
|
multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED
|
||||||
else:
|
else:
|
||||||
multiworld.get_location(location, player).access_rule = lambda state: False
|
multiworld.get_location(location, player).access_rule = lambda state: False
|
||||||
else:
|
else:
|
||||||
# There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing
|
# There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing
|
||||||
# advancement items so that useful items can be placed.
|
# advancement items so that useful items can be placed
|
||||||
for player in no_enemies_players:
|
for player in no_enemies_players:
|
||||||
for location in vendor_locations:
|
for location in vendor_locations:
|
||||||
multiworld.get_location(location, player).item_rule = lambda item: not item.advancement
|
multiworld.get_location(location, player).item_rule = lambda item: not item.advancement
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FFMQLocation(Location):
|
class FFMQLocation(Location):
|
||||||
game = "Final Fantasy Mystic Quest"
|
game = "Final Fantasy Mystic Quest"
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from .Regions import create_regions, location_table, set_rules, stage_set_rules,
|
|||||||
non_dead_end_crest_warps
|
non_dead_end_crest_warps
|
||||||
from .Items import item_table, item_groups, create_items, FFMQItem, fillers
|
from .Items import item_table, item_groups, create_items, FFMQItem, fillers
|
||||||
from .Output import generate_output
|
from .Output import generate_output
|
||||||
from .Options import FFMQOptions
|
from .Options import option_definitions
|
||||||
from .Client import FFMQClient
|
from .Client import FFMQClient
|
||||||
|
|
||||||
|
|
||||||
@@ -25,25 +25,14 @@ from .Client import FFMQClient
|
|||||||
|
|
||||||
|
|
||||||
class FFMQWebWorld(WebWorld):
|
class FFMQWebWorld(WebWorld):
|
||||||
setup_en = Tutorial(
|
tutorials = [Tutorial(
|
||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
"A guide to playing Final Fantasy Mystic Quest with Archipelago.",
|
"A guide to playing Final Fantasy Mystic Quest with Archipelago.",
|
||||||
"English",
|
"English",
|
||||||
"setup_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["Alchav"]
|
["Alchav"]
|
||||||
)
|
)]
|
||||||
|
|
||||||
setup_fr = Tutorial(
|
|
||||||
setup_en.tutorial_name,
|
|
||||||
setup_en.description,
|
|
||||||
"Français",
|
|
||||||
"setup_fr.md",
|
|
||||||
"setup/fr",
|
|
||||||
["Artea"]
|
|
||||||
)
|
|
||||||
|
|
||||||
tutorials = [setup_en, setup_fr]
|
|
||||||
|
|
||||||
|
|
||||||
class FFMQWorld(World):
|
class FFMQWorld(World):
|
||||||
@@ -56,8 +45,7 @@ class FFMQWorld(World):
|
|||||||
|
|
||||||
item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None}
|
item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None}
|
||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
options_dataclass = FFMQOptions
|
option_definitions = option_definitions
|
||||||
options: FFMQOptions
|
|
||||||
|
|
||||||
topology_present = True
|
topology_present = True
|
||||||
|
|
||||||
@@ -79,14 +67,20 @@ class FFMQWorld(World):
|
|||||||
super().__init__(world, player)
|
super().__init__(world, player)
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||||
self.options.brown_boxes.value = 1
|
self.multiworld.brown_boxes[self.player].value = 1
|
||||||
if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value:
|
if self.multiworld.enemies_scaling_lower[self.player].value > \
|
||||||
self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \
|
self.multiworld.enemies_scaling_upper[self.player].value:
|
||||||
self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value
|
(self.multiworld.enemies_scaling_lower[self.player].value,
|
||||||
if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value:
|
self.multiworld.enemies_scaling_upper[self.player].value) =\
|
||||||
self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \
|
(self.multiworld.enemies_scaling_upper[self.player].value,
|
||||||
self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value
|
self.multiworld.enemies_scaling_lower[self.player].value)
|
||||||
|
if self.multiworld.bosses_scaling_lower[self.player].value > \
|
||||||
|
self.multiworld.bosses_scaling_upper[self.player].value:
|
||||||
|
(self.multiworld.bosses_scaling_lower[self.player].value,
|
||||||
|
self.multiworld.bosses_scaling_upper[self.player].value) =\
|
||||||
|
(self.multiworld.bosses_scaling_upper[self.player].value,
|
||||||
|
self.multiworld.bosses_scaling_lower[self.player].value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_generate_early(cls, multiworld):
|
def stage_generate_early(cls, multiworld):
|
||||||
@@ -100,20 +94,20 @@ class FFMQWorld(World):
|
|||||||
rooms_data = {}
|
rooms_data = {}
|
||||||
|
|
||||||
for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"):
|
for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"):
|
||||||
if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards
|
if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or
|
||||||
or world.options.companions_locations):
|
world.multiworld.crest_shuffle[world.player]):
|
||||||
if world.options.map_shuffle_seed.value.isdigit():
|
if world.multiworld.map_shuffle_seed[world.player].value.isdigit():
|
||||||
multiworld.random.seed(int(world.options.map_shuffle_seed.value))
|
multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value))
|
||||||
elif world.options.map_shuffle_seed.value != "random":
|
elif world.multiworld.map_shuffle_seed[world.player].value != "random":
|
||||||
multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value))
|
multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value))
|
||||||
+ int(world.multiworld.seed))
|
+ int(world.multiworld.seed))
|
||||||
|
|
||||||
seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()
|
seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()
|
||||||
map_shuffle = world.options.map_shuffle.value
|
map_shuffle = multiworld.map_shuffle[world.player].value
|
||||||
crest_shuffle = world.options.crest_shuffle.current_key
|
crest_shuffle = multiworld.crest_shuffle[world.player].current_key
|
||||||
battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key
|
battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key
|
||||||
companion_shuffle = world.options.companions_locations.value
|
companion_shuffle = multiworld.companions_locations[world.player].value
|
||||||
kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key
|
kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key
|
||||||
|
|
||||||
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
|
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
|
||||||
|
|
||||||
@@ -181,14 +175,14 @@ class FFMQWorld(World):
|
|||||||
|
|
||||||
def extend_hint_information(self, hint_data):
|
def extend_hint_information(self, hint_data):
|
||||||
hint_data[self.player] = {}
|
hint_data[self.player] = {}
|
||||||
if self.options.map_shuffle:
|
if self.multiworld.map_shuffle[self.player]:
|
||||||
single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"]
|
single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"]
|
||||||
for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg",
|
for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg",
|
||||||
"Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship",
|
"Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship",
|
||||||
"Subregion Doom Castle"]:
|
"Subregion Doom Castle"]:
|
||||||
region = self.multiworld.get_region(subregion, self.player)
|
region = self.multiworld.get_region(subregion, self.player)
|
||||||
for location in region.locations:
|
for location in region.locations:
|
||||||
if location.address and self.options.map_shuffle != "dungeons":
|
if location.address and self.multiworld.map_shuffle[self.player] != "dungeons":
|
||||||
hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1]
|
hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1]
|
||||||
+ (" Region" if subregion not in
|
+ (" Region" if subregion not in
|
||||||
single_location_regions else ""))
|
single_location_regions else ""))
|
||||||
@@ -208,13 +202,14 @@ class FFMQWorld(World):
|
|||||||
for location in exit_check.connected_region.locations:
|
for location in exit_check.connected_region.locations:
|
||||||
if location.address:
|
if location.address:
|
||||||
hint = []
|
hint = []
|
||||||
if self.options.map_shuffle != "dungeons":
|
if self.multiworld.map_shuffle[self.player] != "dungeons":
|
||||||
hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not
|
hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not
|
||||||
in single_location_regions else "")))
|
in single_location_regions else "")))
|
||||||
if self.options.map_shuffle != "overworld":
|
if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \
|
||||||
|
("Subregion Mac's Ship", "Subregion Doom Castle"):
|
||||||
hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu",
|
hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu",
|
||||||
"Pazuzu's"))
|
"Pazuzu's"))
|
||||||
hint = " - ".join(hint).replace(" - Mac Ship", "")
|
hint = " - ".join(hint)
|
||||||
if location.address in hint_data[self.player]:
|
if location.address in hint_data[self.player]:
|
||||||
hint_data[self.player][location.address] += f"/{hint}"
|
hint_data[self.player][location.address] += f"/{hint}"
|
||||||
else:
|
else:
|
||||||
|
|||||||
2450
worlds/ffmq/data/entrances.yaml
Normal file
2450
worlds/ffmq/data/entrances.yaml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
4026
worlds/ffmq/data/rooms.yaml
Normal file
4026
worlds/ffmq/data/rooms.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,5 @@
|
|||||||
# Final Fantasy Mystic Quest
|
# Final Fantasy Mystic Quest
|
||||||
|
|
||||||
## Game page in other languages:
|
|
||||||
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
|
|
||||||
|
|
||||||
## Where is the options page?
|
## Where is the options page?
|
||||||
|
|
||||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
# Final Fantasy Mystic Quest
|
|
||||||
|
|
||||||
## Page d'info dans d'autres langues :
|
|
||||||
* [English](/games/Final%20Fantasy%20Mystic%20Quest/info/en)
|
|
||||||
|
|
||||||
## Où se situe la page d'options?
|
|
||||||
|
|
||||||
La [page de configuration](../player-options) contient toutes les options nécessaires pour créer un fichier de configuration.
|
|
||||||
|
|
||||||
## Qu'est-ce qui est rendu aléatoire dans ce jeu?
|
|
||||||
|
|
||||||
Outre les objets mélangés, il y a plusieurs options pour aussi mélanger les villes et donjons, les pièces dans les donjons, les téléporteurs et les champs de bataille.
|
|
||||||
Il y a aussi plusieurs autres options afin d'ajuster la difficulté du jeu et la vitesse d'une partie.
|
|
||||||
|
|
||||||
## Quels objets et emplacements sont mélangés?
|
|
||||||
|
|
||||||
Les objets normalement reçus des coffres rouges, des PNJ et des champs de bataille sont mélangés. Vous pouvez aussi
|
|
||||||
inclure les objets des coffres bruns (qui contiennent normalement des consommables) dans les objets mélangés.
|
|
||||||
|
|
||||||
## Quels objets peuvent être dans les mondes des autres joueurs?
|
|
||||||
|
|
||||||
Tous les objets qui ont été déterminés mélangés dans les options peuvent être placés dans d'autres mondes.
|
|
||||||
|
|
||||||
## À quoi ressemblent les objets des autres joueurs dans Final Fantasy Mystic Quest?
|
|
||||||
|
|
||||||
Les emplacements qui étaient à l'origine des coffres (rouges ou bruns si ceux-ci sont inclus) apparaîtront comme des coffres.
|
|
||||||
Les coffres rouges seront des objets utiles ou de progression, alors que les coffres bruns seront des objets de remplissage.
|
|
||||||
Les pièges peuvent apparaître comme des coffres rouges ou bruns.
|
|
||||||
Lorsque vous ouvrirez un coffre contenant un objet d'un autre joueur, vous recevrez l'icône d'Archipelago et
|
|
||||||
la boîte de dialogue vous indiquera avoir reçu un "Archipelago Item".
|
|
||||||
|
|
||||||
|
|
||||||
## Lorsqu'un joueur reçoit un objet, qu'arrive-t-il?
|
|
||||||
|
|
||||||
Une boîte de dialogue apparaîtra pour vous montrer l'objet que vous avez reçu. Vous ne pourrez pas recevoir d'objet si vous êtes
|
|
||||||
en combat, dans la mappemonde ou dans les menus (à l'exception de lorsque vous fermez le menu).
|
|
||||||
@@ -17,12 +17,6 @@ The Archipelago community cannot supply you with this.
|
|||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
### Linux Setup
|
|
||||||
|
|
||||||
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. You'll likely be looking for the `.AppImage`.**
|
|
||||||
2. It is recommended to use either RetroArch or BizHawk if you run on linux, as snes9x-rr isn't compatible.
|
|
||||||
|
|
||||||
### Windows Setup
|
### Windows Setup
|
||||||
|
|
||||||
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
|
||||||
@@ -81,7 +75,8 @@ Manually launch the SNI Client, and run the patched ROM in your chosen software
|
|||||||
|
|
||||||
#### With an emulator
|
#### With an emulator
|
||||||
|
|
||||||
If this is the first time SNI launches, you may be prompted to allow it to communicate through the Windows Firewall.
|
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||||
|
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||||
|
|
||||||
##### snes9x-rr
|
##### snes9x-rr
|
||||||
|
|
||||||
@@ -138,10 +133,10 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor
|
|||||||
|
|
||||||
### Connect to the Archipelago Server
|
### Connect to the Archipelago Server
|
||||||
|
|
||||||
SNI serves as the interface between your emulator and the server. Since you launched it manually, you need to tell it what server to connect to.
|
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
|
||||||
If the server is hosted on Archipelago.gg, get the port the server hosts your game on at the top of the game room (last line before the worlds are listed).
|
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
|
||||||
In the SNI client, either type `/connect address` (where `address` is the address of the server, for example `/connect archipelago.gg:12345`), or type the address and port on the "Server" input field, then press `Connect`.
|
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
|
||||||
If the server is hosted locally, simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press `Connect`.
|
into the "Server" input field then press enter.
|
||||||
|
|
||||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user