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;