initial upload
This commit is contained in:
		
							parent
							
								
									2f5a3e24dd
								
							
						
					
					
						commit
						85a4e9d409
					
				| 
						 | 
				
			
			@ -0,0 +1,421 @@
 | 
			
		|||
import aioconsole
 | 
			
		||||
import argparse
 | 
			
		||||
import asyncio
 | 
			
		||||
import functools
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
import urllib.request
 | 
			
		||||
import websockets
 | 
			
		||||
import zlib
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        self.hints_used = 0
 | 
			
		||||
 | 
			
		||||
class Context:
 | 
			
		||||
    def __init__(self, host, port, password):
 | 
			
		||||
        self.data_filename = None
 | 
			
		||||
        self.save_filename = None
 | 
			
		||||
        self.disable_save = False
 | 
			
		||||
        self.players = 0
 | 
			
		||||
        self.rom_names = {}
 | 
			
		||||
        self.locations = {}
 | 
			
		||||
        self.host = host
 | 
			
		||||
        self.port = port
 | 
			
		||||
        self.password = password
 | 
			
		||||
        self.server = None
 | 
			
		||||
        self.clients = []
 | 
			
		||||
        self.received_items = {}
 | 
			
		||||
        self.starttime = datetime.datetime.now()
 | 
			
		||||
 | 
			
		||||
