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)) {