Merge pull request #133 from RaidMax/feature/issue-132-script-command-registration

implement script plugin command registration - issue #132
This commit is contained in:
RaidMax 2020-05-11 16:21:33 -05:00 committed by GitHub
commit 420e0d5ab5
11 changed files with 301 additions and 6 deletions

View File

@ -62,12 +62,13 @@ namespace IW4MAdmin.Application
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory;
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler)
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -92,6 +93,7 @@ namespace IW4MAdmin.Application
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
_eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory;
Plugins = plugins;
}
@ -267,12 +269,12 @@ namespace IW4MAdmin.Application
{
if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this);
await scriptPlugin.Initialize(this, _scriptCommandFactory);
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this);
await scriptPlugin.Initialize(this, _scriptCommandFactory);
}
catch (Exception ex)
@ -817,5 +819,17 @@ namespace IW4MAdmin.Application
{
_operationLookup.Add(operationName, operation);
}
public void AddAdditionalCommand(IManagerCommand command)
{
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
{
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
}
_commands.Add(command);
}
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
}
}

View File

@ -0,0 +1,40 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IScriptCommandFactory
/// </summary>
public class ScriptCommandFactory : IScriptCommandFactory
{
private CommandConfiguration _config;
private readonly ITranslationLookup _transLookup;
public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup)
{
_config = config;
_transLookup = transLookup;
}
/// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
{
var permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument
{
Name = _arg.Item1,
Required = _arg.Item2
}).ToArray();
return new ScriptCommand(name, alias, description, permissionEnum, argsArray, executeAction, _config, _transLookup);
}
}
}

View File

@ -286,6 +286,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider =>

View File

@ -0,0 +1,41 @@
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// generic script command implementation
/// </summary>
public class ScriptCommand : Command
{
private readonly Action<GameEvent> _executeAction;
public ScriptCommand(string name, string alias, string description, Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout)
: base(config, layout)
{
_executeAction = executeAction;
Name = name;
Alias = alias;
Description = description;
Permission = permission;
Arguments = args;
}
public override Task ExecuteAsync(GameEvent E)
{
if (_executeAction == null)
{
throw new InvalidOperationException($"No execute action defined for command \"{Name}\"");
}
return Task.Run(() => _executeAction(E));
}
}
}

View File

@ -1,8 +1,12 @@
using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.CSharp.RuntimeBinder;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@ -34,6 +38,7 @@ namespace IW4MAdmin.Application.Misc
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing;
private bool successfullyLoaded;
private readonly List<string> _registeredCommandNames;
public ScriptPlugin(string filename, string workingDirectory = null)
{
@ -47,6 +52,7 @@ namespace IW4MAdmin.Application.Misc
Watcher.EnableRaisingEvents = true;
_onProcessing = new SemaphoreSlim(1, 1);
_registeredCommandNames = new List<string>();
}
~ScriptPlugin()
@ -55,7 +61,7 @@ namespace IW4MAdmin.Application.Misc
_onProcessing.Dispose();
}
public async Task Initialize(IManager manager)
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory)
{
await _onProcessing.WaitAsync();
@ -75,6 +81,14 @@ namespace IW4MAdmin.Application.Misc
if (!firstRun)
{
await OnUnloadAsync();
foreach (string commandName in _registeredCommandNames)
{
manager.GetLogger(0).WriteDebug($"Removing plugin registered command \"{commandName}\"");
manager.RemoveCommandByName(commandName);
}
_registeredCommandNames.Clear();
}
successfullyLoaded = false;
@ -106,6 +120,26 @@ namespace IW4MAdmin.Application.Misc
Name = pluginObject.name;
Version = (float)pluginObject.version;
var commands = _scriptEngine.GetValue("commands");
if (commands != JsValue.Undefined)
{
try
{
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{
manager.GetLogger(0).WriteDebug($"Adding plugin registered command \"{command.Name}\"");
manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name);
}
}
catch (RuntimeBinderException e)
{
throw new PluginException($"Not all required fields were found: {e.Message}") { PluginFile = _fileName };
}
}
await OnLoadAsync(manager);
try
@ -193,5 +227,67 @@ namespace IW4MAdmin.Application.Misc
await Task.FromResult(_scriptEngine.Execute("plugin.onUnloadAsync()").GetCompletionValue());
}
}
/// <summary>
/// finds declared script commands in the script plugin
/// </summary>
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
public IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
{
List<IManagerCommand> commandList = new List<IManagerCommand>();
// go through each defined command
foreach (var command in commands.AsArray())
{
dynamic dynamicCommand = command.ToObject();
string name = dynamicCommand.name;
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
string permission = dynamicCommand.permission;
List<(string, bool)> args = new List<(string, bool)>();
dynamic arguments = null;
try
{
arguments = dynamicCommand.arguments;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null)
{
foreach (var arg in dynamicCommand.arguments)
{
args.Add((arg.name, (bool)arg.required));
}
}
void execute(GameEvent e)
{
_scriptEngine.SetValue("_event", e);
var jsEventObject = _scriptEngine.GetValue("_event");
try
{
dynamicCommand.execute.Target.Invoke(jsEventObject);
}
catch (JavaScriptException ex)
{
throw new PluginException($"An error occured while executing action for script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
}
}
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, args, execute));
}
return commandList;
}
}
}

