WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
parent
181cc47079
commit
c1e9d0ab4f
165
MultiServer.py
165
MultiServer.py
|
@ -30,13 +30,8 @@ except ImportError:
|
||||||
OperationalError = ConnectionError
|
OperationalError = ConnectionError
|
||||||
|
|
||||||
import NetUtils
|
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
|
import Utils
|
||||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
from Utils import version_tuple, restricted_loads, Version
|
||||||
version_tuple, restricted_loads, Version
|
|
||||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||||
SlotType
|
SlotType
|
||||||
|
|
||||||
|
@ -126,6 +121,11 @@ class Context:
|
||||||
stored_data: typing.Dict[str, object]
|
stored_data: typing.Dict[str, object]
|
||||||
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
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,
|
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",
|
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,
|
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||||
|
@ -190,8 +190,43 @@ class Context:
|
||||||
self.stored_data = {}
|
self.stored_data = {}
|
||||||
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
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:
|
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||||
if not endpoint.socket or not endpoint.socket.open:
|
if not endpoint.socket or not endpoint.socket.open:
|
||||||
return False
|
return False
|
||||||
|
@ -546,7 +581,7 @@ class Context:
|
||||||
self.notify_all(finished_msg)
|
self.notify_all(finished_msg)
|
||||||
if "auto" in self.forfeit_mode:
|
if "auto" in self.forfeit_mode:
|
||||||
forfeit_player(self, client.team, client.slot)
|
forfeit_player(self, client.team, client.slot)
|
||||||
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
|
elif self.forced_auto_forfeits[self.games[client.slot]]:
|
||||||
forfeit_player(self, client.team, client.slot)
|
forfeit_player(self, client.team, client.slot)
|
||||||
if "auto" in self.collect_mode:
|
if "auto" in self.collect_mode:
|
||||||
collect_player(self, client.team, client.slot)
|
collect_player(self, client.team, client.slot)
|
||||||
|
@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||||
'permissions': get_permissions(ctx),
|
'permissions': get_permissions(ctx),
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points,
|
'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
|
'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,
|
'seed_name': ctx.seed_name,
|
||||||
'time': time.time(),
|
'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)
|
send_items_to(ctx, team, target_player, new_item)
|
||||||
|
|
||||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
||||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
||||||
info_text = json_format_send_event(new_item, target_player)
|
info_text = json_format_send_event(new_item, target_player)
|
||||||
ctx.broadcast_team(team, [info_text])
|
ctx.broadcast_team(team, [info_text])
|
||||||
|
|
||||||
|
@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||||
ctx.save()
|
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 = []
|
hints = []
|
||||||
slots: typing.Set[int] = {slot}
|
slots: typing.Set[int] = {slot}
|
||||||
for group_id, group in ctx.groups.items():
|
for group_id, group in ctx.groups.items():
|
||||||
if slot in group:
|
if slot in group:
|
||||||
slots.add(group_id)
|
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 finding_player, check_data in ctx.locations.items():
|
||||||
for location_id, (item_id, receiving_player, item_flags) in check_data.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:
|
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]:
|
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)
|
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:
|
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||||
f"{lookup_any_item_id_to_name[hint.item]} is " \
|
f"{ctx.item_names[hint.item]} is " \
|
||||||
f"at {get_location_name_from_id(hint.location)} " \
|
f"at {ctx.location_names[hint.location]} " \
|
||||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||||
|
|
||||||
if hint.entrance:
|
if hint.entrance:
|
||||||
|
@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||||
return True
|
return True
|
||||||
elif "disabled" in self.ctx.forfeit_mode:
|
elif "disabled" in self.ctx.forfeit_mode:
|
||||||
self.output(
|
self.output("Sorry, client item releasing has been disabled on this server. "
|
||||||
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
|
"You can ask the server admin for a /release")
|
||||||
return False
|
return False
|
||||||
else: # is auto or goal
|
else: # is auto or goal
|
||||||
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:
|
||||||
|
@ -1170,7 +1207,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(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))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
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:
|
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(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))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
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)
|
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
||||||
|
|
||||||
if locations:
|
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")
|
texts.append(f"Found {len(locations)} missing location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.ctx.notify_client_multiple(self.client, texts)
|
||||||
else:
|
else:
|
||||||
|
@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||||
|
|
||||||
if locations:
|
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")
|
texts.append(f"Found {len(locations)} done location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.ctx.notify_client_multiple(self.client, texts)
|
||||||
else:
|
else:
|
||||||
|
@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
def _cmd_getitem(self, item_name: str) -> bool:
|
def _cmd_getitem(self, item_name: str) -> bool:
|
||||||
"""Cheat in an item, if it is enabled on this server"""
|
"""Cheat in an item, if it is enabled on this server"""
|
||||||
if self.ctx.item_cheat:
|
if self.ctx.item_cheat:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
|
||||||
item_name, usable, response = get_intended_text(item_name,
|
item_name, usable, response = get_intended_text(
|
||||||
world.item_names)
|
item_name,
|
||||||
|
names
|
||||||
|
)
|
||||||
if usable:
|
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, False).append(new_item)
|
||||||
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
||||||
self.ctx.notify_all(
|
self.ctx.notify_all(
|
||||||
|
@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
f"You have {points_available} points.")
|
f"You have {points_available} points.")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
game = self.ctx.games[self.client.slot]
|
||||||
names = world.location_names if for_location else world.all_item_and_group_names
|
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,
|
hint_name, usable, response = get_intended_text(input_text,
|
||||||
names)
|
names)
|
||||||
if usable:
|
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.")
|
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||||
hints = []
|
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 = []
|
hints = []
|
||||||
for item in world.item_name_groups[hint_name]:
|
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||||
if item in world.item_name_to_id: # ensure item has an ID
|
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))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||||
elif not for_location and hint_name in world.item_names: # 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)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_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
|
return False
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_hint(self, item: str = "") -> bool:
|
def _cmd_hint(self, item_name: str = "") -> bool:
|
||||||
"""Use !hint {item_name},
|
"""Use !hint {item_name},
|
||||||
for example !hint Lamp to get a spoiler peek for that item.
|
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,
|
If hint costs are on, this will only give you one new result,
|
||||||
you can rerun the command to get more in that case."""
|
you can rerun the command to get more in that case."""
|
||||||
return self.get_hints(item)
|
return self.get_hints(item_name)
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_hint_location(self, location: str = "") -> bool:
|
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":
|
elif cmd == "GetDataPackage":
|
||||||
exclusions = args.get("exclusions", [])
|
exclusions = args.get("exclusions", [])
|
||||||
if "games" in args:
|
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", []))}
|
if name in set(args.get("games", []))}
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
"data": {"games": games}}])
|
"data": {"games": games}}])
|
||||||
# TODO: remove exclusions behaviour around 0.5.0
|
# TODO: remove exclusions behaviour around 0.5.0
|
||||||
elif exclusions:
|
elif exclusions:
|
||||||
exclusions = set(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}
|
if name not in exclusions}
|
||||||
package = network_data_package.copy()
|
|
||||||
package["games"] = games
|
package = {"games": games}
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
"data": package}])
|
"data": package}])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
"data": network_data_package}])
|
"data": {"games": ctx.gamespackage}}])
|
||||||
|
|
||||||
elif client.auth:
|
elif client.auth:
|
||||||
if cmd == "ConnectUpdate":
|
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))
|
create_as_hint: int = int(args.get("create_as_hint", 0))
|
||||||
hints = []
|
hints = []
|
||||||
for location in args["locations"]:
|
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,
|
await ctx.send_msgs(client,
|
||||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||||
"original_cmd": cmd}])
|
"original_cmd": cmd}])
|
||||||
|
@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(item_name)
|
item_name = " ".join(item_name)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
names = self.ctx.item_names_for_game(self.ctx.games[slot])
|
||||||
item, usable, response = get_intended_text(item, world.item_names)
|
item_name, usable, response = get_intended_text(item_name, names)
|
||||||
if usable:
|
if usable:
|
||||||
amount: int = int(amount)
|
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_items_to(self.ctx, team, slot, *new_items)
|
||||||
|
|
||||||
send_new_items(self.ctx)
|
send_new_items(self.ctx)
|
||||||
self.ctx.notify_all(
|
self.ctx.notify_all(
|
||||||
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
'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
|
return True
|
||||||
else:
|
else:
|
||||||
self.output(response)
|
self.output(response)
|
||||||
|
@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
"""Sends an item to the specified player"""
|
"""Sends an item to the specified player"""
|
||||||
return self._cmd_send_multiple(1, player_name, *item_name)
|
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"""
|
"""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())
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(item)
|
item_name = " ".join(item_name)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
game = self.ctx.games[slot]
|
||||||
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
|
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
|
||||||
if usable:
|
if usable:
|
||||||
if item in world.item_name_groups:
|
if item_name in self.ctx.item_name_groups[game]:
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[item]:
|
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
|
||||||
if item in world.item_name_to_id: # ensure item has an ID
|
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))
|
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||||
else: # item name
|
else: # item name
|
||||||
hints = collect_hints(self.ctx, team, slot, item)
|
hints = collect_hints(self.ctx, team, slot, item_name)
|
||||||
|
|
||||||
if hints:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
notify_hints(self.ctx, team, hints)
|
||||||
|
@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
self.output(response)
|
self.output(response)
|
||||||
return False
|
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"""
|
"""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())
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(location)
|
location_name = " ".join(location_name)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
location_name, usable, response = get_intended_text(location_name,
|
||||||
item, usable, response = get_intended_text(item, world.location_names)
|
self.ctx.location_names_for_game(self.ctx.games[slot]))
|
||||||
if usable:
|
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:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
notify_hints(self.ctx, team, hints)
|
||||||
else:
|
else:
|
||||||
|
|
10
Utils.py
10
Utils.py
|
@ -328,16 +328,6 @@ def get_options() -> dict:
|
||||||
return get_options.options
|
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):
|
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||||
path = user_path("_persistent_storage.yaml")
|
path = user_path("_persistent_storage.yaml")
|
||||||
storage: dict = persistent_load()
|
storage: dict = persistent_load()
|
||||||
|
|
|
@ -14,7 +14,7 @@ import Utils
|
||||||
|
|
||||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
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 waitress import serve
|
||||||
|
|
||||||
from WebHostLib.models import db
|
from WebHostLib.models import db
|
||||||
|
@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen
|
||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||||
from WebHostLib.options import create as create_options_files
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
|
||||||
|
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||||
|
|
||||||
|
|
||||||
def get_app():
|
def get_app():
|
||||||
|
register()
|
||||||
app = raw_app
|
app = raw_app
|
||||||
if os.path.exists(configpath):
|
if os.path.exists(configpath):
|
||||||
import yaml
|
import yaml
|
||||||
|
@ -43,6 +42,7 @@ def get_app():
|
||||||
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
worlds = {}
|
worlds = {}
|
||||||
data = []
|
data = []
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
|
|
@ -3,12 +3,11 @@ import uuid
|
||||||
import base64
|
import base64
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import jinja2.exceptions
|
|
||||||
from pony.flask import Pony
|
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_caching import Cache
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
@ -53,8 +52,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||||
cache = Cache(app)
|
cache = Cache(app)
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|
||||||
from werkzeug.routing import BaseConverter
|
|
||||||
|
|
||||||
|
|
||||||
class B64UUIDConverter(BaseConverter):
|
class B64UUIDConverter(BaseConverter):
|
||||||
|
|
||||||
|
@ -69,173 +66,16 @@ class B64UUIDConverter(BaseConverter):
|
||||||
app.url_map.converters["suuid"] = B64UUIDConverter
|
app.url_map.converters["suuid"] = B64UUIDConverter
|
||||||
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
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
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def get_world_theme(game_name: str):
|
from WebHostLib.customserver import run_server_process
|
||||||
if game_name in AutoWorldRegister.world_types:
|
# to trigger app routing picking up on it
|
||||||
return AutoWorldRegister.world_types[game_name].web.theme
|
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
|
||||||
return 'grass'
|
|
||||||
|
|
||||||
|
app.register_blueprint(api.api_endpoints)
|
||||||
@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)
|
|
||||||
|
|
|
@ -184,7 +184,7 @@ class MultiworldInstance():
|
||||||
|
|
||||||
logging.info(f"Spinning up {self.room_id}")
|
logging.info(f"Spinning up {self.room_id}")
|
||||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
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")
|
name="MultiHost")
|
||||||
process.start()
|
process.start()
|
||||||
# bind after start to prevent thread sync issues with guardian.
|
# 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 .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
|
from .generate import gen_game
|
||||||
|
|
|
@ -9,12 +9,13 @@ import time
|
||||||
import random
|
import random
|
||||||
import pickle
|
import pickle
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
import Utils
|
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 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):
|
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||||
|
@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||||
import MultiServer
|
import MultiServer
|
||||||
|
|
||||||
MultiServer.client_message_processor = CustomClientMessageProcessor
|
MultiServer.client_message_processor = CustomClientMessageProcessor
|
||||||
del (MultiServer)
|
del MultiServer
|
||||||
|
|
||||||
|
|
||||||
class DBCommandProcessor(ServerCommandProcessor):
|
class DBCommandProcessor(ServerCommandProcessor):
|
||||||
|
@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||||
|
|
||||||
|
|
||||||
class WebHostContext(Context):
|
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)
|
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.main_loop = asyncio.get_running_loop()
|
||||||
self.video = {}
|
self.video = {}
|
||||||
self.tags = ["AP", "WebHost"]
|
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):
|
def listen_to_db_commands(self):
|
||||||
cmdprocessor = DBCommandProcessor(self)
|
cmdprocessor = DBCommandProcessor(self)
|
||||||
|
|
||||||
|
@ -107,14 +116,32 @@ def get_random_port():
|
||||||
return random.randint(49152, 65535)
|
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
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
Utils.init_logging(str(room_id), write_mode="a")
|
Utils.init_logging(str(room_id), write_mode="a")
|
||||||
ctx = WebHostContext()
|
ctx = WebHostContext(static_server_data)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
|
|
||||||
|
|
|
@ -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_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)
|
|
@ -11,7 +11,7 @@ from worlds.alttp import Items
|
||||||
from WebHostLib import app, cache, Room
|
from WebHostLib import app, cache, Room
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
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
|
from NetUtils import SlotType
|
||||||
|
|
||||||
alttp_icons = {
|
alttp_icons = {
|
||||||
|
@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
|
||||||
for (team, player), data in multisave.get("video", []):
|
for (team, player), data in multisave.get("video", []):
|
||||||
video[(team, player)] = data
|
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,
|
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,
|
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,
|
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||||
|
|
|
@ -45,7 +45,6 @@ class MeritousWorld(World):
|
||||||
item_name_groups = item_groups
|
item_name_groups = item_groups
|
||||||
|
|
||||||
data_version = 2
|
data_version = 2
|
||||||
forced_auto_forfeit = False
|
|
||||||
|
|
||||||
# NOTE: Remember to change this before this game goes live
|
# NOTE: Remember to change this before this game goes live
|
||||||
required_client_version = (0, 2, 4)
|
required_client_version = (0, 2, 4)
|
||||||
|
|
|
@ -35,9 +35,7 @@ class SM64World(World):
|
||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
|
|
||||||
data_version = 6
|
data_version = 6
|
||||||
required_client_version = (0,3,0)
|
required_client_version = (0, 3, 0)
|
||||||
|
|
||||||
forced_auto_forfeit = False
|
|
||||||
|
|
||||||
area_connections: typing.Dict[int, int]
|
area_connections: typing.Dict[int, int]
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ class V6World(World):
|
||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
|
|
||||||
data_version = 1
|
data_version = 1
|
||||||
forced_auto_forfeit = False
|
|
||||||
|
|
||||||
area_connections: typing.Dict[int, int]
|
area_connections: typing.Dict[int, int]
|
||||||
area_cost_map: typing.Dict[int,int]
|
area_cost_map: typing.Dict[int,int]
|
||||||
|
|
Loading…
Reference in New Issue