Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Out of the box [RconCommand] command parser #203

Closed
ghost opened this issue May 8, 2017 · 5 comments
Closed

Out of the box [RconCommand] command parser #203

ghost opened this issue May 8, 2017 · 5 comments
Assignees
Labels
area-Commands Issues related to player/RCON commands feature wontfix

Comments

@ghost
Copy link

ghost commented May 8, 2017

Similar to [Command], but parses Rcon. I've achieved this by basically just copypasting the Command stuff.

@ghost ghost changed the title OOTB [RconCommand] Out of the box [RconCommand] command parser May 8, 2017
@ikkentim
Copy link
Owner

ikkentim commented May 8, 2017

Thanks for creating an issue, I've had the issue before but totally forgot it. Will implement this.

@ikkentim ikkentim added this to the 0.9.0 milestone May 8, 2017
@ghost
Copy link
Author

ghost commented Oct 18, 2017

//
// Shamelessly mostly an edit of the default CommandHandler written by Ikkentim
// for S#.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using SampSharp.Core.Logging;
using SampSharp.GameMode.SAMP.Commands.PermissionCheckers;
using SampSharp.GameMode.World;
using SampSharp.GameMode.SAMP.Commands;
using SampSharp.GameMode;
using SampSharp.GameMode.SAMP.Commands.Parameters;
using SampSharp.GameMode.SAMP.Commands.ParameterTypes;
using SampSharp.GameMode.Controllers;

namespace MetaRPG.Common
{
	/// <summary>
	///     Represents the default commands manager.
	/// </summary>
	public class RconCommandsManager : IService
	{
		private static readonly Type[] SupportedReturnTypes = { typeof(bool), typeof(void) };
		private readonly List<RconCommand> _commands = new List<RconCommand>();

		/// <summary>
		/// Initializes a new instance of the <see cref="RconCommandsManager"/> class.
		/// </summary>
		/// <param name="gameMode">The game mode.</param>
		/// <exception cref="ArgumentNullException"></exception>
		public RconCommandsManager(BaseMode gameMode)
		{
			if (gameMode == null) throw new ArgumentNullException(nameof(gameMode));
			GameMode = gameMode;
		}

		#region Implementation of IService

		/// <summary>
		///     Gets the game mode.
		/// </summary>
		public BaseMode GameMode { get; }

		#endregion

		/// <summary>
		///     Registers the specified command.
		/// </summary>
		/// <param name="commandPaths">The command paths.</param>
		/// <param name="displayName">The display name.</param>
		/// <param name="ignoreCase">if set to <c>true</c> ignore the case of the command.</param>
		/// <param name="permissionCheckers">The permission checkers.</param>
		/// <param name="method">The method.</param>
		/// <param name="usageMessage">The usage message.</param>
		public virtual void Register(CommandPath[] commandPaths, string displayName, bool ignoreCase,
			MethodInfo method, string usageMessage)
		{
			Register(CreateCommand(commandPaths, displayName, ignoreCase, method,
				usageMessage));
		}

		/// <summary>
		///     Creates a command.
		/// </summary>
		/// <param name="commandPaths">The command paths.</param>
		/// <param name="displayName">The display name.</param>
		/// <param name="ignoreCase">if set to <c>true</c> ignore the case the command.</param>
		/// <param name="permissionCheckers">The permission checkers.</param>
		/// <param name="method">The method.</param>
		/// <param name="usageMessage">The usage message.</param>
		/// <returns>The created command</returns>
		protected virtual RconCommand CreateCommand(CommandPath[] commandPaths, string displayName, bool ignoreCase,
			MethodInfo method, string usageMessage)
		{
			return new RconCommand(commandPaths, displayName, ignoreCase, method, usageMessage);
		}

		private static IEnumerable<string> GetCommandGroupPaths(Type type)
		{
			if (type == null || type == typeof(object))
				yield break;

			var count = 0;
			var groups =
				type.GetTypeInfo()
					.GetCustomAttributes<CommandGroupAttribute>()
					.SelectMany(a => a.Paths)
					.Select(g => g.Trim())
					.Where(g => !string.IsNullOrEmpty(g)).ToArray();

			foreach (var group in GetCommandGroupPaths(type.DeclaringType))
			{
				if (groups.Length == 0)
					yield return group;
				else
					foreach (var g in groups)
						yield return $"{group} {g}";

				count++;
			}

			if (count == 0)
				foreach (var g in groups)
					yield return g;
		}

