bunch of fixes after testing round

This commit is contained in:
Fabian Dill 2021-05-14 01:25:41 +02:00
parent b82d6cec31
commit b2f3fd56f4
44 changed files with 18 additions and 15781 deletions

View File

@ -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:

View File

@ -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
View File

@ -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,

View File

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

View File

@ -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
View File

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

View File

@ -1,4 +0,0 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@ -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,
},
};

2
data/web/.gitignore vendored
View File

@ -1,2 +0,0 @@
node_modules
*.map

14170
data/web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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;

View File

@ -1,8 +0,0 @@
const APPEND_MESSAGE = 'APPEND_MESSAGE';
const appendMessage = (content) => ({
type: APPEND_MESSAGE,
content,
});
export default appendMessage;

View File

@ -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;

View File

@ -1,8 +0,0 @@
const SET_SHOW_RELEVANT = 'SET_SHOW_RELEVANT';
const setShowRelevant = (showRelevant) => ({
type: SET_SHOW_RELEVANT,
showRelevant,
});
export default setShowRelevant;

View File

@ -1,8 +0,0 @@
const SET_SIMPLE_FONT = 'SET_SIMPLE_FONT';
const setSimpleFont = (simpleFont) => ({
type: SET_SIMPLE_FONT,
simpleFont,
});
export default setSimpleFont;

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -1,8 +0,0 @@
const SET_AVAILABLE_DEVICES = 'SET_AVAILABLE_DEVICES';
const setAvailableDevices = (devices) => ({
type: SET_AVAILABLE_DEVICES,
devices,
});
export default setAvailableDevices;

View File

@ -1,8 +0,0 @@
const SET_WEBSOCKET = 'SET_WEBSOCKET';
const setWebSocket = (webSocket) => ({
type: SET_WEBSOCKET,
webSocket,
});
export default setWebSocket;

View File

@ -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;

View File

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

View File

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

View File

@ -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)}&nbsp;
{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&nbsp;
{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&nbsp;
{finderSpan(finder, true, iAmFinder)} world at {locationSpan(location)}
{ entranceLocation ? [', which is at ', entranceSpan(entranceLocation)] : null }&nbsp;
({found ? '✔' : '❌'})
</div>
)
}
export default MonitorTools;

View File

@ -1,8 +0,0 @@
const UPDATE_GAME_STATE = 'UPDATE_GAME_STATE';
const updateGameState = (gameState) => ({
type: UPDATE_GAME_STATE,
gameState,
});
export default updateGameState;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -1,4 +0,0 @@
#header-bar{
font-size: 3.4em;
min-width: 1036px;
}

View File

@ -1,4 +0,0 @@
#monitor{
flex-grow: 1;
min-width: 800px;
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -1,6 +0,0 @@
body {
background-color: #131313;
color: #eae703;
letter-spacing: 2px;
margin: 0;
}

View File

@ -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',
},
};

View File

@ -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',
},
};