UNPKG

starblast-modding

Version:

A powerful library for interacting with the Starblast Modding API

251 lines (247 loc) 9.24 kB
'use strict'; const GameSocket = require("../utils/GameSocket.js"); const getEntity = require("../utils/getEntity.js"); const events = require("../resources/Events.js"); const defineProperties = require("../utils/defineProperties.js"); const getJoinPacketName = require("../utils/getJoinPacketName.js"); const deepFreeze = require("../utils/deepFreeze.js"); const standard_modes = ["survival", "team", "invasion", "deathmatch", "battleroyale"]; module.exports.create = function (api, address, token) { if (api.encodeOptionsError) { api.game.error("Failed to encode game options"); api.game.error("Mod will be run with empty options instead") } delete api.encodeOptionsError let socket = GameSocket.create(address.ip, address.port, "https://starblast.data.neuronality.com", api.compressWSMessages); socket.on("open", function() { this.send(JSON.stringify({ name: "run_mod", data: { token: token, options: api.configuration.options } })) }); return new Promise(function (resolve, reject) { api.lastRejectHandler = reject; socket.on("message", function(event, isBinary) { if (!isBinary) { try { event = JSON.parse(event.toString()) ?? {} } catch (e) { event = {} } let data = event.data; switch (event.name) { case "mod_started": Object.assign(api, { socket: socket, ip: address.ip, id: data.id, port: address.port, started: true, stopped: false }); api.mod_data.options = data.options; if (api.extendedMode) { // introduce extra connection to game getJoinPacketName().then(packet => { if (api.stopTriggered) return; api.gameClient.connect(address.ip, data.id, address.port, packet); api.gameClient.initTeamStats(); }).catch(e => { if (!api.gameClient.socket.requestClose) this.error("Failed to establish extensive connection to the game. Customization and extended team data might not be available."); if (!api.mod_data.optionsLocked && api.mod_data.options != null && "object" === typeof api.mod_data.options) { deepFreeze(api.mod_data.options); api.mod_data.optionsLocked = true; } }); } else { deepFreeze(api.mod_data.options); api.mod_data.optionsLocked = true; } while (api.preflight_requests.length > 0) api.set(api.preflight_requests.shift()).send(); resolve(this.link); this.emit(events.MOD_STARTED, this.link, this.options); break; case "tick": api.mod_data.step = data.step; for (let key of ["aliens", "asteroids", "collectibles", "ships", "objects", "teams"]) api.mod_data[key]?.update?.(true); api.updateTimer(); this.emit(events.TICK, data.step); break; case "alien_created": case "asteroid_created": case "collectible_created": { let entity_name = event.name.split("_")[0], entityList = this[entity_name + "s"], uuid = event.request_id; let entity = entityList.array(true).find(entity => { if (entityList.isInstance(entity)) { try { entity.id = null } catch(e) {} return entity.id == null && entity.request_id === uuid } return false }); if (entity == null) { entity = entityList.create(event); entityList.insert(entity) } defineProperties(entity, { id: event.id, createdStep: Math.max(this.timer.step, 0), }); entity.markAsSpawned(); entity.modding.data.lastUpdatedStep = this.timer.step; entityList.update(); let createReqs = api.create_requests.splice(0); api.create_requests.push(...createReqs.filter(uid => !Object.is(uid, uuid))); let resolve = api.handlers.create.get(uuid)?.resolve; api.handlers.create.delete(uuid); resolve?.(entity); this.emit(events[event.name.toUpperCase()], entity); break; } case "ship_update": { let ship = getEntity(api.game, event, this.ships); let evt, args, spawned = ship.isSpawned(), isStandardMode = standard_modes.includes(api.game.options.root_mode); if (!spawned && !ship.modding.data.initialized) { this.objects.forEach(object => api.clientMessage(ship.id, "set_object", {object: object}).send()); this.ships.ui_components.forEach(component => component.persistent && api.clientMessage(ship.id, "set_ui_component", { component: component.raw.backup }).send()); ship.modding.data.initialized = true; } // support some missing ship events in standard modes if (isStandardMode) { if (spawned) { if (ship.alive) { if (!event.alive) { evt = events.SHIP_DESTROYED; args = [ship, null]; } } else if (event.alive) { evt = events.SHIP_RESPAWNED; args = [ship]; } } else { evt = events.SHIP_SPAWNED; args = [ship]; } } // somehow ship update packet comes first before the spawning event packet // do this to prevent any first update packet to overwrite spawned status ship.update(event, false, isStandardMode); if (evt) this.emit(evt, ...args); break; } case "alien_update": case "asteroid_update": getEntity(api.game, event, this[event.name.split("_")[0] + "s"]).update(event); break; case "ship_disconnected": { let ship = getEntity(api.game, event, this.ships); ship.markAsInactive(); this.ships.update(); this.emit(events.SHIP_DISCONNECTED, ship); break; } case "error": { let error = new Error(event.text); switch(event.text) { case "Incorrect data": case "Too many aliens": case "Too many asteroids": case "Too many Collectibles": let uuid = api.create_requests.shift(); let handler = api.handlers.create; let reject = handler.get(uuid)?.reject handler.delete(uuid); this.findStructureByUUID(uuid, true)?.markAsInactive?.(); reject?.(error); break; default: this.error(event.text) } break } case "event": switch (data.name) { case "ship_destroyed": { let ship = this.ships.findById(data.ship, true); let killer = this.ships.findById(data.killer, true); if (ship != null) { ship.modding.data.alive = false; ship.modding.data.lastAliveStep = this.timer.step; let uuid = ship.uuid, handler = api.handlers.destroy, resolve = handler.get(uuid)?.resolve; handler.delete(uuid); resolve?.(ship); } if (ship != null || killer != null) this.emit(events.SHIP_DESTROYED, ship, killer); break; } case "alien_destroyed": case "asteroid_destroyed": { let entity_name = data.name.split("_")[0], entityList = this[entity_name + "s"]; data.id = data[entity_name]; let entity = getEntity(api.game, data, entityList); let killer = this.ships.findById(data.killer, true); entity.markAsInactive(); entityList.update(); let uuid = entity.uuid, handler = api.handlers.destroy, resolve = handler.get(uuid)?.resolve; handler.delete(uuid); resolve?.(entity); this.emit(events[data.name.toUpperCase()], entity, killer); break; } case "ship_spawned": { data.id = data.ship; let ship = getEntity(api.game, data, this.ships); let event_name = events.SHIP_RESPAWNED; if (!ship.isSpawned()) { ship.markAsSpawned(); event_name = events.SHIP_SPAWNED; } this.ships.update(); this.emit(event_name, ship); break; } case "collectible_picked": { data.id = data.collectible; let collectible = getEntity(api.game, data, this.collectibles); let ship = this.ships.findById(data.ship, true); collectible.markAsInactive(); this.emit(events.COLLECTIBLE_PICKED, collectible, ship); break; } case "ui_component_clicked": { let id = data.id; if (["scoreboard", "radar_background"].includes(id)) break; let ship = this.ships.findById(data.ship, true); if (ship != null) { let ship_component = ship.ui_components.findById(id); if (ship_component?.raw?.lastClickable) { this.emit(events.UI_COMPONENT_CLICKED, ship_component, ship); } else { let global_component = this.ships.ui_components.findById(id); if (global_component?.raw?.lastClickable) this.emit(events.UI_COMPONENT_CLICKED, global_component, ship); } } break; } } break; } } }.bind(api.game)); socket.on("close", function () { switch (api.gameClient?.socket?.readyState) { case GameSocket.OPEN: api.gameClient.socket.close(); case GameSocket.CONNECTING: case GameSocket.CLOSING: return api.gameClient.socket.requestClose = true; case GameSocket.CLOSED: api.gameClient.socket.requestClose = true; default: return api.triggerStopEvent(); } }); }.bind(api)) }