Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Renegade X support #643

Merged
merged 8 commits into from
Oct 4, 2024
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)
RattleSN4K3 marked this conversation as resolved.
Show resolved Hide resolved

## 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
Loading