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"""
 | 
			
		||||
        logger.info('Received items:')
 | 
			
		||||
        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)' % (
 | 
			
		||||
                color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
 | 
			
		||||
                color(self.ctx.player_names[item.player], 'yellow'),
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +112,6 @@ class CommonContext():
 | 
			
		|||
        self.team = None
 | 
			
		||||
        self.slot = None
 | 
			
		||||
        self.auth = None
 | 
			
		||||
        self.ui_node = None
 | 
			
		||||
 | 
			
		||||
        self.locations_checked: 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):
 | 
			
		||||
    ui_node = getattr(ctx, "ui_node", None)
 | 
			
		||||
    if ui_node:
 | 
			
		||||
        ui_node.send_connection_status(ctx)
 | 
			
		||||
    cached_address = None
 | 
			
		||||
    if ctx.server and ctx.server.socket:
 | 
			
		||||
        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
 | 
			
		||||
    if not address:
 | 
			
		||||
        logger.info('Please connect to an Archipelago server.')
 | 
			
		||||
        if ui_node:
 | 
			
		||||
            ui_node.poll_for_server_ip()
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        logger.info('Connected')
 | 
			
		||||
        ctx.server_address = address
 | 
			
		||||
        if ui_node:
 | 
			
		||||
            ui_node.send_connection_status(ctx)
 | 
			
		||||
        ctx.current_reconnect_delay = ctx.starting_reconnect_delay
 | 
			
		||||
        async for data in ctx.server.socket:
 | 
			
		||||
            for msg in decode(data):
 | 
			
		||||
| 
						 | 
				
			
			@ -273,8 +261,6 @@ async def server_loop(ctx: CommonContext, address=None):
 | 
			
		|||
        await ctx.connection_closed()
 | 
			
		||||
        if ctx.server_address:
 | 
			
		||||
            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))
 | 
			
		||||
        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.forfeit_mode = args['forfeit_mode']
 | 
			
		||||
        ctx.remaining_mode = args['remaining_mode']
 | 
			
		||||
        if ctx.ui_node:
 | 
			
		||||
            ctx.ui_node.send_game_info(ctx)
 | 
			
		||||
        if len(args['players']) < 1:
 | 
			
		||||
            logger.info('No player connected')
 | 
			
		||||
        else:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										102
									
								
								LttPClient.py
								
								
								
								
							
							
						
						
									
										102
									
								
								LttPClient.py
								
								
								
								
							| 
						 | 
				
			
			@ -24,7 +24,6 @@ ModuleUpdate.update()
 | 
			
		|||
import colorama
 | 
			
		||||
 | 
			
		||||
from NetUtils import *
 | 
			
		||||
import WebUI
 | 
			
		||||
 | 
			
		||||
from worlds.alttp import Regions, Shops
 | 
			
		||||
from worlds.alttp import Items
 | 
			
		||||
| 
						 | 
				
			
			@ -45,12 +44,6 @@ class LttPCommandProcessor(ClientCommandProcessor):
 | 
			
		|||
 | 
			
		||||
        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
 | 
			
		||||
    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"""
 | 
			
		||||
| 
						 | 
				
			
			@ -69,21 +62,9 @@ class LttPCommandProcessor(ClientCommandProcessor):
 | 
			
		|||
 | 
			
		||||
class Context(CommonContext):
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
        self.snes_address = snes_address
 | 
			
		||||
        self.snes_socket = None
 | 
			
		||||
| 
						 | 
				
			
			@ -495,7 +476,7 @@ async def get_snes_devices(ctx: Context):
 | 
			
		|||
            reply = loads(await socket.recv())
 | 
			
		||||
            devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
 | 
			
		||||
 | 
			
		||||
    ctx.ui_node.send_device_list(devices)
 | 
			
		||||
 | 
			
		||||
    await socket.close()
 | 
			
		||||
    return devices
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -517,8 +498,6 @@ async def snes_connect(ctx: Context, address):
 | 
			
		|||
 | 
			
		||||
        if len(devices) == 1:
 | 
			
		||||
            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:
 | 
			
		||||
            if ctx.snes_attached_device[1] in devices:
 | 
			
		||||
                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))
 | 
			
		||||
        ctx.snes_state = SNESState.SNES_ATTACHED
 | 
			
		||||
        ctx.snes_attached_device = (devices.index(device), device)
 | 
			
		||||
        ctx.ui_node.send_connection_status(ctx)
 | 
			
		||||
 | 
			
		||||
        if 'sd2snes' in device.lower() or 'COM' in device:
 | 
			
		||||
            logger.info("SD2SNES/FXPAK Detected")
 | 
			
		||||
