From ad89ecb39d608efb046703f90ee5123b63c60609 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 6 Jun 2023 12:08:58 -0500 Subject: [PATCH] add example module to game interface. convert gi command registration to a iw4madmin request --- GameFiles/GameInterface/_integration_base.gsc | 4 +- GameFiles/GameInterface/_integration_iw4x.gsc | 4 +- .../GameInterface/_integration_shared.gsc | 33 ++++++- GameFiles/GameInterface/example_module.gsc | 85 +++++++++++++++++++ IW4MAdmin.sln | 1 + Integrations/Cod/CodRConConnection.cs | 24 ++++-- Plugins/ScriptPlugins/GameInterface.js | 38 +++++++-- 7 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 GameFiles/GameInterface/example_module.gsc diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 6df78ac30..62f20396c 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -542,11 +542,11 @@ OnExecuteCommand( event ) { if ( IsDefined( executionContextEntity ) ) { - response = executionContextEntity [[command]]( event, data ); + response = executionContextEntity thread [[command]]( event, data ); } else { - [[command]]( event ); + thread [[command]]( event ); } } else diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index 097b4ca72..19a9b1dfc 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -52,9 +52,9 @@ OnPlayerConnect() if ( player IsTestClient() ) { // we don't want to track bots - continue; + continue; } - + player thread SetPersistentData(); player thread WaitForClientEvents(); } diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index c88a1bb35..bb217a67f 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -49,11 +49,14 @@ Setup() level.eventTypes.urlRequested = "UrlRequested"; level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; level.eventTypes.registerCommandRequested = "RegisterCommandRequested"; + level.eventTypes.getCommandsRequested = "GetCommandsRequested"; level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; + level.eventCallbacks[level.eventTypes.getCommandsRequested] = ::OnCommandsRequestedCallback; level.iw4madminIntegrationDefaultPerformance = 200; level.notifyEntities = []; + level.customCommands = []; level notify( level.notifyTypes.sharedFunctionsInitialized ); level waittill( level.notifyTypes.gameFunctionsInitialized ); @@ -194,6 +197,32 @@ SaveTrackingMetrics() // #region register script command +OnCommandsRequestedCallback( event ) +{ + scripts\_integration_base::LogDebug( "Get commands requested" ); + thread SendCommands( event.data["name"] ); +} + +SendCommands( commandName ) +{ + level endon( level.eventTypes.gameEnd ); + + for ( i = 0; i < level.customCommands.size; i++ ) + { + data = level.customCommands[i]; + + if ( IsDefined( commandName ) && commandName != data["name"] ) + { + continue; + } + + scripts\_integration_base::LogDebug( "Sending custom command " + ( i + 1 ) + "/" + level.customCommands.size + ": " + data["name"] ); + commandRegisterRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.registerCommandRequested, "", undefined, data ); + // not threading here as there might be a lot of commands to register + scripts\_integration_base::QueueEvent( commandRegisterRequest, level.eventTypes.registerCommandRequested, undefined ); + } +} + RegisterScriptCommandObject( command ) { RegisterScriptCommand( command.eventKey, command.name, command.alias, command.description, command.minPermission, command.supportedGames, command.requiresTarget, command.handler ); @@ -258,8 +287,7 @@ RegisterScriptCommand( eventKey, name, alias, description, minPermission, suppor scripts\_integration_base::LogWarning( "handler not defined for script command " + name ); } - commandRegisterRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.registerCommandRequested, "", undefined, data ); - thread scripts\_integration_base::QueueEvent( commandRegisterRequest, level.eventTypes.registerCommandRequested, undefined ); + level.customCommands[level.customCommands.size] = data; } // #end region @@ -391,7 +419,6 @@ GetNextNotifyEntity() return max; } - // #end region // #region team balance diff --git a/GameFiles/GameInterface/example_module.gsc b/GameFiles/GameInterface/example_module.gsc new file mode 100644 index 000000000..ede3fd243 --- /dev/null +++ b/GameFiles/GameInterface/example_module.gsc @@ -0,0 +1,85 @@ +Init() +{ + // this gives the game interface time to setup + waittillframeend; + thread ModuleSetup(); +} + +ModuleSetup() +{ + // waiting until the game specific functions are ready + level waittill( level.notifyTypes.gameFunctionsInitialized ); + + RegisterCustomCommands(); +} + +RegisterCustomCommands() +{ + command = SpawnStruct(); + + // unique key for each command (how iw4madmin identifies the command) + command.eventKey = "PrintLineCommand"; + + // name of the command (cannot conflict with existing command names) + command.name = "println"; + + // short version of the command (cannot conflcit with existing command aliases) + command.alias = "pl"; + + // description of what the command does + command.description = "prints line to game"; + + // minimum permision required to execute + // valid values: User, Trusted, Moderator, Administrator, SeniorAdmin, Owner + command.minPermission = "Trusted"; + + // games the command is supported on + // separate with comma or don't define for all + // valid values: IW3, IW4, IW5, IW6, T4, T5, T6, T7, SHG1, CSGO, H1 + command.supportedGames = "IW4,IW5,T5,T6"; + + // indicates if a target player must be provided to execvute on + command.requiresTarget = false; + + // code to run when the command is executed + command.handler = ::PrintLnCommandCallback; + + // register the command with integration to be send to iw4madmin + scripts\_integration_shared::RegisterScriptCommandObject( command ); + + // you can also register via parameters + scripts\_integration_shared::RegisterScriptCommand( "AffirmationCommand", "affirm", "af", "provide affirmations", "User", undefined, false, ::AffirmationCommandCallback ); +} + +PrintLnCommandCallback( event ) +{ + if ( IsDefined( event.data["args"] ) ) + { + IPrintLnBold( event.data["args"] ); + return; + } + + scripts\_integration_base::LogDebug( "No data was provided for PrintLnCallback" ); +} + +AffirmationCommandCallback( event, _ ) +{ + level endon( level.eventTypes.gameEnd ); + + request = SpawnStruct(); + request.url = "https://www.affirmations.dev"; + request.method = "GET"; + + // If making a post request you can also provide more data + // request.body = "Body of the post message"; + // request.headers = []; + // request.headers["Authorization"] = "api-key"; + + scripts\_integration_shared::RequestUrlObject( request ); + request waittill( level.eventTypes.urlRequestCompleted, response ); + + // horrible json parsing.. but it's just an example + parsedResponse = strtok( response, "\"" ); + + self IPrintLnBold ( "^5" + parsedResponse[parsedResponse.size - 2] ); +} diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index b16c20b2c..1b05d620f 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -86,6 +86,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterf GameFiles\GameInterface\_integration_t5zm.gsc = GameFiles\GameInterface\_integration_t5zm.gsc GameFiles\GameInterface\_integration_t6.gsc = GameFiles\GameInterface\_integration_t6.gsc GameFiles\GameInterface\_integration_t6zm_helper.gsc = GameFiles\GameInterface\_integration_t6zm_helper.gsc + GameFiles\GameInterface\example_module.gsc = GameFiles\GameInterface\example_module.gsc EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}" diff --git a/Integrations/Cod/CodRConConnection.cs b/Integrations/Cod/CodRConConnection.cs index b670e6ba1..780600940 100644 --- a/Integrations/Cod/CodRConConnection.cs +++ b/Integrations/Cod/CodRConConnection.cs @@ -147,6 +147,18 @@ namespace Integrations.Cod { var convertedRConPassword = ConvertEncoding(RConPassword); var convertedParameters = ConvertEncoding(parameters); + byte SafeConversion(char c) + { + try + { + return Convert.ToByte(c); + } + + catch + { + return (byte)'.'; + } + }; switch (type) { @@ -154,30 +166,30 @@ namespace Integrations.Cod waitForResponse = true; payload = string .Format(_config.CommandPrefixes.RConGetDvar, convertedRConPassword, - convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + convertedParameters + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.SET_DVAR: payload = string .Format(_config.CommandPrefixes.RConSetDvar, convertedRConPassword, - convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + convertedParameters + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.COMMAND: payload = string .Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, - convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + convertedParameters + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.GET_STATUS: waitForResponse = true; - payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray(); + payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.GET_INFO: waitForResponse = true; - payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray(); + payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.COMMAND_STATUS: waitForResponse = true; payload = string.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0") - .Select(Convert.ToByte).ToArray(); + .Select(SafeConversion).ToArray(); break; } } diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 8b073262f..01c6ce9ca 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -92,9 +92,7 @@ const plugin = { const input = serverState.inQueue.shift(); // if we queued an event then the next loop will be at the value set complete - if (await this.processEventMessage(input, serverValueEvent.server)) { - // return; - } + await this.processEventMessage(input, serverValueEvent.server); } this.logger.logDebug('loop complete'); @@ -134,7 +132,8 @@ const plugin = { serverState.enabled = true; serverState.running = true; serverState.initializationInProgress = false; - + + this.sendEventMessage(responseEvent.server, true, 'GetCommandsRequested', null, null, null, {}); this.requestGetDvar(inDvar, responseEvent.server); }, @@ -145,7 +144,9 @@ const plugin = { const serverState = servers[responseEvent.server.id]; serverState.outQueue.shift(); - if (responseEvent.server.connectedClients.count === 0) { + const utilities = importNamespace('SharedLibraryCore.Utilities'); + + if (responseEvent.server.connectedClients.count === 0 && !utilities.isDevelopment) { // no clients connected so we don't need to query serverState.running = false; return; @@ -304,9 +305,22 @@ const plugin = { this.scriptHelper.requestUrl(urlRequest, response => { this.logger.logDebug('Got response for gamescript web request - {Response}', response); + if ( typeof response !== 'string' && !(response instanceof String) ) + { + response = JSON.stringify(response); + } + const max = 10; this.logger.logDebug(`response length ${response.length}`); - let chunks = chunkString(response.replace(/"/gm, '\\"').replace(/[\n|\t]/gm, ''), 800); + + let quoteReplace = '\\"'; + // todo: may be more than just T6 + if (server.gameCode === 'T6') + { + quoteReplace = '\\\\"'; + } + + let chunks = chunkString(response.replace(/"/gm, quoteReplace).replace(/[\n|\t]/gm, ''), 800); if (chunks.length > max) { this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`); @@ -454,9 +468,15 @@ const plugin = { if (!validateEnabled(gameEvent.owner, gameEvent.origin)) { return; } - sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { - args: gameEvent.data - }); + + if (gameEvent.data === '--reload') + { + this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, { name: gameEvent.extra.name }); + } else { + sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { + args: gameEvent.data + }); + } } }] }