mirror of
https://github.com/tribufu/node-gamedig
synced 2026-05-06 07:07:33 +00:00
feat: add Hawakening support (#648)
* Add support for Hawakening, querying master server * Outsource backend calls into Api class * Define json response via schema, optional data validation with Ajv (commented out) * Add support for Hawakening master query through separate protocol Protocol 'hawakeningmaster' provides full list of processed server info * docs: update CHANGELOG and GAMES_LIST for Hawakening * Additional API check + cleanup * Allowing public/non-authorized master server query for Hawkening severs * Fix: Reference the master protocol correctly in docs/games_list * Reorganized code file, moved schema and API-class to the end
This commit is contained in:
parent
fe9d4f2cfc
commit
d2397b67e7
7 changed files with 765 additions and 2 deletions
|
|
@ -7,6 +7,7 @@
|
|||
* Feat: Update Soldat protocol (#642)
|
||||
* Feat: TOXIKK (2016) - Added support (#641)
|
||||
* Feat: Renegade X (2014) - Added support (#643)
|
||||
* Feat: Hawakening (2024) - Added support (#648)
|
||||
|
||||
## 5.1.3
|
||||
* Fix: `Deus Ex` using the wrong protocol (#621)
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@
|
|||
| gus | Gore: Ultimate Soldier | |
|
||||
| halo | Halo | |
|
||||
| halo2 | Halo 2 | |
|
||||
| hawakening | Hawakening | [Notes](#hawakening) |
|
||||
| heretic2 | Heretic II | |
|
||||
| hexen2 | Hexen II | |
|
||||
| hiddendangerous2 | Hidden & Dangerous 2 | |
|
||||
|
|
@ -480,6 +481,22 @@ If you know your use case, it's better to use a single protocol or make your own
|
|||
Farming Simulator servers need a token (reffered as code in the game). It can be obtained at your server's web interface (http://ip:port/settings.html). It can be passed to GameDig with the additional option: `token`. It does only work for your own server.
|
||||
The response includes much information about the server. Currently, only the fields about server information (name, map, version, etc.), players and mods are parsed.
|
||||
|
||||
|
||||
### <a name='hawakening'></a>Hawakening
|
||||
Querying server info for Hawakening requires a _ServerId_ to be passed to GameDig instead of an IP address. You can acquire such a _ServerId_ from the master query protocol _hawakeningmaster_ [type: `protocol-hawakeningmaster`] (see `raw.servers[]raw.listing.Guid`).
|
||||
|
||||
Additionally, the master server requires authorization. A **user profile is required** for querying the API. Such a profile can be created on the [_official page_](https://hawakening.com/enlist).
|
||||
|
||||
- Provide a Server Id via `serverId`
|
||||
- Provide a client profile with `username` (email address, not callsign)
|
||||
|
||||
And one of the following options for gaining access:
|
||||
- Provide a client access token via the option `token`
|
||||
- Provide the user profile password via the option `password`
|
||||
|
||||
> **_NOTE:_** The protocol `hawakening` will query additional server info by requesting a matchmaking _token_, which will fail for full servers. Due to this, the IP address and port cannot be queried for such servers.
|
||||
|
||||
|
||||
Protocols with Additional Notes
|
||||
---
|
||||
|
||||
|
|
|
|||
12
lib/games.js
12
lib/games.js
|
|
@ -1457,6 +1457,18 @@ export const games = {
|
|||
protocol: 'gamespy2'
|
||||
}
|
||||
},
|
||||
hawakening: {
|
||||
name: 'Hawakening',
|
||||
release_year: 2024,
|
||||
options: {
|
||||
port: 7777,
|
||||
port_query: 27015,
|
||||
protocol: 'hawakening'
|
||||
},
|
||||
extra: {
|
||||
doc_notes: 'hawakening'
|
||||
}
|
||||
},
|
||||
heretic2: {
|
||||
name: 'Heretic II',
|
||||
release_year: 1998,
|
||||
|
|
|
|||
13
protocols/hawakening.js
Normal file
13
protocols/hawakening.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import hawakeningmaster from './hawakeningmaster.js'
|
||||
|
||||
/**
|
||||
* Implements the protocol for Hawakening, a fan project of the UnrealEngine3 based game HAWKEN
|
||||
* using a Meteor backend for the master server
|
||||
*/
|
||||
export default class hawakening extends hawakeningmaster {
|
||||
constructor () {
|
||||
super()
|
||||
this.doQuerySingle = true
|
||||
this.requireToken = true
|
||||
}
|
||||
}
|
||||
718
protocols/hawakeningmaster.js
Normal file
718
protocols/hawakeningmaster.js
Normal file
|
|
@ -0,0 +1,718 @@
|
|||
import Core from './core.js'
|
||||
// import Ajv from 'ajv'
|
||||
// const ajv = new Ajv()
|
||||
|
||||
/**
|
||||
* Implements the protocol for retrieving a master list for Hawakening, a fan project of the UnrealEngine3 based game HAWKEN
|
||||
* using a Meteor backend for the master server
|
||||
*/
|
||||
export default class hawakeningmaster extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
// backend API url for original Hawken release
|
||||
// const meteorUri = 'https://v2-services-live-pc.playhawken.com'
|
||||
// Hawakening API for public release in 2024
|
||||
const meteorUri = 'https://hawakening.com/api'
|
||||
|
||||
this.backendApi = new MeteorBackendApi(this, meteorUri)
|
||||
this.backendApi.setLogger(this.logger)
|
||||
|
||||
// set when querying needs access token
|
||||
this.requireToken = false
|
||||
// set when querying for specific server only
|
||||
this.doQuerySingle = false
|
||||
// set to logout on cleanup (to revoke access token)
|
||||
this.doLogout = true
|
||||
|
||||
// stored user, queried from backend
|
||||
this.userInfo = null
|
||||
|
||||
// Don't use the tcp ping probing
|
||||
this.usedTcp = true
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await this.retrieveClientAccessToken()
|
||||
await this.retrieveUser()
|
||||
|
||||
await this.queryInfo(state)
|
||||
await this.cleanup(state)
|
||||
}
|
||||
|
||||
async queryInfo (state) {
|
||||
if (this.doQuerySingle) {
|
||||
await this.queryInfoSingle(state)
|
||||
} else {
|
||||
await this.queryInfoMultiple(state)
|
||||
}
|
||||
}
|
||||
|
||||
async queryInfoMultiple (state) {
|
||||
const servers = await this.getMasterServerList()
|
||||
|
||||
// pass processed servers as raw list
|
||||
state.raw.servers = servers.map((serverListing) => {
|
||||
// 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, { serverListing })
|
||||
return serverState
|
||||
})
|
||||
}
|
||||
|
||||
async queryInfoSingle (state) {
|
||||
const servers = await this.getMasterServerList()
|
||||
const serverListing = servers.find((server) => {
|
||||
return server.Guid === this.options.serverId
|
||||
})
|
||||
|
||||
this.logger.debug('Server Listing:', serverListing)
|
||||
if (serverListing == null) {
|
||||
throw new Error('Server not found in master server listing')
|
||||
}
|
||||
|
||||
const serverInfo = await this.getServerInfo(serverListing)
|
||||
this.logger.debug('Server Info:', serverInfo)
|
||||
if (!serverInfo) {
|
||||
throw new Error('Invalid server info received')
|
||||
}
|
||||
|
||||
// set state properties based on received server info
|
||||
this.populateProperties(state, { serverListing, serverInfo })
|
||||
}
|
||||
|
||||
async cleanup (state) {
|
||||
await this.sendExitMessage()
|
||||
await this.sendLogout()
|
||||
|
||||
this.backendApi.cleanup()
|
||||
this.userInfo = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates raw properties into known properties
|
||||
* @param {Object} state Parsed data
|
||||
* @param {Object} data Queried data
|
||||
*/
|
||||
populateProperties (state, data) {
|
||||
const { serverListing: listing, serverInfo: info } = data
|
||||
|
||||
if (info) {
|
||||
state.gameHost = info.AssignedServerIp || null
|
||||
state.gamePort = info.AssignedServerPort || null
|
||||
}
|
||||
|
||||
state.name = listing.ServerName || ''
|
||||
state.map = listing.Map || ''
|
||||
state.password = !!listing.DeveloperData?.PasswordHash
|
||||
|
||||
state.numplayers = listing.Users?.length || 0
|
||||
state.maxplayers = listing.MaxUsers || 0
|
||||
state.version = listing.GameVersion || ''
|
||||
|
||||
// provide raw server info
|
||||
Object.assign(state.raw, { listing, info })
|
||||
}
|
||||
|
||||
async retrieveClientAccessToken () {
|
||||
if (this.options.token) {
|
||||
this.doLogout = false
|
||||
this.backendApi.accessToken = this.options.token
|
||||
await this.checkAccess()
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.options.username && !this.requireToken) {
|
||||
this.logger.debug('retrieveClientAccessToken: No username provided but no token required for current protocol.')
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug(`Retrieving user access token for ${this.options.username}...`)
|
||||
const response = await this.backendApi.getClientAccessToken(this.options.username, this.options.password)
|
||||
|
||||
const tag = 'access token'
|
||||
MeteorBackendApi.AssertResponse(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { match: ['Access Grant Not Issued: Unrecognized options for login request'], errorMessage: 'No user name or password' })
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { match: ['Access Grant Not Issued: User not found'], errorMessage: 'Invalid user name' })
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { match: ['Access Grant Not Issued: Incorrect password'], errorMessage: 'Incorrect password' })
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag, { printStatus: true })
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['User Logged In'] })
|
||||
MeteorBackendApi.AssertResponseData(response, tag)
|
||||
|
||||
this.backendApi.accessToken = response.Result
|
||||
await this.checkAccess()
|
||||
}
|
||||
|
||||
async retrieveUser () {
|
||||
if (!this.options.username && !this.requireToken) {
|
||||
this.logger.debug('retrieveUser: No username provided but no token required for current protocol.')
|
||||
return
|
||||
}
|
||||
|
||||
this.userInfo = await this.getUserInfo()
|
||||
}
|
||||
|
||||
async checkAccess () {
|
||||
this.logger.debug('Checking access ...')
|
||||
const responseServices = await this.backendApi.getStatusServices()
|
||||
MeteorBackendApi.AssertResponseStatus(responseServices, 'service status')
|
||||
MeteorBackendApi.AssertResponseMessage(responseServices, 'service status', { expected: ['Status found'] })
|
||||
|
||||
const responseTest = await this.backendApi.getBundles()
|
||||
MeteorBackendApi.AssertResponseStatus(responseTest, 'bundles')
|
||||
MeteorBackendApi.AssertResponseMessage(responseTest, 'bundles', { expected: ['Bundles Filter successful'] })
|
||||
}
|
||||
|
||||
async getUserInfo () {
|
||||
this.logger.debug(`Requesting user info for ${this.options.username} ...`)
|
||||
const response = await this.backendApi.getUserInfo(this.options.username)
|
||||
|
||||
const tag = 'user info'
|
||||
MeteorBackendApi.AssertResponse(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { match: ['User not found'], errorMessage: 'Invalid or no user name' })
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag, { printStatus: true })
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['Userfound'] })
|
||||
MeteorBackendApi.AssertResponseData(response, tag)
|
||||
return response.Result
|
||||
}
|
||||
|
||||
async getMasterServerList () {
|
||||
this.logger.debug('Requesting game servers ...')
|
||||
const response = await this.backendApi.getMasterServerList()
|
||||
|
||||
const tag = 'server list'
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['Listings found'] })
|
||||
MeteorBackendApi.AssertResponseData(response, tag)
|
||||
|
||||
const servers = response.Result
|
||||
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
|
||||
}
|
||||
|
||||
async getServerInfo (serverListing) {
|
||||
// match info is received by requesting a matchmaking "token"
|
||||
// if the server is at capacity, the response won't provide valid data (500 error)
|
||||
// return an empty server info when server is already full
|
||||
if (serverListing.MaxUsers == serverListing.Users?.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const serverToken = await this.getServerToken(serverListing)
|
||||
const matchInfo = await this.getMatchInfo(serverToken)
|
||||
return matchInfo
|
||||
}
|
||||
|
||||
async getServerToken (serverListing) {
|
||||
this.logger.debug(`Requesting server token ${serverListing.Guid} ...`)
|
||||
const response = await this.backendApi.getServerToken(serverListing, this.userInfo)
|
||||
|
||||
const tag = 'server token'
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['Succesfully created the advertisement'] })
|
||||
MeteorBackendApi.AssertResponseData(response, tag)
|
||||
return response.Result
|
||||
}
|
||||
|
||||
async getMatchInfo (serverToken) {
|
||||
this.logger.debug(`Requesting match info ${serverToken} ...`)
|
||||
const response = await this.backendApi.getMatchInfo(serverToken)
|
||||
|
||||
const tag = 'match info'
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['Successfully loaded ClientMatchmakingAdvertisement.'] })
|
||||
MeteorBackendApi.AssertResponseData(response, tag)
|
||||
return response.Result
|
||||
}
|
||||
|
||||
async sendExitMessage () {
|
||||
// in case of non-authorized query, early out and skip sending logout message
|
||||
if (!this.backendApi.accessToken || !this.userInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug('Sending exit notify message ...')
|
||||
const response = await this.backendApi.notifyExit(this.userInfo)
|
||||
|
||||
const tag = 'exit message'
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['Event emission successful'] })
|
||||
}
|
||||
|
||||
async sendLogout () {
|
||||
// in case of no logged user or non-authorized query, early out and skip sending logout message
|
||||
if (!this.doLogout || !this.backendApi.accessToken || !this.userInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.debug(`Sending logout message for ${this.userInfo?.EmailAddress || this.userInfo.Guid}...`)
|
||||
const response = await this.backendApi.logout(this.userInfo)
|
||||
|
||||
const tag = 'logout message'
|
||||
MeteorBackendApi.AssertResponseStatus(response, tag)
|
||||
MeteorBackendApi.AssertResponseMessage(response, tag, { expected: ['AccessGrant Revoked'] })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply merges two objects, combining their properties recursively.
|
||||
*
|
||||
* If both objects have a property with the same key and that property is an object,
|
||||
* the properties of the second object will be merged into the first object's property.
|
||||
* If the property is not an object or if it does not exist in the first object,
|
||||
* the property from the second object will overwrite the property in the first object.
|
||||
*
|
||||
* @param {Object} obj1 - The first object to merge.
|
||||
* @param {Object} obj2 - The second object to merge.
|
||||
* @returns {Object} A new object containing the merged properties of both input objects.
|
||||
*/
|
||||
function deepMerge (obj1, obj2) {
|
||||
const result = { ...obj1 }
|
||||
|
||||
for (const key in obj2) {
|
||||
if (Object.hasOwn(obj2, key)) {
|
||||
if (obj2[key] instanceof Object && obj1[key] instanceof Object) {
|
||||
result[key] = deepMerge(obj1[key], obj2[key])
|
||||
} else {
|
||||
result[key] = obj2[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function isObject (item) {
|
||||
return (typeof item === 'object' && !Array.isArray(item) && item !== null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a client for the Meteor Backend API.
|
||||
*
|
||||
* This class provides methods for interacting with the Meteor Backend API, including
|
||||
* authentication, retrieving user information, and handling server-related operations.
|
||||
*/
|
||||
export class MeteorBackendApi {
|
||||
#accessToken = null
|
||||
#protocol = null
|
||||
#apiUri = null
|
||||
|
||||
/**
|
||||
* Creates an instance of the MeteorBackendApi.
|
||||
*
|
||||
* @param {Object} protocol - The protocol object to handle requests.
|
||||
* @param {string} apiUri - The base URI for the API.
|
||||
*/
|
||||
constructor (protocol, apiUri) {
|
||||
this.#protocol = protocol
|
||||
this.#apiUri = apiUri
|
||||
this.logger = null
|
||||
}
|
||||
|
||||
/**
|
||||
* The base URI of the API.
|
||||
*
|
||||
* @returns {string} The API URI.
|
||||
*/
|
||||
get apiUri () {
|
||||
return this.#apiUri
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current access token
|
||||
* @param {string} value the access token
|
||||
*/
|
||||
set accessToken (value) {
|
||||
this.#accessToken = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current access token
|
||||
*/
|
||||
get accessToken () {
|
||||
return this.#accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the logger for the instance.
|
||||
*
|
||||
* @param {Object} logger - The logger instance to use for logging.
|
||||
*/
|
||||
setLogger (logger) {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an API call to the specified endpoint with the given request parameters.
|
||||
*
|
||||
* @param {string} endpoint - The API endpoint to call.
|
||||
* @param {Object} requestParams - The parameters for the API request.
|
||||
* @param {Object} callParams - Additional parameters for the call.
|
||||
* @param {boolean} [callParams.requireAuth=false] - Whether the call requires authentication.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
makeCall (endpoint, requestParams = null, callParams = null) {
|
||||
const { requireAuth = false } = callParams ?? {}
|
||||
|
||||
const url = `${this.#apiUri}/${endpoint}`
|
||||
const headers = {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
...(requireAuth ? { Authorization: `Basic ${this.accessToken}` } : {})
|
||||
}
|
||||
|
||||
const defaultParams = {
|
||||
url,
|
||||
headers,
|
||||
method: 'GET',
|
||||
responseType: 'json'
|
||||
}
|
||||
const requestCollection = deepMerge(defaultParams, requestParams)
|
||||
|
||||
this.logger?.debug(`${requestCollection.method || 'GET'}: ${url}`)
|
||||
const response = this.#protocol.request(requestCollection)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the instance
|
||||
*/
|
||||
cleanup () {
|
||||
this.accessToken = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of the service.
|
||||
*
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getStatusServices () {
|
||||
const response = this.makeCall('status/services')
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Bundles.
|
||||
*
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getBundles () {
|
||||
const response = this.makeCall('bundles', {}, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an access token for a client using the provided username and password.
|
||||
*
|
||||
* @param {string} userName - The username of the client.
|
||||
* @param {string} password - The password of the client.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getClientAccessToken (userName, password) {
|
||||
const endpoint = `users/${encodeURIComponent(userName)}/accessGrant`
|
||||
const body = { Password: password }
|
||||
const response = this.makeCall(endpoint, { json: body, method: 'POST' })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves user information based on the username.
|
||||
*
|
||||
* @param {string} userName - The username of the user.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getUserInfo (userName) {
|
||||
const endpoint = `users/${encodeURIComponent(userName)}`
|
||||
const response = this.makeCall(endpoint, {}, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of master servers.
|
||||
*
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getMasterServerList () {
|
||||
const response = this.makeCall('gameServerListings', {}, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a server token based on the server listing and user information.
|
||||
*
|
||||
* @param {Object} serverListing - The server listing object containing server details.
|
||||
* @param {Object} userInfo - The user information object.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getServerToken (serverListing, userInfo) {
|
||||
const body = {
|
||||
GameVersion: serverListing.GameVersion,
|
||||
OwnerGuid: userInfo.Guid,
|
||||
Region: serverListing.Region,
|
||||
RequestedServerGuid: serverListing.Guid,
|
||||
Users: [userInfo.Guid]
|
||||
}
|
||||
const response = this.makeCall('hawkenClientMatchmakingAdvertisements', { json: body, method: 'POST' }, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves match information based on the server token.
|
||||
*
|
||||
* @param {string} serverToken - The token of the server.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
getMatchInfo (serverToken) {
|
||||
const endpoint = `hawkenClientMatchmakingAdvertisements/${serverToken}`
|
||||
const response = this.makeCall(endpoint, {}, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the system that a user has exited.
|
||||
*
|
||||
* @param {Object} userInfo - The user information object.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
notifyExit (userInfo) {
|
||||
const body = [{
|
||||
Data: {
|
||||
TimeCreated: (new Date().getTime() / 1000)
|
||||
},
|
||||
Producer: {
|
||||
Id: '\\Hawken-CL142579\\Binaries\\Win32\\HawkenGame-Win32-Shipping.exe',
|
||||
Type: 'HawkenGameClient'
|
||||
},
|
||||
Subject: {
|
||||
Id: userInfo.Guid,
|
||||
Type: 'Player'
|
||||
},
|
||||
Timestamp: (new Date().toISOString()),
|
||||
Verb: 'ExitClient'
|
||||
}]
|
||||
const response = this.makeCall('gameClientEvent', { json: body, method: 'POST' }, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out a user based on their information.
|
||||
*
|
||||
* @param {Object} userInfo - The user information object.
|
||||
* @returns {Promise<Object>} A promise that resolves to the response object from the API call.
|
||||
*/
|
||||
logout (userInfo) {
|
||||
const endpoint = `users/${userInfo.Guid}/accessGrant`
|
||||
const body = { AccessGrant: this.accessToken }
|
||||
const response = this.makeCall(endpoint, { json: body, method: 'PUT' }, { requireAuth: true })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the response is valid.
|
||||
*
|
||||
* @static
|
||||
* @param {Object} response - The response object to validate.
|
||||
* @param {string} tag - A tag for the error message.
|
||||
* @param {Object} [params={}] - Additional parameters.
|
||||
* @param {boolean} [params.printStatus=false] - Whether to include the status in the error message.
|
||||
* @throws {Error} If the response is invalid.
|
||||
*/
|
||||
static AssertResponse (response, tag, params = {}) {
|
||||
const { printStatus = false } = (params || {})
|
||||
if (!response) {
|
||||
const statusMessage = printStatus ? `Response Status: ${response.Status}` : ''
|
||||
throw new Error(`Error retrieving ${tag || 'data'} with no valid response.${statusMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the response status is valid.
|
||||
*
|
||||
* @static
|
||||
* @param {Object} response - The response object to validate.
|
||||
* @param {string} tag - A tag for the error message.
|
||||
* @param {Object} [params={}] - Additional parameters.
|
||||
* @param {boolean} [params.checkStatus=true] - Whether to check the status code.
|
||||
* @param {boolean} [params.printStatus=false] - Whether to include the status in the error message.
|
||||
* @throws {Error} If the response status is invalid.
|
||||
*/
|
||||
static AssertResponseStatus (response, tag, params = {}) {
|
||||
const { checkStatus = true, printStatus = false } = (params || {})
|
||||
if (!response || !checkStatus || response.Status !== 200) {
|
||||
const statusMessage = printStatus ? `Response Status: ${response.Status}` : ''
|
||||
throw new Error(`Error retrieving ${tag || 'data'} with no valid response.${statusMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the response message is valid.
|
||||
*
|
||||
* @static
|
||||
* @param {Object} response - The response object to validate.
|
||||
* @param {string} tag - A tag for the error message.
|
||||
* @param {Object} [params={}] - Additional parameters.
|
||||
* @param {Array<string>} [params.expected=[]] - Expected messages.
|
||||
* @param {Array<string>} [params.match=[]] - Matching messages.
|
||||
* @param {boolean} [params.printCurrent=true] - Whether to include the current message in the error message.
|
||||
* @throws {Error} If the response message is invalid.
|
||||
*/
|
||||
static AssertResponseMessage (response, tag, params = {}) {
|
||||
const { expected = [], match = [], errorMessage, printCurrent = true } = (params || {})
|
||||
const responseMessage = response?.Message?.toLowerCase()
|
||||
|
||||
if (expected?.length && !expected.some(x => responseMessage === `${x}`.toLowerCase())) {
|
||||
const currentMessage = printCurrent ? ` Response message: ${response.Message}` : ''
|
||||
throw new Error(`Invalid ${tag || 'data'} message received.${currentMessage}`)
|
||||
}
|
||||
|
||||
if (match?.some(x => responseMessage === `${x}`.toLowerCase())) {
|
||||
throw new Error(errorMessage || `Invalid ${tag || 'data'} message received.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the response contains valid data.
|
||||
*
|
||||
* @static
|
||||
* @param {Object} response - The response object to validate.
|
||||
* @param {string} tag - A tag for the error message.
|
||||
* @param {string} [key='Result'] - The key to check in the response.
|
||||
* @throws {Error} If the response does not contain valid data.
|
||||
*/
|
||||
static AssertResponseData (response, tag, key = 'Result') {
|
||||
if (response && (!isObject(response) || !response[key])) {
|
||||
throw new Error(`No ${tag || 'data'} received`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const MasterServerServerListingSchema = {
|
||||
type: 'object',
|
||||
required: [
|
||||
'userGuid',
|
||||
'AllowedRoles',
|
||||
'DeveloperData',
|
||||
'Endpoint',
|
||||
'GameType',
|
||||
'GameVersion',
|
||||
'IsMatchmakingVisible',
|
||||
'IsPublicVisible',
|
||||
'LastUpdate',
|
||||
'Map',
|
||||
'MatchCompletionPercent',
|
||||
'MatchId',
|
||||
'MaxUsers',
|
||||
'MinUsers',
|
||||
'Port',
|
||||
'Region',
|
||||
'ServerName',
|
||||
'ServerRanking',
|
||||
'ServerScore',
|
||||
'Status',
|
||||
'Users',
|
||||
'VoiceChannelListing',
|
||||
'Guid'
|
||||
],
|
||||
properties: {
|
||||
userGuid: { type: 'string' },
|
||||
AllowedRoles: {
|
||||
type: 'array',
|
||||
items: {
|
||||
items: {}
|
||||
}
|
||||
},
|
||||
DeveloperData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
AveragePilotLevel: { type: 'string' },
|
||||
MatchState: { type: 'string' },
|
||||
bIgnoreMMR: { type: 'string' },
|
||||
bTournament: { type: 'string' },
|
||||
PasswordHash: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'AveragePilotLevel',
|
||||
'MatchState',
|
||||
'bIgnoreMMR',
|
||||
'bTournament'
|
||||
]
|
||||
},
|
||||
Endpoint: { type: 'null' },
|
||||
GameType: { type: 'string' },
|
||||
GameVersion: { type: 'string' },
|
||||
IsMatchmakingVisible: { type: 'boolean' },
|
||||
IsPublicVisible: { type: 'boolean' },
|
||||
LastUpdate: { type: 'string' },
|
||||
Map: { type: 'string' },
|
||||
MatchCompletionPercent: {
|
||||
type: 'integer',
|
||||
minimum: 0
|
||||
},
|
||||
MatchId: {
|
||||
type: 'string',
|
||||
pattern: '^[A-Fa-f0-9]{32}$'
|
||||
},
|
||||
MaxUsers: {
|
||||
type: 'integer',
|
||||
minimum: 0
|
||||
},
|
||||
MinUsers: {
|
||||
type: 'integer',
|
||||
minimum: 0
|
||||
},
|
||||
Port: {
|
||||
type: 'null'
|
||||
},
|
||||
Region: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'Asia',
|
||||
'Europe',
|
||||
'North-America',
|
||||
'Oceania'
|
||||
]
|
||||
},
|
||||
ServerName: { type: 'string' },
|
||||
ServerRanking: { type: 'integer' },
|
||||
ServerScore: { type: 'string' },
|
||||
Status: { type: 'integer' },
|
||||
Users: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'uuid'
|
||||
}
|
||||
},
|
||||
VoiceChannelListing: { type: 'string' },
|
||||
Guid: {
|
||||
type: 'string',
|
||||
format: 'uuid'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MasterServerResponseSchema = {
|
||||
type: 'array',
|
||||
items: { $ref: '#/$defs/server' },
|
||||
$defs: {
|
||||
server: MasterServerServerListingSchema
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ import gamespy3 from './gamespy3.js'
|
|||
import geneshift from './geneshift.js'
|
||||
import goldsrc from './goldsrc.js'
|
||||
import gtasao from './gtasao.js'
|
||||
import hawakening from './hawakening.js'
|
||||
import hawakeningmaster from './hawakeningmaster.js'
|
||||
import hexen2 from './hexen2.js'
|
||||
import jc2mp from './jc2mp.js'
|
||||
import kspdmp from './kspdmp.js'
|
||||
|
|
@ -70,7 +72,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,
|
||||
fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, gtasao, hawakening, hawakeningmaster, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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', 'renegadexmaster']
|
||||
const ignoredProtocols = ['discord', 'beammpmaster', 'beammp', 'teamspeak2', 'teamspeak3', 'vintagestorymaster', 'renegadexmaster', 'hawakeningmaster']
|
||||
const protocolListFiltered = protocolList.filter((protocol) => !ignoredProtocols.includes(protocol))
|
||||
|
||||
const run = async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue