From 3f0bdfe3a91abcbc367baab3575dec697d2507b7 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 3 Jun 2023 22:46:15 -0500 Subject: [PATCH] implement dynamic command registration through game interface --- .../Plugin/Script/ScriptPluginHelper.cs | 5 ++ Application/Plugin/Script/ScriptPluginV2.cs | 66 +++++++++------ GameFiles/GameInterface/_integration_base.gsc | 16 +++- .../GameInterface/_integration_shared.gsc | 80 ++++++++++++++++++- Plugins/ScriptPlugins/GameInterface.js | 28 +++++++ 5 files changed, 166 insertions(+), 29 deletions(-) diff --git a/Application/Plugin/Script/ScriptPluginHelper.cs b/Application/Plugin/Script/ScriptPluginHelper.cs index be15c580a..86b935f47 100644 --- a/Application/Plugin/Script/ScriptPluginHelper.cs +++ b/Application/Plugin/Script/ScriptPluginHelper.cs @@ -76,6 +76,11 @@ public class ScriptPluginHelper }); } + public void RegisterDynamicCommand(JsValue command) + { + _scriptPlugin.RegisterDynamicCommand(command.ToObject()); + } + private object RequestInternal(ScriptPluginWebRequest request) { var entered = false; diff --git a/Application/Plugin/Script/ScriptPluginV2.cs b/Application/Plugin/Script/ScriptPluginV2.cs index 19b70c790..26edcaa28 100644 --- a/Application/Plugin/Script/ScriptPluginV2.cs +++ b/Application/Plugin/Script/ScriptPluginV2.cs @@ -47,6 +47,7 @@ public class ScriptPluginV2 : IPluginV2 private readonly List _registeredCommandNames = new(); private readonly List _registeredInteractions = new(); private readonly Dictionary> _registeredEvents = new(); + private IManager _manager; private bool _firstInitialization = true; private record ScriptPluginDetails(string Name, string Author, string Version, @@ -112,8 +113,15 @@ public class ScriptPluginV2 : IPluginV2 }, _logger, _fileName, _onProcessingScript); } + public void RegisterDynamicCommand(object command) + { + var parsedCommand = ParseScriptCommandDetails(command); + RegisterCommand(_manager, parsedCommand.First()); + } + private async Task OnLoad(IManager manager, CancellationToken token) { + _manager = manager; var entered = false; try { @@ -253,8 +261,12 @@ public class ScriptPluginV2 : IPluginV2 command.Permission, command.TargetRequired, command.Arguments, Execute, command.SupportedGames); + manager.RemoveCommandByName(scriptCommand.Name); manager.AddAdditionalCommand(scriptCommand); - _registeredCommandNames.Add(scriptCommand.Name); + if (!_registeredCommandNames.Contains(scriptCommand.Name)) + { + _registeredCommandNames.Add(scriptCommand.Name); + } } private void ResetEngineState() @@ -480,6 +492,33 @@ public class ScriptPluginV2 : IPluginV2 } private static ScriptPluginDetails AsScriptPluginInstance(dynamic source) + { + var commandDetails = ParseScriptCommandDetails(source); + + var interactionDetails = Array.Empty(); + if (HasProperty(source, "interactions") && source.interactions is dynamic[]) + { + interactionDetails = ((dynamic[])source.interactions).Select(interaction => + { + var name = HasProperty(interaction, "name") && interaction.name is string + ? (string)interaction.name + : string.Empty; + var action = HasProperty(interaction, "action") && interaction.action is Delegate + ? (Delegate)interaction.action + : null; + + return new ScriptPluginInteractionDetails(name, action); + }).ToArray(); + } + + var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty; + var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty; + var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty; + + return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails); + } + + private static ScriptPluginCommandDetails[] ParseScriptCommandDetails(dynamic source) { var commandDetails = Array.Empty(); if (HasProperty(source, "commands") && source.commands is dynamic[]) @@ -513,7 +552,7 @@ public class ScriptPluginV2 : IPluginV2 (bool)command.targetRequired; var supportedGames = HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable - ? ((IEnumerable)command.supportedGames).Where(game => game?.ToString() is not null) + ? ((IEnumerable)command.supportedGames).Where(game => !string.IsNullOrEmpty(game?.ToString())) .Select(game => Enum.Parse(game.ToString()!)) : Array.Empty(); @@ -523,31 +562,10 @@ public class ScriptPluginV2 : IPluginV2 return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired, commandArgs, supportedGames, execute); - }).ToArray(); } - var interactionDetails = Array.Empty(); - if (HasProperty(source, "interactions") && source.interactions is dynamic[]) - { - interactionDetails = ((dynamic[])source.interactions).Select(interaction => - { - var name = HasProperty(interaction, "name") && interaction.name is string - ? (string)interaction.name - : string.Empty; - var action = HasProperty(interaction, "action") && interaction.action is Delegate - ? (Delegate)interaction.action - : null; - - return new ScriptPluginInteractionDetails(name, action); - }).ToArray(); - } - - var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty; - var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty; - var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty; - - return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails); + return commandDetails; } private static bool HasProperty(dynamic source, string name) diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index e918758ff..6df78ac30 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -295,6 +295,11 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) eventSubtype = "None"; } + if ( !IsDefined( entOrId ) ) + { + entOrId = "-1"; + } + if ( IsPlayer( entOrId ) ) { entOrId = entOrId getEntityNumber(); @@ -311,7 +316,7 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data; -eturn request; + return request; } MonitorBus() @@ -535,7 +540,14 @@ OnExecuteCommand( event ) if ( IsDefined( command ) ) { - response = executionContextEntity [[command]]( event, data ); + if ( IsDefined( executionContextEntity ) ) + { + response = executionContextEntity [[command]]( event, data ); + } + else + { + [[command]]( event ); + } } else { diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index 54f300c97..c88a1bb35 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -46,8 +46,9 @@ Setup() level.eventTypes.spawned = "spawned_player"; level.eventTypes.gameEnd = "game_ended"; - level.eventTypes.urlRequested = "UrlRequested"; - level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; + level.eventTypes.urlRequested = "UrlRequested"; + level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; + level.eventTypes.registerCommandRequested = "RegisterCommandRequested"; level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; @@ -191,6 +192,78 @@ SaveTrackingMetrics() scripts\_integration_base::IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId ); } +// #region register script command + +RegisterScriptCommandObject( command ) +{ + RegisterScriptCommand( command.eventKey, command.name, command.alias, command.description, command.minPermission, command.supportedGames, command.requiresTarget, command.handler ); +} + +RegisterScriptCommand( eventKey, name, alias, description, minPermission, supportedGames, requiresTarget, handler ) +{ + if ( !IsDefined( eventKey ) ) + { + scripts\_integration_base::LogError( "eventKey must be provided for script command" ); + return; + } + + data = []; + + data["eventKey"] = eventKey; + + if ( IsDefined( name ) ) + { + data["name"] = name; + } + else + { + scripts\_integration_base::LogError( "name must be provided for script command" ); + return; + } + + if ( IsDefined( alias ) ) + { + data["alias"] = alias; + } + + if ( IsDefined( description ) ) + { + data["description"] = description; + } + + if ( IsDefined( minPermission ) ) + { + data["minPermission"] = minPermission; + } + + if ( IsDefined( supportedGames ) ) + { + data["supportedGames"] = supportedGames; + } + + data["requiresTarget"] = false; + + if ( IsDefined( requiresTarget ) ) + { + data["requiresTarget"] = requiresTarget; + } + + if ( IsDefined( handler ) ) + { + level.clientCommandCallbacks[eventKey + "Execute"] = handler; + level.clientCommandRusAsTarget[eventKey + "Execute"] = data["requiresTarget"]; + } + else + { + 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 ); +} + +// #end region + // #region web requests RequestUrlObject( request ) @@ -262,7 +335,6 @@ WaitForUrlRequestComplete() scripts\_integration_base::LogDebug( "Request to " + self.url + " completed" ); - //self delete(); level.notifyEntities[self.index] = undefined; } @@ -315,6 +387,8 @@ GetNextNotifyEntity() return i; } } + + return max; } diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index e3c758286..8b073262f 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -320,6 +320,10 @@ const plugin = { } }); } + + if (event.eventType === 'RegisterCommandRequested') { + this.registerDynamicCommand(event); + } tokenSource.dispose(); return messageQueued; @@ -434,6 +438,30 @@ const plugin = { const script = importNamespace('IW4MAdmin.Application.Plugin.Script'); return new script.ScriptPluginWebRequest(url, body, method, contentType, headerDict); + }, + + registerDynamicCommand: function(event) { + const commandWrapper = { + commands: [{ + name: event.data['name'] || 'DEFAULT', + description: event.data['description'] || 'DEFAULT', + alias: event.data['alias'] || 'DEFAULT', + permission: event.data['minPermission'] || 'DEFAULT', + targetRequired: (event.data['targetRequired'] || '0') === '1', + supportedGames: (event.data['supportedGames'] || '').split(','), + + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.owner, gameEvent.origin)) { + return; + } + sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { + args: gameEvent.data + }); + } + }] + } + + this.scriptHelper.registerDynamicCommand(commandWrapper); } };