		private static IEnumerable<string> GetCommandGroupPaths(MethodInfo method)
		{
			var count = 0;
			var groups =
				method.GetCustomAttributes<CommandGroupAttribute>()
					.SelectMany(a => a.Paths)
					.Select(g => g.Trim())
					.Where(g => !string.IsNullOrEmpty(g)).ToArray();

			foreach (var path in GetCommandGroupPaths(method.DeclaringType))
			{
				if (groups.Length == 0)
					yield return path;
				else
					foreach (var g in groups)
						yield return $"{path} {g}";

				count++;
			}

			if (count == 0)
				foreach (var g in groups)
					yield return g;
		}

		/// <summary>
		///     Gets the command for the specified command text.
		/// </summary>
		/// <param name="player">The player.</param>
		/// <param name="commandText">The command text.</param>
		/// <returns>The found command.</returns>
		public RconCommand GetCommandForText(string commandText)
		{
			RconCommand candidate = null;

			foreach (var command in _commands)
			{
				switch (command.CanInvoke(commandText))
				{
					case CommandCallableResponse.True:
						return command;
					case CommandCallableResponse.Optional:
						candidate = command;
						break;
				}
			}

			return candidate;
		}

		#region Implementation of ICommandsManager

		/// <summary>
		///     Gets a read-only collection of all registered commands.
		/// </summary>
		public virtual IReadOnlyCollection<RconCommand> Commands => _commands.AsReadOnly();

		/// <summary>
		///     Loads all tagged commands from the assembly containing the specified type.
		/// </summary>
		/// <typeparam name="T">A type inside the assembly to load the commands form.</typeparam>
		public virtual void RegisterCommands<T>() where T : class
		{
			RegisterCommands(typeof(T));
		}

		/// <summary>
		///     Loads all tagged commands from the assembly containing the specified type.
		/// </summary>
		/// <param name="typeInAssembly">A type inside the assembly to load the commands form.</param>
		public virtual void RegisterCommands(Type typeInAssembly)
		{
			if (typeInAssembly == null) throw new ArgumentNullException(nameof(typeInAssembly));
			RegisterCommands(typeInAssembly.GetTypeInfo().Assembly);
		}

		/// <summary>
		///     Loads all tagged commands from the specified <paramref name="assembly" />.
		/// </summary>
		/// <param name="assembly">The assembly to load the commands from.</param>
		public virtual void RegisterCommands(Assembly assembly)
		{
			foreach (
				var method in
					assembly.GetTypes()
						// Get all classes in the specified assembly.
						.Where(type => !type.GetTypeInfo().IsInterface && type.GetTypeInfo().IsClass && !type.GetTypeInfo().IsAbstract)
						// Select the methods in the type.
						.SelectMany(
							type =>
								type.GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static |
												BindingFlags.Instance))
						// Exclude abstract methods. (none should be since abstract types are excluded)
						.Where(method => !method.IsAbstract)
						// Only include methods with a return type of bool or void.
						.Where(method => SupportedReturnTypes.Contains(method.ReturnType))
						// Only include methods which have a command attribute.
						.Where(method => method.GetCustomAttribute<RconCommandAttribute>() != null)
						// Only include methods which are static
						.Where(RconCommand.IsValidCommandMethod)
				)
			{
				var attribute = method.GetCustomAttribute<RconCommandAttribute>();

				var commandPaths =
					GetCommandGroupPaths(method)
						.SelectMany(g => attribute.Names.Select(n => new CommandPath(g, n)))
						.ToList();

				if (commandPaths.Count == 0)
					commandPaths.AddRange(attribute.Names.Select(n => new CommandPath(n)));

				if (!string.IsNullOrWhiteSpace(attribute.Shortcut))
					commandPaths.Add(new CommandPath(attribute.Shortcut));

				Register(commandPaths.ToArray(), attribute.DisplayName, attribute.IgnoreCase,
					method, attribute.UsageMessage);
			}
		}

