UNPKG

rce.js

Version:

A wrapper for Rust Console Edition (RCE) that allows you to use RCON to create integrations.

766 lines 30.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const types_1 = require("./types"); const socketManager_1 = __importDefault(require("./socket/socketManager")); const events_1 = require("events"); const commandManager_1 = __importDefault(require("./commands/commandManager")); const logger_1 = __importDefault(require("./logger")); const teamInfo_1 = require("./data/teamInfo"); const roleInfo_1 = require("./data/roleInfo"); const types_2 = require("./types"); class RCEManager extends events_1.EventEmitter { servers = new Map(); logger; /** * * @param opts Options for the RCEManager instance. * @param opts.logLevel The log level for the logger. Default is LogLevel.Info * Creates an instance of RCEManager. */ constructor(opts) { super(); this.logger = opts.logger?.instance || new logger_1.default(opts.logger?.level, opts.logger?.file); this.on(types_1.RCEEvent.Error, (payload) => { if (payload.server) { this.logger.error(`[${payload.server.identifier}] ${payload.error}`); } else { this.logger.error(payload.error); } }); this.on(types_1.RCEEvent.PlayerRoleAdd, (payload) => { const server = this.getServer(payload.server.identifier); if (!server) return; if (!server.intents.includes(types_2.RCEIntent.PlayerList) || !server.intents.includes(types_2.RCEIntent.RoleInfo)) { return; } const player = server.players.find((p) => p.ign === payload.player.ign); if (player) { if (payload.role === "Banned") { player.role = null; } else { player.role = payload.role; } this.updateServer(server); } }); this.on(types_1.RCEEvent.PlayerRoleRemove, (payload) => { const server = this.getServer(payload.server.identifier); if (!server) return; if (!server.intents.includes(types_2.RCEIntent.PlayerList) || !server.intents.includes(types_2.RCEIntent.RoleInfo)) { return; } const player = server.players.find((p) => p.ign === payload.player.ign); if (player) { player.role = null; this.updateServer(server); } }); this.on(types_1.RCEEvent.Ready, (payload) => { const server = payload.server; if (!server) { this.logger.warn("[Unknown] Received Ready event with no server data."); return; } const id = server.identifier; // Check intents and set up intervals accordingly if (server.intents.includes(types_2.RCEIntent.ServerInfo)) { this.fetchInfo(id); const timer = server.intentTimers?.[types_2.RCEIntent.ServerInfo] || 60_000; server.intervals.serverInfoInterval = setInterval(() => { this.fetchInfo(id); }, timer); } if (server.intents.includes(types_2.RCEIntent.PlayerList)) { this.updatePlayers(id); const timer = server.intentTimers?.[types_2.RCEIntent.PlayerList] || 15_000; server.intervals.playerListInterval = setInterval(() => { this.updatePlayers(id); }, timer); } if (server.intents.includes(types_2.RCEIntent.Frequencies)) { this.updateBroadcasters(id); const timer = server.intentTimers?.[types_2.RCEIntent.Frequencies] || 30_000; server.intervals.frequenciesInterval = setInterval(() => { this.updateBroadcasters(id); }, timer); } if (server.intents.includes(types_2.RCEIntent.Gibs)) { this.fetchGibs(id); const timer = server.intentTimers?.[types_2.RCEIntent.Gibs] || 30_000; server.intervals.gibsInterval = setInterval(() => { this.fetchGibs(id); }, timer); } if (server.intents.includes(types_2.RCEIntent.Kits)) { this.fetchKits(id); const timer = server.intentTimers?.[types_2.RCEIntent.Kits] || 300_000; server.intervals.kitsInterval = setInterval(() => { this.fetchKits(id); }, timer); } if (server.intents.includes(types_2.RCEIntent.CustomZones)) { this.fetchCustomZones(id); const timer = server.intentTimers?.[types_2.RCEIntent.CustomZones] || 300_000; server.intervals.customZonesInterval = setInterval(() => { this.fetchCustomZones(id); }, timer); } if (server.intents.includes(types_2.RCEIntent.Teams)) { if (server.intents.includes(types_2.RCEIntent.PlayerList)) { this.fetchTeamInfo(id); } } if (server.intents.includes(types_2.RCEIntent.RoleInfo)) { if (server.intents.includes(types_2.RCEIntent.PlayerList)) { this.fetchRoleInfo(id); } } this.logger.info(`[${id}] Server Successfully Added!`); }); } /** * * @param options Options for the server to be added. * @param options.identifier Unique identifier for the server. * @param options.state Any additional information for the server. * @param options.rcon RCON connection options for the server. * @param options.rcon.port RCON port for the server. * @param options.rcon.password RCON password for the server. * @param options.rcon.host RCON host for the server. * Creates a new server instance and adds it to the manager. * @returns Promise<boolean> Whether the server was added successfully. */ async addServer(options) { this.logger.debug(`[${options.identifier}] Attempting To Add Server...`); if (this.servers.has(options.identifier)) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${options.identifier}" Already Exists!`, }); return false; } // 1. Create SocketManager but don't connect yet const socketManager = new socketManager_1.default(this); const server = { identifier: options.identifier, socket: null, socketManager, flags: [], intents: options.intents || [], intentTimers: options.intentTimers || {}, intervals: {}, state: options.state || [], players: [], kits: [], frequencies: [], teams: [], info: undefined, }; // 2. Register server before connecting this.servers.set(options.identifier, server); // 3. Now connect const connected = await socketManager.connect(options); if (!connected) { this.logger.warn(`[${options.identifier}] Failed to connect to WebSocket.`); this.servers.delete(options.identifier); return false; } return true; } /** * * @param server The server object to update. * @param server.identifier Unique identifier for the server. * @returns void * Updates an existing server instance in the manager. */ updateServer(server) { if (!this.servers.has(server.identifier)) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${server.identifier}" Does Not Exist!`, }); return; } this.servers.set(server.identifier, server); } /** * * @param identifier Unique identifier for the server to be removed. * Removes a server instance from the manager. * @returns void */ removeServer(identifier) { if (!this.servers.has(identifier)) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const server = this.servers.get(identifier); if (server) { // Properly destroy the SocketManager to stop reconnection attempts if (server.socketManager) { server.socketManager.destroy(); } if (server.socket) { server.socket.close(); } Object.values(server.intervals).forEach(clearInterval); } this.servers.delete(identifier); this.logger.info(`[${identifier}] Server Successfully Removed!`); } /** * * @param identifier Unique identifier for the server to be fetched. * Retrieves a server instance by its identifier. * @returns IServer | null */ getServer(identifier) { return this.servers.get(identifier) || null; } /** * Retrieves all server instances managed by the RCEManager. * @returns IServer[] */ getServers() { return Array.from(this.servers.values()); } /** * * @param identifier Unique identifier for the server to fetch information from. * Fetches server information using the RCON command "serverinfo". * @returns IRustServerInformation | string | undefined */ async fetchInfo(identifier) { const server = this.getServer(identifier); if (!server) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const info = await this.sendCommand(identifier, "serverinfo"); if (!info) { this.emit(types_1.RCEEvent.Error, { error: `Failed To Fetch Server Information For "${identifier}"!`, }); return; } let cleanOutput = info.replace(/\\n/g, "").trim(); try { const json = JSON.parse(cleanOutput); server.info = json; this.updateServer(server); this.emit(types_1.RCEEvent.ServerInfoUpdated, { info: server.info, server, }); return json; } catch (error) { return info; } } /** * * @param identifier Unique identifier for the server to send the command to. * @param command The command to be sent to the server. * Sends a command to the server using the RCON connection. * @returns Promise<string | undefined> */ async sendCommand(identifier, command) { const server = this.getServer(identifier); if (!server?.socket) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist Or Is Not Connected!`, }); return; } if (!server.flags.includes("READY")) return undefined; if (server.socket.readyState === server.socket.OPEN) { const rand = Math.floor(Math.random() * 999999999); return new Promise(async (resolve, reject) => { commandManager_1.default.add({ identifier, command, uniqueId: rand, resolve, reject, }); server.socket.send(JSON.stringify({ message: command, identifier: rand, })); this.logger.debug(`[${identifier}] Sending Command: ${command} (ID: ${rand})`); const cmd = commandManager_1.default.get(identifier, rand); if (cmd) { cmd.timeout = setTimeout(() => { commandManager_1.default.remove(cmd); resolve(undefined); }, 3_000); } }); } else { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Is Not Connected!`, }); return; } } /** * Destroys the RCEManager instance, cleaning up all resources. * Closes all server sockets, clears intervals, and removes all listeners. */ destroy() { this.servers.forEach((server) => { // Properly destroy the SocketManager to stop reconnection attempts if (server.socketManager) { server.socketManager.destroy(); } if (server.socket) { server.socket.close(); } Object.values(server.intervals).forEach(clearInterval); }); this.servers.clear(); this.removeAllListeners(); this.logger.info("RCEManager Successfully Destroyed!"); } /* Event Handling */ emit(event, ...args) { return super.emit(event, ...args); } on(event, listener) { return super.on(event, listener); } once(event, listener) { return super.once(event, listener); } off(event, listener) { return super.off(event, listener); } /* Zone Caching */ async fetchCustomZones(identifier) { const server = this.getServer(identifier); if (!server) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const zoneList = await this.sendCommand(identifier, "zones.listcustomzones"); const zones = []; if (zoneList) { const zoneRegex = /Name\s*\[(.*?)\]\s*Enabled\s*\[(True|False)\]/gi; let match; while ((match = zoneRegex.exec(zoneList)) !== null) { const zoneInfo = await this.sendCommand(identifier, `zones.customzoneinfo "${match[1]}"`); if (zoneInfo) { const text = zoneInfo.replaceAll("\\n", "\n").trim(); const parseVector = (str) => str .replace(/[()]/g, "") .split(",") .map((n) => parseFloat(n.trim())); const get = (key) => { const match = text.match(new RegExp(`${key}\\s*\\[(.*?)\\]`)); return match ? match[1] : undefined; }; const zone = { name: match[1], enabled: match[2].toLowerCase() === "true", config: { position: parseVector(get("Position") ?? "(0,0,0)"), size: parseFloat(get("Size") ?? "0"), type: get("Type") ?? "Sphere", playerDamage: get("Player Damage") === "True", npcDamage: get("NPC Damage") === "True", radiationDamage: parseFloat(get("Radiation Damage") ?? "0"), playerBuildingDamage: get("Player Building Damage") === "True", allowBuilding: get("Allow Building") === "True", showArea: get("Show Area") === "True", color: parseVector(get("Color") ?? "(255,255,255)"), showChatMessage: get("Show Chat Message") === "True", enterMessage: (get("Enter Message") ?? "").replace(/^"|"$/g, ""), leaveMessage: (get("Leave Message") ?? "").replace(/^"|"$/g, ""), }, }; zones.push(zone); } } } server.customZones = zones; this.updateServer(server); return zones; } /* Kit Caching */ async fetchKits(identifier) { const server = this.getServer(identifier); if (!server) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const kitList = await this.sendCommand(identifier, "kit list"); if (kitList) { const lines = kitList .split("\n") .map((line) => line.trim()) .filter((line) => line && !line.startsWith("[KITMANAGER]")); const kits = lines.map((line) => ({ name: line, items: [], })); for (const kit of kits) { const kitInfo = await this.sendCommand(identifier, `kit info "${kit.name}"`); if (kitInfo) { const cleaned = kitInfo.replaceAll("\\n", "\n"); const itemRegex = /Shortname:\s*(\S+)\s+Amount:\s*\[(\d+)\]\s+Condition:\s*\[(\d+)\]\s+Container:\s*\[(Main|Belt|Wear)\]/g; let match; while ((match = itemRegex.exec(cleaned)) !== null) { kit.items.push({ shortName: match[1], quantity: Number(match[2]), condition: Number(match[3]), container: match[4], }); } } } server.kits = kits; this.updateServer(server); return kits; } } /* Scheduled Tasks */ async updateBroadcasters(identifier) { const server = this.getServer(identifier); if (!server) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const broadcasters = await this.sendCommand(identifier, "rf.listboardcaster"); if (broadcasters) { const broadcasts = []; const regex = /\[(\d+) MHz\] Position: \(([\d.-]+), ([\d.-]+), ([\d.-]+)\), Range: (\d+)/g; let match; while ((match = regex.exec(broadcasters)) !== null) { const frequency = parseInt(match[1], 10); const coordinates = [ parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4]), ]; const range = parseInt(match[5], 10); broadcasts.push({ frequency, coordinates, range }); } server.frequencies.forEach((freq) => { if (!broadcasts.find((b) => parseInt(b.frequency) === freq)) { this.emit(types_1.RCEEvent.FrequencyLost, { server, frequency: freq, }); server.frequencies = server.frequencies.filter((f) => f !== freq); } }); broadcasts.forEach((broadcast) => { if (server.frequencies.includes(broadcast.frequency)) return; server.frequencies.push(broadcast.frequency); if (broadcast.frequency === 4765) { this.emit(types_1.RCEEvent.EventStart, { server, event: "Small Oil Rig", special: false, }); } else if (broadcast.frequency === 4768) { this.emit(types_1.RCEEvent.EventStart, { server, event: "Oil Rig", special: false, }); } this.emit(types_1.RCEEvent.FrequencyGained, { server, frequency: broadcast.frequency, coordinates: broadcast.coordinates, range: broadcast.range, }); }); this.updateServer(server); } } async fetchGibs(identifier) { const server = this.getServer(identifier); if (!server) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const bradley = await this.sendCommand(identifier, "find_entity servergibs_bradley"); const helicopter = await this.sendCommand(identifier, "find_entity servergibs_patrolhelicopter"); if (!bradley || !helicopter) { this.emit(types_1.RCEEvent.Error, { error: `Failed To Fetch Gibs For Server "${identifier}"`, }); return; } if (bradley.includes("servergibs_bradley") && !server.flags.includes("BRADLEY")) { server.flags.push("BRADLEY"); setTimeout(() => { const s = this.getServer(identifier); if (s) { s.flags = s.flags.filter((f) => f !== "BRADLEY"); this.updateServer(s); } }, 60_000 * 10); this.emit(types_1.RCEEvent.EventStart, { server, event: "Bradley APC Debris", special: false, }); } if (helicopter.includes("servergibs_patrolhelicopter") && !server.flags.includes("HELICOPTER")) { server.flags.push("HELICOPTER"); setTimeout(() => { const s = this.getServer(identifier); if (s) { s.flags = s.flags.filter((f) => f !== "HELICOPTER"); this.updateServer(s); } }, 60_000 * 10); this.emit(types_1.RCEEvent.EventStart, { server, event: "Patrol Helicopter Debris", special: false, }); } this.updateServer(server); } /** * Creates a placeholder player or returns existing one, optionally updating player data * @param identifier Server identifier * @param playerName Player's IGN * @param playerData Optional data to set on the player * @returns Player object (existing or newly created placeholder) */ upsertPlayer(identifier, playerName, playerData, markAsOnline = false) { const server = this.getServer(identifier); if (!server) { throw new Error(`Server With Identifier "${identifier}" Does Not Exist!`); } let player = server.players.find((p) => p.ign === playerName); let isNewPlayer = false; let wasOffline = false; if (!player) { // Create new player player = { ign: playerName, ping: 0, timeConnected: 0, health: 0, team: null, platform: undefined, state: [], isOnline: markAsOnline, lastSeen: new Date(), }; server.players.push(player); isNewPlayer = true; } else { // Player exists, check if they were offline wasOffline = !player.isOnline; if (markAsOnline) { player.isOnline = true; player.lastSeen = new Date(); } } // Apply any provided data if (playerData) { Object.assign(player, playerData); } // Emit join event if player is newly created or was offline and now online if (markAsOnline && (isNewPlayer || wasOffline)) { this.emit(types_1.RCEEvent.PlayerJoined, { server, player, }); } // Update server if we created a new player or modified existing data if (isNewPlayer || playerData) { this.updateServer(server); } return player; } async fetchRoleInfo(identifier) { const server = this.getServer(identifier); if (!server) return; const rawRoleInfo = await this.sendCommand(identifier, "getauthlevels"); if (!rawRoleInfo) return; server.players.forEach((player) => { player.role = undefined; // Reset roles before updating }); const { players } = (0, roleInfo_1.parseRoleInfo)(rawRoleInfo, server.players); server.players = players; this.updateServer(server); } /** * Fetches team information and updates team references for all players * @param identifier Server identifier */ async fetchTeamInfo(identifier) { const server = this.getServer(identifier); if (!server) return; const rawTeamInfo = await this.sendCommand(identifier, "relationshipmanager.teaminfoall"); if (!rawTeamInfo) return; // Clear existing team references server.players.forEach((player) => { player.team = null; }); const { teams } = (0, teamInfo_1.parseTeamInfo)(rawTeamInfo, server.players); // Update teams list server.teams = teams; this.updateServer(server); } /** * Gets all teams on the server * @param identifier Server identifier * @returns Array of teams with their leaders and members */ getTeams(identifier) { const server = this.getServer(identifier); return server ? server.teams : []; } /** * Gets a specific team by ID * @param identifier Server identifier * @param teamId Team ID to find * @returns Team object or undefined if not found */ getTeam(identifier, teamId) { const server = this.getServer(identifier); return server ? server.teams.find((team) => team.id === teamId) : undefined; } /** * Gets the team that a specific player belongs to * @param identifier Server identifier * @param playerName Player's IGN * @returns Team object or undefined if player is not in a team */ getPlayerTeam(identifier, playerName) { const server = this.getServer(identifier); if (!server) return undefined; const player = server.players.find((p) => p.ign === playerName); return player?.team || undefined; } async updatePlayers(identifier) { const server = this.getServer(identifier); if (!server) { this.emit(types_1.RCEEvent.Error, { error: `Server With Identifier "${identifier}" Does Not Exist!`, }); return; } const rawPlayerList = await this.sendCommand(identifier, "playerlist"); if (rawPlayerList) { const parsedPlayers = JSON.parse(rawPlayerList); // Update existing players with new data, preserve team and platform references const existingPlayers = server.players; const newPlayerNames = new Set(parsedPlayers.map((p) => p.DisplayName)); const joined = []; const left = existingPlayers.filter((player) => player.isOnline && !newPlayerNames.has(player.ign)); // Update existing players and identify new ones parsedPlayers.forEach((playerData) => { const playerName = playerData.DisplayName; const existingPlayer = existingPlayers.find((p) => p.ign === playerName); if (existingPlayer) { // Update existing player data but preserve team and platform references existingPlayer.ping = playerData.Ping; existingPlayer.timeConnected = playerData.ConnectedSeconds; existingPlayer.health = Math.round(playerData.Health); existingPlayer.isOnline = true; existingPlayer.lastSeen = new Date(); // team and platform are preserved from existing player } else { // Create new player with default values const newPlayer = { ign: playerName, ping: playerData.Ping, timeConnected: playerData.ConnectedSeconds, health: Math.round(playerData.Health), team: null, // Will be set by team events or connection platform: undefined, // Will be set from respawn events role: undefined, // Role information not available from playerlist state: [], isOnline: true, lastSeen: new Date(), }; joined.push(newPlayer); existingPlayers.push(newPlayer); } }); // Mark players as offline who are no longer connected left.forEach((player) => { player.isOnline = false; player.lastSeen = new Date(); }); // Emit events for joined players joined.forEach((player) => { this.emit(types_1.RCEEvent.PlayerJoined, { server, player, }); }); // Emit events for left players left.forEach((player) => { this.emit(types_1.RCEEvent.PlayerLeft, { server, player, }); }); this.updateServer(server); this.emit(types_1.RCEEvent.PlayerListUpdated, { server, players: existingPlayers, joined, left, }); } } } exports.default = RCEManager; //# sourceMappingURL=manager.js.map