ggejs
Version:
A powerful Node.js module for interacting with the server of Goodgame Empire & Goodgame Empire: Four Kingdoms
204 lines (182 loc) • 8.39 kB
JavaScript
const {WebSocket} = require('ws');
const net = require('net');
const {NetworkInstance} = require('e4k-data');
const {onResponse} = require('../commands');
const EmpireError = require("../tools/EmpireError");
const {ConnectionStatus} = require("../utils/Constants");
const versionDateGame = 1756306047494;
class SocketManager {
#connectionStatus = ConnectionStatus.Disconnected;
reconnectTimeout = 300;
serverType = 1;
currData = ""
/**
* @param {BaseClient} client
* @param {NetworkInstance} serverInstance
*/
constructor(client, serverInstance) {
this.client = client;
this.serverInstance = serverInstance;
this.protocol = serverInstance.zone.startsWith('EmpirefourkingdomsExGG') ? 'tcp' : 'wss';
this.url = `${this.protocol}://${serverInstance.server}:${serverInstance.port}`
this.socket = this.protocol === 'tcp' ? new net.Socket().connect(serverInstance.port, serverInstance.server) : new WebSocket(this.url);
this.#addSocketListeners(this.socket);
}
get connectionStatus() {
return this.#connectionStatus;
}
/** @param {number} connectionStatus */
set connectionStatus(connectionStatus) {
this.client.logger.i("[SocketManager] Connection status:", Object.keys(ConnectionStatus).find(k => ConnectionStatus[k] === connectionStatus));
this.#connectionStatus = connectionStatus;
}
async connect() {
this.connectionStatus = ConnectionStatus.Connecting;
await waitForConnectionStatus(this, ConnectionStatus.Connected);
}
async reconnect() {
if (this.reconnectTimeout === -1) return;
if (this.connectionStatus === ConnectionStatus.Connected) {
await this.disconnect();
await new Promise(resolve => setTimeout(resolve, this.reconnectTimeout * 1000));
}
await waitForConnectionStatus(this, ConnectionStatus.Connected);
}
async disconnect() {
if (this.connectionStatus === ConnectionStatus.Disconnected) return;
this.connectionStatus = ConnectionStatus.Disconnecting;
if (this.protocol === 'tcp') this.socket.end(); else this.socket.close();
await waitForConnectionStatus(this, ConnectionStatus.Disconnected);
}
setConnected() {
this.connectionStatus = ConnectionStatus.Connected;
}
/**
* @param {string} commandId
* @param {Object} paramObject
*/
sendCommand(commandId, paramObject) {
const params = [JSON.stringify(paramObject)];
let i = 0;
while (i < params.length) {
params[i] ? "string" == typeof params[i] && (params[i] = getValidSmartFoxText(params[i])) : params[i] = "<RoundHouseKick>";
i++;
}
const message = ["", "xt", this.serverInstance.zone, commandId, 1].concat(params, [""]).join("%");
return this.writeToSocket(message);
}
/** @param {string} msg */
writeToSocket(msg) {
if (this.connectionStatus === ConnectionStatus.Disconnecting || this.connectionStatus === ConnectionStatus.Disconnected) return false;
this.client.logger.t('[WRITE]', msg.substring(0, Math.min(150, msg.length)));
if (this.protocol === "tcp") {
let _buff0 = Buffer.from(msg);
let _buff1 = Buffer.alloc(1);
_buff1.writeInt8(0);
let bytes = Buffer.concat([_buff0, _buff1]);
this.socket.write(bytes, 'utf8', (err) => {
if (err) this.client.logger.w(`\x1b[31m[SOCKET WRITE ERROR] ${err}\x1b[0m`);
});
} else {
this.socket.send(msg, {}, (err) => {
if (err) this.client.logger.w(`\x1b[31m[SOCKET WRITE ERROR] ${err}\x1b[0m`);
});
}
return true;
}
/**
* @param {BaseClient} client
* @param {string} message
*/
onSocketMessage(client, message) {
client.logger.t("[RECEIVED]", message.substring(0, Math.min(150, message.length)));
const params = message.substring(1, message.length - 1).split("%");
if (params[0] === "xt") return onResponse(client, params.splice(1, params.length - 1));
client.logger.w("[DATA] Cannot handle message:", message);
}
/** @param {WebSocket | net.Socket} socket */
#addSocketListeners(socket) {
socket.addListener('ready', () => {
this.currData = ""
onSocketReady(this)
})
socket.addListener('data', (data) => {
this.currData += data.toString()
let nullIndex;
while ((nullIndex = this.currData.indexOf('\x00')) !== -1) {
const message = this.currData.substring(0, nullIndex);
this.currData = this.currData.substring(nullIndex + 1);
this.onSocketMessage(this.client, message)
}
});
socket.addListener('open', () => onSocketReady(this));
socket.addListener('message', (data) => {
const message = data.toString()
this.onSocketMessage(this.client, message)
});
socket.addListener('error', (err) => {
this.client.logger.w(`\x1b[31m[SOCKET ERROR] ${err}\x1b[0m`);
this.client.logger.d(err);
if (this.protocol === 'tcp') socket.end(); else socket.close();
});
socket.addListener('close', () => {
if (this.connectionStatus === ConnectionStatus.Disconnected) return;
this.connectionStatus = ConnectionStatus.Disconnected;
this.client.logger.d(`[SocketManager] Socket Closed!`);
socket.removeAllListeners();
if (this.reconnectTimeout === -1) return;
setTimeout(async () => {
if (this.reconnectTimeout === -1) return;
socket = null;
this.client.logger.i("[SocketManager] Reconnecting!");
while (true) {
try {
const new_socket = this.protocol === 'tcp' ? new net.Socket().connect(this.serverInstance.port, this.serverInstance.server) : new WebSocket(this.url);
this.#addSocketListeners(new_socket);
this.socket = new_socket;
socket = null;
await this.client._reconnect();
break;
} catch (e) {
await new Promise(res => setTimeout(res, 10000));
}
}
}, this.reconnectTimeout * 1000);
});
}
}
module.exports = SocketManager;
/** @param {SocketManager} socketManager */
function onSocketReady(socketManager) {
const languageCode = socketManager.client._language;
const distributorId = 0;
const zone = socketManager.serverInstance.zone;
const pass = `${versionDateGame}%${languageCode}%${distributorId}`;
const msg = `<login z=\'${zone}\'><nick><![CDATA[]]></nick><pword><![CDATA[${pass}]]></pword></login>`;
const message = `<msg t=\'sys\'><body action=\'login\' r=\'0\'>${msg}</body></msg>`;
socketManager.writeToSocket(message);
}
/**
* @param {SocketManager} socketManager
* @param {number} connectionStatus
* @param {number} maxMs
*/
async function waitForConnectionStatus(socketManager, connectionStatus, maxMs = 10000) {
return waitForConnectionStatusTS(socketManager, connectionStatus, new Date(Date.now() + maxMs).getTime());
}
/**
* @param {SocketManager} socketManager
* @param {number} connectionStatus
* @param {number} endDateTimestamp
*/
async function waitForConnectionStatusTS(socketManager, connectionStatus, endDateTimestamp) {
if (socketManager.connectionStatus === connectionStatus) return;
if (endDateTimestamp < Date.now()) throw new EmpireError(socketManager.client, "[Connection Error] Exceeded max time!");
await new Promise(resolve => setTimeout(resolve, 1));
return await waitForConnectionStatusTS(socketManager, connectionStatus, endDateTimestamp);
}
/** @param {string} value */
function getValidSmartFoxText(value) {
value = value.replace(/%/g, "%");
return value.replace(/'/g, "");
}