mirror of
https://github.com/tribufu/node-gamedig
synced 2026-05-06 15:17:36 +00:00
* 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
718 lines
23 KiB
JavaScript
718 lines
23 KiB
JavaScript
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
|
|
}
|
|
}
|