		/// <summary>
		///     Registers the specified command.
		/// </summary>
		/// <param name="command">The command.</param>
		public virtual void Register(RconCommand command)
		{
			if (command == null) throw new ArgumentNullException(nameof(command));

			CoreLog.Log(CoreLogLevel.Debug, $"Registering RCON command {command}");
			Console.WriteLine($"Registering RCON command {command}");
			_commands.Add(command);
		}

		/// <summary>
		///     Processes the specified command.
		/// </summary>
		/// <param name="commandText">The command text.</param>
		/// <returns>true if processed; false otherwise.</returns>
		public virtual bool Process(string commandText)
		{
			var command = GetCommandForText(commandText);

			return command != null && command.Invoke(commandText);
		}

		#endregion
	}


	/// <summary>
	///     Indicates a method is an RCON command.
	/// </summary>
	[AttributeUsage(AttributeTargets.Method)]
	public class RconCommandAttribute : Attribute
	{
		/// <summary>
		///     Initializes a new instance of the <see cref="RconCommandAttribute" /> class.
		/// </summary>
		/// <param name="name">The name of the command.</param>
		public RconCommandAttribute(string name) : this(new[] { name })
		{
		}

		/// <summary>
		///     Initializes a new instance of the <see cref="RconCommandAttribute" /> class.
		/// </summary>
		/// <param name="names">The names of the command.</param>
		public RconCommandAttribute(params string[] names)
		{
			Names = names;
			IgnoreCase = true;
		}

		/// <summary>
		///     Gets the names.
		/// </summary>
		public string[] Names { get; }

		/// <summary>
		///     Gets or sets a value indicating whether to ignore the case of the command.
		/// </summary>
		public bool IgnoreCase { get; set; }

		/// <summary>
		///     Gets or sets the shortcut.
		/// </summary>
		public string Shortcut { get; set; }

		/// <summary>
		///     Gets or sets the display name.
		/// </summary>
		public string DisplayName { get; set; }

		/// <summary>
		///     Gets or sets the usage message.
		/// </summary>
		public string UsageMessage { get; set; }
	}


	/// <summary>
	///     Represents the default command based on a method with command attributes.
	/// </summary>
	public class RconCommand
	{
		private readonly string _displayName;

		/// <summary>
		///     Initializes a new instance of the <see cref="RconCommand" /> class.
		/// </summary>
		/// <param name="names">The names.</param>
		/// <param name="displayName">The display name.</param>
		/// <param name="ignoreCase">if set to <c>true</c> ignore the case of the command.</param>
		/// <param name="permissionCheckers">The permission checkers.</param>
		/// <param name="method">The method.</param>
		/// <param name="usageMessage">The usage message.</param>
		public RconCommand(CommandPath[] names, string displayName, bool ignoreCase,
			MethodInfo method, string usageMessage)
		{
			if (names == null) throw new ArgumentNullException(nameof(names));
			if (method == null) throw new ArgumentNullException(nameof(method));
			if (names.Length == 0) throw new ArgumentException("Must contain at least 1 name", nameof(names));

			if (!IsValidCommandMethod(method))
				throw new ArgumentException("Method unsuitable as command", nameof(method));
						
			var skipCount = 0;
			var index = 0;
			var count = method.GetParameters().Length - skipCount;

			Names = names;
			_displayName = displayName;
			IsCaseIgnored = ignoreCase;
			Method = method;
			UsageMessage = string.IsNullOrWhiteSpace(usageMessage) ? null : usageMessage;
			Parameters = Method.GetParameters()
				.Skip(skipCount)
				.Select(
					p =>
					{
						var type = GetParameterType(p, index++, count);
						return type == null
							? null
							: new CommandParameterInfo(p.Name, type, p.HasDefaultValue, p.DefaultValue);
					})
				.ToArray();

			if (Parameters.Any(v => v == null))
			{
				throw new ArgumentException("Method has parameter of unknown type", nameof(method));
			}
		}

		/// <summary>
		///     Gets the names.
		/// </summary>
		public virtual CommandPath[] Names { get; }

		/// <summary>
		///     Gets the display name.
		/// </summary>
		public virtual string DisplayName
		{
			get { return _displayName ?? Names.OrderByDescending(n => n.Length).First().FullName; }
		}

		/// <summary>
		///     Gets a value indicating whether this instance ignores the case of the command.
		/// </summary>
		public virtual bool IsCaseIgnored { get; }

		/// <summary>
		///     Gets the method.
		/// </summary>
		public MethodInfo Method { get; }

		/// <summary>
		///     Gets the usage message.
		/// </summary>
		public virtual string UsageMessage { get; }

		/// <summary>
		///     Gets the parameters.
		/// </summary>
		public CommandParameterInfo[] Parameters { get; }

		/// <summary>
		///     Determines whether the specified method is a valid command method.
		/// </summary>
		/// <param name="method">The method.</param>
		/// <returns>true if valid; false otherwise.</returns>
		public static bool IsValidCommandMethod(MethodInfo method)
		{
			return (method.IsStatic);
		}

		private bool GetArguments(string commandText, out object[] arguments)
		{
			arguments = new object[Parameters.Length];
			var index = 0;
			foreach (var parameter in Parameters)
			{
				object arg;
				if (!parameter.CommandParameterType.Parse(ref commandText, out arg))
				{
					if (!parameter.IsOptional)
						return false;

					arguments[index] = parameter.DefaultValue;
				}
				else
				{
					arguments[index] = arg;
				}

				index++;
			}

			return true;
		}

		/// <summary>
		///     Gets the type of the specified parameter.
		/// </summary>
		/// <param name="parameter">The parameter.</param>
		/// <param name="index">The index.</param>
		/// <param name="count">The count.</param>
		/// <returns>The type of the parameter.</returns>
		protected virtual ICommandParameterType GetParameterType(ParameterInfo parameter, int index, int count)
		{
			var attribute = parameter.GetCustomAttribute<ParameterAttribute>();

			if (attribute != null && typeof(ICommandParameterType).GetTypeInfo().IsAssignableFrom(attribute.Type))
				return Activator.CreateInstance(attribute.Type) as ICommandParameterType;

			if (parameter.ParameterType == typeof(string))
				return index == count - 1
					? (ICommandParameterType)new TextType()
					: new WordType();

			if (parameter.ParameterType == typeof(int))
				return new IntegerType();

			if (parameter.ParameterType == typeof(float))
				return new FloatType();

			if (typeof(BasePlayer).GetTypeInfo().IsAssignableFrom(parameter.ParameterType))
				return new PlayerType();

			if (parameter.ParameterType.GetTypeInfo().IsEnum)
				return
					Activator.CreateInstance(typeof(EnumType<>).MakeGenericType(parameter.ParameterType))
						as ICommandParameterType;

			return null;
		}

		/// <summary>
		/// Sends the usage message to the console buffer />.
		/// </summary>
		/// <returns>True on success, false otherwise.</returns>
		protected virtual bool SendUsageMessage()
		{
			if (UsageMessage == null)
			{
				Console.WriteLine($"Usage: {this}");
				return true;
			}

			Console.WriteLine(UsageMessage);
			return true;
		}
			

		#region Overrides of Object

		/// <summary>
		///     Returns a string that represents the current object.
		/// </summary>
		/// <returns>
		///     A string that represents the current object.
		/// </returns>
		public override string ToString()
		{
			var name = DisplayName;

			if (Parameters.Any())
				return name + " " +
						string.Join(" ", Parameters.Select(p => p.IsOptional ? $"<{p.Name}>" : $"[{p.Name}]"));
			return name;
		}

		#endregion

		#region Implementation of ICommand

		/// <summary>
		///     Determines whether this instance can be invoked.
		/// </summary>
		/// <param name="commandText">The command text.</param>
		/// <returns>A value indicating whether this instance can be invoked.</returns>
		public virtual CommandCallableResponse CanInvoke(string commandText)
		{
			foreach (var name in Names)
			{
				if (!name.Matches(commandText, IsCaseIgnored)) continue;

				commandText = commandText.Substring(name.Length);
				object[] tmp;
				return GetArguments(commandText, out tmp)
					? CommandCallableResponse.True
					: CommandCallableResponse.Optional;
			}

			return CommandCallableResponse.False;
		}

		/// <summary>
		///     Invokes this command.
		/// </summary>
		/// <param name="commandText">The command text.</param>
		/// <returns>true on success; false otherwise.</returns>
		public virtual bool Invoke(string commandText)
		{
			object[] arguments;
			var name = Names.Cast<CommandPath?>().FirstOrDefault(n => n != null && n.Value.Matches(commandText));

			if (name == null)
				return false;

			commandText = commandText.Substring(name.Value.Length).Trim();

			if (!GetArguments(commandText, out arguments))
				return SendUsageMessage();

			var result = Method.Invoke(null, arguments);

			return result as bool? ?? true;
		}

		#endregion

		#region Controller
		/// <summary>
		///     A controller processing all commands.
		/// </summary>
		[Controller]
		public class RconCommandController : IEventListener, IGameServiceProvider
		{
			/// <summary>
			///     Gets or sets the commands manager.
			/// </summary>
			protected virtual RconCommandsManager CommandsManager { get; set; }

			/// <summary>
			///     Registers the events this <see cref="GlobalObjectController" /> wants to listen to.
			/// </summary>
			/// <param name="gameMode">The running GameMode.</param>
			public virtual void RegisterEvents(BaseMode gameMode)
			{
				gameMode.RconCommand += GameMode_RconCommand;
			}

			#region Implementation of IGameServiceProvider

			/// <summary>
			///     Registers the services this controller provides.
			/// </summary>
			/// <param name="gameMode">The game mode.</param>
			/// <param name="serviceContainer">The service container.</param>
			public virtual void RegisterServices(BaseMode gameMode, GameModeServiceContainer serviceContainer)
			{
				CommandsManager = new RconCommandsManager(gameMode);
				serviceContainer.AddService(CommandsManager);

				// Register commands in game mode.
				CommandsManager.RegisterCommands(gameMode.GetType());
			}

			#endregion


			private void GameMode_RconCommand(object sender, SampSharp.GameMode.Events.RconEventArgs e)
			{
				if (CommandsManager == null)
					return;
				
				e.Success = CommandsManager.Process(e.Command);
			}
		}
		
		#endregion
	}
}

