const toast = require('izitoast'); const cbor = require('borc'); const throttle = require('lodash/throttle'); const groupBy = require('lodash/groupBy'); const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/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[0] !== 'Ping') console.log('outgoing msg', msg); try { ws.send(cbor.encode(msg)); } catch (e) { console.warn(e); } } let ping; function sendPing() { ping = Date.now(); send(['Ping', {}]); } function sendAccountConstructs() { send(['AccountConstructs', {}]); } function sendAccountInstances() { send(['AccountInstances', {}]); } function sendAccountSetTeam(ids) { send(['AccountSetTeam', { ids }]); } function sendSubscriptionEnding(ending) { send(['SubscriptionEnding', { ending }]); } function sendGameState(id) { send(['GameState', { id }]); } function sendGameReady(id) { send(['GameReady', { id }]); } function sendInstanceState(instanceId) { send(['InstanceState', { instance_id: instanceId }]); } function sendInstanceChat(instanceId, index) { send(['InstanceChat', { instance_id: instanceId, index }]); } function sendVboxBuy(instanceId, group, index) { send(['VboxBuy', { instance_id: instanceId, group, index }]); events.clearInstance(); } function sendVboxBuyEquip(instanceId, group, index, constructId) { send(['VboxBuy', { instance_id: instanceId, group, index, construct_id: constructId }]); events.clearInstance(); } function sendVboxApply(instanceId, constructId, index) { send(['VboxApply', { instance_id: instanceId, construct_id: constructId, index }]); events.clearInstance(); } function sendVboxUnequip(instanceId, constructId, target) { send(['VboxUnequip', { instance_id: instanceId, construct_id: constructId, target }]); events.clearInstance(); } function sendVboxUnequipApply(instanceId, constructId, target, targetConstructId) { send(['VboxUnequipApply', { instance_id: instanceId, construct_id: constructId, target, target_construct_id: targetConstructId }]); events.clearInstance(); } function sendVboxRefill(instanceId) { send(['VboxRefill', { instance_id: instanceId }]); events.clearInstance(); } function sendVboxCombine(instanceId, invIndicies, vboxIndicies) { const formatted = {}; vboxIndicies.forEach(p => formatted[p[0]] ? formatted[p[0]].push(p[1]) : formatted[p[0]] = [p[1]]); send(['VboxCombine', { instance_id: instanceId, inv_indices: invIndicies, vbox_indices: formatted }]); events.clearInstance(); } function sendVboxRefund(instanceId, index) { send(['VboxRefund', { instance_id: instanceId, index }]); events.clearInstance(); } function sendItemInfo() { send(['ItemInfo', {}]); } function sendGameSkill(gameId, constructId, targetConstructId, skill) { send(['GameSkill', { game_id: gameId, construct_id: constructId, target_construct_id: targetConstructId, skill }, ]); events.setActiveSkill(null); events.clearTutorialGame(); } function sendGameSkillClear(gameId) { send(['GameSkillClear', { game_id: gameId }]); events.setActiveSkill(null); } function sendGameOfferDraw(gameId) { send(['GameOfferDraw', { game_id: gameId }]); events.setActiveSkill(null); } function sendGameConcede(gameId) { send(['GameConcede', { game_id: gameId }]); events.setActiveSkill(null); } function sendGameTarget(gameId, constructId, skillId) { send(['GameTarget', { game_id: gameId, construct_id: constructId, skill_id: skillId }]); events.setActiveSkill(null); } function sendInstancePractice() { send(['InstancePractice', {}]); } function sendInstanceQueue() { send(['InstanceQueue', {}]); } function sendInstanceLeave() { send(['InstanceLeave', {}]); } function sendInstanceInvite() { send(['InstanceInvite', {}]); } function sendInstanceJoin(code) { send(['InstanceJoin', { code }]); } function sendInstanceReady(instanceId) { send(['InstanceReady', { instance_id: instanceId }]); } function sendInstanceAbandon(instanceId) { send(['InstanceAbandon', { instance_id: instanceId }]); } function sendMtxApply(constructId, mtx, name) { send(['MtxConstructApply', { construct_id: constructId, mtx, name }]); if (mtx === 'Rename') { events.clearMtxActive(); events.clearConstructRename(); } } function sendMtxAccountApply(mtx) { send(['MtxAccountApply', { mtx }]); } function sendMtxBuy(mtx) { send(['MtxBuy', { mtx }]); } function sendMtxConstructSpawn() { send(['MtxConstructSpawn', {}]); } function sendEmailState() { send(['EmailState', {}]); } function sendSubscriptionState() { send(['SubscriptionState', {}]); } // ------------- // Incoming // ------------- function onAccount(login) { events.setAccount(login); } function onAccountShop(shop) { events.setShop(shop); } function onEmailState(v) { events.setEmail(v); } function onAccountInstances(list) { events.setAccountInstances(list); } function onAccountConstructs(constructs) { events.setConstructList(constructs); } function onAccountTeam(constructs) { events.setTeam(constructs); } function onSubscriptionState(sub) { // events.subscriptionState(`Subscription cancelled. Your subscription will remain active until ${exp}. Thank you for your support.`); events.setSubscription(sub); } function onConstructSpawn(construct) { events.setNewConstruct(construct); } function onGameState(game) { events.setGame(game); } function onInstanceState(instance) { events.setInstance(instance); } function onItemInfo(info) { events.setItemInfo(info); } function onDemo(v) { events.setDemo(v); } 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, AccountTeam: onAccountTeam, AccountInstances: onAccountInstances, AccountShop: onAccountShop, SubscriptionState: onSubscriptionState, ConstructSpawn: onConstructSpawn, GameState: onGameState, EmailState: onEmailState, InstanceState: onInstanceState, ItemInfo: onItemInfo, Pong: onPong, Demo: onDemo, // QueueRequested: () => events.notify('PVP queue request received.'), QueueRequested: () => true, QueueJoined: () => { events.notify('You have joined the PVP queue.'); events.setPvp(true); }, QueueLeft: () => { events.notify('You have left the PVP queue.'); events.setPvp(false); }, QueueFound: () => events.notify('Your PVP game has started.'), InviteRequested: () => events.notify('PVP invite request received.'), Invite: code => events.setInvite(code), InstanceChat: chat => events.setInstanceChat(chat), ChatWheel: wheel => events.setChatWheel(wheel), // Joining: () => events.notify('Searching for instance...'), Processing: () => true, Error: errHandler, }; function logout() { window.location.reload(true); } 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 (!handlers[msgType]) return errorToast(`${msgType} handler missing`); return handlers[msgType](params); } // Connection opened function onOpen() { toast.info({ message: 'connected', position: 'topRight', }); sendPing(); sendItemInfo(); events.urlHashChange(); 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) { 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 { sendAccountConstructs, sendAccountInstances, sendAccountSetTeam, sendGameState, sendGameReady, sendGameSkill, sendGameSkillClear, sendGameOfferDraw, sendGameConcede, sendGameTarget, sendInstanceAbandon, // some weird shit happening in face off sendInstanceReady: throttle(sendInstanceReady, 500), sendInstancePractice, sendInstanceQueue, sendInstanceState, sendInstanceInvite, sendInstanceJoin, sendInstanceChat, sendInstanceLeave, sendVboxBuy, sendVboxBuyEquip, sendVboxApply, sendVboxRefund, sendVboxCombine, sendVboxRefill, sendVboxUnequip, sendVboxUnequipApply, sendItemInfo, sendEmailState, sendSubscriptionState, sendSubscriptionEnding, sendMtxAccountApply, sendMtxApply, sendMtxBuy, sendMtxConstructSpawn, connect, }; } module.exports = createSocket;