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) | diff --git a/lib/games.js b/lib/games.js index d45c4be0..3b52ca1f 100644 --- a/lib/games.js +++ b/lib/games.js @@ -2400,6 +2400,13 @@ export const games = { protocol: 'gamespy1' } }, + renegade10: { + 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..4aac12a1 100644 --- a/protocols/index.js +++ b/protocols/index.js @@ -37,6 +37,8 @@ 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 renegadexmaster from './renegadexmaster.js' import rfactor from './rfactor.js' import samp from './samp.js' import satisfactory from './satisfactory.js' @@ -69,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, 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 new file mode 100644 index 00000000..a0f3e81d --- /dev/null +++ b/protocols/renegadex.js @@ -0,0 +1,232 @@ +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 + */ +export default class renegadex extends Core { + constructor () { + super() + this.usedTcp = true + } + + 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 == 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) + // if (!isDataValid) { + // throw new Error(`Received master server data is unknown/invalid: ${ajv.errorsText(ajv.errors)}`) + // } + + return servers + } + + /** + * 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 || ''}` + const numplayers = serverInfo.Players || 0 + const variables = serverInfo.Variables || {} + + state.name = servername + state.map = serverInfo['Current Map'] || '' + state.password = !!variables.bPassworded + + state.numplayers = numplayers + state.maxplayers = variables['Player Limit'] || 0 + + state.raw = serverInfo + state.version = serverInfo['Game Version'] || '' + } +} 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 () => {