mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 00:33:22 -07:00
Merge branch 'Marechal-L:main' into main
This commit is contained in:
@@ -1064,26 +1064,25 @@ class LocationProgressType(IntEnum):
|
||||
|
||||
|
||||
class Location:
|
||||
# If given as integer, then this is the shop's inventory index
|
||||
shop_slot: Optional[int] = None
|
||||
shop_slot_disabled: bool = False
|
||||
game: str = "Generic"
|
||||
player: int
|
||||
name: str
|
||||
address: Optional[int]
|
||||
parent_region: Optional[Region]
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
game: str = "Generic"
|
||||
show_in_spoiler: bool = True
|
||||
crystal: bool = False
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
access_rule = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
parent_region: Optional[Region]
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
||||
self.name: str = name
|
||||
self.address: Optional[int] = address
|
||||
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||
self.player = player
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.parent_region = parent
|
||||
self.player: int = player
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
|
||||
@@ -562,18 +562,21 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
f" for each location checked. Use !hint for more information.")
|
||||
ctx.hint_cost = int(args['hint_cost'])
|
||||
ctx.check_points = int(args['location_check_points'])
|
||||
players = args.get("players", [])
|
||||
if len(players) < 1:
|
||||
logger.info('No player connected')
|
||||
else:
|
||||
players.sort()
|
||||
current_team = -1
|
||||
logger.info('Connected Players:')
|
||||
for network_player in players:
|
||||
if network_player.team != current_team:
|
||||
logger.info(f' Team #{network_player.team + 1}')
|
||||
current_team = network_player.team
|
||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||
|
||||
if "players" in args: # TODO remove when servers sending this are outdated
|
||||
players = args.get("players", [])
|
||||
if len(players) < 1:
|
||||
logger.info('No player connected')
|
||||
else:
|
||||
players.sort()
|
||||
current_team = -1
|
||||
logger.info('Connected Players:')
|
||||
for network_player in players:
|
||||
if network_player.team != current_team:
|
||||
logger.info(f' Team #{network_player.team + 1}')
|
||||
current_team = network_player.team
|
||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||
|
||||
# update datapackage
|
||||
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
||||
|
||||
@@ -723,7 +726,7 @@ if __name__ == '__main__':
|
||||
class TextContext(CommonContext):
|
||||
tags = {"AP", "IgnoreGame", "TextOnly"}
|
||||
game = "" # empty matches any game since 0.3.2
|
||||
items_handling = 0 # don't receive any NetworkItems
|
||||
items_handling = 0b111 # receive all items for /received
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
|
||||
54
Generate.py
54
Generate.py
@@ -7,7 +7,7 @@ import urllib.request
|
||||
import urllib.parse
|
||||
from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||
import os
|
||||
from collections import Counter
|
||||
from collections import Counter, ChainMap
|
||||
import string
|
||||
import enum
|
||||
|
||||
@@ -133,12 +133,14 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
||||
try:
|
||||
weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path)
|
||||
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
||||
meta_weights = weights_cache[args.meta_file_path][-1]
|
||||
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
||||
del(meta_weights["meta_description"])
|
||||
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
||||
del(meta_weights["meta_description"])
|
||||
except Exception as e:
|
||||
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
|
||||
if args.samesettings:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
else:
|
||||
@@ -164,7 +166,7 @@ def main(args=None, callback=ERmain):
|
||||
player_files[player_id] = filename
|
||||
player_id += 1
|
||||
|
||||
args.multi = max(player_id-1, args.multi)
|
||||
args.multi = max(player_id - 1, args.multi)
|
||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
||||
f"{args.plando}")
|
||||
|
||||
@@ -186,26 +188,28 @@ def main(args=None, callback=ERmain):
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
player_path_cache = {}
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
if meta_weights:
|
||||
for category_name, category_dict in meta_weights.items():
|
||||
for key in category_dict:
|
||||
option = get_choice(key, category_dict)
|
||||
option = roll_meta_option(key, category_name, category_dict)
|
||||
if option is not None:
|
||||
for player, path in player_path_cache.items():
|
||||
for path in weights_cache:
|
||||
for yaml in weights_cache[path]:
|
||||
if category_name is None:
|
||||
yaml[key] = option
|
||||
for category in yaml:
|
||||
if category in AutoWorldRegister.world_types and key in Options.common_options:
|
||||
yaml[category][key] = option
|
||||
elif category_name not in yaml:
|
||||
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
||||
else:
|
||||
yaml[category_name][key] = option
|
||||
yaml[category_name][key] = option
|
||||
|
||||
player_path_cache = {}
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||
name_counter = Counter()
|
||||
erargs.player_settings = {}
|
||||
|
||||
@@ -387,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
||||
return weights
|
||||
|
||||
|
||||
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
|
||||
if not game:
|
||||
return get_choice(option_key, category_dict)
|
||||
if game in AutoWorldRegister.world_types:
|
||||
game_world = AutoWorldRegister.world_types[game]
|
||||
options = ChainMap(game_world.options, Options.per_game_common_options)
|
||||
if option_key in options:
|
||||
if options[option_key].supports_weighting:
|
||||
return get_choice(option_key, category_dict)
|
||||
return options[option_key]
|
||||
if game == "A Link to the Past": # TODO wow i hate this
|
||||
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
|
||||
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
|
||||
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
|
||||
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
|
||||
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
|
||||
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
|
||||
"random_sprite_on_event"}:
|
||||
return get_choice(option_key, category_dict)
|
||||
raise Exception(f"Error generating meta option {option_key} for {game}.")
|
||||
|
||||
|
||||
def roll_linked_options(weights: dict) -> dict:
|
||||
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||
for option_set in weights["linked_options"]:
|
||||
|
||||
2
Main.py
2
Main.py
@@ -423,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
logger.info(f'Creating final archive at {zipfilename}.')
|
||||
logger.info(f"Creating final archive at {zipfilename}")
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for file in os.scandir(temp_dir):
|
||||
|
||||
171
MultiServer.py
171
MultiServer.py
@@ -30,13 +30,8 @@ except ImportError:
|
||||
OperationalError = ConnectionError
|
||||
|
||||
import NetUtils
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
version_tuple, restricted_loads, Version
|
||||
from Utils import version_tuple, restricted_loads, Version
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType
|
||||
|
||||
@@ -126,6 +121,11 @@ class Context:
|
||||
stored_data: typing.Dict[str, object]
|
||||
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
||||
|
||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
forced_auto_forfeits: typing.Dict[str, bool]
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||
@@ -190,8 +190,43 @@ class Context:
|
||||
self.stored_data = {}
|
||||
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
||||
|
||||
# General networking
|
||||
# init empty to satisfy linter, I suppose
|
||||
self.gamespackage = {}
|
||||
self.item_name_groups = {}
|
||||
self.all_item_and_group_names = {}
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
|
||||
self.non_hintable_names = {}
|
||||
|
||||
self._load_game_data()
|
||||
self._init_game_data()
|
||||
|
||||
# Datapackage retrieval
|
||||
def _load_game_data(self):
|
||||
import worlds
|
||||
self.gamespackage = worlds.network_data_package["games"]
|
||||
|
||||
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()}
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
|
||||
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||
|
||||
def _init_game_data(self):
|
||||
for game_name, game_package in self.gamespackage.items():
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
self.item_names[item_id] = item_name
|
||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||
self.location_names[location_id] = location_name
|
||||
self.all_item_and_group_names[game_name] = \
|
||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||
|
||||
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||
return self.gamespackage[game]["item_name_to_id"]
|
||||
|
||||
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||
return self.gamespackage[game]["location_name_to_id"]
|
||||
|
||||
# General networking
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
@@ -544,12 +579,12 @@ class Context:
|
||||
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
||||
f' has completed their goal.'
|
||||
self.notify_all(finished_msg)
|
||||
if "auto" in self.forfeit_mode:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
if "auto" in self.collect_mode:
|
||||
collect_player(self, client.team, client.slot)
|
||||
if "auto" in self.forfeit_mode:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
elif self.forced_auto_forfeits[self.games[client.slot]]:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||
@@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'permissions': get_permissions(ctx),
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_version': network_data_package["version"],
|
||||
'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values())
|
||||
if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0,
|
||||
'datapackage_versions': {game: game_data["version"] for game, game_data
|
||||
in network_data_package["games"].items()},
|
||||
in ctx.gamespackage.items()},
|
||||
'seed_name': ctx.seed_name,
|
||||
'time': time.time(),
|
||||
}])
|
||||
@@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
send_items_to(ctx, team, target_player, new_item)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
||||
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
||||
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
|
||||
@@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
ctx.save()
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
|
||||
hints = []
|
||||
slots: typing.Set[int] = {slot}
|
||||
for group_id, group in ctx.groups.items():
|
||||
if slot in group:
|
||||
slots.add(group_id)
|
||||
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
|
||||
|
||||
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
|
||||
for finding_player, check_data in ctx.locations.items():
|
||||
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||
if receiving_player in slots and item_id == seeked_item_id:
|
||||
@@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
|
||||
|
||||
|
||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
|
||||
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
|
||||
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
||||
|
||||
|
||||
@@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
|
||||
|
||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{lookup_any_item_id_to_name[hint.item]} is " \
|
||||
f"at {get_location_name_from_id(hint.location)} " \
|
||||
f"{ctx.item_names[hint.item]} is " \
|
||||
f"at {ctx.location_names[hint.location]} " \
|
||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||
|
||||
if hint.entrance:
|
||||
@@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
elif "disabled" in self.ctx.forfeit_mode:
|
||||
self.output(
|
||||
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
|
||||
self.output("Sorry, client item releasing has been disabled on this server. "
|
||||
"You can ask the server admin for a /release")
|
||||
return False
|
||||
else: # is auto or goal
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
@@ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if self.ctx.remaining_mode == "enabled":
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if remaining_item_ids:
|
||||
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
|
||||
for item_id in remaining_item_ids))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
@@ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if remaining_item_ids:
|
||||
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
|
||||
for item_id in remaining_item_ids))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
@@ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
if locations:
|
||||
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
|
||||
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
|
||||
texts.append(f"Found {len(locations)} missing location checks")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
@@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
if locations:
|
||||
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations]
|
||||
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
||||
texts.append(f"Found {len(locations)} done location checks")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
@@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
def _cmd_getitem(self, item_name: str) -> bool:
|
||||
"""Cheat in an item, if it is enabled on this server"""
|
||||
if self.ctx.item_cheat:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
item_name, usable, response = get_intended_text(item_name,
|
||||
world.item_names)
|
||||
names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
|
||||
item_name, usable, response = get_intended_text(
|
||||
item_name,
|
||||
names
|
||||
)
|
||||
if usable:
|
||||
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot)
|
||||
new_item = NetworkItem(names[item_name], -1, self.client.slot)
|
||||
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
|
||||
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
||||
self.ctx.notify_all(
|
||||
@@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
f"You have {points_available} points.")
|
||||
return True
|
||||
else:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
names = world.location_names if for_location else world.all_item_and_group_names
|
||||
game = self.ctx.games[self.client.slot]
|
||||
names = self.ctx.location_names_for_game(game) \
|
||||
if for_location else \
|
||||
self.ctx.all_item_and_group_names[game]
|
||||
hint_name, usable, response = get_intended_text(input_text,
|
||||
names)
|
||||
if usable:
|
||||
if hint_name in world.hint_blacklist:
|
||||
if hint_name in self.ctx.non_hintable_names[game]:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif not for_location and hint_name in world.item_name_groups: # item group name
|
||||
elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name
|
||||
hints = []
|
||||
for item in world.item_name_groups[hint_name]:
|
||||
if item in world.item_name_to_id: # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||
elif not for_location and hint_name in world.item_names: # item name
|
||||
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
else: # location name
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
@@ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item: str = "") -> bool:
|
||||
def _cmd_hint(self, item_name: str = "") -> bool:
|
||||
"""Use !hint {item_name},
|
||||
for example !hint Lamp to get a spoiler peek for that item.
|
||||
If hint costs are on, this will only give you one new result,
|
||||
you can rerun the command to get more in that case."""
|
||||
return self.get_hints(item)
|
||||
return self.get_hints(item_name)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint_location(self, location: str = "") -> bool:
|
||||
@@ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
elif cmd == "GetDataPackage":
|
||||
exclusions = args.get("exclusions", [])
|
||||
if "games" in args:
|
||||
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
||||
games = {name: game_data for name, game_data in ctx.gamespackage.items()
|
||||
if name in set(args.get("games", []))}
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": {"games": games}}])
|
||||
# TODO: remove exclusions behaviour around 0.5.0
|
||||
elif exclusions:
|
||||
exclusions = set(exclusions)
|
||||
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
||||
games = {name: game_data for name, game_data in ctx.gamespackage.items()
|
||||
if name not in exclusions}
|
||||
package = network_data_package.copy()
|
||||
package["games"] = games
|
||||
|
||||
package = {"games": games}
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": package}])
|
||||
|
||||
else:
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": network_data_package}])
|
||||
"data": {"games": ctx.gamespackage}}])
|
||||
|
||||
elif client.auth:
|
||||
if cmd == "ConnectUpdate":
|
||||
@@ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
create_as_hint: int = int(args.get("create_as_hint", 0))
|
||||
hints = []
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||
if type(location) is not int:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||
"original_cmd": cmd}])
|
||||
@@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item = " ".join(item_name)
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.item_names)
|
||||
item_name = " ".join(item_name)
|
||||
names = self.ctx.item_names_for_game(self.ctx.games[slot])
|
||||
item_name, usable, response = get_intended_text(item_name, names)
|
||||
if usable:
|
||||
amount: int = int(amount)
|
||||
new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))]
|
||||
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
|
||||
send_items_to(self.ctx, team, slot, *new_items)
|
||||
|
||||
send_new_items(self.ctx)
|
||||
self.ctx.notify_all(
|
||||
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
||||
f'"{item}" to {self.ctx.get_aliased_name(team, slot)}')
|
||||
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
|
||||
return True
|
||||
else:
|
||||
self.output(response)
|
||||
@@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
"""Sends an item to the specified player"""
|
||||
return self._cmd_send_multiple(1, player_name, *item_name)
|
||||
|
||||
def _cmd_hint(self, player_name: str, *item: str) -> bool:
|
||||
def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
|
||||
"""Send out a hint for a player's item to their team"""
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item = " ".join(item)
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
|
||||
item_name = " ".join(item_name)
|
||||
game = self.ctx.games[slot]
|
||||
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
|
||||
if usable:
|
||||
if item in world.item_name_groups:
|
||||
if item_name in self.ctx.item_name_groups[game]:
|
||||
hints = []
|
||||
for item in world.item_name_groups[item]:
|
||||
if item in world.item_name_to_id: # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item))
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
|
||||
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||
else: # item name
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
hints = collect_hints(self.ctx, team, slot, item_name)
|
||||
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
@@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
def _cmd_hint_location(self, player_name: str, *location: str) -> bool:
|
||||
def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:
|
||||
"""Send out a hint for a player's location to their team"""
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item = " ".join(location)
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.location_names)
|
||||
location_name = " ".join(location_name)
|
||||
location_name, usable, response = get_intended_text(location_name,
|
||||
self.ctx.location_names_for_game(self.ctx.games[slot]))
|
||||
if usable:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, item)
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
else:
|
||||
|
||||
@@ -44,11 +44,38 @@ nest_asyncio.apply()
|
||||
class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
ctx: SC2Context
|
||||
|
||||
def _cmd_difficulty(self, difficulty: str = "") -> bool:
|
||||
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
|
||||
options = difficulty.split()
|
||||
num_options = len(options)
|
||||
difficulty_choice = options[0].lower()
|
||||
|
||||
if num_options > 0:
|
||||
if difficulty_choice == "casual":
|
||||
self.ctx.difficulty_override = 0
|
||||
elif difficulty_choice == "normal":
|
||||
self.ctx.difficulty_override = 1
|
||||
elif difficulty_choice == "hard":
|
||||
self.ctx.difficulty_override = 2
|
||||
elif difficulty_choice == "brutal":
|
||||
self.ctx.difficulty_override = 3
|
||||
else:
|
||||
self.output("Unable to parse difficulty '" + options[0] + "'")
|
||||
return False
|
||||
|
||||
self.output("Difficulty set to " + options[0])
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Difficulty needs to be specified in the command.")
|
||||
return False
|
||||
|
||||
def _cmd_disable_mission_check(self) -> bool:
|
||||
"""Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
|
||||
the next mission in a chain the other player is doing."""
|
||||
self.ctx.missions_unlocked = True
|
||||
sc2_logger.info("Mission check has been disabled")
|
||||
return True
|
||||
|
||||
def _cmd_play(self, mission_id: str = "") -> bool:
|
||||
"""Start a Starcraft 2 mission"""
|
||||
@@ -64,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
else:
|
||||
sc2_logger.info(
|
||||
"Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -108,6 +136,7 @@ class SC2Context(CommonContext):
|
||||
missions_unlocked = False
|
||||
current_tooltip = None
|
||||
last_loc_list = None
|
||||
difficulty_override = -1
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -470,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
game_state = 0
|
||||
if iteration == 0:
|
||||
start_items = calculate_items(self.ctx.items_received)
|
||||
difficulty = calc_difficulty(self.ctx.difficulty)
|
||||
if self.ctx.difficulty_override >= 0:
|
||||
difficulty = calc_difficulty(self.ctx.difficulty_override)
|
||||
else:
|
||||
difficulty = calc_difficulty(self.ctx.difficulty)
|
||||
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
|
||||
difficulty,
|
||||
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
|
||||
|
||||
21
Utils.py
21
Utils.py
@@ -328,16 +328,6 @@ def get_options() -> dict:
|
||||
return get_options.options
|
||||
|
||||
|
||||
def get_item_name_from_id(code: int) -> str:
|
||||
from worlds import lookup_any_item_id_to_name
|
||||
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
|
||||
|
||||
|
||||
def get_location_name_from_id(code: int) -> str:
|
||||
from worlds import lookup_any_location_id_to_name
|
||||
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
|
||||
|
||||
|
||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||
path = user_path("_persistent_storage.yaml")
|
||||
storage: dict = persistent_load()
|
||||
@@ -613,3 +603,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
root.withdraw()
|
||||
showerror(title, text) if error else showinfo(title, text)
|
||||
root.update()
|
||||
|
||||
|
||||
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||
def sorter(element: str) -> str:
|
||||
parts = element.split(maxsplit=1)
|
||||
if parts[0].lower() in ignore:
|
||||
return parts[1]
|
||||
else:
|
||||
return element
|
||||
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))
|
||||
|
||||
@@ -14,7 +14,7 @@ import Utils
|
||||
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
|
||||
from WebHostLib import app as raw_app
|
||||
from WebHostLib import register, app as raw_app
|
||||
from waitress import serve
|
||||
|
||||
from WebHostLib.models import db
|
||||
@@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen
|
||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||
from WebHostLib.options import create as create_options_files
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||
|
||||
|
||||
def get_app():
|
||||
register()
|
||||
app = raw_app
|
||||
if os.path.exists(configpath):
|
||||
import yaml
|
||||
@@ -43,6 +42,7 @@ def get_app():
|
||||
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
import json
|
||||
import shutil
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
worlds = {}
|
||||
data = []
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
@@ -85,7 +85,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
||||
for games in data:
|
||||
if 'Archipelago' in games['gameTitle']:
|
||||
generic_data = data.pop(data.index(games))
|
||||
sorted_data = [generic_data] + sorted(data, key=lambda entry: entry["gameTitle"].lower())
|
||||
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower())
|
||||
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
|
||||
return sorted_data
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import uuid
|
||||
import base64
|
||||
import socket
|
||||
|
||||
import jinja2.exceptions
|
||||
from pony.flask import Pony
|
||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||
from flask import Flask
|
||||
from flask_caching import Cache
|
||||
from flask_compress import Compress
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from Utils import title_sorted
|
||||
from .models import *
|
||||
|
||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||
@@ -53,8 +53,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||
cache = Cache(app)
|
||||
Compress(app)
|
||||
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
|
||||
class B64UUIDConverter(BaseConverter):
|
||||
|
||||
@@ -68,174 +66,18 @@ class B64UUIDConverter(BaseConverter):
|
||||
# short UUID
|
||||
app.url_map.converters["suuid"] = B64UUIDConverter
|
||||
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
||||
|
||||
# has automatic patch integration
|
||||
import Patch
|
||||
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
|
||||
app.jinja_env.filters["title_sorted"] = title_sorted
|
||||
|
||||
|
||||
def get_world_theme(game_name: str):
|
||||
if game_name in AutoWorldRegister.world_types:
|
||||
return AutoWorldRegister.world_types[game_name].web.theme
|
||||
return 'grass'
|
||||
def register():
|
||||
"""Import submodules, triggering their registering on flask routing.
|
||||
Note: initializes worlds subsystem."""
|
||||
# has automatic patch integration
|
||||
import Patch
|
||||
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
# to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||
def page_not_found(err):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
# Start Playing Page
|
||||
@app.route('/start-playing')
|
||||
def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def weighted_settings():
|
||||
return render_template(f"weighted-settings.html")
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
def game_info(game, lang):
|
||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# List of supported games
|
||||
@app.route('/games')
|
||||
def games():
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world
|
||||
return render_template("supportedGames.html", worlds=worlds)
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
def tutorial(game, file, lang):
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
@app.route('/tutorial/')
|
||||
def tutorial_landing():
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/faq/<string:lang>/')
|
||||
def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/glossary/<string:lang>/')
|
||||
def terms(lang):
|
||||
return render_template("glossary.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
def view_seed(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
|
||||
|
||||
|
||||
@app.route('/new_room/<suuid:seed>')
|
||||
def new_room(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||
commit()
|
||||
return redirect(url_for("host_room", room=room.id))
|
||||
|
||||
|
||||
def _read_log(path: str):
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding="utf-8-sig") as log:
|
||||
yield from log
|
||||
else:
|
||||
yield f"Logfile {path} does not exist. " \
|
||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
||||
|
||||
|
||||
@app.route('/log/<suuid:room>')
|
||||
def display_log(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
if room.owner == session["_id"]:
|
||||
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||
return "Access Denied", 403
|
||||
|
||||
|
||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||
def host_room(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
if request.method == "POST":
|
||||
if room.owner == session["_id"]:
|
||||
cmd = request.form["cmd"]
|
||||
if cmd:
|
||||
Command(room=room, commandtext=cmd)
|
||||
commit()
|
||||
|
||||
with db_session:
|
||||
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
||||
|
||||
return render_template("hostRoom.html", room=room)
|
||||
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
|
||||
@app.route('/discord')
|
||||
def discord():
|
||||
return redirect("https://discord.gg/archipelago")
|
||||
|
||||
|
||||
@app.route('/datapackage')
|
||||
@cache.cached()
|
||||
def get_datapackge():
|
||||
"""A pretty print version of /api/datapackage"""
|
||||
from worlds import network_data_package
|
||||
import json
|
||||
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
|
||||
|
||||
|
||||
@app.route('/index')
|
||||
@app.route('/sitemap')
|
||||
def get_sitemap():
|
||||
available_games = []
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
available_games.append(game)
|
||||
return render_template("siteMap.html", games=available_games)
|
||||
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it
|
||||
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -32,14 +32,14 @@ def room_info(room: UUID):
|
||||
|
||||
@api_endpoints.route('/datapackage')
|
||||
@cache.cached()
|
||||
def get_datapackge():
|
||||
def get_datapackage():
|
||||
from worlds import network_data_package
|
||||
return network_data_package
|
||||
|
||||
|
||||
@api_endpoints.route('/datapackage_version')
|
||||
@cache.cached()
|
||||
def get_datapackge_versions():
|
||||
def get_datapackage_versions():
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
||||
version_package["version"] = network_data_package["version"]
|
||||
|
||||
@@ -184,7 +184,7 @@ class MultiworldInstance():
|
||||
|
||||
logging.info(f"Spinning up {self.room_id}")
|
||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||
args=(self.room_id, self.ponyconfig),
|
||||
args=(self.room_id, self.ponyconfig, get_static_server_data()),
|
||||
name="MultiHost")
|
||||
process.start()
|
||||
# bind after start to prevent thread sync issues with guardian.
|
||||
@@ -238,5 +238,5 @@ def run_guardian():
|
||||
|
||||
|
||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
||||
from .customserver import run_server_process
|
||||
from .customserver import run_server_process, get_static_server_data
|
||||
from .generate import gen_game
|
||||
|
||||
@@ -9,12 +9,13 @@ import time
|
||||
import random
|
||||
import pickle
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import Utils
|
||||
from .models import *
|
||||
from .models import db_session, Room, select, commit, Command, db
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
|
||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
|
||||
|
||||
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
@@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
import MultiServer
|
||||
|
||||
MultiServer.client_message_processor = CustomClientMessageProcessor
|
||||
del (MultiServer)
|
||||
del MultiServer
|
||||
|
||||
|
||||
class DBCommandProcessor(ServerCommandProcessor):
|
||||
@@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||
|
||||
|
||||
class WebHostContext(Context):
|
||||
def __init__(self):
|
||||
def __init__(self, static_server_data: dict):
|
||||
# static server data is used during _load_game_data to load required data,
|
||||
# without needing to import worlds system, which takes quite a bit of memory
|
||||
self.static_server_data = static_server_data
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
|
||||
del self.static_server_data
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
self.tags = ["AP", "WebHost"]
|
||||
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
@@ -107,14 +116,32 @@ def get_random_port():
|
||||
return random.randint(49152, 65535)
|
||||
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict):
|
||||
@cache_argsless
|
||||
def get_static_server_data() -> dict:
|
||||
import worlds
|
||||
data = {
|
||||
"forced_auto_forfeits": {},
|
||||
"non_hintable_names": {},
|
||||
"gamespackage": worlds.network_data_package["games"],
|
||||
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()},
|
||||
}
|
||||
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
|
||||
data["non_hintable_names"][world_name] = world.hint_blacklist
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
|
||||
# establish DB connection for multidata and multisave
|
||||
db.bind(**ponyconfig)
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
Utils.init_logging(str(room_id), write_mode="a")
|
||||
ctx = WebHostContext()
|
||||
ctx = WebHostContext(static_server_data)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
|
||||
|
||||
170
WebHostLib/misc.py
Normal file
170
WebHostLib/misc.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import jinja2.exceptions
|
||||
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||
|
||||
from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import app, cache
|
||||
|
||||
|
||||
def get_world_theme(game_name: str):
|
||||
if game_name in AutoWorldRegister.world_types:
|
||||
return AutoWorldRegister.world_types[game_name].web.theme
|
||||
return 'grass'
|
||||
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||
def page_not_found(err):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
# Start Playing Page
|
||||
@app.route('/start-playing')
|
||||
def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def weighted_settings():
|
||||
return render_template(f"weighted-settings.html")
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
def game_info(game, lang):
|
||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# List of supported games
|
||||
@app.route('/games')
|
||||
def games():
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world
|
||||
return render_template("supportedGames.html", worlds=worlds)
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
def tutorial(game, file, lang):
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
@app.route('/tutorial/')
|
||||
def tutorial_landing():
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/faq/<string:lang>/')
|
||||
def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/glossary/<string:lang>/')
|
||||
def terms(lang):
|
||||
return render_template("glossary.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
def view_seed(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
|
||||
|
||||
|
||||
@app.route('/new_room/<suuid:seed>')
|
||||
def new_room(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||
commit()
|
||||
return redirect(url_for("host_room", room=room.id))
|
||||
|
||||
|
||||
def _read_log(path: str):
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding="utf-8-sig") as log:
|
||||
yield from log
|
||||
else:
|
||||
yield f"Logfile {path} does not exist. " \
|
||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
||||
|
||||
|
||||
@app.route('/log/<suuid:room>')
|
||||
def display_log(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
if room.owner == session["_id"]:
|
||||
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||
return "Access Denied", 403
|
||||
|
||||
|
||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||
def host_room(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
if request.method == "POST":
|
||||
if room.owner == session["_id"]:
|
||||
cmd = request.form["cmd"]
|
||||
if cmd:
|
||||
Command(room=room, commandtext=cmd)
|
||||
commit()
|
||||
|
||||
with db_session:
|
||||
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
||||
|
||||
return render_template("hostRoom.html", room=room)
|
||||
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
|
||||
@app.route('/discord')
|
||||
def discord():
|
||||
return redirect("https://discord.gg/archipelago")
|
||||
|
||||
|
||||
@app.route('/datapackage')
|
||||
@cache.cached()
|
||||
def get_datapackage():
|
||||
"""A pretty print version of /api/datapackage"""
|
||||
from worlds import network_data_package
|
||||
import json
|
||||
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
|
||||
|
||||
|
||||
@app.route('/index')
|
||||
@app.route('/sitemap')
|
||||
def get_sitemap():
|
||||
available_games = []
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
available_games.append(game)
|
||||
return render_template("siteMap.html", games=available_games)
|
||||
@@ -110,7 +110,7 @@ def create():
|
||||
if option.default == "random":
|
||||
this_option["defaultValue"] = "random"
|
||||
|
||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
elif issubclass(option, Options.Range):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
@@ -121,7 +121,7 @@ def create():
|
||||
"max": option.range_end,
|
||||
}
|
||||
|
||||
if hasattr(option, "special_range_names"):
|
||||
if issubclass(option, Options.SpecialRange):
|
||||
game_options[option_name]["type"] = 'special_range'
|
||||
game_options[option_name]["value_names"] = {}
|
||||
for key, val in option.special_range_names.items():
|
||||
@@ -141,7 +141,7 @@ def create():
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
}
|
||||
|
||||
elif hasattr(option, "valid_keys"):
|
||||
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
||||
if option.valid_keys:
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="games" class="markdown">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game_name, world in worlds.items() | sort(attribute=0) %}
|
||||
{% for game_name in worlds | title_sorted %}
|
||||
{% set world = worlds[game_name] %}
|
||||
<h2>{{ game_name }}</h2>
|
||||
<p>
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
|
||||
@@ -11,7 +11,7 @@ from worlds.alttp import Items
|
||||
from WebHostLib import app, cache, Room
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
from MultiServer import get_item_name_from_id, Context
|
||||
from MultiServer import Context
|
||||
from NetUtils import SlotType
|
||||
|
||||
alttp_icons = {
|
||||
@@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||
|
||||
@@ -207,10 +207,10 @@ def ShopSlotFill(world):
|
||||
shops_per_sphere.append(current_shops_slots)
|
||||
candidates_per_sphere.append(current_candidates)
|
||||
for location in sphere:
|
||||
if location.shop_slot is not None:
|
||||
if isinstance(location, ALttPLocation) and location.shop_slot is not None:
|
||||
if not location.shop_slot_disabled:
|
||||
current_shops_slots.append(location)
|
||||
elif not location.locked and not location.item.name in blacklist_words:
|
||||
elif not location.locked and location.item.name not in blacklist_words:
|
||||
current_candidates.append(location)
|
||||
if cumu_weights:
|
||||
x = cumu_weights[-1]
|
||||
|
||||
@@ -6,14 +6,19 @@ from BaseClasses import Location, Item, ItemClassification
|
||||
|
||||
class ALttPLocation(Location):
|
||||
game: str = "A Link to the Past"
|
||||
crystal: bool
|
||||
player_address: Optional[int]
|
||||
_hint_text: Optional[str]
|
||||
shop_slot: Optional[int] = None
|
||||
"""If given as integer, shop_slot is the shop's inventory index."""
|
||||
shop_slot_disabled: bool = False
|
||||
|
||||
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
|
||||
hint_text: Optional[str] = None, parent=None,
|
||||
player_address=None):
|
||||
def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
|
||||
hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None):
|
||||
super(ALttPLocation, self).__init__(player, name, address, parent)
|
||||
self.crystal = crystal
|
||||
self.player_address = player_address
|
||||
self._hint_text: str = hint_text
|
||||
self._hint_text = hint_text
|
||||
|
||||
|
||||
class ALttPItem(Item):
|
||||
|
||||
@@ -14,8 +14,8 @@ from .Rules import set_rules
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Shops import create_shops, ShopSlotFill
|
||||
from .Dungeons import create_dungeons
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \
|
||||
get_base_rom_path, LttPDeltaPatch
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
import Patch
|
||||
from itertools import chain
|
||||
|
||||
@@ -156,6 +156,9 @@ class ALTTPWorld(World):
|
||||
player = self.player
|
||||
world = self.world
|
||||
|
||||
if self.use_enemizer():
|
||||
check_enemizer(world.enemizer)
|
||||
|
||||
# system for sharing ER layouts
|
||||
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
@@ -341,14 +344,19 @@ class ALTTPWorld(World):
|
||||
def stage_post_fill(cls, world):
|
||||
ShopSlotFill(world)
|
||||
|
||||
def use_enemizer(self):
|
||||
world = self.world
|
||||
player = self.player
|
||||
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
world = self.world
|
||||
player = self.player
|
||||
try:
|
||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
use_enemizer = self.use_enemizer()
|
||||
|
||||
rom = LocalRom(get_base_rom_path())
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
||||
files. Your patch file should have a `.apsm` extension.
|
||||
files. Your patch file should have a `.apdkc3` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
||||
client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
@@ -286,6 +286,12 @@ end)
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
local technology = event.research
|
||||
if string.find(technology.force.name, "EE_TESTFORCE") == 1 then
|
||||
--Don't acknowledge AP research as an Editor Extensions test force
|
||||
--Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces
|
||||
--are worked on exclusively in editor mode.
|
||||
return
|
||||
end
|
||||
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
||||
-- check if it came from the server anyway, then we don't need to double send.
|
||||
dumpInfo(technology.force) --is sendable
|
||||
|
||||
@@ -7,8 +7,7 @@ changes it up by allowing you to plan out certain aspects of the game by placing
|
||||
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
|
||||
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
|
||||
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
|
||||
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss
|
||||
plando.
|
||||
by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary.
|
||||
|
||||
### Enabling Plando
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ class MeritousWorld(World):
|
||||
item_name_groups = item_groups
|
||||
|
||||
data_version = 2
|
||||
forced_auto_forfeit = False
|
||||
|
||||
# NOTE: Remember to change this before this game goes live
|
||||
required_client_version = (0, 2, 4)
|
||||
|
||||
@@ -103,19 +103,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001,
|
||||
lambda state: state._sc2wol_has_air(world, player) or True),
|
||||
lambda state: True),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
LocationData("The Moebius Factor", "Beat The Moebius Factor", None,
|
||||
|
||||
@@ -37,7 +37,7 @@ class SC2WoLLogic(LogicMixin):
|
||||
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
|
||||
|
||||
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player)
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
|
||||
|
||||
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
|
||||
|
||||
@@ -35,9 +35,7 @@ class SM64World(World):
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 6
|
||||
required_client_version = (0,3,0)
|
||||
|
||||
forced_auto_forfeit = False
|
||||
required_client_version = (0, 3, 0)
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ class Config:
|
||||
Keysanity: bool = KeyShuffle != KeyShuffle.Null
|
||||
Race: bool = False
|
||||
GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals
|
||||
MinimalAccessibility: bool = False # AP specific accessibility: minimal
|
||||
|
||||
def __init__(self, options: Dict[str, str]):
|
||||
self.GameMode = self.ParseOption(options, GameMode.Multiworld)
|
||||
|
||||
@@ -14,8 +14,7 @@ class SwampPalace(Z3Region, IReward):
|
||||
self.Reward = RewardType.Null
|
||||
self.Locations = [
|
||||
Location(self, 256+135, 0x1EA9D, LocationType.Regular, "Swamp Palace - Entrance")
|
||||
.Allow(lambda item, items: self.Config.Keysanity or self.Config.MinimalAccessibility or
|
||||
item.Is(ItemType.KeySP, self.world)),
|
||||
.Allow(lambda item, items: self.Config.Keysanity or item.Is(ItemType.KeySP, self.world)),
|
||||
Location(self, 256+136, 0x1E986, LocationType.Regular, "Swamp Palace - Map Chest",
|
||||
lambda items: items.KeySP),
|
||||
Location(self, 256+137, 0x1E989, LocationType.Regular, "Swamp Palace - Big Chest",
|
||||
|
||||
@@ -190,7 +190,6 @@ class SMZ3World(World):
|
||||
config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value)
|
||||
config.Keysanity = config.KeyShuffle != KeyShuffle.Null
|
||||
config.GanonInvincible = GanonInvincible.BeforeCrystals
|
||||
config.MinimalAccessibility = self.world.accessibility[self.player] == Accessibility.option_minimal
|
||||
|
||||
self.local_random = random.Random(self.world.random.randint(0, 1000))
|
||||
self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name)
|
||||
@@ -525,6 +524,7 @@ class SMZ3World(World):
|
||||
|
||||
def InitialFillInOwnWorld(self):
|
||||
self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room"))
|
||||
self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance"))
|
||||
|
||||
# /* Check Swords option and place as needed */
|
||||
if self.smz3World.Config.SwordLocation == SwordLocation.Uncle:
|
||||
|
||||
@@ -22,6 +22,11 @@ class SpireWeb(WebWorld):
|
||||
|
||||
|
||||
class SpireWorld(World):
|
||||
"""
|
||||
A deck-building roguelike where you must craft a unique deck, encounter bizarre creatures, discover relics of
|
||||
immense power, and Slay the Spire!
|
||||
"""
|
||||
|
||||
options = spire_options
|
||||
game = "Slay the Spire"
|
||||
topology_present = False
|
||||
|
||||
@@ -35,7 +35,6 @@ class V6World(World):
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 1
|
||||
forced_auto_forfeit = False
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
area_cost_map: typing.Dict[int,int]
|
||||
|
||||
@@ -691,7 +691,7 @@ Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17
|
||||
158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares
|
||||
158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares
|
||||
|
||||
Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323:
|
||||
Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB:
|
||||
158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol
|
||||
158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol
|
||||
158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol
|
||||
@@ -707,8 +707,6 @@ Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323:
|
||||
158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol
|
||||
158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol
|
||||
158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol
|
||||
158611 - 0x17FA0 (Burnt House Discard) - 0x17DDB - Triangles
|
||||
Door - 0x0C323 (Door to Laser House) - 0x17DDB & 0x17DA2 & 0x2700B
|
||||
|
||||
Treehouse Green Bridge (Treehouse):
|
||||
158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers
|
||||
@@ -720,6 +718,12 @@ Treehouse Green Bridge (Treehouse):
|
||||
158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers
|
||||
158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles
|
||||
|
||||
Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323:
|
||||
Door - 0x0C323 (Door to Laser House) - 0x17DA2 & 0x2700B & 0x17DDB
|
||||
|
||||
Treehouse Laser Room Back Platform (Treehouse):
|
||||
158611 - 0x17FA0 (Burnt House Discard) - True - Triangles
|
||||
|
||||
Treehouse Laser Room (Treehouse):
|
||||
158712 - 0x03613 (Laser Panel) - True - True
|
||||
158403 - 0x17CBC (Laser House Door Timer Inside Control) - True - True
|
||||
|
||||
@@ -355,6 +355,7 @@ class WitnessPlayerLogic:
|
||||
"0x19B24": "Shadows Lower Avoid Patterns Visible",
|
||||
"0x2700B": "Open Door to Treehouse Laser House",
|
||||
"0x00055": "Orchard Apple Trees 4 Turns On",
|
||||
"0x17DDB": "Left Orange Bridge Fully Extended",
|
||||
}
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
||||
|
||||
Reference in New Issue
Block a user