bunch of fixes after testing round
This commit is contained in:
parent
b82d6cec31
commit
b2f3fd56f4
|
@ -49,10 +49,6 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
"""List all received items"""
|
"""List all received items"""
|
||||||
logger.info('Received items:')
|
logger.info('Received items:')
|
||||||
for index, item in enumerate(self.ctx.items_received, 1):
|
for index, item in enumerate(self.ctx.items_received, 1):
|
||||||
self.ctx.ui_node.notify_item_received(self.ctx.player_names[item.player], self.ctx.item_name_getter(item.item),
|
|
||||||
self.ctx.location_name_getter(item.location), index,
|
|
||||||
len(self.ctx.items_received),
|
|
||||||
self.ctx.item_name_getter(item.item) in Items.progression_items)
|
|
||||||
logging.info('%s from %s (%s) (%d/%d in list)' % (
|
logging.info('%s from %s (%s) (%d/%d in list)' % (
|
||||||
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
|
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
|
||||||
color(self.ctx.player_names[item.player], 'yellow'),
|
color(self.ctx.player_names[item.player], 'yellow'),
|
||||||
|
@ -116,7 +112,6 @@ class CommonContext():
|
||||||
self.team = None
|
self.team = None
|
||||||
self.slot = None
|
self.slot = None
|
||||||
self.auth = None
|
self.auth = None
|
||||||
self.ui_node = None
|
|
||||||
|
|
||||||
self.locations_checked: typing.Set[int] = set()
|
self.locations_checked: typing.Set[int] = set()
|
||||||
self.locations_scouted: typing.Set[int] = set()
|
self.locations_scouted: typing.Set[int] = set()
|
||||||
|
@ -223,9 +218,6 @@ class CommonContext():
|
||||||
|
|
||||||
|
|
||||||
async def server_loop(ctx: CommonContext, address=None):
|
async def server_loop(ctx: CommonContext, address=None):
|
||||||
ui_node = getattr(ctx, "ui_node", None)
|
|
||||||
if ui_node:
|
|
||||||
ui_node.send_connection_status(ctx)
|
|
||||||
cached_address = None
|
cached_address = None
|
||||||
if ctx.server and ctx.server.socket:
|
if ctx.server and ctx.server.socket:
|
||||||
logger.error('Already connected')
|
logger.error('Already connected')
|
||||||
|
@ -237,8 +229,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||||
# 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:
|
||||||
logger.info('Please connect to an Archipelago server.')
|
logger.info('Please connect to an Archipelago server.')
|
||||||
if ui_node:
|
|
||||||
ui_node.poll_for_server_ip()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
address = f"ws://{address}" if "://" not in address else address
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
|
@ -250,8 +240,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||||
ctx.server = Endpoint(socket)
|
ctx.server = Endpoint(socket)
|
||||||
logger.info('Connected')
|
logger.info('Connected')
|
||||||
ctx.server_address = address
|
ctx.server_address = address
|
||||||
if ui_node:
|
|
||||||
ui_node.send_connection_status(ctx)
|
|
||||||
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
|
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
|
||||||
async for data in ctx.server.socket:
|
async for data in ctx.server.socket:
|
||||||
for msg in decode(data):
|
for msg in decode(data):
|
||||||
|
@ -273,8 +261,6 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||||
await ctx.connection_closed()
|
await ctx.connection_closed()
|
||||||
if ctx.server_address:
|
if ctx.server_address:
|
||||||
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
|
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
|
||||||
if ui_node:
|
|
||||||
ui_node.send_connection_status(ctx)
|
|
||||||
asyncio.create_task(server_autoreconnect(ctx))
|
asyncio.create_task(server_autoreconnect(ctx))
|
||||||
ctx.current_reconnect_delay *= 2
|
ctx.current_reconnect_delay *= 2
|
||||||
|
|
||||||
|
@ -311,8 +297,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||||
ctx.check_points = int(args['location_check_points'])
|
ctx.check_points = int(args['location_check_points'])
|
||||||
ctx.forfeit_mode = args['forfeit_mode']
|
ctx.forfeit_mode = args['forfeit_mode']
|
||||||
ctx.remaining_mode = args['remaining_mode']
|
ctx.remaining_mode = args['remaining_mode']
|
||||||
if ctx.ui_node:
|
|
||||||
ctx.ui_node.send_game_info(ctx)
|
|
||||||
if len(args['players']) < 1:
|
if len(args['players']) < 1:
|
||||||
logger.info('No player connected')
|
logger.info('No player connected')
|
||||||
else:
|
else:
|
||||||
|
|
102
LttPClient.py
102
LttPClient.py
|
@ -24,7 +24,6 @@ ModuleUpdate.update()
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
from NetUtils import *
|
from NetUtils import *
|
||||||
import WebUI
|
|
||||||
|
|
||||||
from worlds.alttp import Regions, Shops
|
from worlds.alttp import Regions, Shops
|
||||||
from worlds.alttp import Items
|
from worlds.alttp import Items
|
||||||
|
@ -45,12 +44,6 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||||
|
|
||||||
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
||||||
|
|
||||||
def _cmd_web(self):
|
|
||||||
if self.ctx.webui_socket_port:
|
|
||||||
webbrowser.open(f'http://localhost:5050?port={self.ctx.webui_socket_port}')
|
|
||||||
else:
|
|
||||||
self.output("Web UI was never started.")
|
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_snes(self, snes_address: str = "") -> bool:
|
def _cmd_snes(self, snes_address: str = "") -> bool:
|
||||||
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices"""
|
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices"""
|
||||||
|
@ -69,21 +62,9 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||||
|
|
||||||
class Context(CommonContext):
|
class Context(CommonContext):
|
||||||
command_processor = LttPCommandProcessor
|
command_processor = LttPCommandProcessor
|
||||||
def __init__(self, snes_address, server_address, password, found_items, port: int):
|
def __init__(self, snes_address, server_address, password, found_items):
|
||||||
super(Context, self).__init__(server_address, password, found_items)
|
super(Context, self).__init__(server_address, password, found_items)
|
||||||
|
|
||||||
# WebUI Stuff
|
|
||||||
self.ui_node = WebUI.WebUiClient()
|
|
||||||
logger.addHandler(self.ui_node)
|
|
||||||
|
|
||||||
self.webui_socket_port: typing.Optional[int] = port
|
|
||||||
self.hint_cost = 0
|
|
||||||
self.check_points = 0
|
|
||||||
self.forfeit_mode = ''
|
|
||||||
self.remaining_mode = ''
|
|
||||||
self.hint_points = 0
|
|
||||||
# End of WebUI Stuff
|
|
||||||
|
|
||||||
# snes stuff
|
# snes stuff
|
||||||
self.snes_address = snes_address
|
self.snes_address = snes_address
|
||||||
self.snes_socket = None
|
self.snes_socket = None
|
||||||
|
@ -495,7 +476,7 @@ async def get_snes_devices(ctx: Context):
|
||||||
reply = loads(await socket.recv())
|
reply = loads(await socket.recv())
|
||||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||||
|
|
||||||
ctx.ui_node.send_device_list(devices)
|
|
||||||
await socket.close()
|
await socket.close()
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
@ -517,8 +498,6 @@ async def snes_connect(ctx: Context, address):
|
||||||
|
|
||||||
if len(devices) == 1:
|
if len(devices) == 1:
|
||||||
device = devices[0]
|
device = devices[0]
|
||||||
elif ctx.ui_node.manual_snes and ctx.ui_node.manual_snes in devices:
|
|
||||||
device = ctx.ui_node.manual_snes
|
|
||||||
elif ctx.snes_reconnect_address:
|
elif ctx.snes_reconnect_address:
|
||||||
if ctx.snes_attached_device[1] in devices:
|
if ctx.snes_attached_device[1] in devices:
|
||||||
device = ctx.snes_attached_device[1]
|
device = ctx.snes_attached_device[1]
|
||||||
|
@ -538,7 +517,6 @@ async def snes_connect(ctx: Context, address):
|
||||||
await ctx.snes_socket.send(dumps(Attach_Request))
|
await ctx.snes_socket.send(dumps(Attach_Request))
|
||||||
ctx.snes_state = SNESState.SNES_ATTACHED
|
ctx.snes_state = SNESState.SNES_ATTACHED
|
||||||
ctx.snes_attached_device = (devices.index(device), device)
|
ctx.snes_attached_device = (devices.index(device), device)
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
|
||||||
|
|
||||||
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")
|
||||||
|
@ -607,7 +585,6 @@ async def snes_recv_loop(ctx: Context):
|
||||||
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
||||||
ctx.snes_recv_queue = asyncio.Queue()
|
ctx.snes_recv_queue = asyncio.Queue()
|
||||||
ctx.hud_message_queue = []
|
ctx.hud_message_queue = []
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
|
||||||
|
|
||||||
ctx.rom = None
|
ctx.rom = None
|
||||||
|
|
||||||
|
@ -743,8 +720,6 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||||
ctx.locations_checked.add(location_id)
|
ctx.locations_checked.add(location_id)
|
||||||
location = ctx.location_name_getter(location_id)
|
location = ctx.location_name_getter(location_id)
|
||||||
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||||
ctx.ui_node.send_location_check(ctx, location)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if roomid in location_shop_ids:
|
if roomid in location_shop_ids:
|
||||||
|
@ -887,10 +862,6 @@ async def game_watcher(ctx: Context):
|
||||||
|
|
||||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
ctx.ui_node.notify_item_received(ctx.player_names[item.player], ctx.item_name_getter(item.item),
|
|
||||||
ctx.location_name_getter(item.location), recv_index + 1,
|
|
||||||
len(ctx.items_received),
|
|
||||||
ctx.item_name_getter(item.item) in Items.progression_items)
|
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received)))
|
ctx.location_name_getter(item.location), recv_index + 1, len(ctx.items_received)))
|
||||||
|
@ -920,57 +891,6 @@ async def run_game(romfile):
|
||||||
subprocess.Popen([auto_start, romfile],
|
subprocess.Popen([auto_start, romfile],
|
||||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
async def websocket_server(websocket: websockets.WebSocketServerProtocol, path, ctx: Context):
|
|
||||||
endpoint = Endpoint(websocket)
|
|
||||||
ctx.ui_node.endpoints.append(endpoint)
|
|
||||||
process_command = LttPCommandProcessor(ctx)
|
|
||||||
try:
|
|
||||||
async for incoming_data in websocket:
|
|
||||||
data = loads(incoming_data)
|
|
||||||
logging.debug(f"WebUIData:{data}")
|
|
||||||
if ('type' not in data) or ('content' not in data):
|
|
||||||
raise Exception('Invalid data received in websocket')
|
|
||||||
|
|
||||||
elif data['type'] == 'webStatus':
|
|
||||||
if data['content'] == 'connections':
|
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
|
||||||
elif data['content'] == 'devices':
|
|
||||||
await get_snes_devices(ctx)
|
|
||||||
elif data['content'] == 'gameInfo':
|
|
||||||
ctx.ui_node.send_game_info(ctx)
|
|
||||||
elif data['content'] == 'checkData':
|
|
||||||
ctx.ui_node.send_location_check(ctx, 'Waiting for check...')
|
|
||||||
|
|
||||||
elif data['type'] == 'webConfig':
|
|
||||||
if 'serverAddress' in data['content']:
|
|
||||||
ctx.server_address = data['content']['serverAddress']
|
|
||||||
await ctx.connect(data['content']['serverAddress'])
|
|
||||||
elif 'deviceId' in data['content']:
|
|
||||||
# Allow a SNES disconnect via UI sending -1 as new device
|
|
||||||
if data['content']['deviceId'] == "-1":
|
|
||||||
ctx.ui_node.manual_snes = None
|
|
||||||
ctx.snes_reconnect_address = None
|
|
||||||
await snes_disconnect(ctx)
|
|
||||||
else:
|
|
||||||
await snes_disconnect(ctx)
|
|
||||||
ctx.ui_node.manual_snes = data['content']['deviceId']
|
|
||||||
await snes_connect(ctx, ctx.snes_address)
|
|
||||||
|
|
||||||
elif data['type'] == 'webControl':
|
|
||||||
if 'disconnect' in data['content']:
|
|
||||||
await ctx.disconnect()
|
|
||||||
|
|
||||||
elif data['type'] == 'webCommand':
|
|
||||||
process_command(data['content'])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
|
||||||
logging.exception(e)
|
|
||||||
finally:
|
|
||||||
await ctx.ui_node.disconnect(endpoint)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
@ -982,8 +902,6 @@ 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('--web_ui', default=False, action='store_true',
|
|
||||||
help="Emit 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))
|
||||||
if args.diff_file:
|
if args.diff_file:
|
||||||
|
@ -1002,23 +920,9 @@ async def main():
|
||||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||||
|
|
||||||
port = None
|
port = None
|
||||||
if args.web_ui:
|
|
||||||
# Find an available port on the host system to use for hosting the websocket server
|
|
||||||
while True:
|
|
||||||
port = randrange(49152, 65535)
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
||||||
if not sock.connect_ex(('localhost', port)) == 0:
|
|
||||||
break
|
|
||||||
import threading
|
|
||||||
WebUI.start_server(
|
|
||||||
port, on_start=threading.Timer(1, webbrowser.open, (f'http://localhost:5050?port={port}',)).start)
|
|
||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password, args.founditems, port)
|
ctx = Context(args.snes, args.connect, args.password, args.founditems)
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
if args.web_ui:
|
|
||||||
ui_socket = websockets.serve(functools.partial(websocket_server, ctx=ctx),
|
|
||||||
'localhost', port, ping_timeout=None, ping_interval=None)
|
|
||||||
await ui_socket
|
|
||||||
|
|
||||||
if ctx.server_task is None:
|
if ctx.server_task is None:
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
|
16
Main.py
16
Main.py
|
@ -509,7 +509,7 @@ def main(args, seed=None):
|
||||||
precollected_items = {player: [] for player in range(1, world.players+1)}
|
precollected_items = {player: [] for player in range(1, world.players+1)}
|
||||||
for item in world.precollected_items:
|
for item in world.precollected_items:
|
||||||
precollected_items[item.player].append(item.code)
|
precollected_items[item.player].append(item.code)
|
||||||
precollected_hints = {player: [] for player in range(1, world.players+1)}
|
precollected_hints = {player: set() for player in range(1, world.players+1)}
|
||||||
# for now special case Factorio visibility
|
# for now special case Factorio visibility
|
||||||
sending_visible_players = set()
|
sending_visible_players = set()
|
||||||
for player in world.factorio_player_ids:
|
for player in world.factorio_player_ids:
|
||||||
|
@ -533,11 +533,17 @@ def main(args, seed=None):
|
||||||
if type(location.address) == int:
|
if type(location.address) == int:
|
||||||
locations_data[location.player][location.address] = (location.item.code, location.item.player)
|
locations_data[location.player][location.address] = (location.item.code, location.item.player)
|
||||||
if location.player in sending_visible_players and location.item.player != location.player:
|
if location.player in sending_visible_players and location.item.player != location.player:
|
||||||
precollected_hints[location.player].append(NetUtils.Hint(location.item.player, location.player, location.address,
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
location.item.code, False))
|
location.item.code, False)
|
||||||
|
precollected_hints[location.player].add(hint)
|
||||||
|
precollected_hints[location.item.player].add(hint)
|
||||||
elif location.item.name in args.start_hints[location.item.player]:
|
elif location.item.name in args.start_hints[location.item.player]:
|
||||||
precollected_hints[location.player].append(NetUtils.Hint(location.item.player, location.player, location.address,
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
location.item.code, False))
|
location.item.code, False,
|
||||||
|
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||||
|
precollected_hints[location.player].add(hint)
|
||||||
|
precollected_hints[location.item.player].add(hint)
|
||||||
|
|
||||||
multidata = zlib.compress(pickle.dumps({
|
multidata = zlib.compress(pickle.dumps({
|
||||||
"slot_data" : slot_data,
|
"slot_data" : slot_data,
|
||||||
"games": games,
|
"games": games,
|
||||||
|
|
|
@ -161,7 +161,7 @@ class Context(Node):
|
||||||
if slot in self.remote_items:
|
if slot in self.remote_items:
|
||||||
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||||
for slot, hints in decoded_obj["precollected_hints"].items():
|
for slot, hints in decoded_obj["precollected_hints"].items():
|
||||||
self.hints[team, slot] = hints
|
self.hints[team, slot].update(hints)
|
||||||
if use_embedded_server_options:
|
if use_embedded_server_options:
|
||||||
server_options = decoded_obj.get("server_options", {})
|
server_options = decoded_obj.get("server_options", {})
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
|
@ -7,16 +7,15 @@ from uuid import UUID
|
||||||
|
|
||||||
from worlds.alttp import Items, Regions
|
from worlds.alttp import Items, Regions
|
||||||
from WebHostLib import app, cache, Room
|
from WebHostLib import app, cache, Room
|
||||||
from NetUtils import Hint
|
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
|
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||||
|
|
||||||
def get_id(item_name):
|
def get_id(item_name):
|
||||||
return Items.item_table[item_name][2]
|
return Items.item_table[item_name][2]
|
||||||
|
|
||||||
|
|
||||||
app.jinja_env.filters["location_name"] = lambda location: Regions.lookup_id_to_name.get(location, location)
|
app.jinja_env.filters["location_name"] = lambda location: lookup_any_location_id_to_name.get(location, location)
|
||||||
app.jinja_env.filters['item_name'] = lambda id: Items.lookup_id_to_name.get(id, id)
|
app.jinja_env.filters['item_name'] = lambda id: lookup_any_item_id_to_name.get(id, id)
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||||
|
|
159
WebUI.py
159
WebUI.py
|
@ -1,159 +0,0 @@
|
||||||
import http.server
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
import typing
|
|
||||||
import socket
|
|
||||||
import socketserver
|
|
||||||
import threading
|
|
||||||
import webbrowser
|
|
||||||
import asyncio
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from NetUtils import Node
|
|
||||||
from LttPClient import Context
|
|
||||||
import Utils
|
|
||||||
|
|
||||||
|
|
||||||
class WebUiClient(Node, logging.Handler):
|
|
||||||
loader = staticmethod(json.loads)
|
|
||||||
dumper = staticmethod(json.dumps)
|
|
||||||
def __init__(self):
|
|
||||||
super(WebUiClient, self).__init__()
|
|
||||||
self.manual_snes = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_message(msg_type: str, content: typing.Union[str, dict]) -> dict:
|
|
||||||
return {'type': msg_type, 'content': content}
|
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord) -> None:
|
|
||||||
self.broadcast_all({"type": record.levelname.lower(), "content": str(record.msg)})
|
|
||||||
|
|
||||||
def send_chat_message(self, message):
|
|
||||||
self.broadcast_all(self.build_message('chat', message))
|
|
||||||
|
|
||||||
def send_connection_status(self, ctx: Context):
|
|
||||||
asyncio.create_task(self._send_connection_status(ctx))
|
|
||||||
|
|
||||||
async def _send_connection_status(self, ctx: Context):
|
|
||||||
cache = Utils.persistent_load()
|
|
||||||
cached_address = cache.get("servers", {}).get("default", None)
|
|
||||||
server_address = ctx.server_address if ctx.server_address else cached_address if cached_address else None
|
|
||||||
|
|
||||||
self.broadcast_all(self.build_message('connections', {
|
|
||||||
'snesDevice': ctx.snes_attached_device[1] if ctx.snes_attached_device else None,
|
|
||||||
'snes': ctx.snes_state,
|
|
||||||
'serverAddress': server_address,
|
|
||||||
'server': 1 if ctx.server is not None and not ctx.server.socket.closed else 0,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_device_list(self, devices):
|
|
||||||
self.broadcast_all(self.build_message('availableDevices', {
|
|
||||||
'devices': devices,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def poll_for_server_ip(self):
|
|
||||||
self.broadcast_all(self.build_message('serverAddress', {}))
|
|
||||||
|
|
||||||
def notify_item_sent(self, finder, recipient, item, location, i_am_finder: bool, i_am_recipient: bool,
|
|
||||||
item_is_unique: bool = False):
|
|
||||||
self.broadcast_all(self.build_message('itemSent', {
|
|
||||||
'finder': finder,
|
|
||||||
'recipient': recipient,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'iAmFinder': int(i_am_finder),
|
|
||||||
'iAmRecipient': int(i_am_recipient),
|
|
||||||
'itemIsUnique': int(item_is_unique),
|
|
||||||
}))
|
|
||||||
|
|
||||||
def notify_item_found(self, finder: str, item: str, location: str, i_am_finder: bool, item_is_unique: bool = False):
|
|
||||||
self.broadcast_all(self.build_message('itemFound', {
|
|
||||||
'finder': finder,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'iAmFinder': int(i_am_finder),
|
|
||||||
'itemIsUnique': int(item_is_unique),
|
|
||||||
}))
|
|
||||||
|
|
||||||
def notify_item_received(self, finder: str, item: str, location: str, item_index: int, queue_length: int,
|
|
||||||
item_is_unique: bool = False):
|
|
||||||
self.broadcast_all(self.build_message('itemReceived', {
|
|
||||||
'finder': finder,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'itemIndex': item_index,
|
|
||||||
'queueLength': queue_length,
|
|
||||||
'itemIsUnique': int(item_is_unique),
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_hint(self, finder, recipient, item, location, found, i_am_finder: bool, i_am_recipient: bool,
|
|
||||||
entrance_location: str = None):
|
|
||||||
self.broadcast_all(self.build_message('hint', {
|
|
||||||
'finder': finder,
|
|
||||||
'recipient': recipient,
|
|
||||||
'item': item,
|
|
||||||
'location': location,
|
|
||||||
'found': int(found),
|
|
||||||
'iAmFinder': int(i_am_finder),
|
|
||||||
'iAmRecipient': int(i_am_recipient),
|
|
||||||
'entranceLocation': entrance_location,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_game_info(self, ctx: Context):
|
|
||||||
self.broadcast_all(self.build_message('gameInfo', {
|
|
||||||
'clientVersion': Utils.__version__,
|
|
||||||
'hintCost': ctx.hint_cost,
|
|
||||||
'checkPoints': ctx.check_points,
|
|
||||||
'forfeitMode': ctx.forfeit_mode,
|
|
||||||
'remainingMode': ctx.remaining_mode,
|
|
||||||
}))
|
|
||||||
|
|
||||||
def send_location_check(self, ctx: Context, last_check: str):
|
|
||||||
self.broadcast_all(self.build_message('locationCheck', {
|
|
||||||
'totalChecks': len(ctx.locations_checked),
|
|
||||||
'hintPoints': ctx.hint_points,
|
|
||||||
'lastCheck': last_check,
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
web_thread = None
|
|
||||||
PORT = 5050
|
|
||||||
|
|
||||||
|
|
||||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
||||||
def log_request(self, code='-', size='-'):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def log_date_time_string(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
Handler = partial(RequestHandler,
|
|
||||||
directory=Utils.local_path("data", "web", "public"))
|
|
||||||
|
|
||||||
|
|
||||||
def start_server(socket_port: int, on_start=lambda: None):
|
|
||||||
global web_thread
|
|
||||||
try:
|
|
||||||
server = socketserver.TCPServer(("", PORT), Handler)
|
|
||||||
except OSError:
|
|
||||||
# In most cases "Only one usage of each socket address (protocol/network address/port) is normally permitted"
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# If the exception is caused by our desired port being unavailable, assume the web server is already running
|
|
||||||
# from another client instance
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
||||||
if sock.connect_ex(('localhost', PORT)) == 0:
|
|
||||||
logging.info("Web server is already running in another client window.")
|
|
||||||
webbrowser.open(f'http://localhost:{PORT}?port={socket_port}')
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the exception is caused by something else, report on it
|
|
||||||
logging.exception("Unable to bind port for local web server. The CLI client should work in all cases.")
|
|
||||||
else:
|
|
||||||
print("serving at port", PORT)
|
|
||||||
on_start()
|
|
||||||
web_thread = threading.Thread(target=server.serve_forever).start()
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["@babel/preset-react", "@babel/preset-env"],
|
|
||||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'airbnb',
|
|
||||||
],
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
globals: {
|
|
||||||
Atomics: 'readonly',
|
|
||||||
SharedArrayBuffer: 'readonly',
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'react',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
"react/jsx-filename-extension": 0,
|
|
||||||
"react/jsx-one-expression-per-line": 0,
|
|
||||||
"react/destructuring-assignment": 0,
|
|
||||||
"react/jsx-curly-spacing": [2, { "when": "always" }],
|
|
||||||
"react/prop-types": 0,
|
|
||||||
"react/no-access-state-in-setstate": 0,
|
|
||||||
"react/button-has-type": 0,
|
|
||||||
"max-len": [2, { code: 120 }],
|
|
||||||
"operator-linebreak": [2, "after"],
|
|
||||||
"no-console": [2, { allow: ["error", "warn"] }],
|
|
||||||
"linebreak-style": 0,
|
|
||||||
"jsx-a11y/no-static-element-interactions": 0,
|
|
||||||
"jsx-a11y/click-events-have-key-events": 0,
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,2 +0,0 @@
|
||||||
node_modules
|
|
||||||
*.map
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,50 +0,0 @@
|
||||||
{
|
|
||||||
"name": "web-ui",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.jsx",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --config webpack.config.js",
|
|
||||||
"dev": "webpack --config webpack.dev.js"
|
|
||||||
},
|
|
||||||
"author": "LegendaryLinux",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
|
||||||
"crypto-browserify": "^3.12.0",
|
|
||||||
"crypto-js": "^4.0.0",
|
|
||||||
"css-loader": "^5.1.3",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"prop-types": "^15.7.2",
|
|
||||||
"react": "^17.0.1",
|
|
||||||
"react-dom": "^17.0.1",
|
|
||||||
"react-redux": "^7.2.2",
|
|
||||||
"react-router-dom": "^5.2.0",
|
|
||||||
"redux": "^4.0.5",
|
|
||||||
"redux-devtools-extension": "^2.13.9",
|
|
||||||
"sass-loader": "^10.1.1",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"webpack-cli": "^4.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.13.10",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
|
||||||
"@babel/preset-env": "^7.13.10",
|
|
||||||
"@babel/preset-react": "^7.12.13",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"babel-loader": "^8.2.2",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"eslint": "^7.22.0",
|
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
|
||||||
"eslint-plugin-import": "^2.22.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
||||||
"eslint-plugin-react": "^7.22.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"stream-browserify": "^3.0.0",
|
|
||||||
"webpack": "^5.27.1"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Berserker Multiworld Web GUI</title>
|
|
||||||
<script type="application/ecmascript" src="assets/index.bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<!-- Populated by React/JSX -->
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 242 KiB |
|
@ -1,10 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import '../../../styles/HeaderBar/components/HeaderBar.scss';
|
|
||||||
|
|
||||||
const HeaderBar = () => (
|
|
||||||
<div id="header-bar">
|
|
||||||
Multiworld WebUI
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default HeaderBar;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const APPEND_MESSAGE = 'APPEND_MESSAGE';
|
|
||||||
|
|
||||||
const appendMessage = (content) => ({
|
|
||||||
type: APPEND_MESSAGE,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default appendMessage;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const SET_MONITOR_FONT_SIZE = 'SET_MONITOR_FONT_SIZE';
|
|
||||||
|
|
||||||
const setMonitorFontSize = (fontSize) => ({
|
|
||||||
type: SET_MONITOR_FONT_SIZE,
|
|
||||||
fontSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setMonitorFontSize;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const SET_SHOW_RELEVANT = 'SET_SHOW_RELEVANT';
|
|
||||||
|
|
||||||
const setShowRelevant = (showRelevant) => ({
|
|
||||||
type: SET_SHOW_RELEVANT,
|
|
||||||
showRelevant,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setShowRelevant;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const SET_SIMPLE_FONT = 'SET_SIMPLE_FONT';
|
|
||||||
|
|
||||||
const setSimpleFont = (simpleFont) => ({
|
|
||||||
type: SET_SIMPLE_FONT,
|
|
||||||
simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setSimpleFont;
|
|
|
@ -1,42 +0,0 @@
|
||||||
import _assign from 'lodash-es/assign';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
fontSize: 18,
|
|
||||||
simpleFont: false,
|
|
||||||
showRelevantOnly: false,
|
|
||||||
messageLog: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const appendToLog = (log, item) => {
|
|
||||||
const trimmedLog = log.slice(-349);
|
|
||||||
trimmedLog.push(item);
|
|
||||||
return trimmedLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitorReducer = (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_MONITOR_FONT_SIZE':
|
|
||||||
return _assign({}, state, {
|
|
||||||
fontSize: action.fontSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'SET_SIMPLE_FONT':
|
|
||||||
return _assign({}, state, {
|
|
||||||
simpleFont: action.simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'SET_SHOW_RELEVANT':
|
|
||||||
return _assign({}, state, {
|
|
||||||
showRelevantOnly: action.showRelevant,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'APPEND_MESSAGE':
|
|
||||||
return _assign({}, state, {
|
|
||||||
messageLog: appendToLog(state.messageLog, action.content),
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default monitorReducer;
|
|
|
@ -1,13 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import '../../../styles/Monitor/components/Monitor.scss';
|
|
||||||
import MonitorControls from '../containers/MonitorControls';
|
|
||||||
import MonitorWindow from '../containers/MonitorWindow';
|
|
||||||
|
|
||||||
const Monitor = () => (
|
|
||||||
<div id="monitor">
|
|
||||||
<MonitorControls />
|
|
||||||
<MonitorWindow />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Monitor;
|
|
|
@ -1,218 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import _forEach from 'lodash-es/forEach';
|
|
||||||
import WebSocketUtils from '../../global/WebSocketUtils';
|
|
||||||
import '../../../styles/Monitor/containers/MonitorControls.scss';
|
|
||||||
|
|
||||||
// Redux actions
|
|
||||||
import setMonitorFontSize from '../Redux/actions/setMonitorFontSize';
|
|
||||||
import setShowRelevant from '../Redux/actions/setShowRelevant';
|
|
||||||
import setSimpleFont from '../Redux/actions/setSimpleFont';
|
|
||||||
|
|
||||||
const mapReduxStateToProps = (reduxState) => ({
|
|
||||||
fontSize: reduxState.monitor.fontSize,
|
|
||||||
webSocket: reduxState.webUI.webSocket,
|
|
||||||
availableDevices: reduxState.webUI.availableDevices,
|
|
||||||
snesDevice: reduxState.gameState.connections.snesDevice,
|
|
||||||
snesConnected: reduxState.gameState.connections.snesConnected,
|
|
||||||
serverAddress: reduxState.gameState.connections.serverAddress,
|
|
||||||
serverConnected: reduxState.gameState.connections.serverConnected,
|
|
||||||
simpleFont: reduxState.monitor.simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
updateFontSize: (fontSize) => {
|
|
||||||
dispatch(setMonitorFontSize(fontSize));
|
|
||||||
},
|
|
||||||
doToggleRelevance: (showRelevantOnly) => {
|
|
||||||
dispatch(setShowRelevant(showRelevantOnly));
|
|
||||||
},
|
|
||||||
doSetSimpleFont: (simpleFont) => {
|
|
||||||
dispatch(setSimpleFont(simpleFont));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class MonitorControls extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
deviceId: null,
|
|
||||||
serverAddress: this.props.serverAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.props.webSocket) {
|
|
||||||
// Poll for available devices
|
|
||||||
this.pollSnesDevices();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
// If there is only one SNES device available, connect to it automatically
|
|
||||||
if (
|
|
||||||
prevProps.availableDevices.length !== this.props.availableDevices.length &&
|
|
||||||
this.props.availableDevices.length === 1
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
|
||||||
this.setState({ deviceId: this.props.availableDevices[0] }, () => {
|
|
||||||
if (!this.props.snesConnected) {
|
|
||||||
this.connectToSnes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have moved from a disconnected state (default) into a connected state, request the game information
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
(prevProps.snesConnected !== this.props.snesConnected) || // SNES status changed
|
|
||||||
(prevProps.serverConnected !== this.props.serverConnected) // OR server status changed
|
|
||||||
) && ((this.props.serverConnected) && (this.props.snesConnected)) // AND both are connected
|
|
||||||
) {
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'gameInfo'));
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'checkData'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
increaseTextSize = () => {
|
|
||||||
if (this.props.fontSize >= 25) return;
|
|
||||||
this.props.updateFontSize(this.props.fontSize + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
decreaseTextSize = () => {
|
|
||||||
if (this.props.fontSize <= 10) return;
|
|
||||||
this.props.updateFontSize(this.props.fontSize - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateSnesOptions = () => {
|
|
||||||
const options = [];
|
|
||||||
// No available devices, show waiting for devices
|
|
||||||
if (this.props.availableDevices.length === 0) {
|
|
||||||
options.push(<option key="0" value="-1">Waiting for devices...</option>);
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
// More than one available device, list all options
|
|
||||||
options.push(<option key="-1" value="-1">Select a device</option>);
|
|
||||||
_forEach(this.props.availableDevices, (device) => {
|
|
||||||
options.push(<option key={ device } value={ device }>{device}</option>);
|
|
||||||
});
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDeviceId = (event) => this.setState({ deviceId: event.target.value }, this.connectToSnes);
|
|
||||||
|
|
||||||
pollSnesDevices = () => {
|
|
||||||
if (!this.props.webSocket) { return; }
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'devices'));
|
|
||||||
}
|
|
||||||
|
|
||||||
connectToSnes = () => {
|
|
||||||
if (!this.props.webSocket) { return; }
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webConfig', { deviceId: this.state.deviceId }));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateServerAddress = (event) => this.setState({ serverAddress: event.target.value ? event.target.value : null });
|
|
||||||
|
|
||||||
connectToServer = (event) => {
|
|
||||||
if (event.key !== 'Enter') { return; }
|
|
||||||
|
|
||||||
// If the user presses enter on an empty textbox, disconnect from the server
|
|
||||||
if (!event.target.value) {
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webControl', 'disconnect'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.webSocket.send(
|
|
||||||
WebSocketUtils.formatSocketData('webConfig', { serverAddress: this.state.serverAddress }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRelevance = (event) => {
|
|
||||||
this.props.doToggleRelevance(event.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
setSimpleFont = (event) => this.props.doSetSimpleFont(event.target.checked);
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="monitor-controls">
|
|
||||||
<div id="connection-status">
|
|
||||||
<div id="snes-connection">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>SNES Device:</td>
|
|
||||||
<td>
|
|
||||||
<select
|
|
||||||
onChange={ this.updateDeviceId }
|
|
||||||
disabled={ this.props.availableDevices.length === 0 }
|
|
||||||
value={ this.state.deviceId }
|
|
||||||
>
|
|
||||||
{this.generateSnesOptions()}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status:</td>
|
|
||||||
<td>
|
|
||||||
<span className={ this.props.snesConnected ? 'connected' : 'not-connected' }>
|
|
||||||
{this.props.snesConnected ? 'Connected' : 'Not Connected'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="server-connection">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Server:</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
defaultValue={ this.props.serverAddress }
|
|
||||||
onKeyUp={ this.updateServerAddress }
|
|
||||||
onKeyDown={ this.connectToServer }
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status:</td>
|
|
||||||
<td>
|
|
||||||
<span className={ this.props.serverConnected ? 'connected' : 'not-connected' }>
|
|
||||||
{this.props.serverConnected ? 'Connected' : 'Not Connected'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="accessibility">
|
|
||||||
<div>
|
|
||||||
Text Size:
|
|
||||||
<button disabled={ this.props.fontSize <= 10 } onClick={ this.decreaseTextSize }>-</button>
|
|
||||||
{ this.props.fontSize }
|
|
||||||
<button disabled={ this.props.fontSize >= 25 } onClick={ this.increaseTextSize }>+</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Only show my items <input type="checkbox" onChange={ this.toggleRelevance } />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Use alternate font
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
onChange={ this.setSimpleFont }
|
|
||||||
defaultChecked={ this.props.simpleFont }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapReduxStateToProps, mapDispatchToProps)(MonitorControls);
|
|
|
@ -1,96 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import md5 from 'crypto-js/md5';
|
|
||||||
import WebSocketUtils from '../../global/WebSocketUtils';
|
|
||||||
import '../../../styles/Monitor/containers/MonitorWindow.scss';
|
|
||||||
|
|
||||||
// Redux actions
|
|
||||||
import appendMessage from '../Redux/actions/appendMessage';
|
|
||||||
|
|
||||||
const mapReduxStateToProps = (reduxState) => ({
|
|
||||||
fontSize: reduxState.monitor.fontSize,
|
|
||||||
webSocket: reduxState.webUI.webSocket,
|
|
||||||
messageLog: reduxState.monitor.messageLog,
|
|
||||||
showRelevantOnly: reduxState.monitor.showRelevantOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
doAppendMessage: (message) => dispatch(appendMessage(
|
|
||||||
<div
|
|
||||||
key={ `${md5(message)}${Math.floor((Math.random() * 1000000))}` }
|
|
||||||
className="user-command relevant"
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</div>,
|
|
||||||
)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class MonitorWindow extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.monitorRef = React.createRef();
|
|
||||||
this.commandRef = React.createRef();
|
|
||||||
this.commandInputRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// Adjust the monitor height to match user's viewport
|
|
||||||
this.adjustMonitorHeight();
|
|
||||||
|
|
||||||
// Resize the monitor as the user adjusts the window size
|
|
||||||
window.addEventListener('resize', this.adjustMonitorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.monitorRef.current.style.fontSize = `${this.props.fontSize}px`;
|
|
||||||
this.adjustMonitorHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// If one day we have different components occupying the main viewport, let us not attempt to
|
|
||||||
// perform actions on an unmounted component
|
|
||||||
window.removeEventListener('resize', this.adjustMonitorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustMonitorHeight = () => {
|
|
||||||
const monitorDimensions = this.monitorRef.current.getBoundingClientRect();
|
|
||||||
const commandDimensions = this.commandRef.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Set monitor height
|
|
||||||
const newMonitorHeight = window.innerHeight - monitorDimensions.top - commandDimensions.height - 30;
|
|
||||||
this.monitorRef.current.style.height = `${newMonitorHeight}px`;
|
|
||||||
this.scrollToBottom();
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollToBottom = () => {
|
|
||||||
this.monitorRef.current.scrollTo(0, this.monitorRef.current.scrollHeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
sendCommand = (event) => {
|
|
||||||
// If the user didn't press enter, or the command is empty, do nothing
|
|
||||||
if (event.key !== 'Enter' || !event.target.value) return;
|
|
||||||
this.props.doAppendMessage(event.target.value);
|
|
||||||
this.scrollToBottom();
|
|
||||||
this.props.webSocket.send(WebSocketUtils.formatSocketData('webCommand', event.target.value));
|
|
||||||
this.commandInputRef.current.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="monitor-window-wrapper">
|
|
||||||
<div
|
|
||||||
id="monitor-window"
|
|
||||||
ref={ this.monitorRef }
|
|
||||||
className={ `${this.props.showRelevantOnly ? 'relevant-only' : null}` }
|
|
||||||
>
|
|
||||||
{ this.props.messageLog }
|
|
||||||
</div>
|
|
||||||
<div id="command-wrapper" ref={ this.commandRef }>
|
|
||||||
Command: <input onKeyDown={ this.sendCommand } ref={ this.commandInputRef } />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapReduxStateToProps, mapDispatchToProps)(MonitorWindow);
|
|
|
@ -1,8 +0,0 @@
|
||||||
const SET_AVAILABLE_DEVICES = 'SET_AVAILABLE_DEVICES';
|
|
||||||
|
|
||||||
const setAvailableDevices = (devices) => ({
|
|
||||||
type: SET_AVAILABLE_DEVICES,
|
|
||||||
devices,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setAvailableDevices;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const SET_WEBSOCKET = 'SET_WEBSOCKET';
|
|
||||||
|
|
||||||
const setWebSocket = (webSocket) => ({
|
|
||||||
type: SET_WEBSOCKET,
|
|
||||||
webSocket,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default setWebSocket;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import _assign from 'lodash-es/assign';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
webSocket: null,
|
|
||||||
availableDevices: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const webUIReducer = (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_WEBSOCKET':
|
|
||||||
return _assign({}, state, {
|
|
||||||
webSocket: action.webSocket,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'SET_AVAILABLE_DEVICES':
|
|
||||||
return _assign({}, state, {
|
|
||||||
availableDevices: action.devices,
|
|
||||||
});
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default webUIReducer;
|
|
|
@ -1,109 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import HeaderBar from '../../HeaderBar/components/HeaderBar';
|
|
||||||
import Monitor from '../../Monitor/components/Monitor';
|
|
||||||
import WidgetArea from '../../WidgetArea/containers/WidgetArea';
|
|
||||||
import MonitorTools from '../../global/MonitorTools';
|
|
||||||
import '../../../styles/WebUI/containers/WebUI.scss';
|
|
||||||
|
|
||||||
// Redux actions
|
|
||||||
import setWebSocket from '../Redux/actions/setWebSocket';
|
|
||||||
import WebSocketUtils from '../../global/WebSocketUtils';
|
|
||||||
import updateGameState from '../../global/Redux/actions/updateGameState';
|
|
||||||
import appendMessage from '../../Monitor/Redux/actions/appendMessage';
|
|
||||||
|
|
||||||
const mapReduxStateToProps = (reduxState) => ({
|
|
||||||
connections: reduxState.gameState.connections,
|
|
||||||
simpleFont: reduxState.monitor.simpleFont,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
doSetWebSocket: (webSocket) => dispatch(setWebSocket(webSocket)),
|
|
||||||
handleIncomingMessage: (message) => dispatch(WebSocketUtils.handleIncomingMessage(message)),
|
|
||||||
doUpdateGameState: (gameState) => dispatch(updateGameState(gameState)),
|
|
||||||
appendMonitorMessage: (message) => dispatch(appendMessage(message)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class WebUI extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.webSocket = null;
|
|
||||||
this.maxConnectionAttempts = 20;
|
|
||||||
this.webUiRef = React.createRef();
|
|
||||||
this.state = {
|
|
||||||
connectionAttempts: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.webSocketConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocketConnect = () => {
|
|
||||||
this.props.appendMonitorMessage(MonitorTools.createTextDiv(
|
|
||||||
`Attempting to connect to MultiClient (attempt ${this.state.connectionAttempts + 1})...`,
|
|
||||||
));
|
|
||||||
this.setState({ connectionAttempts: this.state.connectionAttempts + 1 }, () => {
|
|
||||||
if (this.state.connectionAttempts >= 20) {
|
|
||||||
this.props.appendMonitorMessage(MonitorTools.createTextDiv(
|
|
||||||
'Unable to connect to MultiClient. Maximum of 20 attempts exceeded.',
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParams = new URLSearchParams(document.location.search.substring(1));
|
|
||||||
const port = getParams.get('port');
|
|
||||||
if (!port) { throw new Error('Unable to determine socket port from GET parameters'); }
|
|
||||||
|
|
||||||
const webSocketAddress = `ws://localhost:${port}`;
|
|
||||||
try {
|
|
||||||
this.props.webSocket.close();
|
|
||||||
this.props.doSetWebSocket(null);
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors caused by attempting to close an invalid WebSocket object
|
|
||||||
}
|
|
||||||
|
|
||||||
const webSocket = new WebSocket(webSocketAddress);
|
|
||||||
webSocket.onerror = () => {
|
|
||||||
this.props.doUpdateGameState({
|
|
||||||
connections: {
|
|
||||||
snesDevice: this.props.connections.snesDevice,
|
|
||||||
snesConnected: false,
|
|
||||||
serverAddress: this.props.connections.serverAddress,
|
|
||||||
serverConnected: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (this.state.connectionAttempts < this.maxConnectionAttempts) {
|
|
||||||
setTimeout(this.webSocketConnect, 5000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dispatch a custom event when websocket messages are received
|
|
||||||
webSocket.onmessage = (message) => {
|
|
||||||
this.props.handleIncomingMessage(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the webSocket object in the Redux store so other components can access it
|
|
||||||
webSocket.onopen = () => {
|
|
||||||
this.props.doSetWebSocket(webSocket);
|
|
||||||
webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'connections'));
|
|
||||||
this.props.appendMonitorMessage(MonitorTools.createTextDiv('Connected to MultiClient.'));
|
|
||||||
this.setState({ connectionAttempts: 0 });
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="web-ui" ref={ this.webUiRef } className={ this.props.simpleFont ? 'simple-font' : null }>
|
|
||||||
<HeaderBar />
|
|
||||||
<div id="content-middle">
|
|
||||||
<Monitor />
|
|
||||||
<WidgetArea />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapReduxStateToProps, mapDispatchToProps)(WebUI);
|
|
|
@ -1,117 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import '../../../styles/WidgetArea/containers/WidgetArea.scss';
|
|
||||||
|
|
||||||
const mapReduxStateToProps = (reduxState) => ({
|
|
||||||
clientVersion: reduxState.gameState.clientVersion,
|
|
||||||
forfeitMode: reduxState.gameState.forfeitMode,
|
|
||||||
remainingMode: reduxState.gameState.remainingMode,
|
|
||||||
hintCost: reduxState.gameState.hintCost,
|
|
||||||
checkPoints: reduxState.gameState.checkPoints,
|
|
||||||
hintPoints: reduxState.gameState.hintPoints,
|
|
||||||
totalChecks: reduxState.gameState.totalChecks,
|
|
||||||
lastCheck: reduxState.gameState.lastCheck,
|
|
||||||
});
|
|
||||||
|
|
||||||
class WidgetArea extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
collapsed: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
saveNotes = (event) => {
|
|
||||||
localStorage.setItem('notes', event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
|
||||||
toggleCollapse = () => this.setState({ collapsed: !this.state.collapsed });
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div id="widget-area" className={ `${this.state.collapsed ? 'collapsed' : null}` }>
|
|
||||||
{
|
|
||||||
this.state.collapsed ? (
|
|
||||||
<div id="widget-button-row">
|
|
||||||
<button className="collapse-button" onClick={ this.toggleCollapse }>↩</button>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.collapsed ? null : (
|
|
||||||
<div id="widget-area-contents">
|
|
||||||
<div id="game-info">
|
|
||||||
<div id="game-info-title">
|
|
||||||
Game Info:
|
|
||||||
<button className="collapse-button" onClick={ this.toggleCollapse }>↪</button>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Client Version:</th>
|
|
||||||
<td>{this.props.clientVersion}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Forfeit Mode:</th>
|
|
||||||
<td>{this.props.forfeitMode}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Remaining Mode:</th>
|
|
||||||
<td>{this.props.remainingMode}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="check-data">
|
|
||||||
<div id="check-data-title">Checks:</div>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Total Checks:</th>
|
|
||||||
<td>{this.props.totalChecks}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Last Check:</th>
|
|
||||||
<td>{this.props.lastCheck}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="hint-data">
|
|
||||||
<div id="hint-data-title">
|
|
||||||
Hint Data:
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Hint Cost:</th>
|
|
||||||
<td>{this.props.hintCost}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Check Points:</th>
|
|
||||||
<td>{this.props.checkPoints}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Current Points:</th>
|
|
||||||
<td>{this.props.hintPoints}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="notes">
|
|
||||||
<div id="notes-title">
|
|
||||||
<div>Notes:</div>
|
|
||||||
</div>
|
|
||||||
<textarea defaultValue={ localStorage.getItem('notes') } onKeyUp={ this.saveNotes } />
|
|
||||||
</div>
|
|
||||||
More tools Coming Soon™
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapReduxStateToProps)(WidgetArea);
|
|
|
@ -1,69 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import md5 from 'crypto-js/md5';
|
|
||||||
|
|
||||||
const finderSpan = (finder, possessive = false, ownItem = false) => (
|
|
||||||
<span className={ `finder-span ${ownItem ? 'mine' : null}` }>{finder}{possessive ? "'s" : null}</span>
|
|
||||||
);
|
|
||||||
const recipientSpan = (recipient, possessive = false, ownItem = false) => (
|
|
||||||
<span className={ `recipient-span ${ownItem ? 'mine' : null}` }>{recipient}{possessive ? "'s" : null}</span>
|
|
||||||
);
|
|
||||||
const itemSpan = (item, unique) => <span className={ `item-span ${unique ? 'unique' : ''}` }>{item}</span>;
|
|
||||||
const locationSpan = (location) => <span className="location-span">{location}</span>;
|
|
||||||
const entranceSpan = (entrance) => <span className="entrance-span">{entrance}</span>;
|
|
||||||
|
|
||||||
class MonitorTools {
|
|
||||||
/** Convert plaintext into a React-friendly div */
|
|
||||||
static createTextDiv = (text) => (
|
|
||||||
<div key={ `${md5(text)}${Math.floor((Math.random() * 1000000))}` }>
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Sent an item to another player */
|
|
||||||
static sentItem = (finder, recipient, item, location, iAmFinder = false, iAmRecipient = false, unique = false) => (
|
|
||||||
<div
|
|
||||||
key={ `${md5(finder + recipient + item + location)}${Math.floor((Math.random() * 1000000))}` }
|
|
||||||
className={ (iAmFinder || iAmRecipient) ? 'relevant' : null }
|
|
||||||
>
|
|
||||||
{finderSpan(finder, false, iAmFinder)} found {recipientSpan(recipient, true, iAmRecipient)}
|
|
||||||
{itemSpan(item, unique)} at {locationSpan(location)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Received item from another player */
|
|
||||||
static receivedItem = (finder, item, location, itemIndex, queueLength, unique = false) => (
|
|
||||||
<div
|
|
||||||
key={ `${md5(finder + item + location)}${Math.floor((Math.random() * 1000000))}` }
|
|
||||||
className="relevant"
|
|
||||||
>
|
|
||||||
({itemIndex}/{queueLength}) {finderSpan(finder, false)} found your
|
|
||||||
{itemSpan(item, unique)} at {locationSpan(location)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Player found their own item (local or remote player) */
|
|
||||||
static foundItem = (finder, item, location, iAmFinder = false, unique = false) => (
|
|
||||||
<div
|
|
||||||
key={ `${md5(finder + item + location)}${Math.floor((Math.random() * 1000000))}` }
|
|
||||||
className={ iAmFinder ? 'relevant' : null }
|
|
||||||
>
|
|
||||||
{finderSpan(finder, false, iAmFinder)} found their own {itemSpan(item, unique)} at {locationSpan(location)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Hint message */
|
|
||||||
static hintMessage = (finder, recipient, item, location, found, iAmFinder = false, iAmRecipient = false,
|
|
||||||
entranceLocation = null) => (
|
|
||||||
<div
|
|
||||||
key={ `${md5(finder + recipient + item + location)}${Math.floor((Math.random() * 1000000))}` }
|
|
||||||
className={ (iAmFinder || iAmRecipient) ? 'relevant' : null }
|
|
||||||
>
|
|
||||||
{recipientSpan(recipient, true, iAmRecipient)} {itemSpan(item)} can be found in
|
|
||||||
{finderSpan(finder, true, iAmFinder)} world at {locationSpan(location)}
|
|
||||||
{ entranceLocation ? [', which is at ', entranceSpan(entranceLocation)] : null }
|
|
||||||
({found ? '✔' : '❌'})
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MonitorTools;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const UPDATE_GAME_STATE = 'UPDATE_GAME_STATE';
|
|
||||||
|
|
||||||
const updateGameState = (gameState) => ({
|
|
||||||
type: UPDATE_GAME_STATE,
|
|
||||||
gameState,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default updateGameState;
|
|
|
@ -1,30 +0,0 @@
|
||||||
import _assign from 'lodash-es/assign';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
clientVersion: null,
|
|
||||||
forfeitMode: null,
|
|
||||||
remainingMode: null,
|
|
||||||
connections: {
|
|
||||||
snesDevice: '',
|
|
||||||
snesConnected: false,
|
|
||||||
serverAddress: null,
|
|
||||||
serverConnected: false,
|
|
||||||
},
|
|
||||||
totalChecks: 0,
|
|
||||||
lastCheck: null,
|
|
||||||
hintCost: null,
|
|
||||||
checkPoints: null,
|
|
||||||
hintPoints: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const gameStateReducer = (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'UPDATE_GAME_STATE':
|
|
||||||
return _assign({}, state, action.gameState);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default gameStateReducer;
|
|
|
@ -1,106 +0,0 @@
|
||||||
import MonitorTools from './MonitorTools';
|
|
||||||
|
|
||||||
// Redux actions
|
|
||||||
import appendMessage from '../Monitor/Redux/actions/appendMessage';
|
|
||||||
import updateGameState from './Redux/actions/updateGameState';
|
|
||||||
import setAvailableDevices from '../WebUI/Redux/actions/setAvailableDevices';
|
|
||||||
|
|
||||||
class WebSocketUtils {
|
|
||||||
static formatSocketData = (eventType, content) => JSON.stringify({
|
|
||||||
type: eventType,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle incoming websocket data and return appropriate data for dispatch
|
|
||||||
* @param message
|
|
||||||
* @returns Object
|
|
||||||
*/
|
|
||||||
static handleIncomingMessage = (message) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(message.data);
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
// Client sent snes and server connection statuses
|
|
||||||
case 'connections':
|
|
||||||
return updateGameState({
|
|
||||||
connections: {
|
|
||||||
snesDevice: data.content.snesDevice ? data.content.snesDevice : '',
|
|
||||||
snesConnected: parseInt(data.content.snes, 10) === 3,
|
|
||||||
serverAddress: data.content.serverAddress ? data.content.serverAddress.replace(/^.*\/\//, '') : null,
|
|
||||||
serverConnected: parseInt(data.content.server, 10) === 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'availableDevices':
|
|
||||||
return setAvailableDevices(data.content.devices);
|
|
||||||
|
|
||||||
// Client unable to automatically connect to multiworld server
|
|
||||||
case 'serverAddress':
|
|
||||||
return appendMessage(MonitorTools.createTextDiv(
|
|
||||||
'Unable to automatically connect to multiworld server. Please enter an address manually.',
|
|
||||||
));
|
|
||||||
|
|
||||||
case 'itemSent':
|
|
||||||
return appendMessage(MonitorTools.sentItem(data.content.finder, data.content.recipient,
|
|
||||||
data.content.item, data.content.location, parseInt(data.content.iAmFinder, 10) === 1,
|
|
||||||
parseInt(data.content.iAmRecipient, 10) === 1, parseInt(data.content.itemIsUnique, 10) === 1));
|
|
||||||
|
|
||||||
case 'itemReceived':
|
|
||||||
return appendMessage(MonitorTools.receivedItem(data.content.finder, data.content.item,
|
|
||||||
data.content.location, data.content.itemIndex, data.content.queueLength,
|
|
||||||
parseInt(data.content.itemIsUnique, 10) === 1));
|
|
||||||
|
|
||||||
case 'itemFound':
|
|
||||||
return appendMessage(MonitorTools.foundItem(data.content.finder, data.content.item, data.content.location,
|
|
||||||
parseInt(data.content.iAmFinder, 10) === 1, parseInt(data.content.itemIsUnique, 10) === 1));
|
|
||||||
|
|
||||||
case 'hint':
|
|
||||||
return appendMessage(MonitorTools.hintMessage(data.content.finder, data.content.recipient,
|
|
||||||
data.content.item, data.content.location, parseInt(data.content.found, 10) === 1,
|
|
||||||
parseInt(data.content.iAmFinder, 10) === 1, parseInt(data.content.iAmRecipient, 10) === 1,
|
|
||||||
data.content.entranceLocation));
|
|
||||||
|
|
||||||
case 'gameInfo':
|
|
||||||
return updateGameState({
|
|
||||||
clientVersion: data.content.clientVersion,
|
|
||||||
forfeitMode: data.content.forfeitMode,
|
|
||||||
remainingMode: data.content.remainingMode,
|
|
||||||
hintCost: parseInt(data.content.hintCost, 10),
|
|
||||||
checkPoints: parseInt(data.content.checkPoints, 10),
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'locationCheck':
|
|
||||||
return updateGameState({
|
|
||||||
totalChecks: parseInt(data.content.totalChecks, 10),
|
|
||||||
lastCheck: data.content.lastCheck,
|
|
||||||
hintPoints: parseInt(data.content.hintPoints, 10),
|
|
||||||
});
|
|
||||||
|
|
||||||
// The client prints several types of messages to the console
|
|
||||||
case 'critical':
|
|
||||||
case 'error':
|
|
||||||
case 'warning':
|
|
||||||
case 'info':
|
|
||||||
case 'chat':
|
|
||||||
return appendMessage(MonitorTools.createTextDiv(
|
|
||||||
(typeof (data.content) === 'string') ? data.content : JSON.stringify(data.content),
|
|
||||||
));
|
|
||||||
default:
|
|
||||||
console.warn(`Unknown message type received: ${data.type}`);
|
|
||||||
console.warn(data.content);
|
|
||||||
return { type: 'NO_OP' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(message);
|
|
||||||
console.error(error);
|
|
||||||
// The returned value from this function will be dispatched to Redux. If an error occurs,
|
|
||||||
// Redux and the SPA in general should live on. Dispatching something with the correct format
|
|
||||||
// but that matches no known Redux action will cause the state to update to itself, which is
|
|
||||||
// treated as a no-op.
|
|
||||||
return { type: 'NO_OP' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WebSocketUtils;
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDom from 'react-dom';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { createStore, combineReducers } from 'redux';
|
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
|
|
||||||
import WebUI from './WebUI/containers/WebUI';
|
|
||||||
import '../styles/index.scss';
|
|
||||||
|
|
||||||
// Redux reducers
|
|
||||||
import webUI from './WebUI/Redux/reducers/webUIReducer';
|
|
||||||
import gameState from './global/Redux/reducers/gameStateReducer';
|
|
||||||
import monitor from './Monitor/Redux/reducers/monitorReducer';
|
|
||||||
|
|
||||||
const store = createStore(combineReducers({
|
|
||||||
webUI,
|
|
||||||
gameState,
|
|
||||||
monitor,
|
|
||||||
}), composeWithDevTools());
|
|
||||||
|
|
||||||
const App = () => (
|
|
||||||
<Provider store={ store }>
|
|
||||||
<WebUI />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
ReactDom.render(<App />, document.getElementById('app'));
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
#header-bar{
|
|
||||||
font-size: 3.4em;
|
|
||||||
min-width: 1036px;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
#monitor{
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 800px;
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
#monitor-controls{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
|
|
||||||
#connection-status{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
#snes-connection, #server-connection{
|
|
||||||
margin-right: 1em;
|
|
||||||
table{
|
|
||||||
td{
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.connected{
|
|
||||||
color: #008000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-connected{
|
|
||||||
color: #ff0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#accessibility{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
button{
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
#monitor-window-wrapper{
|
|
||||||
#monitor-window{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
background-color: #414042;
|
|
||||||
color: #dce7df;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
div{
|
|
||||||
width: calc(100% - 14px);
|
|
||||||
padding: 7px;
|
|
||||||
border-bottom: 1px solid #000000;
|
|
||||||
|
|
||||||
&.user-command{
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #575757;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.relevant-only{
|
|
||||||
div:not(.relevant){
|
|
||||||
visibility: collapse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-span{
|
|
||||||
color: #67e9ff;
|
|
||||||
|
|
||||||
&.unique{
|
|
||||||
color: #ff884e;
|
|
||||||
text-shadow: #000000 2px 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.location-span{ color: #f5e63c; }
|
|
||||||
.entrance-span{ color: #73ae38; }
|
|
||||||
.finder-span{ color: #f96cb8; }
|
|
||||||
.recipient-span{ color: #9b8aff; }
|
|
||||||
.mine{ color: #ffa500; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#command-wrapper{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 25px;
|
|
||||||
line-height: 25px;
|
|
||||||
|
|
||||||
input{
|
|
||||||
margin-left: 0.5em;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
@font-face{
|
|
||||||
font-family: HyliaSerif;
|
|
||||||
src: local('HyliaSerif'), url('../../../assets/HyliaSerif.otf')
|
|
||||||
}
|
|
||||||
|
|
||||||
#web-ui{
|
|
||||||
width: calc(100% - 1.5em);
|
|
||||||
padding: 0.75em;
|
|
||||||
font-family: HyliaSerif, sans-serif;
|
|
||||||
|
|
||||||
&.simple-font{
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-middle{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
#widget-area{
|
|
||||||
margin-left: 0.5em;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
padding: 0.25em;
|
|
||||||
border: 2px solid #6a6a6a;
|
|
||||||
|
|
||||||
&:not(.collapsed){
|
|
||||||
width: calc(20% - 1.5em - 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#widget-button-row{
|
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.collapse-button{
|
|
||||||
width: 35px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#widget-area-contents{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
table{
|
|
||||||
th{
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
td{
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#game-info{
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
#game-info-title{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#check-data{
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hint-data{
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notes{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
textarea{
|
|
||||||
height: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: #131313;
|
|
||||||
color: #eae703;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'production',
|
|
||||||
watch: false,
|
|
||||||
resolve: {
|
|
||||||
fallback: {
|
|
||||||
crypto: require.resolve('crypto-browserify'),
|
|
||||||
buffer: require.resolve('buffer/'),
|
|
||||||
stream: require.resolve('stream-browserify'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
entry: {
|
|
||||||
index: './src/js/index.js',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx|es6)$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
compact: true,
|
|
||||||
minified: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.((css)|(s[a|c]ss))$/,
|
|
||||||
use: [
|
|
||||||
{ loader: 'style-loader' },
|
|
||||||
{ loader: 'css-loader' },
|
|
||||||
{ loader: 'sass-loader' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(otf)$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: '[name].[ext]',
|
|
||||||
outputPath: 'fonts/',
|
|
||||||
publicPath: 'assets/fonts/',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: `${path.resolve(__dirname)}/public/assets`,
|
|
||||||
publicPath: '/',
|
|
||||||
filename: '[name].bundle.js',
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,52 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'development',
|
|
||||||
watch: true,
|
|
||||||
resolve: {
|
|
||||||
fallback: {
|
|
||||||
crypto: require.resolve('crypto-browserify'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
entry: {
|
|
||||||
index: './src/js/index.js',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx|es6)$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
compact: false,
|
|
||||||
minified: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.((css)|(s[a|c]ss))$/,
|
|
||||||
use: [
|
|
||||||
{ loader: 'style-loader' },
|
|
||||||
{ loader: 'css-loader' },
|
|
||||||
{ loader: 'sass-loader' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(otf)$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: '[name].[ext]',
|
|
||||||
outputPath: 'fonts/',
|
|
||||||
publicPath: 'assets/fonts/',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: `${path.resolve(__dirname)}/public/assets`,
|
|
||||||
publicPath: '/',
|
|
||||||
filename: '[name].bundle.js',
|
|
||||||
},
|
|
||||||
};
|
|
Loading…
Reference in New Issue