node-insim
Version:
An InSim library for NodeJS with TypeScript support
267 lines (266 loc) • 10.2 kB
JavaScript
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: '',
};