initial upload

This commit is contained in:
Fabian Dill 2020-01-12 17:03:30 +01:00
parent 2f5a3e24dd
commit 85a4e9d409
3 changed files with 551 additions and 6 deletions

421
HintedMultiServer.py Normal file
View File

@ -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()

118
MultiMystery.py Normal file
View File

@ -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)}")

View File

@ -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}')