Server: add slot_info key to Connected

This commit is contained in:
Fabian Dill 2022-01-30 13:57:12 +01:00
parent ddd3073132
commit 0bd252e7f5
6 changed files with 67 additions and 41 deletions

View File

@ -12,6 +12,7 @@ import random
import Options import Options
import Utils import Utils
import NetUtils
class MultiWorld(): class MultiWorld():
@ -39,6 +40,7 @@ class MultiWorld():
def __init__(self, players: int): def __init__(self, players: int):
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
self.players = players self.players = players
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.glitch_triforce = False self.glitch_triforce = False
self.algorithm = 'balanced' self.algorithm = 'balanced'
self.dungeons: Dict[Tuple[str, int], Dungeon] = {} self.dungeons: Dict[Tuple[str, int], Dungeon] = {}

12
Main.py
View File

@ -9,7 +9,7 @@ import tempfile
import zipfile import zipfile
from typing import Dict, Tuple, Optional 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.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned 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 import NetUtils
slot_data = {} slot_data = {}
client_versions = {} client_versions = {}
minimum_versions = {"server": (0, 1, 8), "clients": client_versions}
games = {} 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: for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version() client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot] 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] precollected_items = {player: [item.code for item in world_precollected]
for player, world_precollected in world.precollected_items.items()} for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, world.players + 1)} 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 = { multidata = {
"slot_data": slot_data, "slot_data": slot_data,
"games": games, "slot_info": slot_info,
"names": [[name for player, name in sorted(world.player_name.items())]], "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()}, "connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if "remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items}, world.worlds[player].remote_items},

View File

@ -33,7 +33,8 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
import Utils import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \ from Utils import get_item_name_from_id, get_location_name_from_id, \
version_tuple, restricted_loads, Version 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() colorama.init()
@ -104,6 +105,7 @@ class Context:
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False): log_network: bool = False):
super(Context, self).__init__() super(Context, self).__init__()
self.slot_info: typing.Dict[int, NetworkSlot] = {}
self.log_network = log_network self.log_network = log_network
self.endpoints = [] self.endpoints = []
self.clients = {} self.clients = {}
@ -292,18 +294,35 @@ class Context:
self.slot_data = decoded_obj['slot_data'] self.slot_data = decoded_obj['slot_data']
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()} 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()} for player, loc_data in decoded_obj["er_hint_data"].items()}
self.games = decoded_obj["games"]
# load start inventory: # load start inventory:
for slot, item_codes in decoded_obj["precollected_items"].items(): 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] self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for team in range(len(decoded_obj['names'])): for team in range(len(decoded_obj['names'])):
for slot, hints in decoded_obj["precollected_hints"].items(): for slot, hints in decoded_obj["precollected_hints"].items():
self.hints[team, slot].update(hints) self.hints[team, slot].update(hints)
# declare slots without checks as done, as they're assumed to be spectators if "slot_info" in decoded_obj:
for slot, locations in self.locations.items(): self.slot_info = decoded_obj["slot_info"]
if not locations: 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: for team in self.clients:
self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL
if use_embedded_server_options: if use_embedded_server_options:
server_options = decoded_obj.get("server_options", {}) server_options = decoded_obj.get("server_options", {})
self._set_options(server_options) self._set_options(server_options)
@ -541,6 +560,8 @@ async def on_client_connected(ctx: Context, client: Client):
'cmd': 'RoomInfo', 'cmd': 'RoomInfo',
'password': bool(ctx.password), 'password': bool(ctx.password),
'players': players, '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)], 'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)],
# tags are for additional features in the communication. # tags are for additional features in the communication.
# Name them by feature or fork, as you feel is appropriate. # 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"{verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}).") f"Client({version_str}), {client.tags}).")
ctx.notify_client(client, "Now that you are connected, " 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, " "If your client supports it, "
"you may have additional local commands you can list with /help.") "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) 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: if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
for location in new_locations: for location in new_locations:
if len(ctx.locations[slot][location]) == 3: item_id, target_player, flags = ctx.locations[slot][location]
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
new_item = NetworkItem(item_id, location, slot, flags) new_item = NetworkItem(item_id, location, slot, flags)
if target_player != slot: 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] seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
for finding_player, check_data in ctx.locations.items(): for finding_player, check_data in ctx.locations.items():
for location_id, result in check_data.items(): for location_id, result in check_data.items():
if len(result) == 3: item_id, receiving_player, item_flags = result
item_id, receiving_player, item_flags = result
else:
# TODO: remove around version 0.2.5
item_id, receiving_player = result
item_flags = 0
if receiving_player == slot and item_id == seeked_item_id: if receiving_player == slot and item_id == seeked_item_id:
found = location_id in ctx.location_checks[team, finding_player] 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] seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
result = ctx.locations[slot].get(seeked_location, (None, None, None)) result = ctx.locations[slot].get(seeked_location, (None, None, None))
if result: if result:
if len(result) == 3: item_id, receiving_player, item_flags = result
item_id, receiving_player, item_flags = result
else:
# TODO: remove around version 0.2.5
item_id, receiving_player = result
item_flags = 0
found = seeked_location in ctx.location_checks[team, slot] found = seeked_location in ctx.location_checks[team, slot]
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") 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(), "players": ctx.get_players_package(),
"missing_locations": get_missing_checks(ctx, team, slot), "missing_locations": get_missing_checks(ctx, team, slot),
"checked_locations": get_checked_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) start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items) 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', [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}]) "original_cmd": cmd}])
return 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)) locs.append(NetworkItem(target_item, location, target_player, flags))
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])

View File

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import logging
import typing import typing
import enum import enum
from json import JSONEncoder, JSONDecoder from json import JSONEncoder, JSONDecoder
@ -29,7 +28,18 @@ class ClientStatus(enum.IntEnum):
CLIENT_GOAL = 30 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 disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion goal = 0b010 # 2, allows manual use after goal completion
@ -49,12 +59,20 @@ class Permission(enum.IntEnum):
class NetworkPlayer(typing.NamedTuple): class NetworkPlayer(typing.NamedTuple):
"""Represents a particular player on a particular team."""
team: int team: int
slot: int slot: int
alias: str alias: str
name: str name: str
class NetworkSlot(typing.NamedTuple):
"""Represents a particular slot across teams."""
name: str
game: str
type: SlotType
class NetworkItem(typing.NamedTuple): class NetworkItem(typing.NamedTuple):
item: int item: int
location: int location: int
@ -122,9 +140,6 @@ class Endpoint:
def __init__(self, socket): def __init__(self, socket):
self.socket = socket self.socket = socket
async def disconnect(self):
raise NotImplementedError
class HandlerMeta(type): class HandlerMeta(type):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):

View File

@ -349,7 +349,7 @@ class RestrictedUnpickler(pickle.Unpickler):
if module == "builtins" and name in safe_builtins: if module == "builtins" and name in safe_builtins:
return getattr(builtins, name) return getattr(builtins, name)
# used by MultiServer -> savegame/multidata # 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) return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate # Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:

View File

@ -18,6 +18,9 @@ class GenericWorld(World):
} }
hidden = True hidden = True
def generate_early(self):
self.world.player_types[self.player] = 0 # mark as spectator
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
if name == "Nothing": if name == "Nothing":
return Item(name, False, -1, self.player) return Item(name, False, -1, self.player)