def get_room_info(ctx : Context):
 | 
			
		||||
    return {
 | 
			
		||||
        'password': ctx.password is not None,
 | 
			
		||||
        'slots': ctx.players,
 | 
			
		||||
        'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
def same_name(lhs, rhs):
 | 
			
		||||
    return lhs.lower() == rhs.lower()
 | 
			
		||||
 | 
			
		||||
def same_team(lhs, rhs):
 | 
			
		||||
    return (type(lhs) is type(rhs)) and ((not lhs and not rhs) or (lhs.lower() == rhs.lower()))
 | 
			
		||||
 | 
			
		||||
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 same_team(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 : str, text : str):
 | 
			
		||||
    print("Team notice (%s): %s" % ("Default" if not team else team, text))
 | 
			
		||||
    broadcast_team(ctx, team, [['Print', text]])
 | 
			
		||||
 | 
			
		||||
def notify_client(client : Client, text : str):
 | 
			
		||||
    if not client.auth:
 | 
			
		||||
        return
 | 
			
		||||
    print("Player notice (%s): %s" % (client.name, 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', get_room_info(ctx)]])
 | 
			
		||||
 | 
			
		||||
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 has joined the game as player %d for %s" % (client.name, client.slot, "the default team" if not client.team else "team %s" % client.team))
 | 
			
		||||
 | 
			
		||||
async def on_client_left(ctx : Context, client : Client):
 | 
			
		||||
    notify_all(ctx, "%s (Player %d, %s) has left the game" % (client.name, client.slot, "Default team" if not client.team else "Team %s" % client.team))
 | 
			
		||||
 | 
			
		||||
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: ('' if not c.team else c.team.lower(), c.slot))
 | 
			
		||||
    current_team = 0
 | 
			
		||||
    text = ''
 | 
			
		||||
    for c in auth_clients:
 | 
			
		||||
        if c.team != current_team:
 | 
			
		||||
            text += '::' + ('default team' if not c.team else c.team) + ':: '
 | 
			
		||||
            current_team = c.team
 | 
			
		||||
        text += '%d:%s ' % (c.slot, c.name)
 | 
			
		||||
    return 'Connected players: ' + text[:-1]
 | 
			
		||||
 | 
			
		||||
def get_player_name_in_team(ctx : Context, team, slot):
 | 
			
		||||
    for client in ctx.clients:
 | 
			
		||||
        if client.auth and same_team(team, client.team) and client.slot == slot:
 | 
			
		||||
            return client.name
 | 
			
		||||
    return "Player %d" % slot
 | 
			
		||||
 | 
			
		||||
def get_client_from_name(ctx : Context, name):
 | 
			
		||||
    for client in ctx.clients:
 | 
			
		||||
        if client.auth and same_name(name, client.name):
 | 
			
		||||
            return client
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
def get_received_items(ctx : Context, team, player):
 | 
			
		||||
    for (c_team, c_id), items in ctx.received_items.items():
 | 
			
		||||
        if c_id == player and same_team(c_team, team):
 | 
			
		||||
            return items
 | 
			
		||||
    ctx.received_items[(team, player)] = []
 | 
			
		||||
    return ctx.received_items[(team, player)]
 | 
			
		||||
 | 
			
		||||
def tuplize_received_items(items):
 | 
			
		||||
    return [(item.item, item.location, item.player_id, item.player_name) 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, name):
 | 
			
		||||
    all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int]
 | 
			
		||||
    notify_all(ctx, "%s (Player %d) in team %s has forfeited" % (name, slot, team if team else 'default'))
 | 
			
		||||
    register_location_checks(ctx, name, team, slot, all_locations)
 | 
			
		||||
 | 
			
		||||
def register_location_checks(ctx : Context, name, 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_id == slot:
 | 
			
		||||
                        found = True
 | 
			
		||||
                        break
 | 
			
		||||
                if not found:
 | 
			
		||||
                    new_item = ReceivedItem(target_item, location, slot, name)
 | 
			
		||||
                    recvd_items.append(new_item)
 | 
			
		||||
                    target_player_name = get_player_name_in_team(ctx, team, target_player)
 | 
			
		||||
                    broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]])
 | 
			
		||||
                    print('(%s) %s sent %s to %s (%s)' % (team if team else 'Team', name, get_item_name_from_id(target_item), target_player_name, 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((ctx.players,
 | 
			
		||||
                                      [(k, v) for k, v in 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 \
 | 
			
		||||
                'name' not in args or type(args['name']) is not str or \
 | 
			
		||||
                'team' not in args or type(args['team']) not in [str, type(None)] or \
 | 
			
		||||
                'slot' not in args or type(args['slot']) not in [int, type(None)]:
 | 
			
		||||
            await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        errors = set()
 | 
			
		||||
        if ctx.password is not None and ('password' not in args or args['password'] != ctx.password):
 | 
			
		||||
            errors.add('InvalidPassword')
 | 
			
		||||
 | 
			
		||||
        if 'name' not in args or not args['name'] or not re.match(r'\w{1,10}', args['name']):
 | 
			
		||||
            errors.add('InvalidName')
 | 
			
		||||
        elif any([same_name(c.name, args['name']) for c in ctx.clients if c.auth]):
 | 
			
		||||
            errors.add('NameAlreadyTaken')
 | 
			
		||||
        else:
 | 
			
		||||
            client.name = args['name']
 | 
			
		||||
 | 
			
		||||
        if 'team' in args and args['team'] is not None and not re.match(r'\w{1,15}', args['team']):
 | 
			
		||||
            errors.add('InvalidTeam')
 | 
			
		||||
        else:
 | 
			
		||||
            client.team = args['team'] if 'team' in args else None
 | 
			
		||||
 | 
			
		||||
        if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]):
 | 
			
		||||
            errors.add('SlotAlreadyTaken')
 | 
			
		||||
        elif 'slot' not in args or not args['slot']:
 | 
			
		||||
            for slot in range(1, ctx.players + 1):
 | 
			
		||||
                if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]:
 | 
			
		||||
                    client.slot = slot
 | 
			
		||||
                    break
 | 
			
		||||
                elif slot == ctx.players:
 | 
			
		||||
                    errors.add('SlotAlreadyTaken')
 | 
			
		||||
        elif args['slot'] not in range(1, ctx.players + 1):
 | 
			
		||||
            errors.add('InvalidSlot')
 | 
			
		||||
        else:
 | 
			
		||||
            client.slot = args['slot']
 | 
			
		||||
 | 
			
		||||
        if errors:
 | 
			
		||||
            client.name = None
 | 
			
		||||
            client.team = None
 | 
			
		||||
            client.slot = None
 | 
			
		||||
            await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
 | 
			
		||||
        else:
 | 
			
		||||
            client.auth = True
 | 
			
		||||
            reply = [['Connected', ctx.rom_names[client.slot]]]
 | 
			
		||||
            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.name, 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[:8] == '!players':
 | 
			
		||||
            notify_all(ctx, get_connected_players_string(ctx))
 | 
			
		||||
        elif args[:8] == '!forfeit':
 | 
			
		||||
            forfeit_player(ctx, client.team, client.slot, client.name)
 | 
			
		||||
        elif args.startswith("!hint"):
 | 
			
		||||
            pass
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
        command = input.split()
 | 
			
		||||
        if not command:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        if command[0] == '/exit':
 | 
			
		||||
            ctx.server.ws_server.close()
 | 
			
		||||
            break
 | 
			
		||||
        try:
 | 
			
		||||
            if command[0] == '/players':
 | 
			
		||||
                print(get_connected_players_string(ctx))
 | 
			
		||||
            elif command[0] == '/password':
 | 
			
		||||
                set_password(ctx, command[1] if len(command) > 1 else None)
 | 
			
		||||
            elif command[0] == '/kick' and len(command) > 1:
 | 
			
		||||
                client = get_client_from_name(ctx, command[1])
 | 
			
		||||
                if client and client.socket and not client.socket.closed:
 | 
			
		||||
                    await client.socket.close()
 | 
			
		||||
 | 
			
		||||
            elif command[0] == '/forfeitslot' and len(command) == 3 and command[2].isdigit():
 | 
			
		||||
                team = command[1] if command[1] != 'default' else None
 | 
			
		||||
                slot = int(command[2])
 | 
			
		||||
                name = get_player_name_in_team(ctx, team, slot)
 | 
			
		||||
                forfeit_player(ctx, team, slot, name)
 | 
			
		||||
            elif command[0] == '/forfeitplayer' and len(command) > 1:
 | 
			
		||||
                client = get_client_from_name(ctx, command[1])
 | 
			
		||||
                if client:
 | 
			
		||||
                    forfeit_player(ctx, client.team, client.slot, client.name)
 | 
			
		||||
            elif command[0] == '/senditem' and len(command) > 2:
 | 
			
		||||
                [(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
 | 
			
		||||
                if item in Items.item_table:
 | 
			
		||||
                    client = get_client_from_name(ctx, player)
 | 
			
		||||
                    if client:
 | 
			
		||||
                        new_item = ReceivedItem(Items.item_table[item][3], "cheat console", 0, "server")
 | 
			
		||||
                        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)
 | 
			
		||||
            elif command[0] == '/hint':
 | 
			
		||||
                if len(command) > 2:
 | 
			
		||||
                    player_number = int(command[1])
 | 
			
		||||
                    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 == player_number and item_id == seeked_item_id:
 | 
			
		||||
                                location_id, finding_player = check
 | 
			
		||||
                                notify_all(ctx, f"[Hint]: P{player_number}'s {item} can be found in {get_location_name_from_address(location_id)} in "
 | 
			
		||||
                                      f"P{finding_player}'s World")
 | 
			
		||||
 | 
			
		||||
                    else:
 | 
			
		||||
                        print("Unknown item: " + item)
 | 
			
		||||
                else:
 | 
			
		||||
                    print("Use /hint {playernumber} {itemname}\nFor example /hint 1 Lamp")
 | 
			
		||||
            elif command[0][0] != '/':
 | 
			
		||||
                notify_all(ctx, '[Server]: ' + input)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            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')
 | 
			
		||||
    parser.add_argument('--hint_timer', default=-1)
 | 
			
		||||
    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"))
 | 
			
		||||
            ctx.players = jsonobj[0]
 | 
			
		||||
            ctx.rom_names = {k: v for k, v 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 of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
 | 
			
		||||
 | 
			
		||||
    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"))
 | 
			
		||||
                players = jsonobj[0]
 | 
			
		||||
                rom_names = {k: v for k, v in jsonobj[1]}
 | 
			
		||||
                received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]}
 | 
			
		||||
                if players != ctx.players or rom_names != ctx.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()
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
__author__ = "Berserker55" # you can find me on the ALTTP Randomizer Discord
 | 
			
		||||
__version__ = 1.4
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
This script launches a Multiplayer "Multiworld" Mystery Game
 | 
			
		||||
 | 
			
		||||
.yaml files for all participating players should be placed in a /Players folder.
 | 
			
		||||
For every player a mystery game is rolled and a ROM created.
 | 
			
		||||
After generation the server is automatically launched.
 | 
			
		||||
It is still up to the host to forward the correct port (38281 by default) and distribute the roms to the players.
 | 
			
		||||
Regular Mystery has to work for this first, such as a ALTTP Base ROM and Enemizer Setup.
 | 
			
		||||
A guide can be found here: https://docs.google.com/document/d/19FoqUkuyStMqhOq8uGiocskMo1KMjOW4nEeG81xrKoI/edit
 | 
			
		||||
This script itself should be placed within the Bonta Multiworld folder, that you download in step 1
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
####config####
 | 
			
		||||
#location of your Enemizer CLI, available here: https://github.com/Bonta0/Enemizer/releases
 | 
			
		||||
enemizer_location:str = "EnemizerCLI/EnemizerCLI.Core.exe"
 | 
			
		||||
 | 
			
		||||
#Where to place the resulting files
 | 
			
		||||
outputpath:str = "MultiMystery"
 | 
			
		||||
 | 
			
		||||
#automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
 | 
			
		||||
#does nothing if the name is not found
 | 
			
		||||
#example: player_name = "Berserker"
 | 
			
		||||
player_name:str = ""
 | 
			
		||||
 | 
			
		||||
#Zip the resulting roms
 | 
			
		||||
#0 -> Don't
 | 
			
		||||
#1 -> Create a zip
 | 
			
		||||
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
 | 
			
		||||
zip_roms:int = 1
 | 
			
		||||
 | 
			
		||||
#create a spoiler file
 | 
			
		||||
create_spoiler:bool = True
 | 
			
		||||
 | 
			
		||||
#folder from which the player yaml files are pulled from
 | 
			
		||||
player_files_folder:str = "Players"
 | 
			
		||||
 | 
			
		||||
#Version of python to use for Bonta Multiworld. Probably leave this as is, if you don't know what this does.
 | 
			
		||||
#can be tagged for bitness, for example "3.8-32" would be latest installed 3.8 on 32 bits
 | 
			
		||||
py_version:str = "3.7"
 | 
			
		||||
####end of config####
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
def feedback(text:str):
 | 
			
		||||
    print(text)
 | 
			
		||||
    input("Press Enter to ignore and probably crash.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    if not os.path.exists(enemizer_location):
 | 
			
		||||
        feedback(f"Enemizer not found at {enemizer_location}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
 | 
			
		||||
    if not os.path.exists("Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"):
 | 
			
		||||
        feedback("Base rom is expected as Zelda no Densetsu - Kamigami no Triforce (Japan).sfc in the Multiworld root folder please place/rename it there.")
 | 
			
		||||
    player_files = []
 | 
			
		||||
    os.makedirs(player_files_folder, exist_ok=True)
 | 
			
		||||
    for file in os.listdir(player_files_folder):
 | 
			
		||||
        if file.lower().endswith(".yaml"):
 | 
			
		||||
            player_files.append(file)
 | 
			
		||||
            print(f"Player {file[:-5]} found.")
 | 
			
		||||
    if len(player_files) == 0:
 | 
			
		||||
        feedback(f"No player files found. Please put them in a {player_files_folder} folder.")
 | 
			
		||||
    player_string = ""
 | 
			
		||||
 | 
			
		||||
    for i,file in enumerate(player_files):
 | 
			
		||||
        player_string += f"--p{i+1} {os.path.join(player_files_folder, file)} "
 | 
			
		||||
 | 
			
		||||
    player_names = list(file[:-5] for file in player_files)
 | 
			
		||||
 | 
			
		||||
    command = f"py -{py_version} Mystery.py --multi {len(player_files)} {player_string} " \
 | 
			
		||||
              f"--names {','.join(player_names)} --enemizercli {enemizer_location} " \
 | 
			
		||||
              f"--outputpath {outputpath}" + " --create_spoiler" if create_spoiler else ""
 | 
			
		||||
    print(command)
 | 
			
		||||
    import time
 | 
			
		||||
    start = time.perf_counter()
 | 
			
		||||
    text = subprocess.check_output(command, shell=True).decode()
 | 
			
		||||
    print(f"Took {time.perf_counter()-start:3} seconds to generate seed.")
 | 
			
		||||
    seedname = ""
 | 
			
		||||
 | 
			
		||||
    for segment in text.split():
 | 
			
		||||
        if segment.startswith("M"):
 | 
			
		||||
            seedname = segment
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    multidataname = f"ER_{seedname}_multidata"
 | 
			
		||||
 | 
			
		||||
    romfilename = ""
 | 
			
		||||
    if player_name:
 | 
			
		||||
        try:
 | 
			
		||||
            index = player_names.index(player_name)
 | 
			
		||||
        except IndexError:
 | 
			
		||||
            print(f"Could not find Player {player_name}")
 | 
			
		||||
        else:
 | 
			
		||||
            romfilename = os.path.join(outputpath, f"ER_{seedname}_P{index+1}_{player_name}.sfc")
 | 
			
		||||
            import webbrowser
 | 
			
		||||
            if os.path.exists(romfilename):
 | 
			
		||||
                print(f"Launching ROM file {romfilename}")
 | 
			
		||||
                webbrowser.open(romfilename)
 | 
			
		||||
 | 
			
		||||
    if zip_roms:
 | 
			
		||||
        zipname = os.path.join(outputpath, f"ER_{seedname}.zip")
 | 
			
		||||
        print(f"Creating zipfile {zipname}")
 | 
			
		||||
        import zipfile
 | 
			
		||||
        with zipfile.ZipFile(zipname, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
 | 
			
		||||
            for file in os.listdir(outputpath):
 | 
			
		||||
                if file.endswith(".sfc") and seedname in file:
 | 
			
		||||
                    zf.write(os.path.join(outputpath, file), file)
 | 
			
		||||
                    print(f"Packed {file} into zipfile {zipname}")
 | 
			
		||||
                    if zip_roms == 2 and player_name.lower() not in file.lower():
 | 
			
		||||
                        os.remove(file)
 | 
			
		||||
                        print(f"Removed file {file} that is now present in the zipfile")
 | 
			
		||||
 | 
			
		||||
    serverfile = "HintedMultiServer.py" if os.path.exists("HintedMultiServer.py") else "MultiServer.py"
 | 
			
		||||
    subprocess.call(f"py -{py_version} {serverfile} --multidata {os.path.join(outputpath, multidataname)}")
 | 
			
		||||
							
								
								
									
										18
									
								
								Mystery.py
								
								
								
								
							
							
						
						
									
										18
									
								
								Mystery.py
								
								
								
								
							| 
						 | 
				
			
			@ -65,14 +65,17 @@ def main():
 | 
			
		|||
        path = getattr(args, f'p{player}')
 | 
			
		||||
        if path:
 | 
			
		||||
            if path not in weights_cache:
 | 
			
		||||
                weights_cache[path] = get_weights(path)
 | 
			
		||||
                try:
 | 
			
		||||
                    weights_cache[path] = get_weights(path)
 | 
			
		||||
                except:
 | 
			
		||||
                    raise ValueError(f"File {path} is destroyed. Please fix your yaml.")
 | 
			
		||||
            print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}")
 | 
			
		||||
 | 
			
		||||
    erargs = parse_arguments(['--multi', str(args.multi)])
 | 
			
		||||
    erargs.seed = seed
 | 
			
		||||
    erargs.names = args.names
 | 
			
		||||
    erargs.create_spoiler = args.create_spoiler
 | 
			
		||||
    erargs.race = True
 | 
			
		||||
    erargs.race = False
 | 
			
		||||
    erargs.outputname = seedname
 | 
			
		||||
    erargs.outputpath = args.outputpath
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -86,10 +89,13 @@ def main():
 | 
			
		|||
    for player in range(1, args.multi + 1):
 | 
			
		||||
        path = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
 | 
			
		||||
        if path:
 | 
			
		||||
            settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
 | 
			
		||||
            for k, v in vars(settings).items():
 | 
			
		||||
                if v is not None:
 | 
			
		||||
                    getattr(erargs, k)[player] = v
 | 
			
		||||
            try:
 | 
			
		||||
                settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
 | 
			
		||||
                for k, v in vars(settings).items():
 | 
			
		||||
                    if v is not None:
 | 
			
		||||
                        getattr(erargs, k)[player] = v
 | 
			
		||||
            except:
 | 
			
		||||
                raise ValueError(f"File {path} is destroyed. Please fix your yaml.")
 | 
			
		||||
        else:
 | 
			
		||||
            raise RuntimeError(f'No weights specified for player {player}')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue