move networks commands to [str, Optional[dict]] across the board

and some other updates
PrintHTML is an experiment and is unlikely the solution I'll go with
This commit is contained in:
Fabian Dill 2021-01-21 23:37:58 +01:00
parent 670b8b4b11
commit c604dfe509
6 changed files with 246 additions and 177 deletions

View File

@ -8,12 +8,9 @@ import functools
import webbrowser import webbrowser
import multiprocessing import multiprocessing
import socket import socket
import sys
import typing
import os import os
import subprocess import subprocess
import base64 import base64
from json import loads, dumps
from random import randrange from random import randrange
@ -26,11 +23,10 @@ import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
import colorama import colorama
import websockets
import prompt_toolkit import prompt_toolkit
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from NetUtils import Endpoint from NetUtils import *
import WebUI import WebUI
from worlds.alttp import Regions from worlds.alttp import Regions
@ -44,10 +40,10 @@ logger = logging.getLogger("Client")
def create_named_task(coro, *args, name=None): def create_named_task(coro, *args, name=None):
if not name: if not name:
name = coro.__name__ name = coro.__name__
if sys.version_info.major > 2 and sys.version_info.minor > 7: return asyncio.create_task(coro, *args, name=name)
return asyncio.create_task(coro, *args, name=name)
else:
return asyncio.create_task(coro, *args) coloramaparser = HTMLtoColoramaParser()
class Context(): class Context():
@ -125,19 +121,6 @@ class Context():
await self.server.socket.send(dumps(msgs)) await self.server.socket.send(dumps(msgs))
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
def color_code(*args):
return '\033[' + ';'.join([str(color_codes[arg]) for arg in args]) + 'm'
def color(text, *args):
return color_code(*args) + text + color_code('reset')
START_RECONNECT_DELAY = 5 START_RECONNECT_DELAY = 5
SNES_RECONNECT_DELAY = 5 SNES_RECONNECT_DELAY = 5
SERVER_RECONNECT_DELAY = 5 SERVER_RECONNECT_DELAY = 5
@ -516,7 +499,6 @@ async def snes_connect(ctx: Context, address):
await snes_disconnect(ctx) await snes_disconnect(ctx)
return return
logger.info("Attaching to " + device) logger.info("Attaching to " + device)
Attach_Request = { Attach_Request = {
@ -532,7 +514,7 @@ async def snes_connect(ctx: Context, address):
if 'sd2snes' in device.lower() or 'COM' in device: if 'sd2snes' in device.lower() or 'COM' in device:
logger.info("SD2SNES/FXPAK Detected") logger.info("SD2SNES/FXPAK Detected")
ctx.is_sd2snes = True ctx.is_sd2snes = True
await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"})) await ctx.snes_socket.send(dumps({"Opcode": "Info", "Space": "SNES"}))
reply = loads(await ctx.snes_socket.recv()) reply = loads(await ctx.snes_socket.recv())
if reply and 'Results' in reply: if reply and 'Results' in reply:
logger.info(reply['Results']) logger.info(reply['Results'])
@ -605,7 +587,7 @@ async def snes_recv_loop(ctx: Context):
asyncio.create_task(snes_autoreconnect(ctx)) asyncio.create_task(snes_autoreconnect(ctx))
async def snes_read(ctx : Context, address, size): async def snes_read(ctx: Context, address, size):
try: try:
await ctx.snes_request_lock.acquire() await ctx.snes_request_lock.acquire()
@ -613,9 +595,9 @@ async def snes_read(ctx : Context, address, size):
return None return None
GetAddress_Request = { GetAddress_Request = {
"Opcode" : "GetAddress", "Opcode": "GetAddress",
"Space" : "SNES", "Space": "SNES",
"Operands" : [hex(address)[2:], hex(size)[2:]] "Operands": [hex(address)[2:], hex(size)[2:]]
} }
try: try:
await ctx.snes_socket.send(dumps(GetAddress_Request)) await ctx.snes_socket.send(dumps(GetAddress_Request))
@ -634,7 +616,7 @@ async def snes_read(ctx : Context, address, size):
if len(data): if len(data):
logger.error(str(data)) logger.error(str(data))
logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.' logger.warning('Unable to connect to SNES Device because QUsb2Snes broke temporarily.'
'Try un-selecting and re-selecting the SNES Device.') 'Try un-selecting and re-selecting the SNES Device.')
if ctx.snes_socket is not None and not ctx.snes_socket.closed: if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
return None return None
@ -644,7 +626,7 @@ async def snes_read(ctx : Context, address, size):
ctx.snes_request_lock.release() ctx.snes_request_lock.release()
async def snes_write(ctx : Context, write_list): async def snes_write(ctx: Context, write_list):
try: try:
await ctx.snes_request_lock.acquire() await ctx.snes_request_lock.acquire()
@ -661,15 +643,15 @@ async def snes_write(ctx : Context, write_list):
logger.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data))) logger.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
return False return False
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START): for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
cmd += b'\xA9' # LDA cmd += b'\xA9' # LDA
cmd += bytes([byte]) cmd += bytes([byte])
cmd += b'\x8F' # STA.l cmd += b'\x8F' # STA.l
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF]) cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08' cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
PutAddress_Request['Space'] = 'CMD' PutAddress_Request['Space'] = 'CMD'
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"] PutAddress_Request['Operands'] = ["2C00", hex(len(cmd) - 1)[2:], "2C00", "1"]
try: try:
if ctx.snes_socket is not None: if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request)) await ctx.snes_socket.send(dumps(PutAddress_Request))
@ -681,7 +663,7 @@ async def snes_write(ctx : Context, write_list):
else: else:
PutAddress_Request['Space'] = 'SNES' PutAddress_Request['Space'] = 'SNES'
try: try:
#will pack those requests as soon as qusb2snes actually supports that for real # will pack those requests as soon as qusb2snes actually supports that for real
for address, data in write_list: for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None: if ctx.snes_socket is not None:
@ -697,7 +679,7 @@ async def snes_write(ctx : Context, write_list):
ctx.snes_request_lock.release() ctx.snes_request_lock.release()
def snes_buffered_write(ctx : Context, address, data): def snes_buffered_write(ctx: Context, address, data):
if ctx.snes_write_buffer and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address: if ctx.snes_write_buffer and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
# append to existing write command, bundling them # append to existing write command, bundling them
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data) ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
@ -705,7 +687,7 @@ def snes_buffered_write(ctx : Context, address, data):
ctx.snes_write_buffer.append((address, data)) ctx.snes_write_buffer.append((address, data))
async def snes_flush_writes(ctx : Context): async def snes_flush_writes(ctx: Context):
if not ctx.snes_write_buffer: if not ctx.snes_write_buffer:
return return
@ -733,11 +715,11 @@ async def server_loop(ctx: Context, address=None):
if address is None: # see if this is an old connection if address is None: # see if this is an old connection
await asyncio.sleep(0.5) # wait for snes connection to succeed if possible. await asyncio.sleep(0.5) # wait for snes connection to succeed if possible.
rom = ctx.rom if ctx.rom else None rom = ctx.rom if ctx.rom else None
if rom:
servers = Utils.persistent_load()["servers"] servers = Utils.persistent_load()["servers"]
if rom in servers: if rom in servers:
address = servers[rom] address = servers[rom]
cached_address = True cached_address = True
# Wait for the user to provide a multiworld server address # Wait for the user to provide a multiworld server address
if not address: if not address:
@ -758,15 +740,14 @@ async def server_loop(ctx: Context, address=None):
SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY
async for data in ctx.server.socket: async for data in ctx.server.socket:
for msg in loads(data): for msg in loads(data):
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None) await process_server_cmd(ctx, msg[0], msg[1])
await process_server_cmd(ctx, cmd, args)
logger.warning('Disconnected from multiworld server, type /connect to reconnect') logger.warning('Disconnected from multiworld server, type /connect to reconnect')
except WebUI.WaitingForUiException: except WebUI.WaitingForUiException:
pass pass
except ConnectionRefusedError: except ConnectionRefusedError:
if cached_address: if cached_address:
logger.error('Unable to connect to multiworld server at cached address. ' logger.error('Unable to connect to multiworld server at cached address. '
'Please use the connect button above.') 'Please use the connect button above.')
else: else:
logger.error('Connection refused by the multiworld server') logger.error('Connection refused by the multiworld server')
except (OSError, websockets.InvalidURI): except (OSError, websockets.InvalidURI):
@ -791,6 +772,7 @@ async def server_loop(ctx: Context, address=None):
asyncio.create_task(server_autoreconnect(ctx)) asyncio.create_task(server_autoreconnect(ctx))
SERVER_RECONNECT_DELAY *= 2 SERVER_RECONNECT_DELAY *= 2
async def server_autoreconnect(ctx: Context): async def server_autoreconnect(ctx: Context):
# unfortunately currently broken. See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1033 # unfortunately currently broken. See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1033
# with prompt_toolkit.shortcuts.ProgressBar() as pb: # with prompt_toolkit.shortcuts.ProgressBar() as pb:
@ -801,7 +783,8 @@ async def server_autoreconnect(ctx: Context):
ctx.server_task = asyncio.create_task(server_loop(ctx)) ctx.server_task = asyncio.create_task(server_loop(ctx))
async def process_server_cmd(ctx: Context, cmd, args): async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]):
if cmd == 'RoomInfo': if cmd == 'RoomInfo':
logger.info('--------------------------------') logger.info('--------------------------------')
logger.info('Room Information:') logger.info('Room Information:')
@ -815,7 +798,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
logger.info("Server protocol tags: " + ", ".join(args["tags"])) logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']: if args['password']:
logger.info('Password required') logger.info('Password required')
if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here
logging.info(f"Forfeit setting: {args['forfeit_mode']}") logging.info(f"Forfeit setting: {args['forfeit_mode']}")
logging.info(f"Remaining setting: {args['remaining_mode']}") logging.info(f"Remaining setting: {args['remaining_mode']}")
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}" logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
@ -839,64 +822,71 @@ async def process_server_cmd(ctx: Context, cmd, args):
await server_auth(ctx, args['password']) await server_auth(ctx, args['password'])
elif cmd == 'ConnectionRefused': elif cmd == 'ConnectionRefused':
if 'InvalidRom' in args: errors = args["errors"]
if 'InvalidRom' in errors:
if ctx.snes_socket is not None and not ctx.snes_socket.closed: if ctx.snes_socket is not None and not ctx.snes_socket.closed:
asyncio.create_task(ctx.snes_socket.close()) asyncio.create_task(ctx.snes_socket.close())
raise Exception('Invalid ROM detected, ' raise Exception('Invalid ROM detected, '
'please verify that you have loaded the correct rom and reconnect your snes (/snes)') 'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
elif 'SlotAlreadyTaken' in args: elif 'SlotAlreadyTaken' in errors:
Utils.persistent_store("servers", ctx.rom, ctx.server_address) Utils.persistent_store("servers", ctx.rom, ctx.server_address)
raise Exception('Player slot already in use for that team') raise Exception('Player slot already in use for that team')
elif 'IncompatibleVersion' in args: elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible') raise Exception('Server reported your client version as incompatible')
#last to check, recoverable problem # last to check, recoverable problem
elif 'InvalidPassword' in args: elif 'InvalidPassword' in errors:
logger.error('Invalid password') logger.error('Invalid password')
ctx.password = None ctx.password = None
await server_auth(ctx, True) await server_auth(ctx, True)
else: else:
raise Exception("Unknown connection errors: "+str(args)) raise Exception("Unknown connection errors: " + str(errors))
raise Exception('Connection refused by the multiworld host, no reason provided') raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected': elif cmd == 'Connected':
if ctx.send_unsafe: if ctx.send_unsafe:
ctx.send_unsafe = False ctx.send_unsafe = False
logger.info(f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true') logger.info(
f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true')
Utils.persistent_store("servers", ctx.rom, ctx.server_address) Utils.persistent_store("servers", ctx.rom, ctx.server_address)
ctx.team, ctx.slot = args[0] ctx.team = args["team"]
ctx.player_names = {p: n for p, n in args[1]} ctx.slot = args["slot"]
ctx.player_names = {p: n for p, n in args["playernames"]}
msgs = [] msgs = []
if ctx.locations_checked: if ctx.locations_checked:
msgs.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) msgs.append(['LocationChecks',
{"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}])
if ctx.locations_scouted: if ctx.locations_scouted:
msgs.append(['LocationScouts', list(ctx.locations_scouted)]) msgs.append(['LocationScouts', {"locations": list(ctx.locations_scouted)}])
if msgs: if msgs:
await ctx.send_msgs(msgs) await ctx.send_msgs(msgs)
if ctx.finished_game: if ctx.finished_game:
await send_finished_game(ctx) await send_finished_game(ctx)
ctx.items_missing = args[2] if len(args) >= 3 else [] # Get the server side view of missing as of time of connecting.
# Get the server side view of missing as of time of connecting.
# This list is used to only send to the server what is reported as ACTUALLY Missing. # This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously # This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing. # when /missing is used for the client side view of what is missing.
if not ctx.items_missing: ctx.items_missing = args["missing_checks"]
asyncio.create_task(ctx.send_msgs([['Say', '!missing']]))
elif cmd == 'ReceivedItems': elif cmd == 'ReceivedItems':
start_index, items = args start_index = args["index"]
if start_index == 0: if start_index == 0:
ctx.items_received = [] ctx.items_received = []
elif start_index != len(ctx.items_received): elif start_index != len(ctx.items_received):
sync_msg = [['Sync']] sync_msg = [['Sync', None]]
if ctx.locations_checked: if ctx.locations_checked:
sync_msg.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) sync_msg.append(['LocationChecks',
{"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}])
await ctx.send_msgs(sync_msg) await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received): if start_index == len(ctx.items_received):
for item in items:
for item in args['items']:
ctx.items_received.append(ReceivedItem(*item)) ctx.items_received.append(ReceivedItem(*item))
ctx.watcher_event.set() ctx.watcher_event.set()
elif cmd == 'LocationInfo': elif cmd == 'LocationInfo':
for location, item, player in args: for item, location, player in args['locations']:
if location not in ctx.locations_info: if location not in ctx.locations_info:
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'} replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
item_name = replacements.get(item, get_item_name_from_id(item)) item_name = replacements.get(item, get_item_name_from_id(item))
@ -906,19 +896,20 @@ async def process_server_cmd(ctx: Context, cmd, args):
ctx.watcher_event.set() ctx.watcher_event.set()
elif cmd == 'ItemSent': elif cmd == 'ItemSent':
player_sent, location, player_recvd, item = args found = ReceivedItem(*args["item"])
ctx.ui_node.notify_item_sent(ctx.player_names[player_sent], ctx.player_names[player_recvd], receiving_player = args["receiver"]
get_item_name_from_id(item), get_location_name_from_address(location), ctx.ui_node.notify_item_sent(ctx.player_names[found.player], ctx.player_names[receiving_player],
player_sent == ctx.slot, player_recvd == ctx.slot) get_item_name_from_id(found.item), get_location_name_from_address(found.location),
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green') found.player == ctx.slot, receiving_player == ctx.slot)
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta') item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green')
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta') found_player = color(ctx.player_names[found.player], 'yellow' if found.player != ctx.slot else 'magenta')
receiving_player = color(ctx.player_names[receiving_player], 'yellow' if receiving_player != ctx.slot else 'magenta')
logging.info( logging.info(
'%s sent %s to %s (%s)' % (player_sent, item, player_recvd, color(get_location_name_from_address(location), '%s sent %s to %s (%s)' % (found_player, item, receiving_player,
'blue_bg', 'white'))) color(get_location_name_from_address(found.location), 'blue_bg', 'white')))
elif cmd == 'ItemFound': elif cmd == 'ItemFound':
found = ReceivedItem(*args) found = ReceivedItem(*args["item"])
ctx.ui_node.notify_item_found(ctx.player_names[found.player], get_item_name_from_id(found.item), ctx.ui_node.notify_item_found(ctx.player_names[found.player], get_item_name_from_id(found.item),
get_location_name_from_address(found.location), found.player == ctx.slot) get_location_name_from_address(found.location), found.player == ctx.slot)
item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green') item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green')
@ -926,9 +917,8 @@ async def process_server_cmd(ctx: Context, cmd, args):
logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location), logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location),
'blue_bg', 'white'))) 'blue_bg', 'white')))
elif cmd == 'Hint': elif cmd == 'Hint':
hints = [Utils.Hint(*hint) for hint in args] hints = [Utils.Hint(*hint) for hint in args["hints"]]
for hint in hints: for hint in hints:
ctx.ui_node.send_hint(ctx.player_names[hint.finding_player], ctx.player_names[hint.receiving_player], ctx.ui_node.send_hint(ctx.player_names[hint.finding_player], ctx.player_names[hint.receiving_player],
get_item_name_from_id(hint.item), get_location_name_from_address(hint.location), get_item_name_from_id(hint.item), get_location_name_from_address(hint.location),
@ -947,17 +937,24 @@ async def process_server_cmd(ctx: Context, cmd, args):
text += " at " + color(hint.entrance, 'white_bg', 'black') text += " at " + color(hint.entrance, 'white_bg', 'black')
logging.info(text + (f". {color('(found)', 'green_bg', 'black')} " if hint.found else ".")) logging.info(text + (f". {color('(found)', 'green_bg', 'black')} " if hint.found else "."))
elif cmd == "AliasUpdate": elif cmd == "RoomUpdate":
ctx.player_names = {p: n for p, n in args} if "playernames" in args:
ctx.player_names = {p: n for p, n in args["playernames"]}
elif cmd == 'Print': elif cmd == 'Print':
logger.info(args) logger.info(args["text"])
elif cmd == 'PrintHTML':
logger.info(coloramaparser.get_colorama_text(args["text"]))
elif cmd == 'HintPointUpdate': elif cmd == 'HintPointUpdate':
ctx.hint_points = args[0] ctx.hint_points = args['points']
elif cmd == 'InvalidArguments':
logger.warning(f"Invalid Arguments: {args['text']}")
else: else:
logger.debug(f"unknown command {args}") logger.debug(f"unknown command {cmd}")
def get_tags(ctx: Context): def get_tags(ctx: Context):
@ -981,11 +978,11 @@ async def server_auth(ctx: Context, password_requested):
auth = base64.b64encode(ctx.rom).decode() auth = base64.b64encode(ctx.rom).decode()
await ctx.send_msgs([['Connect', { await ctx.send_msgs([['Connect', {
'password': ctx.password, 'rom': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx), 'password': ctx.password, 'rom': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx),
'uuid': Utils.get_unique_identifier() 'uuid': Utils.get_unique_identifier(), 'game': "ALTTP"
}]]) }]])
async def console_input(ctx : Context): async def console_input(ctx: Context):
ctx.input_requests += 1 ctx.input_requests += 1
return await ctx.input_queue.get() return await ctx.input_queue.get()
@ -1081,7 +1078,8 @@ class ClientCommandProcessor(CommandProcessor):
count += 1 count += 1
if count: if count:
self.output(f"Found {count} missing location checks{f'. {checked_count} locations checks previously visited.' if checked_count else ''}") self.output(
f"Found {count} missing location checks{f'. {checked_count} locations checks previously visited.' if checked_count else ''}")
else: else:
self.output("No missing location checks found.") self.output("No missing location checks found.")
return True return True
@ -1115,13 +1113,14 @@ class ClientCommandProcessor(CommandProcessor):
"""Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect.""" """Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
if toggle: if toggle:
self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"} self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
logger.info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.') logger.info(
f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
else: else:
logger.info("You must specify /send_unsafe true explicitly.") logger.info("You must specify /send_unsafe true explicitly.")
self.ctx.send_unsafe = False self.ctx.send_unsafe = False
def default(self, raw: str): def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([['Say', raw]])) asyncio.create_task(self.ctx.send_msgs([['Say', {"text": raw}]]))
async def console_loop(ctx: Context): async def console_loop(ctx: Context):
@ -1145,7 +1144,7 @@ async def console_loop(ctx: Context):
await snes_flush_writes(ctx) await snes_flush_writes(ctx)
async def track_locations(ctx : Context, roomid, roomdata): async def track_locations(ctx: Context, roomid, roomdata):
new_locations = [] new_locations = []
def new_check(location): def new_check(location):
@ -1155,13 +1154,14 @@ async def track_locations(ctx : Context, roomid, roomdata):
for location, (loc_roomid, loc_mask) in location_table_uw.items(): for location, (loc_roomid, loc_mask) in location_table_uw.items():
try: try:
if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0: if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (
roomdata << 4) & loc_mask != 0:
new_check(location) new_check(location)
except Exception as e: except Exception as e:
logger.exception(f"Exception: {e}") logger.exception(f"Exception: {e}")
uw_begin = 0x129 uw_begin = 0x129
uw_end = 0 ow_end = uw_end = 0
uw_unchecked = {} uw_unchecked = {}
for location, (roomid, mask) in location_table_uw.items(): for location, (roomid, mask) in location_table_uw.items():
if location not in ctx.unsafe_locations_checked: if location not in ctx.unsafe_locations_checked:
@ -1178,7 +1178,6 @@ async def track_locations(ctx : Context, roomid, roomdata):
new_check(location) new_check(location)
ow_begin = 0x82 ow_begin = 0x82
ow_end = 0
ow_unchecked = {} ow_unchecked = {}
for location, screenid in location_table_ow.items(): for location, screenid in location_table_ow.items():
if location not in ctx.unsafe_locations_checked: if location not in ctx.unsafe_locations_checked:
@ -1204,7 +1203,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4) misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
if misc_data is not None: if misc_data is not None:
for location, (offset, mask) in location_table_misc.items(): for location, (offset, mask) in location_table_misc.items():
assert(0x3c6 <= offset <= 0x3c9) assert (0x3c6 <= offset <= 0x3c9)
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.unsafe_locations_checked: if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.unsafe_locations_checked:
new_check(location) new_check(location)
@ -1213,18 +1212,19 @@ async def track_locations(ctx : Context, roomid, roomdata):
ctx.locations_checked.add(location) ctx.locations_checked.add(location)
new_locations.append(Regions.lookup_name_to_id[location]) new_locations.append(Regions.lookup_name_to_id[location])
await ctx.send_msgs([['LocationChecks', new_locations]]) if new_locations:
await ctx.send_msgs([['LocationChecks', {"locations": new_locations}]])
async def send_finished_game(ctx: Context): async def send_finished_game(ctx: Context):
try: try:
await ctx.send_msgs([['GameFinished', '']]) await ctx.send_msgs([['StatusUpdate', {"status": CLIENT_GOAL}]])
ctx.finished_game = True ctx.finished_game = True
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
async def game_watcher(ctx : Context): async def game_watcher(ctx: Context):
prev_game_timer = 0 prev_game_timer = 0
perf_counter = time.perf_counter() perf_counter = time.perf_counter()
while not ctx.exit_event.is_set(): while not ctx.exit_event.is_set():
@ -1316,7 +1316,7 @@ async def game_watcher(ctx : Context):
if scout_location > 0 and scout_location not in ctx.locations_scouted: if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location) ctx.locations_scouted.add(scout_location)
logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}') logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
await ctx.send_msgs([['LocationScouts', [scout_location]]]) await ctx.send_msgs([['LocationScouts', {"locations": [scout_location]}]])
await track_locations(ctx, roomid, roomdata) await track_locations(ctx, roomid, roomdata)
@ -1391,7 +1391,8 @@ async def main():
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--founditems', default=False, action='store_true', parser.add_argument('--founditems', default=False, action='store_true',
help='Show items found by other players for themselves.') help='Show items found by other players for themselves.')
parser.add_argument('--disable_web_ui', default=False, action='store_true', help="Turn off emitting a webserver for the webbrowser based user interface.") parser.add_argument('--disable_web_ui', default=False, action='store_true',
help="Turn off emitting a webserver for the webbrowser based user interface.")
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
@ -1454,6 +1455,7 @@ async def main():
await input_task await input_task
if __name__ == '__main__': if __name__ == '__main__':
colorama.init() colorama.init()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

View File

@ -20,6 +20,7 @@ import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
import websockets import websockets
import colorama
import prompt_toolkit import prompt_toolkit
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from fuzzywuzzy import process as fuzzy_process from fuzzywuzzy import process as fuzzy_process
@ -28,14 +29,11 @@ from worlds.alttp import Items, Regions
import Utils import Utils
from Utils import get_item_name_from_id, get_location_name_from_address, \ from Utils import get_item_name_from_id, get_location_name_from_address, \
ReceivedItem, _version_tuple, restricted_loads ReceivedItem, _version_tuple, restricted_loads
from NetUtils import Node, Endpoint from NetUtils import Node, Endpoint, CLIENT_GOAL
colorama.init()
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups) | set(Regions.key_drop_data)) console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups) | set(Regions.key_drop_data))
CLIENT_PLAYING = 0
CLIENT_GOAL = 1
class Client(Endpoint): class Client(Endpoint):
version: typing.List[int] = [0, 0, 0] version: typing.List[int] = [0, 0, 0]
@ -196,8 +194,8 @@ class Context(Node):
self.saving = enabled self.saving = enabled
if self.saving: if self.saving:
if not self.save_filename: if not self.save_filename:
self.save_filename = (self.data_filename[:-9] if self.data_filename.endswith('.archipelago') else ( self.save_filename = (self.data_filename[:-11] if self.data_filename.endswith('.archipelago') else (
self.data_filename + '_')) + 'save' self.data_filename + '_')) + 'apsave'
try: try:
with open(self.save_filename, 'rb') as f: with open(self.save_filename, 'rb') as f:
save_data = restricted_loads(zlib.decompress(f.read())) save_data = restricted_loads(zlib.decompress(f.read()))
@ -301,18 +299,18 @@ class Context(Node):
def notify_all(self, text): def notify_all(self, text):
logging.info("Notice (all): %s" % text) logging.info("Notice (all): %s" % text)
self.broadcast_all([['Print', text]]) self.broadcast_all([['Print', {"text": text}]])
def notify_client(self, client: Client, text: str): def notify_client(self, client: Client, text: str):
if not client.auth: if not client.auth:
return return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
asyncio.create_task(self.send_msgs(client, [['Print', text]])) asyncio.create_task(self.send_msgs(client, [['Print', {"text": text}]]))
def notify_client_multiple(self, client: Client, texts: typing.List[str]): def notify_client_multiple(self, client: Client, texts: typing.List[str]):
if not client.auth: if not client.auth:
return return
asyncio.create_task(self.send_msgs(client, [['Print', text] for text in texts])) asyncio.create_task(self.send_msgs(client, [['Print', {"text": text}] for text in texts]))
def broadcast_team(self, team, msgs): def broadcast_team(self, team, msgs):
for client in self.endpoints: for client in self.endpoints:
@ -332,8 +330,8 @@ class Context(Node):
# separated out, due to compatibilty between clients # separated out, due to compatibilty between clients
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
cmd = dumps([["Hint", hints]]) # make sure it is a list, as it can be set internally cmd = dumps([["Hint", {"hints", hints}]])
texts = [['Print', format_hint(ctx, team, hint)] for hint in hints] texts = [['PrintHTML', format_hint(ctx, team, hint)] for hint in hints]
for _, text in texts: for _, text in texts:
logging.info("Notice (Team #%d): %s" % (team + 1, text)) logging.info("Notice (Team #%d): %s" % (team + 1, text))
for client in ctx.endpoints: for client in ctx.endpoints:
@ -342,9 +340,9 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None): def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
cmd = dumps([["AliasUpdate", cmd = dumps([["RoomUpdate",
[(key[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if {"playernames": [(key[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if
key[0] == team]]]) key[0] == team]}]])
if client is None: if client is None:
for client in ctx.endpoints: for client in ctx.endpoints:
if client.team == team and client.auth: if client.team == team and client.auth:
@ -361,13 +359,7 @@ async def server(websocket, path, ctx: Context):
await on_client_connected(ctx, client) await on_client_connected(ctx, client)
async for data in websocket: async for data in websocket:
for msg in loads(data): for msg in loads(data):
if len(msg) == 1: await process_client_cmd(ctx, client, msg[0], 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: except Exception as e:
if not isinstance(e, websockets.WebSocketException): if not isinstance(e, websockets.WebSocketException):
logging.exception(e) logging.exception(e)
@ -401,12 +393,6 @@ async def on_client_joined(ctx: Context, client: Client):
ctx.notify_all( ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. " f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. "
f"Client({version_str}), {client.tags}).") f"Client({version_str}), {client.tags}).")
if client.version < [2, 1, 0] and "auto" in ctx.forfeit_mode:
ctx.notify_client(
client,
"Your client is too old to send game beaten information. "
"The automatic forfeit feature will not work."
)
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)
@ -469,7 +455,8 @@ def send_new_items(ctx: Context):
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if len(items) > client.send_index: if len(items) > client.send_index:
asyncio.create_task(ctx.send_msgs(client, [ asyncio.create_task(ctx.send_msgs(client, [
['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]])) ['ReceivedItems', {"index": client.send_index,
"items": tuplize_received_items(items)[client.send_index:]}]]))
client.send_index = len(items) client.send_index = len(items)
@ -488,7 +475,7 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
return sorted(items) return sorted(items)
def register_location_checks(ctx: Context, team: int, slot: int, locations): def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]):
found_items = False found_items = False
new_locations = set(locations) - ctx.location_checks[team, slot] new_locations = set(locations) - ctx.location_checks[team, slot]
known_locations = set() known_locations = set()
@ -499,7 +486,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
known_locations.add(location) known_locations.add(location)
target_item, target_player = ctx.locations[(location, slot)] target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot or slot in ctx.remote_items: if target_player != slot or slot in ctx.remote_items:
found = False found: bool = False
recvd_items = get_received_items(ctx, team, target_player) recvd_items = get_received_items(ctx, team, target_player)
for recvd_item in recvd_items: for recvd_item in recvd_items:
if recvd_item.location == location and recvd_item.player == slot: if recvd_item.location == location and recvd_item.player == slot:
@ -510,7 +497,9 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
new_item = ReceivedItem(target_item, location, slot) new_item = ReceivedItem(target_item, location, slot)
recvd_items.append(new_item) recvd_items.append(new_item)
if slot != target_player: if slot != target_player:
ctx.broadcast_team(team, [['ItemSent', (slot, location, target_player, target_item)]]) ctx.broadcast_team(team,
[['ItemSent', {"item": new_item,
"receiver" : target_player}]])
logging.info('(Team #%d) %s sent %s to %s (%s)' % ( logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item),
ctx.player_names[(team, target_player)], get_location_name_from_address(location))) ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
@ -520,20 +509,21 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
for client in ctx.endpoints: for client in ctx.endpoints:
if client.team == team and client.wants_item_notification: if client.team == team and client.wants_item_notification:
asyncio.create_task( asyncio.create_task(
ctx.send_msgs(client, [['ItemFound', (target_item, location, slot)]])) ctx.send_msgs(client, [['ItemFound',
{"item": ReceivedItem(target_item, location, slot)}]]))
ctx.location_checks[team, slot] |= known_locations ctx.location_checks[team, slot] |= known_locations
send_new_items(ctx) send_new_items(ctx)
if found_items: if found_items:
for client in ctx.endpoints: for client in ctx.endpoints:
if client.team == team and client.slot == slot: if client.team == team and client.slot == slot:
asyncio.create_task(ctx.send_msgs(client, [["HintPointUpdate", (get_client_points(ctx, client),)]])) asyncio.create_task(ctx.send_msgs(client, [["HintPointUpdate", {"points": get_client_points(ctx, client)}]]))
ctx.save() ctx.save()
def notify_team(ctx: Context, team: int, text: str): def notify_team(ctx: Context, team: int, text: str):
logging.info("Notice (Team #%d): %s" % (team + 1, text)) logging.info("Notice (Team #%d): %s" % (team + 1, text))
ctx.broadcast_team(team, [['Print', text]]) ctx.broadcast_team(team, [['Print', {"text": text}]])
@ -974,23 +964,26 @@ def get_client_points(ctx: Context, client: Client) -> int:
ctx.hint_cost * ctx.hints_used[client.team, client.slot]) ctx.hint_cost * ctx.hints_used[client.team, client.slot])
async def process_client_cmd(ctx: Context, client: Client, cmd, args): async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typing.Optional[dict]):
if type(cmd) is not str: if type(cmd) is not str:
await ctx.send_msgs(client, [['InvalidCmd']]) await ctx.send_msgs(client, [['InvalidCmd', {"text": f"Command should be str, got {type(cmd)}"}]])
return
if args is not None and type(args) != dict:
await ctx.send_msgs(client, [['InvalidArguments',
{'text': f'Expected Optional[dict], got {type(args)} for {cmd}'}]])
return return
if cmd == 'Connect': if cmd == 'Connect':
if not args or type(args) is not dict or \ if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
'password' not in args or type(args['password']) not in [str, type(None)] or \ 'game' not in args:
'rom' not in args or type(args['rom']) not in (list, str): await ctx.send_msgs(client, [['InvalidArguments', {'text': 'Connect'}]])
await ctx.send_msgs(client, [['InvalidArguments', 'Connect']])
return return
errors = set() errors = set()
if ctx.password is not None and args['password'] != ctx.password: if ctx.password and args['password'] != ctx.password:
errors.add('InvalidPassword') errors.add('InvalidPassword')
if type(args["rom"]) == list:
args["rom"] = bytes(letter for letter in args["rom"]).decode()
if args['rom'] not in ctx.rom_names: if args['rom'] not in ctx.rom_names:
logging.info((args["rom"], ctx.rom_names)) logging.info((args["rom"], ctx.rom_names))
errors.add('InvalidRom') errors.add('InvalidRom')
@ -1012,28 +1005,29 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
client.team = team client.team = team
client.slot = slot client.slot = slot
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0)))) minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
if minver > tuple(args.get('version', Client.version)): if minver > tuple(args['version']):
errors.add('IncompatibleVersion') errors.add('IncompatibleVersion')
if ctx.compatibility == 1 and "AP" not in args.get('tags', Client.tags): if ctx.compatibility == 1 and "AP" not in args['tags']:
errors.add('IncompatibleVersion') errors.add('IncompatibleVersion')
#only exact version match allowed #only exact version match allowed
elif ctx.compatibility == 0 and tuple(args.get('version', Client.version)) != _version_tuple: elif ctx.compatibility == 0 and tuple(args['version']) != _version_tuple:
errors.add('IncompatibleVersion') errors.add('IncompatibleVersion')
if errors: if errors:
logging.info(f"A client connection was refused due to: {errors}") logging.info(f"A client connection was refused due to: {errors}")
await ctx.send_msgs(client, [['ConnectionRefused', list(errors)]]) await ctx.send_msgs(client, [['ConnectionRefused', {"errors": list(errors)}]])
else: else:
ctx.client_ids[client.team, client.slot] = args.get("uuid", None) ctx.client_ids[client.team, client.slot] = args["uuid"]
client.auth = True client.auth = True
client.version = args.get('version', Client.version) client.version = args['version']
client.tags = args.get('tags', Client.tags) client.tags = args['tags']
reply = [['Connected', [(client.team, client.slot), reply = [['Connected', {"team": client.team, "slot": client.slot,
[(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if "playernames": [(p, ctx.get_aliased_name(t, p)) for (t, p), n in
t == client.team], get_missing_checks(ctx, client)]]] ctx.player_names.items() if t == client.team],
"missing_checks": get_missing_checks(ctx, client)}]]
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))]) reply.append(['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}])
client.send_index = len(items) client.send_index = len(items)
await ctx.send_msgs(client, reply) await ctx.send_msgs(client, reply)
await on_client_joined(ctx, client) await on_client_joined(ctx, client)
@ -1043,22 +1037,16 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
client.send_index = len(items) client.send_index = len(items)
await ctx.send_msgs(client, [['ReceivedItems', (0, tuplize_received_items(items))]]) await ctx.send_msgs(client, [['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}]])
elif cmd == 'LocationChecks': elif cmd == 'LocationChecks':
if type(args) is not list: register_location_checks(ctx, client.team, client.slot, args["locations"])
await ctx.send_msgs(client, [['InvalidArguments', 'LocationChecks']])
return
register_location_checks(ctx, client.team, client.slot, args)
elif cmd == 'LocationScouts': elif cmd == 'LocationScouts':
if type(args) is not list:
await ctx.send_msgs(client, [['InvalidArguments', 'LocationScouts']])
return
locs = [] locs = []
for location in args: for location in args["locations"]:
if type(location) is not int or 0 >= location > len(Regions.location_table): if type(location) is not int or 0 >= location > len(Regions.location_table):
await ctx.send_msgs(client, [['InvalidArguments', 'LocationScouts']]) await ctx.send_msgs(client, [['InvalidArguments', {"text": 'LocationScouts'}]])
return return
loc_name = list(Regions.location_table.keys())[location - 1] loc_name = list(Regions.location_table.keys())[location - 1]
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)] target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
@ -1068,31 +1056,34 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
if item_type: if item_type:
target_item = replacements.get(item_type[0], target_item) target_item = replacements.get(item_type[0], target_item)
locs.append([loc_name, location, target_item, target_player]) locs.append([target_item, location, target_player])
# logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}") # logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
await ctx.send_msgs(client, [['LocationInfo', [l[1:] for l in locs]]]) await ctx.send_msgs(client, [['LocationInfo', {'locations': locs}]])
elif cmd == 'UpdateTags': elif cmd == 'UpdateTags':
if not args or type(args) is not list: if not args or type(args) is not list:
await ctx.send_msgs(client, [['InvalidArguments', 'UpdateTags']]) await ctx.send_msgs(client, [['InvalidArguments', {"text": 'UpdateTags'}]])
return return
client.tags = args client.tags = args
elif cmd == 'GameFinished': elif cmd == 'StatusUpdate':
if ctx.client_game_state[client.team, client.slot] != CLIENT_GOAL: current = ctx.client_game_state[client.team, client.slot]
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has found the triforce.' if current != CLIENT_GOAL: # can't undo goal completion
ctx.notify_all(finished_msg) if args["status"] == CLIENT_GOAL:
ctx.client_game_state[client.team, client.slot] = CLIENT_GOAL finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
if "auto" in ctx.forfeit_mode: ctx.notify_all(finished_msg)
forfeit_player(ctx, client.team, client.slot) if "auto" in ctx.forfeit_mode:
forfeit_player(ctx, client.team, client.slot)
ctx.client_game_state[client.team, client.slot] = args["status"]
if cmd == 'Say': if cmd == 'Say':
if type(args) is not str or not args.isprintable(): if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
await ctx.send_msgs(client, [['InvalidArguments', 'Say']]) await ctx.send_msgs(client, [['InvalidArguments', {"text" : 'Say'}]])
return return
client.messageprocessor(args) client.messageprocessor(args["text"])
class ServerCommandProcessor(CommonCommandProcessor): class ServerCommandProcessor(CommonCommandProcessor):

View File

@ -19,6 +19,7 @@ from worlds.alttp.Main import get_seed, seeddigits
from worlds.alttp.Items import item_name_groups, item_table from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data
def mystery_argparse(): def mystery_argparse():

View File

@ -2,10 +2,12 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import typing import typing
from html.parser import HTMLParser
from json import loads, dumps from json import loads, dumps
import websockets import websockets
class Node: class Node:
endpoints: typing.List endpoints: typing.List
dumper = staticmethod(dumps) dumper = staticmethod(dumps)
@ -20,7 +22,7 @@ class Node:
for endpoint in self.endpoints: for endpoint in self.endpoints:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs)) asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
async def send_msgs(self, endpoint: Endpoint, msgs): async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[typing.Sequence[str, typing.Optional[dict]]]):
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed: if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
return return
try: try:
@ -51,3 +53,61 @@ class Endpoint:
async def disconnect(self): async def disconnect(self):
raise NotImplementedError raise NotImplementedError
class HTMLtoColoramaParser(HTMLParser):
def get_colorama_text(self, input_text: str) -> str:
self.feed(input_text)
self.close()
data = self.data
self.reset()
return data
def handle_data(self, data):
self.data += data
def handle_starttag(self, tag, attrs):
if tag in {"span", "div", "p"}:
for attr in attrs:
subtag, data = attr
if subtag == "style":
for subdata in data.split(";"):
if subdata.startswith("color"):
color = subdata.split(":", 1)[-1].strip()
if color in color_codes:
self.data += color_code(color)
self.colored = tag
def handle_endtag(self, tag):
if tag == self.colored:
self.colored = False
self.data += color_code("reset")
def reset(self):
super(HTMLtoColoramaParser, self).reset()
self.data = ""
self.colored = False
def close(self):
super(HTMLtoColoramaParser, self).close()
if self.colored:
self.handle_endtag(self.colored)
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
def color_code(*args):
return '\033[' + ';'.join([str(color_codes[arg]) for arg in args]) + 'm'
def color(text, *args):
return color_code(*args) + text + color_code('reset')
CLIENT_UNKNOWN = 0
CLIENT_READY = 10
CLIENT_PLAYING = 20
CLIENT_GOAL = 30

View File

@ -384,6 +384,8 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name): def find_class(self, module, name):
if module == "builtins" and name in safe_builtins: if module == "builtins" and name in safe_builtins:
return getattr(builtins, name) return getattr(builtins, name)
if module == "Utils" and name in {"ReceivedItem"}:
return globals()[name]
# Forbid everything else. # Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name)) (module, name))

View File

@ -1,11 +1,24 @@
from typing import NamedTuple, Union from typing import NamedTuple, Union
import logging
class PlandoItem(NamedTuple): class PlandoItem(NamedTuple):
item: str item: str
location: str location: str
world: Union[bool, str] = False # False -> own world, True -> not own world world: Union[bool, str] = False # False -> own world, True -> not own world
from_pool: bool = True # if item should be removed from item pool from_pool: bool = True # if item should be removed from item pool
force: str = 'silent' # false -> warns if item not successfully placed. true -> errors out on failure to place item.
def warn(self, warning: str):
if self.force in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
logging.warning(f'{warning}')
else:
logging.debug(f'{warning}')
def failed(self, warning: str, exception=Exception):
if self.force in ['true', 'fail', 'failure']:
raise exception(warning)
else:
self.warn(warning)
class PlandoConnection(NamedTuple): class PlandoConnection(NamedTuple):