mirror of
https://github.com/tribufu/node-gamedig
synced 2026-06-01 09:42:41 +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: Update Soldat protocol (#642)
|
||||||
* Feat: TOXIKK (2016) - Added support (#641)
|
* Feat: TOXIKK (2016) - Added support (#641)
|
||||||
* Feat: Renegade X (2014) - Added support (#643)
|
* Feat: Renegade X (2014) - Added support (#643)
|
||||||
|
* Feat: Hawakening (2024) - Added support (#648)
|
||||||
|
|
||||||
## 5.1.3
|
## 5.1.3
|
||||||
* Fix: `Deus Ex` using the wrong protocol (#621)
|
* Fix: `Deus Ex` using the wrong protocol (#621)
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@
|
||||||
| gus | Gore: Ultimate Soldier | |
|
| gus | Gore: Ultimate Soldier | |
|
||||||
| halo | Halo | |
|
| halo | Halo | |
|
||||||
| halo2 | Halo 2 | |
|
| halo2 | Halo 2 | |
|
||||||
|
| hawakening | Hawakening | [Notes](#hawakening) |
|
||||||
| heretic2 | Heretic II | |
|
| heretic2 | Heretic II | |
|
||||||
| hexen2 | Hexen II | |
|
| hexen2 | Hexen II | |
|
||||||
| hiddendangerous2 | Hidden & Dangerous 2 | |
|
| 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.
|
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.
|
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
|
Protocols with Additional Notes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
12
lib/games.js
12
lib/games.js
|
|
@ -1457,6 +1457,18 @@ export const games = {
|
||||||
protocol: 'gamespy2'
|
protocol: 'gamespy2'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hawakening: {
|
||||||
|
name: 'Hawakening',
|
||||||
|
release_year: 2024,
|
||||||
|
options: {
|
||||||
|
port: 7777,
|
||||||
|
port_query: 27015,
|
||||||
|
protocol: 'hawakening'
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
doc_notes: 'hawakening'
|
||||||
|
}
|
||||||
|
},
|
||||||
heretic2: {
|
heretic2: {
|
||||||
name: 'Heretic II',
|
name: 'Heretic II',
|
||||||
release_year: 1998,
|
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 geneshift from './geneshift.js'
|
||||||
import goldsrc from './goldsrc.js'
|
import goldsrc from './goldsrc.js'
|
||||||
import gtasao from './gtasao.js'
|
import gtasao from './gtasao.js'
|
||||||
|
import hawakening from './hawakening.js'
|
||||||
|
import hawakeningmaster from './hawakeningmaster.js'
|
||||||
import hexen2 from './hexen2.js'
|
import hexen2 from './hexen2.js'
|
||||||
import jc2mp from './jc2mp.js'
|
import jc2mp from './jc2mp.js'
|
||||||
import kspdmp from './kspdmp.js'
|
import kspdmp from './kspdmp.js'
|
||||||
|
|
@ -70,7 +72,7 @@ import vintagestory from './vintagestory.js'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, factorio, farmingsimulator, ffow,
|
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,
|
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,
|
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
|
vcmp, ventrilo, warsow, eldewrito, beammpmaster, beammp, dayz, theisleevrima, xonotic, altvmp, vintagestorymaster, vintagestory
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const gamedig = new GameDig(options)
|
||||||
const protocolList = []
|
const protocolList = []
|
||||||
Object.keys(protocols).forEach((key) => protocolList.push(key))
|
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 protocolListFiltered = protocolList.filter((protocol) => !ignoredProtocols.includes(protocol))
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue