diff --git a/CHANGELOG.md b/CHANGELOG.md index db8d5fd..d0606e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * Added Deno support: the library and CLI can now be experimentally used with the [Deno runtime](https://deno.com) * `deno run --allow-net --allow-read=. bin/gamedig.js --type tf2 127.0.0.1` * Added code examples. +* Added Epic Online Services protocol. #### Games * Added support by @dgibbs64: Eco (2018), Core Keeper (2022), ARMA: Reforger (2022), @@ -39,6 +40,7 @@ San Andreas OpenMP. a placeholder in the `players` field. * Fixed wrong field being parsed for `maxplayers` on Doom3. * Stabilized field `numplayers`. +* Added support by @GuilhermeWerner: ARK: Survival Ascended (2023). ### 4.1.0 * Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues. diff --git a/games.txt b/games.txt index 12c80e2..16174c1 100644 --- a/games.txt +++ b/games.txt @@ -8,6 +8,7 @@ aoe2|Age of Empires 2 (1999)|ase|port_query=27224 alienarena|Alien Arena (2004)|quake2|port_query=27910 alienswarm|Alien Swarm (2010)|valve|port=27015 arkse|Ark: Survival Evolved (2017)|valve|port=7777,port_query=27015 +asa|Ark: Survival Ascended (2023)|asa|port=7777 assettocorsa|Assetto Corsa (2014)|assettocorsa|port=9610 atlas|Atlas (2018)|valve|port=5761,port_query_offset=51800 avorion|Avorion (2020)|valve|port=27000,port_query_offset=20 diff --git a/package-lock.json b/package-lock.json index df2e823..0de4ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "4.1.0", "license": "MIT", "dependencies": { + "axios": "^1.6.1", "cheerio": "^1.0.0-rc.12", "gbxremote": "^0.2.1", "got": "^13.0.0", @@ -396,6 +397,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -408,6 +414,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -578,6 +594,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -716,6 +743,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1366,6 +1401,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1375,6 +1429,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -2118,6 +2185,25 @@ "node": ">=10" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -2411,6 +2497,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3353,12 +3444,27 @@ "is-shared-array-buffer": "^1.0.2" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "axios": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3495,6 +3601,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3591,6 +3705,11 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4064,6 +4183,11 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4073,6 +4197,16 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -4596,6 +4730,19 @@ "yallist": "^4.0.0" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -4805,6 +4952,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/package.json b/package.json index ab8f0dd..d57a12b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "README.md" ], "dependencies": { + "axios": "^1.6.1", "cheerio": "^1.0.0-rc.12", "gbxremote": "^0.2.1", "got": "^13.0.0", diff --git a/protocols/asa.js b/protocols/asa.js new file mode 100644 index 0000000..72e18c4 --- /dev/null +++ b/protocols/asa.js @@ -0,0 +1,12 @@ +import Epic from './epic.js' + +export default class asa extends Epic { + constructor () { + super() + + // OAuth2 credentials extracted from ARK: Survival Ascended files. + this.clientId = 'xyza7891muomRmynIIHaJB9COBKkwj6n' + this.clientSecret = 'PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s' + this.deploymentId = 'ad9a8feffb3b4b2ca315546f038c3ae2' + } +} diff --git a/protocols/epic.js b/protocols/epic.js new file mode 100644 index 0000000..5b9e2e9 --- /dev/null +++ b/protocols/epic.js @@ -0,0 +1,107 @@ +import Core from './core.js' +import axios from 'axios' + +export default class Epic extends Core { + constructor () { + super() + + /** + * To get information about game servers using Epic's EOS, you need some credentials to authenticate using OAuth2. + * + * https://dev.epicgames.com/docs/web-api-ref/authentication + * + * These credentials can be provided by the game developers or extracted from the game's files. + */ + this.clientId = null + this.clientSecret = null + this.deploymentId = null + this.epicApi = 'https://api.epicgames.dev' + this.accessToken = null + } + + async run (state) { + await this.getAccessToken() + await this.queryInfo(state) + await this.cleanup(state) + } + + async getAccessToken () { + this.logger.debug('Requesting acess token ...') + + const url = `${this.epicApi}/auth/v1/oauth/token` + const body = `grant_type=client_credentials&deployment_id=${this.deploymentId}` + const headers = { + Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + + this.logger.debug(`POST: ${url}`) + const response = await axios.post(url, body, { headers }) + if (response.status !== 200) { + throw new Error('Failed to get OAuth token') + } + + this.accessToken = response.data.access_token + } + + async queryInfo (state) { + const url = `${this.epicApi}/matchmaking/v1/${this.deploymentId}/filter` + const body = { + criteria: [ + { + key: 'attributes.ADDRESS_s', + op: 'EQUAL', + value: this.options.address + } + ] + } + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${this.accessToken}` + } + + this.logger.debug(`POST: ${url}`) + const response = await axios.post(url, body, { headers }) + if (response.status !== 200) { + throw new Error('Failed to get server info') + } + + const reader = response.data + + // Epic returns a list of sessions, we need to find the one with the desired port. + const hasDesiredPort = (session) => session.attributes.ADDRESSBOUND_s === `0.0.0.0:${this.options.port}` || + session.attributes.ADDRESSBOUND_s === `${this.options.address}:${this.options.port}` + + const desiredServer = reader.sessions.find(hasDesiredPort) + + if (!desiredServer) { + throw new Error('Server not found') + } + + state.name = desiredServer.attributes.CUSTOMSERVERNAME_s + state.map = desiredServer.attributes.MAPNAME_s + state.password = desiredServer.attributes.SERVERPASSWORD_b + state.maxplayers = desiredServer.settings.maxPublicPlayers + + // If the game returns the player list, we can use it otherwise we use the total players. + if (desiredServer.totalPlayers === desiredServer.publicPlayers.length) { + for (const player of desiredServer.publicPlayers) { + state.players.push({ + name: player.name, + raw: player + }) + } + } else { + for (let i = 0; i < desiredServer.totalPlayers; i++) { + state.players.push('') + } + } + + state.raw = desiredServer + } + + async cleanup (state) { + this.accessToken = null + } +} diff --git a/protocols/index.js b/protocols/index.js index c581d36..16c9488 100644 --- a/protocols/index.js +++ b/protocols/index.js @@ -1,5 +1,6 @@ import armagetron from './armagetron.js' import ase from './ase.js' +import asa from './asa.js' import assettocorsa from './assettocorsa.js' import battlefield from './battlefield.js' import buildandshoot from './buildandshoot.js' @@ -7,6 +8,7 @@ import cs2d from './cs2d.js' import discord from './discord.js' import doom3 from './doom3.js' import eco from './eco.js' +import epic from './epic.js' import ffow from './ffow.js' import fivem from './fivem.js' import gamespy1 from './gamespy1.js' @@ -46,10 +48,10 @@ import vcmp from './vcmp.js' import ventrilo from './ventrilo.js' import warsow from './warsow.js' -export { - armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1, +export { + armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, ffow, fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp, savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve, - vcmp, ventrilo, warsow + vcmp, ventrilo, warsow }