| 
						 | 
				
			
			@ -607,7 +585,6 @@ async def snes_recv_loop(ctx: Context):
 | 
			
		|||
        ctx.snes_state = SNESState.SNES_DISCONNECTED
 | 
			
		||||
        ctx.snes_recv_queue = asyncio.Queue()
 | 
			
		||||
        ctx.hud_message_queue = []
 | 
			
		||||
        ctx.ui_node.send_connection_status(ctx)
 | 
			
		||||
 | 
			
		||||
        ctx.rom = None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -743,8 +720,6 @@ async def track_locations(ctx: Context, roomid, roomdata):
 | 
			
		|||
        ctx.locations_checked.add(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)})')
 | 
			
		||||
        ctx.ui_node.send_location_check(ctx, location)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        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:
 | 
			
		||||
            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)' % (
 | 
			
		||||
                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)))
 | 
			
		||||
| 
						 | 
				
			
			@ -920,57 +891,6 @@ async def run_game(romfile):
 | 
			
		|||
        subprocess.Popen([auto_start, romfile],
 | 
			
		||||
                         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():
 | 
			
		||||
    multiprocessing.freeze_support()
 | 
			
		||||
    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('--founditems', default=False, action='store_true',
 | 
			
		||||
                        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()
 | 
			
		||||
    logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
 | 
			
		||||
    if args.diff_file:
 | 
			
		||||
| 
						 | 
				
			
			@ -1002,23 +920,9 @@ async def main():
 | 
			
		|||
        asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
 | 
			
		||||
 | 
			
		||||
    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")
 | 
			
		||||
    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:
 | 
			
		||||
        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)}
 | 
			
		||||
        for item in world.precollected_items:
 | 
			
		||||
            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
 | 
			
		||||
        sending_visible_players = set()
 | 
			
		||||
        for player in world.factorio_player_ids:
 | 
			
		||||
| 
						 | 
				
			
			@ -533,11 +533,17 @@ def main(args, seed=None):
 | 
			
		|||
            if type(location.address) == int:
 | 
			
		||||
                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:
 | 
			
		||||
                    precollected_hints[location.player].append(NetUtils.Hint(location.item.player, location.player, location.address,
 | 
			
		||||
                                                                        location.item.code, False))
 | 
			
		||||
                    hint = NetUtils.Hint(location.item.player, location.player, location.address,
 | 
			
		||||
                                         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]:
 | 
			
		||||
                    precollected_hints[location.player].append(NetUtils.Hint(location.item.player, location.player, location.address,
 | 
			
		||||
                                                                             location.item.code, False))
 | 
			
		||||
                    hint = NetUtils.Hint(location.item.player, location.player, location.address,
 | 
			
		||||
                                         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({
 | 
			
		||||
            "slot_data" : slot_data,
 | 
			
		||||
            "games": games,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -161,7 +161,7 @@ class Context(Node):
 | 
			
		|||
                if slot in self.remote_items:
 | 
			
		||||
                    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():
 | 
			
		||||
                self.hints[team, slot] = hints
 | 
			
		||||
                self.hints[team, slot].update(hints)
 | 
			
		||||
        if use_embedded_server_options:
 | 
			
		||||
            server_options = decoded_obj.get("server_options", {})
 | 
			
		||||
            self._set_options(server_options)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,16 +7,15 @@ from uuid import UUID
 | 
			
		|||
 | 
			
		||||
from worlds.alttp import Items, Regions
 | 
			
		||||
from WebHostLib import app, cache, Room
 | 
			
		||||
from NetUtils import Hint
 | 
			
		||||
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):
 | 
			
		||||
    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['item_name'] = lambda id: Items.lookup_id_to_name.get(id, id)
 | 
			
		||||
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: lookup_any_item_id_to_name.get(id, id)
 | 
			
		||||
 | 
			
		||||
icons = {
 | 
			
		||||
    "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