Skip to content

Commit

Permalink
feat: add Renegade X support (#643)
Browse files Browse the repository at this point in the history
  • Loading branch information
RattleSN4K3 authored Oct 4, 2024
1 parent b394716 commit fe9d4f2
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions GAMES_LIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
7 changes: 7 additions & 0 deletions lib/games.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion protocols/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
232 changes: 232 additions & 0 deletions protocols/renegadex.js
Original file line number Diff line number Diff line change
@@ -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'] || ''
}
}
21 changes: 21 additions & 0 deletions protocols/renegadexmaster.js
Original file line number Diff line number Diff line change
@@ -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
})
}
}
2 changes: 1 addition & 1 deletion tools/attempt_protocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down

0 comments on commit fe9d4f2

Please sign in to comment.