UNPKG

node-insim

Version:

An InSim library for NodeJS with TypeScript support

267 lines (266 loc) 10.2 kB
import { __awaiter } from "tslib"; import crypto from 'crypto'; import defaults from 'lodash.defaults'; import { TypedEmitter } from 'tiny-typed-emitter'; import unicodeToLfs from 'unicode-to-lfs'; import { InSimError } from './errors'; import { unpack } from './lfspack'; import { log as baseLog } from './log'; import { IS_ISI, IS_MSL, IS_MST, IS_MSX, IS_MTC, IS_TINY, MessageSound, MST_MSG_MAX_LENGTH, PacketType, packetTypeToClass, TinyType, } from './packets'; import { TCP, UDP } from './protocols'; const log = baseLog.extend('insim'); export class InSim extends TypedEmitter { constructor(id) { super(); this._options = defaultInSimOptions; /** The main connection to InSim (TCP or UDP) */ this.connection = null; this.sizeMultiplier = 4; /** * Connect to a server via InSim. */ this.connect = (options) => { if (this.connection !== null) { log('Cannot connect - already connected'); return; } this._connect(options); }; /** * Connect to InSim Relay. * * After you are connected you can request a host list, so you can see * which hosts you can connect to. * Then you can send a packet to the Relay to select a host. After that * the Relay will send you all insim data from that host. * * Some hosts require a spectator password in order to be selectable. * * You do not need to specify a spectator password if you use a valid administrator password. * * If you connect with an administrator password, you can send just about every * regular InSim packet there is available in LFS, just like as if you were connected * to the host directly. * * Regular insim packets that a relay client can send to host: * * For anyone * TINY_VER * TINY_PING * TINY_SCP * TINY_SST * TINY_GTH * TINY_ISM * TINY_NCN * TINY_NPL * TINY_RES * TINY_REO * TINY_RST * TINY_AXI * * Admin only * TINY_VTC * ISP_MST * ISP_MSX * ISP_MSL * ISP_MTC * ISP_SCH * ISP_BFN * ISP_BTN * * The relay will also accept, but not forward * TINY_NONE // for relay-connection maintenance */ this.connectRelay = () => { this._connect({ Host: 'isrelay.lfs.net', Port: 47474, Protocol: 'TCP', }, true); }; this._connect = (options, isRelay = false) => { this._options = defaults(options, defaultInSimOptions); log(`Connecting to ${this._options.Host}:${this._options.Port} using ${this._options.Protocol}...`); this.sizeMultiplier = isRelay ? 1 : 4; this.connection = this._options.Protocol === 'TCP' ? new TCP(this._options.Host, this._options.Port, this.sizeMultiplier) : new UDP({ host: this._options.Host, port: this._options.Port, packetSizeMultiplier: this.sizeMultiplier, socketInitialisationMode: 'connect', }); this.connection.connect(); this.connection.on('connect', () => { if (!isRelay) { this.send(new IS_ISI({ Flags: this._options.Flags, Prefix: this._options.Prefix, Admin: this._options.Admin, UDPPort: this._options.UDPPort, ReqI: this._options.ReqI, Interval: this._options.Interval, IName: this._options.IName, InSimVer: InSim.INSIM_VERSION, })); } this.emit('connect', this); }); this.connection.on('disconnect', () => { this.emit('disconnect', this); }); this.connection.on('error', (error) => { throw new InSimError(`${this._options.Protocol} connection error: ${error.message}`); }); this.connection.on('data', (data) => this.handlePacket(data)); }; this.disconnect = () => { if (this.connection !== null) { log('Disconnecting...'); this.connection.disconnect(); } }; this.send = (packet) => { if (this.connection === null) { log('Cannot send a packet - not connected'); return; } packet.SIZE_MULTIPLIER = this.sizeMultiplier; log('Send packet:', PacketType[packet.Type], packet); const data = packet.pack(); this.connection.send(data); }; /** * Send a message or command to LFS * * If the message starts with a slash (`/`), it will be treated as a command. * Otherwise, it will be treated as a message. * * The maximum length of the message is {@link MSX_MSG_MAX_LENGTH} characters. */ this.sendMessage = (message) => { log('Send message:', message); if (message.startsWith(InSim.COMMAND_PREFIX)) { return this.send(new IS_MST({ Msg: message, })); } const encodedMessageLength = unicodeToLfs(message).length; if (encodedMessageLength >= MST_MSG_MAX_LENGTH) { return this.send(new IS_MSX({ Msg: message, })); } return this.send(new IS_MST({ Msg: message, })); }; /** * Send a message which will appear on the local computer only. * * The maximum length of the message is {@link MSL_MSG_MAX_LENGTH} characters. */ this.sendLocalMessage = (message, sound = MessageSound.SND_SILENT) => { log('Send local message:', message); return this.send(new IS_MSL({ Msg: message, Sound: sound, })); }; /** Send a message to a specific connection */ this.sendMessageToConnection = (ucid, message, sound = MessageSound.SND_SILENT) => { log('Send message to connection:', ucid, message); this.send(new IS_MTC({ UCID: ucid, Text: message, Sound: sound, })); }; /** Send a message to a specific player */ this.sendMessageToPlayer = (plid, message, sound = MessageSound.SND_SILENT) => { log('Send message to player:', plid, message); this.send(new IS_MTC({ PLID: plid, Text: message, Sound: sound, })); }; this.sendAwait = (packet, packetTypeToAwait, filterPacketData = () => true) => { return new Promise((resolve, reject) => { if (this.connection === null) { log('Cannot send a packet with await - not connected'); reject(); return; } this.send(packet); log('Await packet:', PacketType[packetTypeToAwait]); const packetListener = (receivedPacket) => { if (receivedPacket.ReqI === packet.ReqI && filterPacketData(receivedPacket)) { resolve(receivedPacket); } }; this.once(packetTypeToAwait, packetListener); this.on('disconnect', () => { this.removeListener(packetTypeToAwait, packetListener); reject(); }); }); }; this.handlePacket = (data) => __awaiter(this, void 0, void 0, function* () { const header = unpack('<BB', data.buffer); if (!header) { log(`Incomplete packet header received: ${data.join()}`); return; } const packetType = header[1]; const packetTypeString = PacketType[packetType]; if (packetTypeString === undefined) { log(`Unknown packet type received: ${packetType}`); return; } const PacketClass = packetTypeToClass[packetType]; if (PacketClass === undefined) { log(`Packet handler not found for ${packetTypeString}`); return; } const packetInstance = new PacketClass(); packetInstance.SIZE_MULTIPLIER = this.sizeMultiplier; this.emit(packetType, packetInstance.unpack(data), this); }); this.handleKeepAlive = (packet) => { if (packet.SubT === TinyType.TINY_NONE) { this.send(new IS_TINY({ SubT: TinyType.TINY_NONE, })); } }; this.id = id !== null && id !== void 0 ? id : crypto.randomUUID(); this.on('connect', () => log(`Connected to ${this._options.Host}:${this._options.Port}`)); this.on('disconnect', () => { this.connection = null; log(`Disconnected from ${this._options.Host}:${this._options.Port}`); }); this.on(PacketType.ISP_TINY, this.handleKeepAlive); } get options() { return this._options; } } /** Currently supported InSim version */ InSim.INSIM_VERSION = 9; InSim.COMMAND_PREFIX = '/'; InSim.defaultMaxListeners = 255; const defaultInSimOptions = { Host: '127.0.0.1', Port: 29999, Protocol: 'TCP', ReqI: 0, UDPPort: 0, Flags: 0, Prefix: '', Interval: 0, Admin: '', IName: '', };