The code initializes a UDP client socket, imports various modules, and sets up constants and variables, including an array to store master information and an object to store the next response. It defines two functions, mergeMaster
and updateInfo
, which are used to merge incoming master information and handle incoming UDP messages, respectively.
npm run import -- "quake 3 server commands"
var path = require('path')
var fs = require('fs')
var zlib = require('zlib')
var dgram = require('dgram')
var udpClient = dgram.createSocket('udp4')
udpClient.on('message', updateInfo)
var importer = require('../Core')
var mdfour = importer.import("md4 checksum")
var {
getServersResponse, statusResponse, infoResponse
} = importer.import("quake 3 server responses")
var lookupDNS = importer.import("dns lookup")
var {
compressMessage,
writeBits
} = importer.import("huffman decode")
var decodeClientMessage = importer.import("decode client message")
var MAX_TIMEOUT = process.env.DEFAULT_TIMEOUT || 10000
var MAX_RELIABLE_COMMANDS = 64
var DEFAULT_MASTER = process.env.DEFAULT_MASTER || '207.246.91.235' || '192.168.0.4'
var DEFAULT_PASS = process.env.DEFAULT_PASS || 'password123!'
var masters = []
var nextResponse = {
nextInfo: null,
nextStatus: null,
nextServer: null,
nextPrint: null,
}
function mergeMaster(master) {
var found = false
masters.forEach((ma, i) => {
if(ma['ip'] == master['ip'] && ma['port'] == master['port']) {
found = true
Object.assign(masters[i], master)
Object.assign(master, masters[i])
return false
}
})
if(!found)
masters.push(master)
return master
}
async function updateInfo(m, rinfo) {
var master = mergeMaster({
ip: rinfo.address,
port: rinfo.port
})
if(m[0] == 255 && m[1] == 255 && m[2] == 255 && m[3] == 255)
m = m.slice(4, m.length)
else {
if(master.connected) {
master.channel = master.channel || {}
var commandNumber = master.channel.commandSequence
var channel = await decodeClientMessage(m, master.channel)
if(channel === false) {
return
}
//console.log(channel)
if (channel.messageType == 2) { // svc_gamestate
nextResponse.nextGamestate =
master.nextResponse.nextGamestate = channel
} else if (channel.messageType == 7) { // svc_snapshot
nextResponse.nextSnapshot =
master.nextResponse.nextSnapshot = channel
} else if (channel.messageType > 0) {
}
if(commandNumber < channel.commandSequence) {
for(var j = commandNumber + 1; j <= channel.commandSequence; j++) {
var index = j & (MAX_RELIABLE_COMMANDS-1)
if((channel.serverCommands[index] + '').match(/^chat /i)) {
nextResponse.nextChat =
master.nextResponse.nextChat = channel.serverCommands[index] + ''
}
console.log('serverCommand:', j, channel.serverCommands[index])
}
}
// always respond with input event
// response to snapshots automatically and not waiting,
// so new messages can be received
sendSequence(rinfo.address, rinfo.port, channel)
nextResponse.nextChannel =
master.nextResponse.nextChannel = channel
return
}
}
if(m.slice(0, 'getserversResponse'.length).toString('utf-8').toLowerCase() == 'getserversresponse'
|| m.slice(0, 'getserversExtResponse'.length).toString('utf-8').toLowerCase() == 'getserversextresponse') {
var masters = getServersResponse(m)
for(var m in masters) {
nextResponse.nextServer =
master.nextResponse.nextServer = mergeMaster(masters[m])
await getStatus(masters[m].ip, masters[m].port)
}
} else if (m.slice(0, 'statusResponse'.length).toString('utf-8').toLowerCase() == 'statusresponse') {
var status = mergeMaster(Object.assign(statusResponse(m), {
ip: rinfo.address,
port: rinfo.port
}))
nextResponse.nextStatus =
master.nextResponse.nextStatus = status
} else if (m.slice(0, 'infoResponse'.length).toString('utf-8').toLowerCase() == 'inforesponse') {
var info = mergeMaster(Object.assign(infoResponse(m), {
ip: rinfo.address,
port: rinfo.port
}))
nextResponse.nextInfo =
master.nextResponse.nextInfo = info
} else if (m.slice(0, 'print'.length).toString('utf-8') == 'print') {
var print = mergeMaster(Object.assign({
content: m.slice('print'.length).toString('utf-8')
}, {
ip: rinfo.address,
port: rinfo.port
}))
nextResponse.nextPrint =
master.nextResponse.nextPrint = print
} else if (m.slice(0, 'challengeResponse'.length).toString('utf-8').toLowerCase() == 'challengeresponse') {
var challenge = mergeMaster(Object.assign({
challenge: m.slice('challengeResponse'.length).toString('utf-8').trim().split(/\s+/ig)[0]
}, {
ip: rinfo.address,
port: rinfo.port
}))
nextResponse.nextChallenge =
master.nextResponse.nextChallenge = challenge
} else if (m.slice(0, 'connectResponse'.length).toString('utf-8').toLowerCase() == 'connectresponse') {
var challenge = mergeMaster(Object.assign({
// begin netchan compression
connected: true,
channel: {
compat: false,
incomingSequence: 0,
fragmentSequence: 0,
serverSequence: 0,
outgoingSequence: 0,
reliableSequence: 0,
reliableCommands: [],
challenge: m.slice('connectResponse'.length).toString('utf-8').trim().split(/\s+/ig)[0]
}
}, {
ip: rinfo.address,
port: rinfo.port
}))
nextResponse.nextConnect =
master.nextResponse.nextConnect = challenge
} else {
console.log('unknown message:', m.toString('utf-8'))
}
nextResponse.nextAny =
master.nextResponse.nextAny = {
content: m.toString('utf-8')
}
Object.assign(nextResponse.nextAny, {
ip: rinfo.address,
port: rinfo.port
})
if(!nextResponse.nextAll) {
nextResponse.nextAll = []
}
if(!master.nextResponse.nextAll) {
master.nextResponse.nextAll = []
}
nextResponse.nextAll.push(nextResponse.nextAny)
master.nextResponse.nextAll.push(nextResponse.nextAny)
}
async function getNextResponse(key, address, port = 27960) {
var timeout = 0
var server
if(address) {
var dstIP = await lookupDNS(address)
server = mergeMaster({
ip: dstIP,
port: port
})
if(!server.nextResponse) {
server.nextResponse = {}
}
server.nextResponse.nextAll = []
server.nextResponse[key] = null
}
nextResponse.nextAll = []
nextResponse[key] = null
return new Promise(resolve => {
var waiter
waiter = setInterval(() => {
timeout += 20
if((!address && nextResponse[key] != null)
|| (address && server.nextResponse[key] != null)
|| timeout >= MAX_TIMEOUT) {
clearInterval(waiter)
resolve(nextResponse[key])
}
}, 20)
})
}
async function nextInfoResponse(address, port = 27960) {
return await getNextResponse('nextInfo', address, port)
}
async function nextServerResponse(address, port = 27960) {
return await getNextResponse('nextServer', address, port)
}
async function nextStatusResponse(address, port = 27960) {
return await getNextResponse('nextStatus', address, port)
}
async function nextPrintResponse(address, port = 27960) {
return await getNextResponse('nextPrint', address, port)
}
async function nextChallengeResponse(address, port = 27960) {
return await getNextResponse('nextChallenge', address, port)
}
async function nextChannelMessage(address, port = 27960) {
return await getNextResponse('nextChannel', address, port)
}
async function nextGamestate(address, port = 27960) {
return await getNextResponse('nextGamestate', address, port)
}
async function nextConnectResponse(address, port = 27960) {
return await getNextResponse('nextConnect', address, port)
}
async function nextSnapshot(address, port = 27960) {
return await getNextResponse('nextSnapshot', address, port)
}
async function nextChat(address, port = 27960) {
return await getNextResponse('nextChat', address, port)
}
async function nextAnyResponse(address, port = 27960) {
return await getNextResponse('nextAny', address, port)
}
async function nextAllResponses(address, port = 27960) {
return await getNextResponse('nextAll', address, port)
}
async function getChallenge(address, port = 27960, challenge, gamename) {
var dstIP = await lookupDNS(address)
var msgBuff = new Buffer.from(`\xFF\xFF\xFF\xFFgetchallenge ${challenge} ${gamename}`.split('').map(c => c.charCodeAt(0)))
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
}
async function sendConnect(address, port = 27960, info) {
var connectInfo = typeof info == 'string'
? info
: Object.keys(info).map(k => '\\' + k + '\\' + info[k]).join('')
var dstIP = await lookupDNS(address)
var compressedInfo = await compressMessage(`\xFF\xFF\xFF\xFFconnect "${connectInfo}"`)
var msgBuff = new Buffer.from(compressedInfo)
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
}
function NETCHAN_GENCHECKSUM(challenge, sequence) {
return (challenge) ^ ((sequence) * (challenge))
}
var pak8pk3 = [0,695294960,269430381,2656948387,485997170,1095318617]
async function sendSequence(address, port, channel) {
var msg
//console.log('seq', channel.serverSequence, channel.commandSequence)
msg = writeBits([] , 0 , channel.serverId || 0, 32)
msg = writeBits(msg[1], msg[0], channel.serverId ? (channel.serverSequence || 0) : 0, 32)
msg = writeBits(msg[1], msg[0], channel.serverId ? (channel.commandSequence || 0) : 0, 32)
// write any unacknowledged clientCommands
for ( var i = channel.reliableAcknowledge + 1 ; i <= channel.reliableSequence ; i++ ) {
msg = writeBits(msg[1], msg[0], 4, 8) // clc_clientCommand
msg = writeBits(msg[1], msg[0], i, 32)
var command = channel.reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ]
for ( var c = 0 ; c < command.length; c++ ) {
// get rid of 0x80+ and '%' chars, because old clients don't like them
var v
if ( command[c] & 0x80 || command[c] == '%' )
v = '.'.charCodeAt(0);
else
v = command[c].charCodeAt(0);
msg = writeBits(msg[1], msg[0], v, 8)
}
msg = writeBits(msg[1], msg[0], 0, 8)
}
// empty movement
msg = writeBits(msg[1], msg[0], 3, 8) // clc_moveNoDelta
// write the command count
msg = writeBits(msg[1], msg[0], 1, 8)
// write delta user cmd key
msg = writeBits(msg[1], msg[0], 1, 1)
// TODO: spectate and record player locations
msg = writeBits(msg[1], msg[0], 0, 1) // no change
var dstIP = await lookupDNS(address)
var qport = udpClient.address().port
var checksum = NETCHAN_GENCHECKSUM(channel.challenge, channel.outgoingSequence)
var msgBuff = Buffer.concat([new Buffer.from([
(channel.outgoingSequence >> 0) & 0xFF,
(channel.outgoingSequence >> 8) & 0xFF,
(channel.outgoingSequence >> 16) & 0xFF,
(channel.outgoingSequence >> 24) & 0xFF,
(qport >> 0) & 0xFF,
(qport >> 8) & 0xFF,
(checksum >> 0) & 0xFF,
(checksum >> 8) & 0xFF,
(checksum >> 16) & 0xFF,
(checksum >> 24) & 0xFF,
]), msg[1]])
channel.outgoingSequence++
//console.log(qport, channel.commandSequence)
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
}
async function sendReliable(address, port, cmd) {
var dstIP = await lookupDNS(address)
var server = mergeMaster({
ip: dstIP,
port: port
})
if(typeof server.channel != 'undefined') {
console.log('clientCommand: ', cmd)
var channel = server.channel
channel.reliableSequence++
var index = channel.reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 )
channel.reliableCommands[index] = cmd
await sendSequence(address, port, channel)
} else
console.log('Not connected')
}
async function sendPureChecksums(address, port, channel) {
// TODO: calculate different checksums for other games QVMs
var checksum = pak8pk3[0] = channel.checksumFeed
var headers = new Uint8Array(Uint32Array.from(pak8pk3).buffer)
var digest = new Uint32Array(4)
mdfour(digest, headers, headers.length)
var unsigned = new Uint32Array(1)
unsigned[0] = digest[0] ^ digest[1] ^ digest[2] ^ digest[3]
checksum ^= unsigned[0]
checksum ^= 1
sendReliable(address, port, 'cp ' + channel.serverId
+ ' ' + unsigned[0]
+ ' ' + unsigned[0]
+ ' @ ' + unsigned[0]
+ ' ' + checksum)
}
async function sendRcon(address, port = 27960, command, password = DEFAULT_PASS) {
var dstIP = await lookupDNS(address)
var msgBuff = new Buffer.from(`\xFF\xFF\xFF\xFFrcon "${password}" ${command}`.split('').map(c => c.charCodeAt(0)))
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
}
async function getStatus(address, port = 27960) {
var dstIP = await lookupDNS(address)
var msgBuff = new Buffer.from('\xFF\xFF\xFF\xFFgetstatus'.split('').map(c => c.charCodeAt(0)))
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
}
async function getInfo(address, port = 27960) {
var dstIP = await lookupDNS(address)
var msgBuff = new Buffer.from('\xFF\xFF\xFF\xFFgetinfo xxx'.split('').map(c => c.charCodeAt(0)))
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
}
async function listMasters(master = DEFAULT_MASTER, port = 27950, wait = true) {
var dstIP = await lookupDNS(master)
var msgBuff = new Buffer.from('\xFF\xFF\xFF\xFFgetservers 68 empty'.split('').map(c => c.charCodeAt(0)))
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP)
if(wait) {
await new Promise(resolve => setTimeout(resolve, MAX_TIMEOUT))
} else {
var timeout = 0
var timer
// can't use nextInfoResponse() because it depends on at least 1 statusResponse
await nextStatusResponse()
}
return masters
}
module.exports = {
listMasters,
getInfo,
getStatus,
getChallenge,
sendRcon,
sendConnect,
sendSequence,
sendReliable,
sendPureChecksums,
udpClient,
nextInfoResponse,
nextStatusResponse,
nextServerResponse,
nextPrintResponse,
nextChallengeResponse,
nextConnectResponse,
nextChannelMessage,
nextGamestate,
nextAnyResponse,
nextSnapshot,
nextChat,
nextAllResponses,
}
```javascript
const path = require('path');
const fs = require('fs');
const zlib = require('zlib');
const dgram = require('dgram');
const udpClient = dgram.createSocket('udp4');
class Quake3Client {
constructor() {
this.maxTimeout = process.env.DEFAULT_TIMEOUT || 10000;
this.maxReliableCommands = 64;
this.defaultMaster = process.env.DEFAULT_MASTER || '207.246.91.235' || '192.168.0.4';
this.defaultPass = process.env.DEFAULT_PASS || 'password123!';
this.masters = [];
this.nextResponse = {
nextInfo: null,
nextStatus: null,
nextServer: null,
nextPrint: null,
};
this.importer = require('../Core');
this.decoder = this.importer.import('decode client message');
this.huffman = this.importer.import('huffman decode');
this.md4 = this.importer.import('md4 checksum');
this.quakeResponses = this.importer.import('quake 3 server responses');
this.dnsLookup = this.importer.import('dns lookup');
this.statusResponse = this.quakeResponses.statusResponse;
this.infoResponse = this.quakeResponses.infoResponse;
this.get ServersResponse = this.quakeResponses.getServersResponse;
this.compressMessage = this.huffman.compressMessage;
this.writeBits = this.huffman.writeBits;
this.netchanGenChecksum = this.md4.NETCHAN_GENCHECKSUM;
}
async updateInfo(m, rinfo) {
const master = this.mergeMaster({
ip: rinfo.address,
port: rinfo.port,
});
if (m.slice(0, 4).toString('utf-8') === 'FFFF') {
m = m.slice(4, m.length);
} else if (master.connected) {
const channel = await this.decodeClientMessage(m, master.channel);
if (channel === false) {
return;
}
this.updateNextResponse(master, channel);
await this.sendSequence(rinfo.address, rinfo.port, channel);
} else {
this.handleMessage(m, rinfo);
}
}
updateNextResponse(master, channel) {
if (channel.messageType === 2) { // svc_gamestate
master.nextResponse.nextGamestate = channel;
this.nextResponse.nextGamestate = channel;
} else if (channel.messageType === 7) { // svc_snapshot
master.nextResponse.nextSnapshot = channel;
this.nextResponse.nextSnapshot = channel;
} else if (channel.messageType > 0) {
this.nextResponse.nextChannel = channel;
master.nextResponse.nextChannel = channel;
}
if (channel.messageType === 2 || channel.messageType === 7) {
this.nextResponse.nextServer = master;
master.nextResponse.nextServer = master;
}
if (channel.messageType > 0 && channel.messageType < 8) {
this.nextResponse.nextChat = channel.serverCommands[channel.commandSequence - 1];
master.nextResponse.nextChat = channel.serverCommands[channel.commandSequence - 1];
}
}
handleMessage(m, rinfo) {
if (
m.slice(0, 'getserversResponse'.length).toString('utf-8').toLowerCase() ===
'getserversresponse'
) {
const masters = this.getServersResponse(m);
masters.forEach((master) => {
this.mergeMaster(master);
});
} else if (m.slice(0,'statusResponse'.length).toString('utf-8').toLowerCase() ==='statusresponse') {
const status = this.mergeMaster(Object.assign(this.statusResponse(m), {
ip: rinfo.address,
port: rinfo.port,
}));
this.nextResponse.nextStatus = status;
status.nextResponse.nextStatus = status;
} else if (m.slice(0, 'infoResponse'.length).toString('utf-8').toLowerCase() === 'inforesponse') {
const info = this.mergeMaster(Object.assign(this.infoResponse(m), {
ip: rinfo.address,
port: rinfo.port,
}));
this.nextResponse.nextInfo = info;
info.nextResponse.nextInfo = info;
} else if (m.slice(0, 'print'.length).toString('utf-8') === 'print') {
const print = this.mergeMaster(Object.assign({
content: m.slice('print'.length).toString('utf-8'),
}, {
ip: rinfo.address,
port: rinfo.port,
}));
this.nextResponse.nextPrint = print;
print.nextResponse.nextPrint = print;
} else if (m.slice(0, 'challengeResponse'.length).toString('utf-8').toLowerCase() === 'challengeresponse') {
const challenge = this.mergeMaster(Object.assign({
challenge: m.slice('challengeResponse'.length).toString('utf-8').trim().split(/\s+/ig)[0],
}, {
ip: rinfo.address,
port: rinfo.port,
}));
this.nextResponse.nextChallenge = challenge;
challenge.nextResponse.nextChallenge = challenge;
} else if (m.slice(0, 'connectResponse'.length).toString('utf-8').toLowerCase() === 'connectresponse') {
const challenge = this.mergeMaster(Object.assign({
connected: true,
channel: {
compat: false,
incomingSequence: 0,
fragmentSequence: 0,
serverSequence: 0,
outgoingSequence: 0,
reliableSequence: 0,
reliableCommands: [],
challenge: m.slice('connectResponse'.length).toString('utf-8').trim().split(/\s+/ig)[0],
},
}, {
ip: rinfo.address,
port: rinfo.port,
}));
this.nextResponse.nextConnect = challenge;
challenge.nextResponse.nextConnect = challenge;
} else {
console.log('unknown message:', m.toString('utf-8'));
}
}
async getNextResponse(key, address, port = 27960) {
let timeout = 0;
let server = null;
if (address) {
const dstIP = await this.dnsLookup(address);
server = this.mergeMaster({
ip: dstIP,
port: port,
});
if (!server.nextResponse) {
server.nextResponse = {};
}
server.nextResponse.nextAll = [];
server.nextResponse[key] = null;
}
this.nextResponse.nextAll = [];
this.nextResponse[key] = null;
return new Promise((resolve) => {
let waiter;
waiter = setInterval(() => {
timeout += 20;
if (
(!address && this.nextResponse[key]!== null) ||
(address && server.nextResponse[key]!== null) ||
timeout >= this.maxTimeout
) {
clearInterval(waiter);
resolve(this.nextResponse[key]);
}
}, 20);
});
}
async nextInfoResponse(address, port = 27960) {
return await this.getNextResponse('nextInfo', address, port);
}
async nextServerResponse(address, port = 27960) {
return await this.getNextResponse('nextServer', address, port);
}
async nextStatusResponse(address, port = 27960) {
return await this.getNextResponse('nextStatus', address, port);
}
async nextPrintResponse(address, port = 27960) {
return await this.getNextResponse('nextPrint', address, port);
}
async nextChallengeResponse(address, port = 27960) {
return await this.getNextResponse('nextChallenge', address, port);
}
async nextChannelMessage(address, port = 27960) {
return await this.getNextResponse('nextChannel', address, port);
}
async nextGamestate(address, port = 27960) {
return await this.getNextResponse('nextGamestate', address, port);
}
async nextConnectResponse(address, port = 27960) {
return await this.getNextResponse('nextConnect', address, port);
}
async nextSnapshot(address, port = 27960) {
return await this.getNextResponse('nextSnapshot', address, port);
}
async nextChat(address, port = 27960) {
return await this.getNextResponse('nextChat', address, port);
}
async nextAnyResponse(address, port = 27960) {
return await this.getNextResponse('nextAny', address, port);
}
async nextAllResponses(address, port = 27960) {
return await this.getNextResponse('nextAll', address, port);
}
async getChallenge(address, port = 27960, challenge, gamename) {
const dstIP = await this.dnsLookup(address);
const msgBuff = new Buffer.from(`\xFF\xFF\xFF\xFFgetchallenge ${challenge} ${gamename}`.split('').map((c) => c.charCodeAt(0)));
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
}
async sendConnect(address, port = 27960, info) {
const connectInfo = typeof info ==='string'? info : JSON.stringify(info);
const dstIP = await this.dnsLookup(address);
const compressedInfo = await this.compressMessage(`\xFF\xFF\xFF\xFFconnect "${connectInfo}"`);
const msgBuff = new Buffer.from(compressedInfo);
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
}
async sendSequence(address, port, channel) {
const msg = this.writeBits([], 0, channel.serverId || 0, 32);
msg = this.writeBits(msg[1], msg[0], channel.serverId? (channel.serverSequence || 0) : 0, 32);
msg = this.writeBits(msg[1], msg[0], channel.serverId? (channel.commandSequence || 0) : 0, 32);
for (let i = channel.reliableAcknowledge + 1; i <= channel.reliableSequence; i++) {
msg = this.writeBits(msg[1], msg[0], 4, 8); // clc_clientCommand
msg = this.writeBits(msg[1], msg[0], i, 32);
const command = channel.reliableCommands[i & (this.maxReliableCommands - 1)];
for (let c = 0; c < command.length; c++) {
let v;
if (command[c] & 0x80 || command[c] === '%') {
v = '.'.charCodeAt(0);
} else {
v = command[c].charCodeAt(0);
}
msg = this.writeBits(msg[1], msg[0], v, 8);
}
msg = this.writeBits(msg[1], msg[0], 0, 8);
}
msg = this.writeBits(msg[1], msg[0], 3, 8); // clc_moveNoDelta
msg = this.writeBits(msg[1], msg[0], 1, 8);
msg = this.writeBits(msg[1], msg[0], 1, 1);
msg = this.writeBits(msg[1], msg[0], 0, 1); // no change
const dstIP = await this.dnsLookup(address);
const qport = udpClient.address().port;
const checksum = this.netchanGenChecksum(channel.challenge, channel.outgoingSequence);
const msgBuff = Buffer.concat([new Buffer.from([channel.outgoingSequence >> 0 & 0xFF, channel.outgoingSequence >> 8 & 0xFF, channel.outgoingSequence >> 16 & 0xFF, channel.outgoingSequence >> 24 & 0xFF, qport >> 0 & 0xFF, qport >> 8 & 0xFF, checksum >> 0 & 0xFF, checksum >> 8 & 0xFF, checksum >> 16 & 0xFF, checksum >> 24 & 0xFF, ]), msg[1]]);
channel.outgoingSequence++;
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
}
async sendReliable(address, port, cmd) {
const dstIP = await this.dnsLookup(address);
const server = this.mergeMaster({
ip: dstIP,
port: port,
});
if (typeof server.channel!== 'undefined') {
console.log('clientCommand: ', cmd);
const channel = server.channel;
channel.reliableSequence++;
const index = channel.reliableSequence & (this.maxReliableCommands - 1);
channel.reliableCommands[index] = cmd;
await this.sendSequence(address, port, channel);
} else {
console.log('Not connected');
}
}
async sendPureChecksums(address, port, channel) {
// TODO: calculate different checksums for other games QVMs
const checksum = pak8pk3[0] = channel.checksumFeed;
const headers = new Uint8Array(Uint32Array.from(pak8pk3).buffer);
const digest = new Uint32Array(4);
this.md4.mdfour(digest, headers, headers.length);
const unsigned = new Uint32Array(1);
unsigned[0] = digest[0] ^ digest[1] ^ digest[2] ^ digest[3];
checksum ^= unsigned[0];
checksum ^= 1;
this.sendReliable(address, port, 'cp'+ channel.serverId +'' + unsigned[0] +'' + unsigned[0] +'@'+ unsigned[0] +'' + checksum);
}
async sendRcon(address, port = 27960, command, password = this.defaultPass) {
const dstIP = await this.dnsLookup(address);
const msgBuff = new Buffer.from(`\xFF\xFF\xFF\xFFrcon "${password}" ${command}`.split('').map((c) => c.charCodeAt(0)));
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
}
async getStatus(address, port = 27960) {
const dstIP = await this.dnsLookup(address);
const msgBuff = new Buffer.from('\xFF\xFF\xFF\xFFgetstatus'.split('').map((c) => c.charCodeAt(0)));
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
}
async getInfo(address, port = 27960) {
const dstIP = await this.dnsLookup(address);
const msgBuff = new Buffer.from('\xFF\xFF\xFF\xFFgetinfo xxx'.split('').map((c) => c.charCodeAt(0)));
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
}
async listMasters(master = this.defaultMaster, port = 27950, wait = true) {
const dstIP = await this.dnsLookup(master);
const msgBuff = new Buffer.from('\xFF\xFF\xFF\xFFgetservers 68 empty'.split('').map((c) => c.charCodeAt(0)));
udpClient.send(msgBuff, 0, msgBuff.length, port, dstIP);
if (wait) {
await new Promise((resolve) => setTimeout(resolve, this.maxTimeout));
} else {
const timeout = 0;
const timer;
// can't use nextInfoResponse() because it depends on at least 1 statusResponse
await this.nextStatusResponse();
}
return this.masters;
}
async decodeClientMessage(message, channel) {
return this.decoder(message, channel);
}
async mergeMaster(master) {
const found = this.masters.some((existing) => existing.ip === master.ip && existing.port === master.port);
if (found) {
Object.assign(this.masters[this.masters.indexOf(existing)], master);
} else {
this.masters.push(master);
}
return master;
}
async dnsLookup(address) {
return this.dnsLookup(address);
}
async compressMessage(message) {
return this.compressMessage(message);
}
async writeBits(bits, bit1, bit0, length) {
return this.writeBits(bits, bit1, bit0, length);
}
async netchanGenChecksum(challenge, sequence) {
return this.netchanGenChecksum(challenge, sequence);
}
}
module.exports = {
Quake3Client,
udpClient,
};
The code begins by importing various Node.js modules and setting up a UDP client socket.
var path = require('path')
: imports the path
module for working with file paths.var fs = require('fs')
: imports the fs
module for interacting with the file system.var zlib = require('zlib')
: imports the zlib
module for working with compressed data.var dgram = require('dgram')
: imports the dgram
module for working with UDP sockets.var udpClient = dgram.createSocket('udp4')
: creates a UDP client socket.udpClient.on('message', updateInfo)
: sets up an event listener for incoming UDP messages.The code then imports various modules from a Core module.
var importer = require('../Core')
: imports the Core module, which contains other modules.var mdfour = importer.import('md4 checksum')
: imports the mdfour
module, which provides a function for calculating MD4 checksums.var { getServersResponse, statusResponse, infoResponse } = importer.import('quake 3 server responses')
: imports three functions from the quake 3 server responses
module: getServersResponse
, statusResponse
, and infoResponse
.var lookupDNS = importer.import('dns lookup')
: imports the lookupDNS
module, which provides a function for performing DNS lookups.var { compressMessage, writeBits } = importer.import('huffman decode')
: imports two functions from the huffman decode
module: compressMessage
and writeBits
.var decodeClientMessage = importer.import('decode client message')
: imports the decodeClientMessage
module, which provides a function for decoding client messages.The code sets up various constants and variables.
var MAX_TIMEOUT = process.env.DEFAULT_TIMEOUT || 10000
: sets the MAX_TIMEOUT
constant to the value of the DEFAULT_TIMEOUT
environment variable or 10000 if it's not set.var MAX_RELIABLE_COMMANDS = 64
: sets the MAX_RELIABLE_COMMANDS
constant to 64.var DEFAULT_MASTER = process.env.DEFAULT_MASTER || '207.246.91.235' || '192.168.0.4'
: sets the DEFAULT_MASTER
constant to the value of the DEFAULT_MASTER
environment variable or '207.246.91.235' or '192.168.0.4' if it's not set.var DEFAULT_PASS = process.env.DEFAULT_PASS || 'password123!'
: sets the DEFAULT_PASS
constant to the value of the DEFAULT_PASS
environment variable or 'password123!' if it's not set.var masters = []
: initializes an empty array masters
.var nextResponse = { nextInfo: null, nextStatus: null, nextServer: null, nextPrint: null, }
: initializes an object nextResponse
with four properties: nextInfo
, nextStatus
, nextServer
, and nextPrint
, all set to null
.The code defines two functions: mergeMaster
and updateInfo
.
function mergeMaster(master)
: takes a master
object as input and merges it with an existing master in the masters
array if it already exists, or adds it to the array if it doesn't exist.async function updateInfo(m, rinfo)
: takes two arguments m
(the incoming UDP message) and rinfo
(information about the sender), and performs the following actions:
mergeMaster
to merge the sender's information with the existing masters
array.decodeClientMessage
function and checks the decoded message type. If it's a "svc_gamestate" message (type 2), it updates the nextResponse
object with the decoded message.