From a07dc43db08a37079a1c94fb98f744f664e6ada3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 16 Jan 2020 18:41:25 +0100 Subject: [PATCH] Merge branch 'master' of E:\ROMS\Randomizer\ALttPEntranceRandomizer with conflicts. --- MultiServer.py | 400 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 MultiServer.py diff --git a/MultiServer.py b/MultiServer.py new file mode 100644 index 00000000..34e40ea6 --- /dev/null +++ b/MultiServer.py @@ -0,0 +1,400 @@ +import aioconsole +import argparse +import asyncio +import functools +import json +import logging +import re +import shlex +import urllib.request +import websockets +import zlib + +import Items +import Regions +from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address + +class Client: + def __init__(self, socket): + self.socket = socket + self.auth = False + self.name = None + self.team = None + self.slot = None + self.send_index = 0 + +class Context: + def __init__(self, host, port, password): + self.data_filename = None + self.save_filename = None + self.disable_save = False + self.player_names = {} + self.rom_names = {} + self.locations = {} + self.host = host + self.port = port + self.password = password + self.server = None + self.countdown_timer = 0 + self.clients = [] + self.received_items = {} + +async def send_msgs(websocket, msgs): + if not websocket or not websocket.open or websocket.closed: + return + try: + await websocket.send(json.dumps(msgs)) + except websockets.ConnectionClosed: + pass + +def broadcast_all(ctx : Context, msgs): + for client in ctx.clients: + if client.auth: + asyncio.create_task(send_msgs(client.socket, msgs)) + +def broadcast_team(ctx : Context, team, msgs): + for client in ctx.clients: + if client.auth and client.team == team: + asyncio.create_task(send_msgs(client.socket, msgs)) + +def notify_all(ctx : Context, text): + print("Notice (all): %s" % text) + broadcast_all(ctx, [['Print', text]]) + +def notify_team(ctx : Context, team : int, text : str): + print("Notice (Team #%d): %s" % (team+1, text)) + broadcast_team(ctx, team, [['Print', text]]) + +def notify_client(client : Client, text : str): + if not client.auth: + return + print("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text)) + asyncio.create_task(send_msgs(client.socket, [['Print', text]])) + +async def server(websocket, path, ctx : Context): + client = Client(websocket) + ctx.clients.append(client) + + try: + await on_client_connected(ctx, client) + async for data in websocket: + for msg in json.loads(data): + if len(msg) == 1: + cmd = msg + args = None + else: + cmd = msg[0] + args = msg[1] + await process_client_cmd(ctx, client, cmd, args) + except Exception as e: + if not isinstance(e, websockets.WebSocketException): + logging.exception(e) + finally: + await on_client_disconnected(ctx, client) + ctx.clients.remove(client) + +async def on_client_connected(ctx : Context, client : Client): + await send_msgs(client.socket, [['RoomInfo', { + 'password': ctx.password is not None, + 'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth] + }]]) + +async def on_client_disconnected(ctx : Context, client : Client): + if client.auth: + await on_client_left(ctx, client) + +async def on_client_joined(ctx : Context, client : Client): + notify_all(ctx, "%s (Team #%d) has joined the game" % (client.name, client.team + 1)) + +async def on_client_left(ctx : Context, client : Client): + notify_all(ctx, "%s (Team #%d) has left the game" % (client.name, client.team + 1)) + +async def countdown(ctx : Context, timer): + notify_all(ctx, f'[Server]: Starting countdown of {timer}s') + if ctx.countdown_timer: + ctx.countdown_timer = timer + return + + ctx.countdown_timer = timer + while ctx.countdown_timer > 0: + notify_all(ctx, f'[Server]: {ctx.countdown_timer}') + ctx.countdown_timer -= 1 + await asyncio.sleep(1) + notify_all(ctx, f'[Server]: GO') + +def get_connected_players_string(ctx : Context): + auth_clients = [c for c in ctx.clients if c.auth] + if not auth_clients: + return 'No player connected' + + auth_clients.sort(key=lambda c: (c.team, c.slot)) + current_team = 0 + text = 'Team #1: ' + for c in auth_clients: + if c.team != current_team: + text += f':: Team #{c.team + 1}: ' + current_team = c.team + text += f'{c.name} ' + return 'Connected players: ' + text[:-1] + +def get_received_items(ctx : Context, team, player): + return ctx.received_items.setdefault((team, player), []) + +def tuplize_received_items(items): + return [(item.item, item.location, item.player) for item in items] + +def send_new_items(ctx : Context): + for client in ctx.clients: + if not client.auth: + continue + items = get_received_items(ctx, client.team, client.slot) + if len(items) > client.send_index: + asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]])) + client.send_index = len(items) + +def forfeit_player(ctx : Context, team, slot): + all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int] + notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1)) + register_location_checks(ctx, team, slot, all_locations) + +def register_location_checks(ctx : Context, team, slot, locations): + found_items = False + for location in locations: + if (location, slot) in ctx.locations: + target_item, target_player = ctx.locations[(location, slot)] + if target_player != slot: + found = False + recvd_items = get_received_items(ctx, team, target_player) + for recvd_item in recvd_items: + if recvd_item.location == location and recvd_item.player == slot: + found = True + break + if not found: + new_item = ReceivedItem(target_item, location, slot) + recvd_items.append(new_item) + broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]]) + print('(Team #%d) %s sent %s to %s (%s)' % (team, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location))) + found_items = True + send_new_items(ctx) + + if found_items and not ctx.disable_save: + try: + with open(ctx.save_filename, "wb") as f: + jsonstr = json.dumps((list(ctx.rom_names.items()), + [(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()])) + f.write(zlib.compress(jsonstr.encode("utf-8"))) + except Exception as e: + logging.exception(e) + +async def process_client_cmd(ctx : Context, client : Client, cmd, args): + if type(cmd) is not str: + await send_msgs(client.socket, [['InvalidCmd']]) + return + + if cmd == 'Connect': + if not args or type(args) is not dict or \ + 'password' not in args or type(args['password']) not in [str, type(None)] or \ + 'rom' not in args or type(args['rom']) is not list: + await send_msgs(client.socket, [['InvalidArguments', 'Connect']]) + return + + errors = set() + if ctx.password is not None and args['password'] != ctx.password: + errors.add('InvalidPassword') + + if tuple(args['rom']) not in ctx.rom_names: + errors.add('InvalidRom') + else: + team, slot = ctx.rom_names[tuple(args['rom'])] + if any([c.slot == slot and c.team == team for c in ctx.clients if c.auth]): + errors.add('SlotAlreadyTaken') + else: + client.name = ctx.player_names[(team, slot)] + client.team = team + client.slot = slot + + if errors: + await send_msgs(client.socket, [['ConnectionRefused', list(errors)]]) + else: + client.auth = True + reply = [['Connected', [(client.team, client.slot), [(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]] + items = get_received_items(ctx, client.team, client.slot) + if items: + reply.append(['ReceivedItems', (0, tuplize_received_items(items))]) + client.send_index = len(items) + await send_msgs(client.socket, reply) + await on_client_joined(ctx, client) + + if not client.auth: + return + + if cmd == 'Sync': + items = get_received_items(ctx, client.team, client.slot) + if items: + client.send_index = len(items) + await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))]) + + if cmd == 'LocationChecks': + if type(args) is not list: + await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']]) + return + register_location_checks(ctx, client.team, client.slot, args) + + if cmd == 'Say': + if type(args) is not str or not args.isprintable(): + await send_msgs(client.socket, [['InvalidArguments', 'Say']]) + return + + notify_all(ctx, client.name + ': ' + args) + + if args.startswith('!players'): + notify_all(ctx, get_connected_players_string(ctx)) + if args.startswith('!forfeit'): + forfeit_player(ctx, client.team, client.slot) + if args.startswith('!countdown'): + try: + timer = int(args.split()[1]) + except (IndexError, ValueError): + timer = 10 + asyncio.create_task(countdown(ctx, timer)) + +def set_password(ctx : Context, password): + ctx.password = password + print('Password set to ' + password if password is not None else 'Password disabled') + +async def console(ctx : Context): + while True: + input = await aioconsole.ainput() + try: + + command = shlex.split(input) + if not command: + continue + + if command[0] == '/exit': + ctx.server.ws_server.close() + break + + if command[0] == '/players': + print(get_connected_players_string(ctx)) + if command[0] == '/password': + set_password(ctx, command[1] if len(command) > 1 else None) + if command[0] == '/kick' and len(command) > 1: + team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None + for client in ctx.clients: + if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team): + if client.socket and not client.socket.closed: + await client.socket.close() + + if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit(): + if len(command) > 2 and command[2].isdigit(): + team = int(command[1]) - 1 + slot = int(command[2]) + else: + team = 0 + slot = int(command[1]) + forfeit_player(ctx, team, slot) + if command[0] == '/forfeitplayer' and len(command) > 1: + team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None + for client in ctx.clients: + if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team): + if client.socket and not client.socket.closed: + forfeit_player(ctx, client.team, client.slot) + if command[0] == '/senditem' and len(command) > 2: + [(player, item)] = re.findall(r'\S* (\S*) (.*)', input) + if item in Items.item_table: + for client in ctx.clients: + if client.auth and client.name.lower() == player.lower(): + new_item = ReceivedItem(Items.item_table[item][3], "cheat console", client.slot) + get_received_items(ctx, client.team, client.slot).append(new_item) + notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name) + send_new_items(ctx) + else: + print("Unknown item: " + item) + if command[0] == '/hint': + for (team,slot), name in ctx.player_names.items(): + if len(command) == 1: + print("Use /hint {Playername} {Itemname}\nFor example /hint Berserker Lamp") + elif name.lower() == command[1].lower(): + item = " ".join(command[2:]) + if item in Items.item_table: + seeked_item_id = Items.item_table[item][3] + for check, result in ctx.locations.items(): + item_id, receiving_player = result + if receiving_player == slot and item_id == seeked_item_id: + location_id, finding_player = check + name_finder = ctx.player_names[team, finding_player] + hint = f"[Hint]: {name}'s {item} can be found in " \ + f"{get_location_name_from_address(location_id)} in {name_finder}'s World" + notify_team(ctx, team, hint) + else: + print("Unknown item: " + item) + if command[0][0] != '/': + notify_all(ctx, '[Server]: ' + input) + except: + import traceback + traceback.print_exc() + +async def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--host', default=None) + parser.add_argument('--port', default=38281, type=int) + parser.add_argument('--password', default=None) + parser.add_argument('--multidata', default=None) + parser.add_argument('--savefile', default=None) + parser.add_argument('--disable_save', default=False, action='store_true') + args = parser.parse_args() + + ctx = Context(args.host, args.port, args.password) + + ctx.data_filename = args.multidata + + try: + if not ctx.data_filename: + import tkinter + import tkinter.filedialog + root = tkinter.Tk() + root.withdraw() + ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),)) + + with open(ctx.data_filename, 'rb') as f: + jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) + for team, names in enumerate(jsonobj[0]): + for player, name in enumerate(names, 1): + ctx.player_names[(team, player)] = name + ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj[1]} + ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]} + except Exception as e: + print('Failed to read multiworld data (%s)' % e) + return + + ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host + print('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password)) + + ctx.disable_save = args.disable_save + if not ctx.disable_save: + if not ctx.save_filename: + ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave' + try: + with open(ctx.save_filename, 'rb') as f: + jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8")) + rom_names = jsonobj[0] + received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[1]} + if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]): + raise Exception('Save file mismatch, will start a new game') + ctx.received_items = received_items + print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items))) + except FileNotFoundError: + print('No save data found, starting a new game') + except Exception as e: + print(e) + + ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None) + await ctx.server + await console(ctx) + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks())) + loop.close() \ No newline at end of file