From b18c5359061ca4852a6ec8d7a1a39ae8254f48ad Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Fri, 20 Sep 2024 00:55:27 +0200 Subject: [PATCH 1/8] Add support for Renegade X, querying master server --- lib/games.js | 7 ++++++ protocols/index.js | 3 ++- protocols/renegadex.js | 52 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 protocols/renegadex.js diff --git a/lib/games.js b/lib/games.js index d45c4be0..f0de56e0 100644 --- a/lib/games.js +++ b/lib/games.js @@ -2400,6 +2400,13 @@ export const games = { protocol: 'gamespy1' } }, + renegadex: { + name: 'Renegade X', + release_year: 2014, + options: { + protocol: 'renegadex' + } + }, rdr2r: { name: 'Red Dead Redemption 2 - RedM', release_year: 2018, diff --git a/protocols/index.js b/protocols/index.js index d1ccd4b7..d0b88158 100644 --- a/protocols/index.js +++ b/protocols/index.js @@ -37,6 +37,7 @@ import palworld from './palworld.js' import quake1 from './quake1.js' import quake2 from './quake2.js' import quake3 from './quake3.js' +import renegadex from './renegadex.js' import rfactor from './rfactor.js' import samp from './samp.js' import satisfactory from './satisfactory.js' @@ -69,7 +70,7 @@ import vintagestory from './vintagestory.js' export { armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, factorio, farmingsimulator, ffow, fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, gtasao, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, - minecraftbedrock, minecraftvanilla, minetest, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, rfactor, ragemp, samp, + minecraftbedrock, minecraftvanilla, minetest, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, renegadex, 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 } diff --git a/protocols/renegadex.js b/protocols/renegadex.js new file mode 100644 index 00000000..1b2814e7 --- /dev/null +++ b/protocols/renegadex.js @@ -0,0 +1,52 @@ +import Core from './core.js' + +/** + * Implements the protocol for Renegade X, an UnrealEngine3 based game, using a custom master server + */ +export default class renegadex extends Core { + constructor () { + super() + this.usedTcp = true + } + + async run (state) { + const servers = await this.request({ + url: 'https://serverlist-rx.totemarts.services/servers.jsp', + responseType: 'json' + }) + + if (!servers) { + throw new Error('Unable to retrieve master server list') + } + + const serverInfo = servers.find( + (server) => + server.IP === this.options.address && server.Port === this.options.port + ) + + if (serverInfo == null) { + throw new Error('Server not found in master server list') + } + + let emptyPrefix = '' + if (serverInfo.NamePrefix) emptyPrefix = serverInfo.NamePrefix + ' ' + const servername = `${emptyPrefix}${serverInfo.Name}` + const numplayers = serverInfo.Players || 0 + const numbots = serverInfo.Bots || 0 + const variables = serverInfo.Variables || {} + + state.name = servername + state.map = serverInfo['Current Map'] + state.password = Math.abs(!!variables.bPassworded) + + state.numplayers = numplayers + state.maxplayers = variables['Player Limit'] || 0 + + // due to master server not providing bot/player list, and standard result has no bot count, add list with dummy values + state.players = Array.from(new Array(numplayers).keys(), (i) => ({ name: `Player #${i + 1}`, raw: {} })) + state.bots = Array.from(new Array(numbots).keys(), (i) => ({ name: `Bot #${i + 1}`, raw: {} })) + + state.raw = variables + state.version = serverInfo['Game Version'] + } +} From 200ce1607ad7ebfe54e0705673bb6a54d9f67b59 Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Sat, 21 Sep 2024 18:46:06 +0200 Subject: [PATCH 2/8] Fix game id being compliant to naming rule --- lib/games.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/games.js b/lib/games.js index f0de56e0..3b52ca1f 100644 --- a/lib/games.js +++ b/lib/games.js @@ -2400,7 +2400,7 @@ export const games = { protocol: 'gamespy1' } }, - renegadex: { + renegade10: { name: 'Renegade X', release_year: 2014, options: { From 4cd87fb070bff71a003ba9c11427da1d23e016cc Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Fri, 20 Sep 2024 01:16:59 +0200 Subject: [PATCH 3/8] docs: update CHANGELOG and GAMES_LIST for Renegade X --- CHANGELOG.md | 1 + GAMES_LIST.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c84ab697..00f9e644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Feat: Satisfactory - Added support (By @Smidy13 #645) * Feat: Update Soldat protocol (#642) * Feat: TOXIKK (2016) - Added support (#641) +* Feat: Renegade X (2014) - Added support (#643) ## 5.1.3 * Fix: `Deus Ex` using the wrong protocol (#621) diff --git a/GAMES_LIST.md b/GAMES_LIST.md index c9ec1567..70a9f07a 100644 --- a/GAMES_LIST.md +++ b/GAMES_LIST.md @@ -243,6 +243,7 @@ | redline | Redline | | | redorchestra | Red Orchestra | | | redorchestra2 | Red Orchestra 2 | [Valve Protocol](#valve) | +| renegade10 | Renegade X | | | rfactor | rFactor | | | rfactor2 | rFactor 2 | [Valve Protocol](#valve) | | ricochet | Ricochet | [Valve Protocol](#valve) | From cfa743b6696fe155d46e15c43e8de13dcda3bdb9 Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Sat, 21 Sep 2024 17:12:22 +0200 Subject: [PATCH 4/8] Pass state.password as raw + move to separate method for subclassing --- protocols/renegadex.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/protocols/renegadex.js b/protocols/renegadex.js index 1b2814e7..b2c5007e 100644 --- a/protocols/renegadex.js +++ b/protocols/renegadex.js @@ -28,6 +28,15 @@ export default class renegadex extends Core { throw new Error('Server not found in master server list') } + // set state properties based on received server info + this.populateProperties(state, serverInfo) + } + + /** + * Translates raw properties into known properties + * @param {Object} state Parsed data + */ + populateProperties (state, serverInfo) { let emptyPrefix = '' if (serverInfo.NamePrefix) emptyPrefix = serverInfo.NamePrefix + ' ' const servername = `${emptyPrefix}${serverInfo.Name}` @@ -37,7 +46,7 @@ export default class renegadex extends Core { state.name = servername state.map = serverInfo['Current Map'] - state.password = Math.abs(!!variables.bPassworded) + state.password = variables.bPassworded state.numplayers = numplayers state.maxplayers = variables['Player Limit'] || 0 From 94782044eb68723cdf8f52b9c10e1ccbc02e5173 Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Sat, 21 Sep 2024 18:44:14 +0200 Subject: [PATCH 5/8] Define json response via schema, optional data validation with Ajv (commented out) --- protocols/renegadex.js | 160 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/protocols/renegadex.js b/protocols/renegadex.js index b2c5007e..4c71a048 100644 --- a/protocols/renegadex.js +++ b/protocols/renegadex.js @@ -1,4 +1,158 @@ import Core from './core.js' +// import Ajv from 'ajv' +// const ajv = new Ajv() + +export const MasterServerServerInfoSchema = { + type: 'object', + required: [ + 'IP', + 'Port', + 'Name', + 'Current Map', + 'Bots', + 'Players', + 'Game Version', + 'Variables' + ], + properties: { + IP: { + type: 'string', + format: 'ipv4', + description: 'IP of the server' + }, + Port: { + type: 'integer', + minimum: 0, + maximum: 65535, + description: 'The port of the server instance to connect to for joining' + }, + Name: { + type: 'string', + description: 'Name of the server, i.e.: Bob\'s Server.' + }, + NamePrefix: { + type: 'string', + description: 'A prefix of the server' + }, + 'Current Map': { + type: 'string', + description: 'The current map\'s name the server is running is running' + }, + Players: { + type: 'integer', + description: 'The number of players connected to the server', + minimum: 0 + }, + Bots: { + type: 'integer', + minimum: 0, + description: 'The number of bots' + }, + 'Game Version': { + type: 'string', + pattern: '^Open Beta (.*?)?$', + description: 'Version of the build of the server' + }, + Variables: { + type: 'object', + properties: { + 'Player Limit': { + type: 'integer', + minimum: 0, + description: 'Maximum number of players allowed by this server' + }, + 'Time Limit': { + type: 'integer', + minimum: 0, + description: 'time limit in minutes' + }, + 'Team Mode': { + type: 'integer', + description: 'Determines how teams are organized between matches.', + enum: [ + 0, // static, + 1, // swap + 2, // random swap + 3, // shuffle + 4, // traditional (assign as players connect) + 5, // traditional + free swap + 6 // ladder rank + ] + }, + 'Game Type': { + type: 'integer', + description: 'Type of the game the server is running', + enum: [ + 0, // Rx_Game_MainMenu + 1, // Rx_Game + 2, // TS_Game + 3 // SP_Game + // < 3 x < 1000 = RenX Unused/Reserved + // < 1000 < x < 2^31 - 1 = Unassigned / Mod space + ] + }, + 'Vehicle Limit': { + type: 'integer', + minimum: 0, + description: 'Maximum number of vehicles allowed by this server' + }, + 'Mine Limit': { + type: 'integer', + minimum: 0, + description: 'Maximum number of mines allowed by this server' + }, + bPassworded: { + type: 'boolean', + description: 'Whether a password is required to enter the game' + }, + bSteamRequired: { + type: 'boolean', + description: 'Whether clients required to be logged into Steam to play on this server' + }, + bRanked: { + type: 'boolean', + description: 'Whether the serer is ranked/official' + }, + bAllowPrivateMessaging: { + type: 'boolean', + description: 'Whether the server allows non-admin clients to PM each other' + }, + bPrivateMessageTeamOnly: { + type: 'boolean', + description: 'whether private messaging is restricted to just teammates' + }, + bAutoBalanceTeams: { // alias of 'bSpawnCrates' + type: 'boolean', + description: 'Whether the server will spawn crates in this game for balancing' + }, + bSpawnCrates: { + type: 'boolean', + description: 'Whether the server will spawn crates in this game for balancing' + }, + CrateRespawnAfterPickup: { + type: 'integer', + minimum: 0, + description: 'interval for crate respawn (after pickup)' + } + }, + required: [ + 'Player Limit', + 'Time Limit', + 'Team Mode', + 'Game Type', + 'Vehicle Limit', + 'Mine Limit' + ] + } + } +} +export const MasterServerResponseSchema = { + type: 'array', + items: { $ref: '#/$defs/server' }, + $defs: { + server: MasterServerServerInfoSchema + } +} /** * Implements the protocol for Renegade X, an UnrealEngine3 based game, using a custom master server @@ -19,6 +173,12 @@ export default class renegadex extends Core { throw new Error('Unable to retrieve master server list') } + // TODO: Ajv response validation + // const isDataValid = ajv.validate(MasterServerResponseSchema, servers) + // if (!isDataValid) { + // throw new Error(`Received master server data is unknown/invalid: ${ajv.errorsText(ajv.errors)}`) + // } + const serverInfo = servers.find( (server) => server.IP === this.options.address && server.Port === this.options.port From 83f00990d6bdd4bc598243a8cb8c0788cd6012f0 Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Wed, 25 Sep 2024 21:19:43 +0200 Subject: [PATCH 6/8] Remove virtualized player/bot list in results --- protocols/renegadex.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/protocols/renegadex.js b/protocols/renegadex.js index 4c71a048..33997e77 100644 --- a/protocols/renegadex.js +++ b/protocols/renegadex.js @@ -201,7 +201,6 @@ export default class renegadex extends Core { if (serverInfo.NamePrefix) emptyPrefix = serverInfo.NamePrefix + ' ' const servername = `${emptyPrefix}${serverInfo.Name}` const numplayers = serverInfo.Players || 0 - const numbots = serverInfo.Bots || 0 const variables = serverInfo.Variables || {} state.name = servername @@ -211,10 +210,6 @@ export default class renegadex extends Core { state.numplayers = numplayers state.maxplayers = variables['Player Limit'] || 0 - // due to master server not providing bot/player list, and standard result has no bot count, add list with dummy values - state.players = Array.from(new Array(numplayers).keys(), (i) => ({ name: `Player #${i + 1}`, raw: {} })) - state.bots = Array.from(new Array(numbots).keys(), (i) => ({ name: `Bot #${i + 1}`, raw: {} })) - state.raw = variables state.version = serverInfo['Game Version'] } From 9939ee6080d6003f00363f3a4c629f8a570fb3b5 Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Wed, 25 Sep 2024 22:18:14 +0200 Subject: [PATCH 7/8] Provide full server response as raw data --- protocols/renegadex.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/protocols/renegadex.js b/protocols/renegadex.js index 33997e77..530c007d 100644 --- a/protocols/renegadex.js +++ b/protocols/renegadex.js @@ -199,18 +199,18 @@ export default class renegadex extends Core { populateProperties (state, serverInfo) { let emptyPrefix = '' if (serverInfo.NamePrefix) emptyPrefix = serverInfo.NamePrefix + ' ' - const servername = `${emptyPrefix}${serverInfo.Name}` + const servername = `${emptyPrefix}${serverInfo.Name || ''}` const numplayers = serverInfo.Players || 0 const variables = serverInfo.Variables || {} state.name = servername - state.map = serverInfo['Current Map'] - state.password = variables.bPassworded + state.map = serverInfo['Current Map'] || '' + state.password = !!variables.bPassworded state.numplayers = numplayers state.maxplayers = variables['Player Limit'] || 0 - state.raw = variables - state.version = serverInfo['Game Version'] + state.raw = serverInfo + state.version = serverInfo['Game Version'] || '' } } From 14e8f3e406d4839e9566f9759304aced62e7d4be Mon Sep 17 00:00:00 2001 From: RattleSN4K3 Date: Sat, 28 Sep 2024 18:56:13 +0200 Subject: [PATCH 8/8] Add support for Renegade X master query through separate protocol Protocol 'renegadexmaster' provides full list of processed server info --- protocols/index.js | 3 ++- protocols/renegadex.js | 40 +++++++++++++++++++++++++----------- protocols/renegadexmaster.js | 21 +++++++++++++++++++ tools/attempt_protocols.js | 2 +- 4 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 protocols/renegadexmaster.js diff --git a/protocols/index.js b/protocols/index.js index d0b88158..4aac12a1 100644 --- a/protocols/index.js +++ b/protocols/index.js @@ -38,6 +38,7 @@ import quake1 from './quake1.js' import quake2 from './quake2.js' import quake3 from './quake3.js' import renegadex from './renegadex.js' +import renegadexmaster from './renegadexmaster.js' import rfactor from './rfactor.js' import samp from './samp.js' import satisfactory from './satisfactory.js' @@ -70,7 +71,7 @@ import vintagestory from './vintagestory.js' export { armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, factorio, farmingsimulator, ffow, fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, gtasao, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, - minecraftbedrock, minecraftvanilla, minetest, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, renegadex, rfactor, ragemp, samp, + minecraftbedrock, minecraftvanilla, minetest, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, renegadex, renegadexmaster, 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 } diff --git a/protocols/renegadex.js b/protocols/renegadex.js index 530c007d..a0f3e81d 100644 --- a/protocols/renegadex.js +++ b/protocols/renegadex.js @@ -164,14 +164,40 @@ export default class renegadex extends Core { } async run (state) { + // query master list and find specific server + const servers = await this.getMasterServerList() + const serverInfo = servers.find((server) => { + return server.IP === this.options.address && server.Port === this.options.port + }) + + if (serverInfo == null) { + throw new Error('Server not found in master server list') + } + + // set state properties based on received server info + this.populateProperties(state, serverInfo) + } + + /** + * Retrieves server list from master server + * @throws {Error} Will throw error when no master list was received + * @returns a list of servers as raw data + */ + async getMasterServerList () { const servers = await this.request({ url: 'https://serverlist-rx.totemarts.services/servers.jsp', responseType: 'json' }) - if (!servers) { + if (servers == null) { throw new Error('Unable to retrieve master server list') } + if (!Array.isArray(servers)) { + throw new Error('Invalid data received from master server. Expecting list of data') + } + if (servers.length === 0) { + throw new Error('No data received from master server.') + } // TODO: Ajv response validation // const isDataValid = ajv.validate(MasterServerResponseSchema, servers) @@ -179,17 +205,7 @@ export default class renegadex extends Core { // throw new Error(`Received master server data is unknown/invalid: ${ajv.errorsText(ajv.errors)}`) // } - const serverInfo = servers.find( - (server) => - server.IP === this.options.address && server.Port === this.options.port - ) - - if (serverInfo == null) { - throw new Error('Server not found in master server list') - } - - // set state properties based on received server info - this.populateProperties(state, serverInfo) + return servers } /** diff --git a/protocols/renegadexmaster.js b/protocols/renegadexmaster.js new file mode 100644 index 00000000..2499acf1 --- /dev/null +++ b/protocols/renegadexmaster.js @@ -0,0 +1,21 @@ +import renegadex from './renegadex.js' + +/** + * Implements the protocol for retrieving a master list for Renegade X, an UnrealEngine3 based game + */ +export default class renegadexmaster extends renegadex { + async run (state) { + const servers = await this.getMasterServerList() + + // pass processed servers as raw list + state.raw.servers = servers.map((serverInfo) => { + // TODO: may use any other deep-copy method like structuredClone() (in Node.js 17+) + // or use a method of Core to retrieve a clean state + const serverState = JSON.parse(JSON.stringify(state)) + + // set state properties based on received server info + this.populateProperties(serverState, serverInfo) + return serverState + }) + } +} diff --git a/tools/attempt_protocols.js b/tools/attempt_protocols.js index 956a6809..0dec095f 100644 --- a/tools/attempt_protocols.js +++ b/tools/attempt_protocols.js @@ -20,7 +20,7 @@ const gamedig = new GameDig(options) const protocolList = [] Object.keys(protocols).forEach((key) => protocolList.push(key)) -const ignoredProtocols = ['discord', 'beammpmaster', 'beammp', 'teamspeak2', 'teamspeak3', 'vintagestorymaster'] +const ignoredProtocols = ['discord', 'beammpmaster', 'beammp', 'teamspeak2', 'teamspeak3', 'vintagestorymaster', 'renegadexmaster'] const protocolListFiltered = protocolList.filter((protocol) => !ignoredProtocols.includes(protocol)) const run = async () => {