From 0bd252e7f5d14636de4cbd51882ac83fae6b9a3d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 30 Jan 2022 13:57:12 +0100 Subject: [PATCH] Server: add slot_info key to Connected --- BaseClasses.py | 2 ++ Main.py | 12 ++++--- MultiServer.py | 64 ++++++++++++++++++++------------------ NetUtils.py | 25 ++++++++++++--- Utils.py | 2 +- worlds/generic/__init__.py | 3 ++ 6 files changed, 67 insertions(+), 41 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index f662be5c..9bf5fccb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -12,6 +12,7 @@ import random import Options import Utils +import NetUtils class MultiWorld(): @@ -39,6 +40,7 @@ class MultiWorld(): def __init__(self, players: int): self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.players = players + self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.glitch_triforce = False self.algorithm = 'balanced' self.dungeons: Dict[Tuple[str, int], Dungeon] = {} diff --git a/Main.py b/Main.py index 46395279..037327b3 100644 --- a/Main.py +++ b/Main.py @@ -9,7 +9,7 @@ import tempfile import zipfile from typing import Dict, Tuple, Optional -from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType +from BaseClasses import MultiWorld, CollectionState, Region, RegionType from worlds.alttp.Items import item_name_groups from worlds.alttp.Regions import lookup_vanilla_location_to_entrance from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned @@ -250,11 +250,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No import NetUtils slot_data = {} client_versions = {} - minimum_versions = {"server": (0, 1, 8), "clients": client_versions} games = {} + minimum_versions = {"server": (0, 2, 4), "clients": client_versions} + slot_info = {} + names = [[name for player, name in sorted(world.player_name.items())]] for slot in world.player_ids: client_versions[slot] = world.worlds[slot].get_required_client_version() games[slot] = world.game[slot] + slot_info[slot] = NetUtils.NetworkSlot(names[0][slot+1], world.game[slot], world.player_types[slot]) precollected_items = {player: [item.code for item in world_precollected] for player, world_precollected in world.precollected_items.items()} precollected_hints = {player: set() for player in range(1, world.players + 1)} @@ -288,8 +291,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multidata = { "slot_data": slot_data, - "games": games, - "names": [[name for player, name in sorted(world.player_name.items())]], + "slot_info": slot_info, + "names": names, # TODO: remove around 0.2.5 in favor of slot_info + "games": games, # TODO: remove around 0.2.5 in favor of slot_info "connect_names": {name: (0, player) for player, name in world.player_name.items()}, "remote_items": {player for player in world.player_ids if world.worlds[player].remote_items}, diff --git a/MultiServer.py b/MultiServer.py index 45749893..547eee23 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -33,7 +33,8 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_ import Utils from Utils import get_item_name_from_id, get_location_name_from_id, \ version_tuple, restricted_loads, Version -from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission +from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ + SlotType colorama.init() @@ -104,6 +105,7 @@ class Context: remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, log_network: bool = False): super(Context, self).__init__() + self.slot_info: typing.Dict[int, NetworkSlot] = {} self.log_network = log_network self.endpoints = [] self.clients = {} @@ -292,18 +294,35 @@ class Context: self.slot_data = decoded_obj['slot_data'] self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()} for player, loc_data in decoded_obj["er_hint_data"].items()} - self.games = decoded_obj["games"] + # load start inventory: for slot, item_codes in decoded_obj["precollected_items"].items(): self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes] + for team in range(len(decoded_obj['names'])): for slot, hints in decoded_obj["precollected_hints"].items(): self.hints[team, slot].update(hints) - # declare slots without checks as done, as they're assumed to be spectators - for slot, locations in self.locations.items(): - if not locations: + if "slot_info" in decoded_obj: + self.slot_info = decoded_obj["slot_info"] + self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} + + else: + self.games = decoded_obj["games"] + + self.slot_info = { + slot: NetworkSlot( + self.player_names[0, slot], + self.games[slot], + SlotType(int(bool(locations)))) + for slot, locations in self.locations.items() + } + + # declare slots that aren't players as done + for slot, slot_info in self.slot_info.items(): + if slot_info.type.always_goal: for team in self.clients: self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL + if use_embedded_server_options: server_options = decoded_obj.get("server_options", {}) self._set_options(server_options) @@ -541,6 +560,8 @@ async def on_client_connected(ctx: Context, client: Client): 'cmd': 'RoomInfo', 'password': bool(ctx.password), 'players': players, + # TODO remove around 0.2.5 in favor of slot_info ? + # Maybe convert into a list of games that are present to fetch relevant datapackage entries before Connect? 'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)], # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. @@ -579,7 +600,7 @@ async def on_client_joined(ctx: Context, client: Client): f"{verb} {ctx.games[client.slot]} has joined. " f"Client({version_str}), {client.tags}).") ctx.notify_client(client, "Now that you are connected, " - "you can use !help to list commands to run via the server." + "you can use !help to list commands to run via the server. " "If your client supports it, " "you may have additional local commands you can list with /help.") ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) @@ -705,12 +726,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi if count_activity: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) for location in new_locations: - if len(ctx.locations[slot][location]) == 3: - item_id, target_player, flags = ctx.locations[slot][location] - else: - # TODO: remove around version 0.2.5 - item_id, target_player = ctx.locations[slot][location] - flags = 0 + item_id, target_player, flags = ctx.locations[slot][location] new_item = NetworkItem(item_id, location, slot, flags) if target_player != slot: @@ -739,12 +755,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item] for finding_player, check_data in ctx.locations.items(): for location_id, result in check_data.items(): - if len(result) == 3: - item_id, receiving_player, item_flags = result - else: - # TODO: remove around version 0.2.5 - item_id, receiving_player = result - item_flags = 0 + item_id, receiving_player, item_flags = result if receiving_player == slot and item_id == seeked_item_id: found = location_id in ctx.location_checks[team, finding_player] @@ -759,12 +770,7 @@ def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location] result = ctx.locations[slot].get(seeked_location, (None, None, None)) if result: - if len(result) == 3: - item_id, receiving_player, item_flags = result - else: - # TODO: remove around version 0.2.5 - item_id, receiving_player = result - item_flags = 0 + item_id, receiving_player, item_flags = result found = seeked_location in ctx.location_checks[team, slot] entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") @@ -1347,7 +1353,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): "players": ctx.get_players_package(), "missing_locations": get_missing_checks(ctx, team, slot), "checked_locations": get_checked_checks(ctx, team, slot), - "slot_data": ctx.slot_data[client.slot] + "slot_data": ctx.slot_data[client.slot], + "slot_info": ctx.slot_info }] start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory) items = get_received_items(ctx, client.team, client.slot, client.remote_items) @@ -1431,13 +1438,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', "original_cmd": cmd}]) return - if len(ctx.locations[client.slot][location]) == 3: - target_item, target_player, flags = ctx.locations[client.slot][location] - else: - # TODO: remove around version 0.2.5 - target_item, target_player = ctx.locations[client.slot][location] - flags = 0 + target_item, target_player, flags = ctx.locations[client.slot][location] locs.append(NetworkItem(target_item, location, target_player, flags)) await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) diff --git a/NetUtils.py b/NetUtils.py index 0499578c..f748bbb2 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import typing import enum from json import JSONEncoder, JSONDecoder @@ -29,7 +28,18 @@ class ClientStatus(enum.IntEnum): CLIENT_GOAL = 30 -class Permission(enum.IntEnum): +class SlotType(enum.IntFlag): + spectator = 0b00 + player = 0b01 + group = 0b10 + + @property + def always_goal(self) -> bool: + """Mark this slot has having reached its goal instantly.""" + return self.value != 0b01 + + +class Permission(enum.IntFlag): disabled = 0b000 # 0, completely disables access enabled = 0b001 # 1, allows manual use goal = 0b010 # 2, allows manual use after goal completion @@ -49,12 +59,20 @@ class Permission(enum.IntEnum): class NetworkPlayer(typing.NamedTuple): + """Represents a particular player on a particular team.""" team: int slot: int alias: str name: str +class NetworkSlot(typing.NamedTuple): + """Represents a particular slot across teams.""" + name: str + game: str + type: SlotType + + class NetworkItem(typing.NamedTuple): item: int location: int @@ -122,9 +140,6 @@ class Endpoint: def __init__(self, socket): self.socket = socket - async def disconnect(self): - raise NotImplementedError - class HandlerMeta(type): def __new__(mcs, name, bases, attrs): diff --git a/Utils.py b/Utils.py index 61b21926..3af55a6a 100644 --- a/Utils.py +++ b/Utils.py @@ -349,7 +349,7 @@ class RestrictedUnpickler(pickle.Unpickler): if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata - if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}: + if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 50fb1038..75e18929 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -18,6 +18,9 @@ class GenericWorld(World): } hidden = True + def generate_early(self): + self.world.player_types[self.player] = 0 # mark as spectator + def create_item(self, name: str) -> Item: if name == "Nothing": return Item(name, False, -1, self.player)