@ikkentim
Copy link
Owner

ikkentim commented Jul 5, 2018

Will be implementing this by decoupling the player command system so it can also be used for rcon commands.

@ikkentim ikkentim modified the milestones: 0.9.0, 0.8.0 Jul 5, 2018
@ikkentim ikkentim added the area-Commands Issues related to player/RCON commands label Jul 5, 2018
@ikkentim ikkentim self-assigned this Jul 8, 2018
@ikkentim ikkentim modified the milestones: 0.8.0, 0.8.1 Nov 15, 2018
@ikkentim ikkentim modified the milestones: 0.9.0, 0.10.0 Jan 15, 2020
@ikkentim
Copy link
Owner

Decided not to implement this for SampSharp.GameMode, but did implement this for SampSharp.Entities. If anyone wants this they can use the code provided above.

@KirillBorunov
Copy link
Contributor

It's a shame that this will not be implemented in SampSharp.GameMode.
This feature is very useful for technical server administrators to have console commands without logging in to the game.

The @ghost code works but need to fix lines:

.Where(type => !type.GetTypeInfo().IsInterface && type.GetTypeInfo().IsClass && !type.GetTypeInfo().IsAbstract)
change to:
.Where(type => !type.GetTypeInfo().IsInterface && type.GetTypeInfo().IsClass)
Because it will not find commands inside static classes. More info: https://stackoverflow.com/questions/1175888/determine-if-a-type-is-static

: new CommandParameterInfo(p.Name, type, p.HasDefaultValue, p.DefaultValue);
change to:
: new CommandParameterInfo(p.Name, type, p.HasDefaultValue, p.DefaultValue, p.GetCustomAttribute<NullableParamAttribute>() != null);
Because this parameter is now requiered, original code probably changed.

Console.WriteLine($"Registering RCON command {command}");
remove. Its duplicating the message.

I created a gist with the fixed code here https://gist.github.com/KirillBorunov/f7c28b662c184f95c910f073368ae555
(maybe useful for someone because this issue shows up on google search about SampSharp rcon commands)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Commands Issues related to player/RCON commands feature wontfix
Projects
None yet
Development

No branches or pull requests

2 participants