diff --git a/CHANGELOG.md b/CHANGELOG.md index 489058c..db4fa7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## To Be Released... ## 5.X.Y * Fix: HTTP requests would end up making more retries than needed due to got's internal retry mechanism (#690, thanks @RattleSN4K3) +* Feat: 7 Days to Die add Telnet support for missing player names (#692, thanks @jammsen) ## 5.3.0 * Docs: Arma Reforger query setup note (#670, thanks @xCausxn) diff --git a/GAMES_LIST.md b/GAMES_LIST.md index f6a8744..b17c556 100644 --- a/GAMES_LIST.md +++ b/GAMES_LIST.md @@ -264,7 +264,7 @@ | rust | Rust | [Valve Protocol](#valve) | | s2ats | Savage 2: A Tortured Soul | | | satisfactory | Satisfactory | [Notes](#satisfactory) | -| sdtd | 7 Days to Die | [Valve Protocol](#valve) | +| sdtd | 7 Days to Die | [Notes](#sdtd), [Valve Protocol](#valve) | | serioussam | Serious Sam | | | serioussam2 | Serious Sam 2 | | | shatteredhorizon | Shattered Horizon | [Valve Protocol](#valve) | @@ -547,3 +547,8 @@ Does not provide players names, using a plugin like this [one](https://github.co ### Ace of Spades / Build and Shoot Requires usage of the status query server enabled in the config.txt. `status_server.enabled` to `true` + +### 7 Days to Die +Does not provide player names but can be provided via telnet commands, to use these make sure in `serverconfig.xml` you +have configured `TelnetEnabled` to `true`, `TelnetPort` and `TelnetPassword` and pass `telnetPort` and `telnetPassword` +to the query parameters. diff --git a/lib/games.js b/lib/games.js index 65107f7..359249b 100644 --- a/lib/games.js +++ b/lib/games.js @@ -2602,10 +2602,11 @@ export const games = { options: { port: 26900, port_query_offset: 1, - protocol: 'valve' + protocol: 'sdtd' }, extra: { - old_id: '7d2d' + old_id: '7d2d', + doc_notes: 'sdtd' } }, satisfactory: { diff --git a/package-lock.json b/package-lock.json index 37bb468..8fb3243 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "long": "5.3.2", "minimist": "1.2.8", "seek-bzip": "2.0.0", + "telnet-client": "2.2.5", "varint": "6.0.0" }, "bin": { @@ -1020,6 +1021,14 @@ "node": ">=6.0.0" } }, + "node_modules/emitter-component": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", + "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", @@ -2444,6 +2453,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==" + }, "node_modules/normalize-url": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", @@ -2964,6 +2978,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", + "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", + "dependencies": { + "emitter-component": "^1.1.1" + } + }, "node_modules/string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -3118,6 +3140,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/telnet-client": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.5.tgz", + "integrity": "sha512-X5xEkmKHgpCpngnH7QnkFX87UyBErauHsjzlCGVp87MbhnmHoaAeacuALGfqovHh3MXAfrpPs+g30PgyBpy8Jw==", + "dependencies": { + "net": "^1.0.2", + "stream": "^0.0.2" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3970,6 +4005,11 @@ "esutils": "^2.0.2" } }, + "emitter-component": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", + "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==" + }, "es-abstract": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", @@ -4997,6 +5037,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==" + }, "normalize-url": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", @@ -5353,6 +5398,14 @@ "object-inspect": "^1.9.0" } }, + "stream": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", + "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", + "requires": { + "emitter-component": "^1.1.1" + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -5470,6 +5523,15 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "telnet-client": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.5.tgz", + "integrity": "sha512-X5xEkmKHgpCpngnH7QnkFX87UyBErauHsjzlCGVp87MbhnmHoaAeacuALGfqovHh3MXAfrpPs+g30PgyBpy8Jw==", + "requires": { + "net": "^1.0.2", + "stream": "^0.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index f321282..8eb24e4 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "long": "5.3.2", "minimist": "1.2.8", "seek-bzip": "2.0.0", + "telnet-client": "2.2.5", "varint": "6.0.0" }, "devDependencies": { diff --git a/protocols/core.js b/protocols/core.js index 1528bc1..b53c2e8 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -7,6 +7,7 @@ import Logger from '../lib/Logger.js' import DnsResolver from '../lib/DnsResolver.js' import { Results } from '../lib/Results.js' import Promises from '../lib/Promises.js' +import { Telnet } from 'telnet-client' let uid = 0 @@ -27,6 +28,8 @@ export default class Core extends EventEmitter { this.udpSocket = null this.shortestRTT = 0 this.usedTcp = false + + this.telnetClient = new Telnet() } // Runs a single attempt with a timeout and cleans up afterward @@ -336,4 +339,23 @@ export default class Core extends EventEmitter { requestPromise?.cancel() } } + + async telnetConnect (params) { + await this.telnetClient.connect({ + timeout: 2000, + execTimeout: 2000, + host: this.options.host, + debug: this.debugEnabled, + ...params + }) + } + + async telnetExecute (command) { + return await this.telnetClient.exec(command) + } + + async telnetClose () { + await this.telnetClient.end() + await this.telnetClient.destroy() + } } diff --git a/protocols/index.js b/protocols/index.js index 6eb7531..51e9a59 100644 --- a/protocols/index.js +++ b/protocols/index.js @@ -72,11 +72,12 @@ import xonotic from './xonotic.js' import altvmp from './altvmp.js' import vintagestorymaster from './vintagestorymaster.js' import vintagestory from './vintagestory.js' +import sdtd from './sdtd.js' export { armagetron, ase, asa, assettocorsa, battlefield, brokeprotocol, brokeprotocolmaster, buildandshoot, cs2d, discord, doom3, eco, epic, factorio, farmingsimulator, ffow, fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, gtasao, hawakening, hawakeningmaster, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, minecraftbedrock, minecraftvanilla, minetest, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, renegadex, renegadexmaster, renown, rfactor, ragemp, samp, satisfactory, soldat, savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, toxikk, tribes1, tribes1master, unreal2, ut3, valve, - vcmp, ventrilo, warsow, eldewrito, beammpmaster, beammp, dayz, theisleevrima, xonotic, altvmp, vintagestorymaster, vintagestory + vcmp, ventrilo, warsow, eldewrito, beammpmaster, beammp, dayz, theisleevrima, xonotic, altvmp, vintagestorymaster, vintagestory, sdtd } diff --git a/protocols/sdtd.js b/protocols/sdtd.js new file mode 100644 index 0000000..3e72362 --- /dev/null +++ b/protocols/sdtd.js @@ -0,0 +1,61 @@ +import Valve from './valve.js' +import { Players } from '../lib/Results.js' + +const playerLineRegex = /(?<=id=\d+,\s*)(?\S[^,]*)(?=,)/ + +const sanitizeTelnetResponse = response => { + return response + .split(/\r?\n/) + .map(l => l.replace(/\r$/, '').trim()) + .filter(l => l.length > 0) +} + +export default class sdtd extends Valve { + async run (state) { + await super.run(state) + await this.telnetCalls(state) + } + + async telnetCalls (state) { + const telnetPort = this.options.telnetPort + const telnetPassword = this.options.telnetPassword + + if (!telnetPort || !telnetPassword) { + this.logger.debug('No telnet args given, skipping.') + return + } + + if (!this.options.requestPlayers) { + return + } + + await this.telnetConnect({ + port: telnetPort, + password: telnetPassword, + passwordPrompt: /Please enter password:/i, + shellPrompt: /\r\n$/ + }) + + await this.telnetCallPlayers(state) + + await this.telnetClose() + } + + async telnetCallPlayers (state) { + const playersResponse = await this.telnetExecute('listplayers') + state.players = new Players() + for (const possiblePlayerLine of sanitizeTelnetResponse(playersResponse)) { + const match = possiblePlayerLine.match(playerLineRegex) + + const name = match?.groups?.name + if (name) { + state.players.push({ + name, + responseLine: possiblePlayerLine + }) + } + } + + state.raw.telnetPlayersResponse = playersResponse + } +} diff --git a/tools/generate_games_list.js b/tools/generate_games_list.js index ee16ac2..42eab64 100644 --- a/tools/generate_games_list.js +++ b/tools/generate_games_list.js @@ -55,7 +55,7 @@ Object.entries(sortedGames).forEach(([id, game]) => { if (game?.extra?.doc_notes) { notes.push('[Notes](#' + game.extra.doc_notes + ')') } - if (['valve', 'dayz'].includes(game.options.protocol)) { + if (['valve', 'dayz', 'sdtd'].includes(game.options.protocol)) { notes.push('[Valve Protocol](#valve)') } if (['epic', 'asa', 'theisleevrima', 'renown'].includes(game.options.protocol)) {