View File

@ -45,6 +45,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
Plugins\ScriptPlugins\ParserT7.js = Plugins\ScriptPlugins\ParserT7.js
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
Plugins\ScriptPlugins\SampleScriptPluginCommand.js = Plugins\ScriptPlugins\SampleScriptPluginCommand.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection

View File

@ -0,0 +1,54 @@
let commands = [{
// required
name: "pingpong",
// required
description: "pongs a ping",
// required
alias: "pp",
// required
permission: "User",
// optional
arguments: [{
name: "times to ping",
required: true
}],
// required
execute: (gameEvent) => {
// parse the first argument (number of times)
let times = parseInt(gameEvent.Data);
// we only want to allow ping pong up to 5 times
if (times > 5 || times <= 0) {
gameEvent.Origin.Tell("You can only ping pong between 1 and 5 times");
return;
}
// we want to print out a pong message for the number of times they requested
for (var i = 0; i < times; i++) {
gameEvent.Origin.Tell(`^${i}pong #${i + 1}^7`);
// don't want to wait if it's the last pong
if (i < times - 1) {
System.Threading.Tasks.Task.Delay(1000).Wait();
}
}
}
}];
let plugin = {
author: 'RaidMax',
version: 1.0,
name: 'Ping Pong Sample Command Plugin',
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -0,0 +1,11 @@
using System;
namespace SharedLibraryCore.Exceptions
{
public class PluginException : Exception
{
public PluginException(string message) : base(message) { }
public string PluginFile { get; set; }
}
}

View File

@ -70,5 +70,15 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
/// <param name="gameEvent">event to be processed</param>
void AddEvent(GameEvent gameEvent);
/// <summary>
/// adds an additional (script) command to the command list
/// </summary>
/// <param name="command"></param>
void AddAdditionalCommand(IManagerCommand command);
/// <summary>
/// removes a command by its name
/// </summary>
/// <param name="name">name of command</param>
void RemoveCommandByName(string name);
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// defines capabilities of script command factory
/// </summary>
public interface IScriptCommandFactory
{
/// <summary>
/// generate a new script command from parsed source
/// </summary>
/// <param name="name">name of command</param>
/// <param name="alias">alias of command</param>
/// <param name="description">description of command</param>
/// <param name="permission">minimum required permission</param>
/// <param name="args">command arguments (name, is required)</param>
/// <param name="executeAction">action to peform when commmand is executed</param>
/// <returns></returns>
IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction);
}
}

View File

@ -12,8 +12,11 @@ namespace WebfrontCore.Controllers
{
public class HomeController : BaseController
{
public HomeController(IManager manager) : base(manager)
private readonly ITranslationLookup _translationLookup;
public HomeController(IManager manager, ITranslationLookup translationLookup) : base(manager)
{
_translationLookup = translationLookup;
}
public async Task<IActionResult> Index(Game? game = null)
@ -69,7 +72,8 @@ namespace WebfrontCore.Controllers
// we need the plugin type the command is defined in
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type => _type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
return pluginType == null ?
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HELP_COMMAND_NATIVE"] :
_translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
pluginType.Name == "ScriptPlugin" ? _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"] :
Manager.Plugins.First(_plugin => _plugin.GetType() == pluginType).Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
})
.Select(_grp => (_grp.Key, _grp.AsEnumerable()));