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:
parent
670b8b4b11
commit
c604dfe509
196
MultiClient.py
196
MultiClient.py
|
@ -8,12 +8,9 @@ import functools
|
|||
import webbrowser
|
||||
import multiprocessing
|
||||
import socket
|
||||
import sys
|
||||
import typing
|
||||
import os
|
||||
import subprocess
|
||||
import base64
|
||||
from json import loads, dumps
|
||||
|
||||
from random import randrange
|
||||
|
||||
|
@ -26,11 +23,10 @@ import ModuleUpdate
|
|||
ModuleUpdate.update()
|
||||
|
||||
import colorama
|
||||
import websockets
|
||||
import prompt_toolkit
|
||||
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from NetUtils import Endpoint
|
||||
from NetUtils import *
|
||||
import WebUI
|
||||
|
||||
from worlds.alttp import Regions
|
||||
|
@ -44,10 +40,10 @@ logger = logging.getLogger("Client")
|
|||
def create_named_task(coro, *args, name=None):
|
||||
if not name:
|
||||
name = coro.__name__
|
||||
if sys.version_info.major > 2 and sys.version_info.minor > 7:
|
||||
return asyncio.create_task(coro, *args, name=name)
|
||||
else:
|
||||
return asyncio.create_task(coro, *args)
|
||||
return asyncio.create_task(coro, *args, name=name)
|
||||
|
||||
|
||||
coloramaparser = HTMLtoColoramaParser()
|
||||
|
||||
|
||||
class Context():
|
||||
|
@ -125,19 +121,6 @@ class Context():
|
|||
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
|
||||
SNES_RECONNECT_DELAY = 5
|
||||
SERVER_RECONNECT_DELAY = 5
|
||||
|
@ -516,7 +499,6 @@ async def snes_connect(ctx: Context, address):
|
|||
await snes_disconnect(ctx)
|
||||
return
|
||||
|
||||
|
||||
logger.info("Attaching to " + device)
|
||||
|
||||
Attach_Request = {
|
||||
|
@ -532,7 +514,7 @@ async def snes_connect(ctx: Context, address):
|
|||
if 'sd2snes' in device.lower() or 'COM' in device:
|
||||
logger.info("SD2SNES/FXPAK Detected")
|
||||
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())
|
||||
if reply and 'Results' in reply:
|
||||
logger.info(reply['Results'])
|
||||
|
@ -605,7 +587,7 @@ async def snes_recv_loop(ctx: Context):
|
|||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
|
||||
|
||||
async def snes_read(ctx : Context, address, size):
|
||||
async def snes_read(ctx: Context, address, size):
|
||||
try:
|
||||
await ctx.snes_request_lock.acquire()
|
||||
|
||||
|
@ -613,9 +595,9 @@ async def snes_read(ctx : Context, address, size):
|
|||
return None
|
||||
|
||||
GetAddress_Request = {
|
||||
"Opcode" : "GetAddress",
|
||||
"Space" : "SNES",
|
||||
"Operands" : [hex(address)[2:], hex(size)[2:]]
|
||||
"Opcode": "GetAddress",
|
||||
"Space": "SNES",
|
||||
"Operands": [hex(address)[2:], hex(size)[2:]]
|
||||
}
|
||||
try:
|
||||
await ctx.snes_socket.send(dumps(GetAddress_Request))
|
||||
|
@ -634,7 +616,7 @@ async def snes_read(ctx : Context, address, size):
|
|||
if len(data):
|
||||
logger.error(str(data))
|
||||
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:
|
||||
await ctx.snes_socket.close()
|
||||
return None
|
||||
|
@ -644,7 +626,7 @@ async def snes_read(ctx : Context, address, size):
|
|||
ctx.snes_request_lock.release()
|
||||
|
||||
|
||||
async def snes_write(ctx : Context, write_list):
|
||||
async def snes_write(ctx: Context, write_list):
|
||||
try:
|
||||
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)))
|
||||
return False
|
||||
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
||||
cmd += b'\xA9' # LDA
|
||||
cmd += b'\xA9' # LDA
|
||||
cmd += bytes([byte])
|
||||
cmd += b'\x8F' # STA.l
|
||||
cmd += b'\x8F' # STA.l
|
||||
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'
|
||||
|
||||
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:
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
|
@ -681,7 +663,7 @@ async def snes_write(ctx : Context, write_list):
|
|||
else:
|
||||
PutAddress_Request['Space'] = 'SNES'
|
||||
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:
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
|
@ -697,7 +679,7 @@ async def snes_write(ctx : Context, write_list):
|
|||
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:
|
||||
# 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)
|
||||
|
@ -705,7 +687,7 @@ def snes_buffered_write(ctx : Context, 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:
|
||||
return
|
||||
|
||||
|
@ -733,11 +715,11 @@ async def server_loop(ctx: Context, address=None):
|
|||
if address is None: # see if this is an old connection
|
||||
await asyncio.sleep(0.5) # wait for snes connection to succeed if possible.
|
||||
rom = ctx.rom if ctx.rom else None
|
||||
|
||||
servers = Utils.persistent_load()["servers"]
|
||||
if rom in servers:
|
||||
address = servers[rom]
|
||||
cached_address = True
|
||||
if rom:
|
||||
servers = Utils.persistent_load()["servers"]
|
||||
if rom in servers:
|
||||
address = servers[rom]
|
||||
cached_address = True
|
||||
|
||||
# Wait for the user to provide a multiworld server address
|
||||
if not address:
|
||||
|
@ -758,15 +740,14 @@ async def server_loop(ctx: Context, address=None):
|
|||
SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY
|
||||
async for data in ctx.server.socket:
|
||||
for msg in loads(data):
|
||||
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
|
||||
await process_server_cmd(ctx, cmd, args)
|
||||
await process_server_cmd(ctx, msg[0], msg[1])
|
||||
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
|
||||
except WebUI.WaitingForUiException:
|
||||
pass
|
||||
except ConnectionRefusedError:
|
||||
if 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:
|
||||
logger.error('Connection refused by the multiworld server')
|
||||
except (OSError, websockets.InvalidURI):
|
||||
|
@ -791,6 +772,7 @@ async def server_loop(ctx: Context, address=None):
|
|||
asyncio.create_task(server_autoreconnect(ctx))
|
||||
SERVER_RECONNECT_DELAY *= 2
|
||||
|
||||
|
||||
async def server_autoreconnect(ctx: Context):
|
||||
# unfortunately currently broken. See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1033
|
||||
# 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))
|
||||
|
||||
|
||||
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':
|
||||
logger.info('--------------------------------')
|
||||
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"]))
|
||||
if args['password']:
|
||||
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"Remaining setting: {args['remaining_mode']}")
|
||||
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'])
|
||||
|
||||
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:
|
||||
asyncio.create_task(ctx.snes_socket.close())
|
||||
raise Exception('Invalid ROM detected, '
|
||||
'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)
|
||||
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')
|
||||
#last to check, recoverable problem
|
||||
elif 'InvalidPassword' in args:
|
||||
# last to check, recoverable problem
|
||||
elif 'InvalidPassword' in errors:
|
||||
logger.error('Invalid password')
|
||||
ctx.password = None
|
||||
await server_auth(ctx, True)
|
||||
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')
|
||||
|
||||
elif cmd == 'Connected':
|
||||
if ctx.send_unsafe:
|
||||
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)
|
||||
ctx.team, ctx.slot = args[0]
|
||||
ctx.player_names = {p: n for p, n in args[1]}
|
||||
ctx.team = args["team"]
|
||||
ctx.slot = args["slot"]
|
||||
ctx.player_names = {p: n for p, n in args["playernames"]}
|
||||
msgs = []
|
||||
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:
|
||||
msgs.append(['LocationScouts', list(ctx.locations_scouted)])
|
||||
msgs.append(['LocationScouts', {"locations": list(ctx.locations_scouted)}])
|
||||
if msgs:
|
||||
await ctx.send_msgs(msgs)
|
||||
if ctx.finished_game:
|
||||
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 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.
|
||||
if not ctx.items_missing:
|
||||
asyncio.create_task(ctx.send_msgs([['Say', '!missing']]))
|
||||
ctx.items_missing = args["missing_checks"]
|
||||
|
||||
elif cmd == 'ReceivedItems':
|
||||
start_index, items = args
|
||||
start_index = args["index"]
|
||||
|
||||
if start_index == 0:
|
||||
ctx.items_received = []
|
||||
elif start_index != len(ctx.items_received):
|
||||
sync_msg = [['Sync']]
|
||||
sync_msg = [['Sync', None]]
|
||||
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)
|
||||
if start_index == len(ctx.items_received):
|
||||
for item in items:
|
||||
|
||||
for item in args['items']:
|
||||
ctx.items_received.append(ReceivedItem(*item))
|
||||
ctx.watcher_event.set()
|
||||
|
||||
elif cmd == 'LocationInfo':
|
||||
for location, item, player in args:
|
||||
for item, location, player in args['locations']:
|
||||
if location not in ctx.locations_info:
|
||||
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
|
||||
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()
|
||||
|
||||
elif cmd == 'ItemSent':
|
||||
player_sent, location, player_recvd, item = args
|
||||
ctx.ui_node.notify_item_sent(ctx.player_names[player_sent], ctx.player_names[player_recvd],
|
||||
get_item_name_from_id(item), get_location_name_from_address(location),
|
||||
player_sent == ctx.slot, player_recvd == ctx.slot)
|
||||
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
|
||||
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
|
||||
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
|
||||
found = ReceivedItem(*args["item"])
|
||||
receiving_player = args["receiver"]
|
||||
ctx.ui_node.notify_item_sent(ctx.player_names[found.player], ctx.player_names[receiving_player],
|
||||
get_item_name_from_id(found.item), get_location_name_from_address(found.location),
|
||||
found.player == ctx.slot, receiving_player == ctx.slot)
|
||||
item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green')
|
||||
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(
|
||||
'%s sent %s to %s (%s)' % (player_sent, item, player_recvd, color(get_location_name_from_address(location),
|
||||
'blue_bg', 'white')))
|
||||
'%s sent %s to %s (%s)' % (found_player, item, receiving_player,
|
||||
color(get_location_name_from_address(found.location), 'blue_bg', 'white')))
|
||||
|
||||
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),
|
||||
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')
|
||||
|
@ -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),
|
||||
'blue_bg', 'white')))
|
||||
|
||||
|
||||
elif cmd == 'Hint':
|
||||
hints = [Utils.Hint(*hint) for hint in args]
|
||||
hints = [Utils.Hint(*hint) for hint in args["hints"]]
|
||||
for hint in hints:
|
||||
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),
|
||||
|
@ -947,17 +937,24 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
|||
text += " at " + color(hint.entrance, 'white_bg', 'black')
|
||||
logging.info(text + (f". {color('(found)', 'green_bg', 'black')} " if hint.found else "."))
|
||||
|
||||
elif cmd == "AliasUpdate":
|
||||
ctx.player_names = {p: n for p, n in args}
|
||||
elif cmd == "RoomUpdate":
|
||||
if "playernames" in args:
|
||||
ctx.player_names = {p: n for p, n in args["playernames"]}
|
||||
|
||||
elif cmd == 'Print':
|
||||
logger.info(args)
|
||||
logger.info(args["text"])
|
||||
|
||||
elif cmd == 'PrintHTML':
|
||||
logger.info(coloramaparser.get_colorama_text(args["text"]))
|
||||
|
||||
elif cmd == 'HintPointUpdate':
|
||||
ctx.hint_points = args[0]
|
||||
ctx.hint_points = args['points']
|
||||
|
||||
elif cmd == 'InvalidArguments':
|
||||
logger.warning(f"Invalid Arguments: {args['text']}")
|
||||
|
||||
else:
|
||||
logger.debug(f"unknown command {args}")
|
||||
logger.debug(f"unknown command {cmd}")
|
||||
|
||||
|
||||
def get_tags(ctx: Context):
|
||||
|
@ -981,11 +978,11 @@ async def server_auth(ctx: Context, password_requested):
|
|||
auth = base64.b64encode(ctx.rom).decode()
|
||||
await ctx.send_msgs([['Connect', {
|
||||
'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
|
||||
return await ctx.input_queue.get()
|
||||
|
||||
|
@ -1081,7 +1078,8 @@ class ClientCommandProcessor(CommandProcessor):
|
|||
count += 1
|
||||
|
||||
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:
|
||||
self.output("No missing location checks found.")
|
||||
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."""
|
||||
if toggle:
|
||||
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:
|
||||
logger.info("You must specify /send_unsafe true explicitly.")
|
||||
self.ctx.send_unsafe = False
|
||||
|
||||
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):
|
||||
|
@ -1145,7 +1144,7 @@ async def console_loop(ctx: Context):
|
|||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
async def track_locations(ctx : Context, roomid, roomdata):
|
||||
async def track_locations(ctx: Context, roomid, roomdata):
|
||||
new_locations = []
|
||||
|
||||
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():
|
||||
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)
|
||||
except Exception as e:
|
||||
logger.exception(f"Exception: {e}")
|
||||
|
||||
uw_begin = 0x129
|
||||
uw_end = 0
|
||||
ow_end = uw_end = 0
|
||||
uw_unchecked = {}
|
||||
for location, (roomid, mask) in location_table_uw.items():
|
||||
if location not in ctx.unsafe_locations_checked:
|
||||
|
@ -1178,7 +1178,6 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
new_check(location)
|
||||
|
||||
ow_begin = 0x82
|
||||
ow_end = 0
|
||||
ow_unchecked = {}
|
||||
for location, screenid in location_table_ow.items():
|
||||
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)
|
||||
if misc_data is not None:
|
||||
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:
|
||||
new_check(location)
|
||||
|
||||
|
@ -1213,18 +1212,19 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
ctx.locations_checked.add(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):
|
||||
try:
|
||||
await ctx.send_msgs([['GameFinished', '']])
|
||||
await ctx.send_msgs([['StatusUpdate', {"status": CLIENT_GOAL}]])
|
||||
ctx.finished_game = True
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
|
||||
|
||||
async def game_watcher(ctx : Context):
|
||||
async def game_watcher(ctx: Context):
|
||||
prev_game_timer = 0
|
||||
perf_counter = time.perf_counter()
|
||||
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:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -1391,7 +1391,8 @@ async def main():
|
|||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--founditems', default=False, action='store_true',
|
||||
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()
|
||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
|
||||
|
@ -1454,6 +1455,7 @@ async def main():
|
|||
|
||||
await input_task
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
147
MultiServer.py
147
MultiServer.py
|
@ -20,6 +20,7 @@ import ModuleUpdate
|
|||
ModuleUpdate.update()
|
||||
|
||||
import websockets
|
||||
import colorama
|
||||
import prompt_toolkit
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from fuzzywuzzy import process as fuzzy_process
|
||||
|
@ -28,14 +29,11 @@ from worlds.alttp import Items, Regions
|
|||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||
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))
|
||||
|
||||
CLIENT_PLAYING = 0
|
||||
CLIENT_GOAL = 1
|
||||
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version: typing.List[int] = [0, 0, 0]
|
||||
|
@ -196,8 +194,8 @@ class Context(Node):
|
|||
self.saving = enabled
|
||||
if self.saving:
|
||||
if not self.save_filename:
|
||||
self.save_filename = (self.data_filename[:-9] if self.data_filename.endswith('.archipelago') else (
|
||||
self.data_filename + '_')) + 'save'
|
||||
self.save_filename = (self.data_filename[:-11] if self.data_filename.endswith('.archipelago') else (
|
||||
self.data_filename + '_')) + 'apsave'
|
||||
try:
|
||||
with open(self.save_filename, 'rb') as f:
|
||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||
|
@ -301,18 +299,18 @@ class Context(Node):
|
|||
|
||||
def notify_all(self, 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):
|
||||
if not client.auth:
|
||||
return
|
||||
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]):
|
||||
if not client.auth:
|
||||
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):
|
||||
for client in self.endpoints:
|
||||
|
@ -332,8 +330,8 @@ class Context(Node):
|
|||
|
||||
# separated out, due to compatibilty between clients
|
||||
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
|
||||
texts = [['Print', format_hint(ctx, team, hint)] for hint in hints]
|
||||
cmd = dumps([["Hint", {"hints", hints}]])
|
||||
texts = [['PrintHTML', format_hint(ctx, team, hint)] for hint in hints]
|
||||
for _, text in texts:
|
||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||
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):
|
||||
cmd = dumps([["AliasUpdate",
|
||||
[(key[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if
|
||||
key[0] == team]]])
|
||||
cmd = dumps([["RoomUpdate",
|
||||
{"playernames": [(key[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if
|
||||
key[0] == team]}]])
|
||||
if client is None:
|
||||
for client in ctx.endpoints:
|
||||
if client.team == team and client.auth:
|
||||
|
@ -361,13 +359,7 @@ async def server(websocket, path, ctx: Context):
|
|||
await on_client_connected(ctx, client)
|
||||
async for data in websocket:
|
||||
for msg in 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)
|
||||
await process_client_cmd(ctx, client, msg[0], msg[1])
|
||||
except Exception as e:
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logging.exception(e)
|
||||
|
@ -401,12 +393,6 @@ async def on_client_joined(ctx: Context, client: Client):
|
|||
ctx.notify_all(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. "
|
||||
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)
|
||||
|
||||
|
@ -469,7 +455,8 @@ def send_new_items(ctx: Context):
|
|||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if len(items) > client.send_index:
|
||||
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)
|
||||
|
||||
|
||||
|
@ -488,7 +475,7 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
|||
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
|
||||
new_locations = set(locations) - ctx.location_checks[team, slot]
|
||||
known_locations = set()
|
||||
|
@ -499,7 +486,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
|||
known_locations.add(location)
|
||||
target_item, target_player = ctx.locations[(location, slot)]
|
||||
if target_player != slot or slot in ctx.remote_items:
|
||||
found = False
|
||||
found: bool = False
|
||||
recvd_items = get_received_items(ctx, team, target_player)
|
||||
for recvd_item in recvd_items:
|
||||
if recvd_item.location == location and recvd_item.player == slot:
|
||||
|
@ -510,7 +497,9 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
|||
new_item = ReceivedItem(target_item, location, slot)
|
||||
recvd_items.append(new_item)
|
||||
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)' % (
|
||||
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)))
|
||||
|
@ -520,20 +509,21 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
|||
for client in ctx.endpoints:
|
||||
if client.team == team and client.wants_item_notification:
|
||||
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
|
||||
send_new_items(ctx)
|
||||
|
||||
if found_items:
|
||||
for client in ctx.endpoints:
|
||||
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()
|
||||
|
||||
|
||||
def notify_team(ctx: Context, team: int, text: str):
|
||||
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])
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
if cmd == 'Connect':
|
||||
if not args or type(args) is not dict or \
|
||||
'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||
'rom' not in args or type(args['rom']) not in (list, str):
|
||||
await ctx.send_msgs(client, [['InvalidArguments', 'Connect']])
|
||||
if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||
'game' not in args:
|
||||
await ctx.send_msgs(client, [['InvalidArguments', {'text': 'Connect'}]])
|
||||
return
|
||||
|
||||
errors = set()
|
||||
if ctx.password is not None and args['password'] != ctx.password:
|
||||
if ctx.password and args['password'] != ctx.password:
|
||||
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:
|
||||
logging.info((args["rom"], ctx.rom_names))
|
||||
errors.add('InvalidRom')
|
||||
|
@ -1012,28 +1005,29 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
|||
client.team = team
|
||||
client.slot = slot
|
||||
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')
|
||||
|
||||
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')
|
||||
#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')
|
||||
if 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:
|
||||
ctx.client_ids[client.team, client.slot] = args.get("uuid", None)
|
||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||
client.auth = True
|
||||
client.version = args.get('version', Client.version)
|
||||
client.tags = args.get('tags', Client.tags)
|
||||
reply = [['Connected', [(client.team, client.slot),
|
||||
[(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if
|
||||
t == client.team], get_missing_checks(ctx, client)]]]
|
||||
client.version = args['version']
|
||||
client.tags = args['tags']
|
||||
reply = [['Connected', {"team": client.team, "slot": client.slot,
|
||||
"playernames": [(p, ctx.get_aliased_name(t, p)) for (t, p), n in
|
||||
ctx.player_names.items() if t == client.team],
|
||||
"missing_checks": get_missing_checks(ctx, client)}]]
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
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)
|
||||
await ctx.send_msgs(client, reply)
|
||||
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)
|
||||
if 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':
|
||||
if type(args) is not list:
|
||||
await ctx.send_msgs(client, [['InvalidArguments', 'LocationChecks']])
|
||||
return
|
||||
register_location_checks(ctx, client.team, client.slot, args)
|
||||
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
||||
|
||||
elif cmd == 'LocationScouts':
|
||||
if type(args) is not list:
|
||||
await ctx.send_msgs(client, [['InvalidArguments', 'LocationScouts']])
|
||||
return
|
||||
locs = []
|
||||
for location in args:
|
||||
for location in args["locations"]:
|
||||
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
|
||||
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||
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:
|
||||
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])}")
|
||||
await ctx.send_msgs(client, [['LocationInfo', [l[1:] for l in locs]]])
|
||||
await ctx.send_msgs(client, [['LocationInfo', {'locations': locs}]])
|
||||
|
||||
elif cmd == 'UpdateTags':
|
||||
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
|
||||
client.tags = args
|
||||
|
||||
elif cmd == 'GameFinished':
|
||||
if 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 found the triforce.'
|
||||
ctx.notify_all(finished_msg)
|
||||
ctx.client_game_state[client.team, client.slot] = CLIENT_GOAL
|
||||
if "auto" in ctx.forfeit_mode:
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
elif cmd == 'StatusUpdate':
|
||||
current = ctx.client_game_state[client.team, client.slot]
|
||||
if current != CLIENT_GOAL: # can't undo goal completion
|
||||
if args["status"] == CLIENT_GOAL:
|
||||
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
|
||||
ctx.notify_all(finished_msg)
|
||||
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 type(args) is not str or not args.isprintable():
|
||||
await ctx.send_msgs(client, [['InvalidArguments', 'Say']])
|
||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
||||
await ctx.send_msgs(client, [['InvalidArguments', {"text" : 'Say'}]])
|
||||
return
|
||||
|
||||
client.messageprocessor(args)
|
||||
client.messageprocessor(args["text"])
|
||||
|
||||
|
||||
class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
|
|
@ -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 import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.alttp.Regions import location_table, key_drop_data
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
|
|
62
NetUtils.py
62
NetUtils.py
|
@ -2,10 +2,12 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
from html.parser import HTMLParser
|
||||
from json import loads, dumps
|
||||
|
||||
import websockets
|
||||
|
||||
|
||||
class Node:
|
||||
endpoints: typing.List
|
||||
dumper = staticmethod(dumps)
|
||||
|
@ -20,7 +22,7 @@ class Node:
|
|||
for endpoint in self.endpoints:
|
||||
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:
|
||||
return
|
||||
try:
|
||||
|
@ -51,3 +53,61 @@ class Endpoint:
|
|||
|
||||
async def disconnect(self):
|
||||
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
|
2
Utils.py
2
Utils.py
|
@ -384,6 +384,8 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||
def find_class(self, module, name):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
if module == "Utils" and name in {"ReceivedItem"}:
|
||||
return globals()[name]
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
(module, name))
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
from typing import NamedTuple, Union
|
||||
|
||||
import logging
|
||||
|
||||
class PlandoItem(NamedTuple):
|
||||
item: str
|
||||
location: str
|
||||
world: Union[bool, str] = False # False -> own world, True -> not own world
|
||||
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):
|
||||
|
|
Loading…
Reference in New Issue