mnml/client/src/socket.jsx
2019-06-17 19:50:09 +10:00

338 lines
9.4 KiB
JavaScript

const toast = require('izitoast');
const cbor = require('borc');
const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/api/ws' : 'ws://localhost:40000/api/ws';
function errorToast(err) {
console.error(err);
return toast.error({
title: 'BEEP BOOP',
message: err,
position: 'topRight',
});
}
function createSocket(events) {
let ws;
// // handle account auth within the socket itself
// // https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
// let account;
// try {
// account = JSON.parse(localStorage.getItem('account'));
// } catch (e) {
// localStorage.removeItem('account');
// }
// -------------
// Outgoing
// -------------
function send(msg) {
if (msg.method !== 'ping') console.log('outgoing msg', msg);
ws.send(cbor.encode(msg));
}
let ping;
function sendPing() {
ping = Date.now();
send({ method: 'ping', params: {} });
}
function sendAccountLogin(name, password) {
send({ method: 'account_login', params: { name, password } });
}
function sendAccountCreate(name, password, code) {
send({ method: 'account_create', params: { name, password, code } });
}
function sendAccountConstructs() {
send({ method: 'account_constructs', params: {} });
}
function sendAccountInstances() {
send({ method: 'account_instances', params: {} });
}
function sendConstructSpawn(name) {
send({ method: 'construct_spawn', params: { name } });
}
function sendConstructDelete(id) {
send({ method: 'construct_delete', params: { id } });
}
function sendGameState(id) {
send({ method: 'game_state', params: { id } });
}
function sendGameReady(id) {
send({ method: 'game_ready', params: { id } });
}
function sendInstanceState(instanceId) {
send({ method: 'instance_state', params: { instance_id: instanceId } });
}
function sendInstanceList() {
send({ method: 'instance_list', params: {} });
}
function sendVboxAccept(instanceId, group, index) {
send({ method: 'vbox_accept', params: { instance_id: instanceId, group, index } });
events.clearInstance();
}
function sendVboxApply(instanceId, constructId, index) {
send({ method: 'vbox_apply', params: { instance_id: instanceId, construct_id: constructId, index } });
events.clearInstance();
}
function sendVboxUnequip(instanceId, constructId, target) {
send({ method: 'vbox_unequip', params: { instance_id: instanceId, construct_id: constructId, target } });
events.clearInstance();
}
function sendVboxDiscard(instanceId) {
send({ method: 'vbox_discard', params: { instance_id: instanceId } });
events.clearInstance();
}
function sendVboxCombine(instanceId, indices) {
send({ method: 'vbox_combine', params: { instance_id: instanceId, indices } });
events.clearCombiner();
}
function sendVboxReclaim(instanceId, index) {
send({ method: 'vbox_reclaim', params: { instance_id: instanceId, index } });
events.clearInstance();
}
function sendItemInfo() {
send({ method: 'item_info', params: {} });
}
function sendGameSkill(gameId, constructId, targetConstructId, skill) {
send({
method: 'game_skill',
params: {
game_id: gameId, construct_id: constructId, target_construct_id: targetConstructId, skill,
},
});
events.setActiveSkill(null);
}
function sendGameTarget(gameId, constructId, skillId) {
send({ method: 'game_target', params: { game_id: gameId, construct_id: constructId, skill_id: skillId } });
events.setActiveSkill(null);
}
function sendInstanceJoin(instanceId, constructs) {
send({ method: 'instance_join', params: { instance_id: instanceId, construct_ids: constructs } });
}
function sendInstanceNew(constructs, name, pve) {
send({ method: 'instance_new', params: { construct_ids: constructs, name, pve } });
}
function sendInstanceReady(instanceId) {
send({ method: 'instance_ready', params: { instance_id: instanceId } });
}
// -------------
// Incoming
// -------------
function onAccount(login) {
events.setAccount(login);
sendAccountConstructs();
sendAccountInstances();
}
function onAccountInstances(list) {
events.setAccountInstances(list);
setTimeout(sendAccountInstances, 5000);
}
function onAccountConstructs(constructs) {
events.setConstructList(constructs);
}
function onGameState(game) {
events.setGame(game);
}
let gameStateTimeout;
function startGameStateTimeout(id) {
clearTimeout(gameStateTimeout);
gameStateTimeout = setTimeout(() => sendGameState(id), 1000);
return true;
}
function clearGameStateTimeout() {
clearTimeout(gameStateTimeout);
}
let instanceStateTimeout;
function startInstanceStateTimeout(id) {
clearTimeout(instanceStateTimeout);
instanceStateTimeout = setTimeout(() => sendInstanceState(id), 1000);
return true;
}
function onInstanceState(instance) {
events.setInstance(instance);
return true;
}
function onOpenInstances(list) {
events.setInstanceList(list);
return true;
}
function clearInstanceStateTimeout() {
clearTimeout(instanceStateTimeout);
}
function onItemInfo(info) {
events.setItemInfo(info);
}
let pongTimeout;
function onPong() {
events.setPing(Date.now() - ping);
pongTimeout = setTimeout(sendPing, 1000);
}
// -------------
// Setup
// -------------
// when the server sends a reply it will have one of these message types
// this object wraps the reply types to a function
const handlers = {
AccountState: onAccount,
AccountConstructs: onAccountConstructs,
AccountInstances: onAccountInstances,
GameState: onGameState,
InstanceState: onInstanceState,
ItemInfo: onItemInfo,
OpenInstances: onOpenInstances,
Pong: onPong,
};
function logout() {
localStorage.removeItem('account');
account = null;
event.setAccount(null);
}
function errHandler(error) {
switch (error) {
case 'invalid token': return logout();
case 'no constructs selected': return events.errorPrompt('select_constructs');
case 'node requirements not met': return events.errorPrompt('complete_nodes');
case 'construct at max skills (4)': return events.errorPrompt('max_skills');
default: return errorToast(error);
}
}
// decodes the cbor and
// calls the handlers defined above based on message type
function onMessage(event) {
// decode binary msg from server
const blob = new Uint8Array(event.data);
const res = cbor.decode(blob);
const [msgType, params] = res;
if (msgType !== 'Pong') console.log(res);
// check for error and split into response type and data
if (res.err) return errHandler(res.err);
if (!handlers[msgType]) return errorToast(`${msgType} handler missing`);
return handlers[msgType](params);
}
// Connection opened
function onOpen() {
toast.info({
message: 'connected',
position: 'topRight',
});
sendPing();
sendItemInfo();
return true;
}
function onError(event) {
console.error('WebSocket error', event);
}
function onClose(event) {
console.error('WebSocket closed', event);
toast.warning({
message: 'disconnected',
position: 'topRight',
});
return setTimeout(connect, 5000);
}
function connect() {
if (ws) {
clearGameStateTimeout();
clearInstanceStateTimeout();
clearTimeout(pongTimeout);
ws.removeEventListener('open', onOpen);
ws.removeEventListener('message', onMessage);
ws.removeEventListener('error', onError);
ws.removeEventListener('close', onClose);
ws = null;
}
ws = new WebSocket(SOCKET_URL);
ws.binaryType = 'arraybuffer';
// Listen for messages
ws.addEventListener('open', onOpen);
ws.addEventListener('message', onMessage);
ws.addEventListener('error', onError);
ws.addEventListener('close', onClose);
return ws;
}
return {
clearGameStateTimeout,
clearInstanceStateTimeout,
sendAccountLogin,
sendAccountCreate,
sendAccountConstructs,
sendAccountInstances,
sendGameState,
sendGameReady,
sendGameSkill,
sendGameTarget,
sendConstructSpawn,
sendConstructDelete,
sendInstanceJoin,
sendInstanceList,
sendInstanceReady,
sendInstanceNew,
sendInstanceState,
sendVboxAccept,
sendVboxApply,
sendVboxReclaim,
sendVboxCombine,
sendVboxDiscard,
sendVboxUnequip,
sendItemInfo,
startInstanceStateTimeout,
startGameStateTimeout,
connect,
};
}
module.exports = createSocket;