// WingsEmu // // Developed by NosWings Team using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using PhoenixLib.Logging; using Qmmands; using WingsEmu.Commands.Checks; using WingsEmu.Commands.Entities; using WingsEmu.Commands.Interfaces; using WingsEmu.DTOs.Account; using WingsEmu.Game; using WingsEmu.Game.Commands; using WingsEmu.Game.Extensions; using WingsEmu.Game.Networking; using WingsEmu.Packets.Enums.Chat; namespace WingsEmu.Commands { /* todo: find a better way to deal with TAP in world and here. * handle errors correctly and return them to the user ingame. */ public class CommandHandler : ICommandContainer, IGlobalCommandExecutor { private readonly CommandService _commands; /// /// This class should be instanciated with our Container. /// public CommandHandler(IServiceProvider provider) { _commands = new CommandService(new CommandServiceConfiguration { StringComparison = StringComparison.OrdinalIgnoreCase }); _commands.CommandExecuted += _commands_CommandExecuted; _commands.CommandExecutionFailed += _commands_CommandErrored; Services = provider; } public IServiceProvider Services { get; } public void AddModule() where T : SaltyModuleBase { Log.Info($"[ADD_MODULE] {typeof(T).Name}"); _commands.AddModule(); IReadOnlyList readOnlyList = _commands.GetAllModules().FirstOrDefault(s => s.Type == typeof(T))?.Commands; if (readOnlyList != null) { foreach (Command command in readOnlyList) { Log.Info($"[ADD_COMMAND] {command}"); } } } public void RemoveModule() where T : SaltyModuleBase { Module module = _commands.GetAllModules().FirstOrDefault(s => s.Type == typeof(T)); if (module is null) { throw new ArgumentException("The given module is not registered in the command container."); } _commands.RemoveModule(module); } public Module[] GetModulesByName(string name, bool caseSensitive = true) { return _commands.GetAllModules().Where(x => caseSensitive ? x.Name == name : x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToArray(); } public Command[] GetCommandsByName(string name, bool caseSensitive = true) { return _commands.GetAllCommands().Where(x => caseSensitive ? x.Name == name : x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)).ToArray(); } public void AddTypeParser(TypeParser typeParser) { _commands.AddTypeParser(typeParser); Log.Info($"[ADD_TYPE_PARSER] {typeParser.GetType().Name}"); } /// /// /// This is where every message from the InGame tchat starting with our prefix will arrive. /// In our case, the parameter message represents the raw message sent by the user. /// The parameter of type object would represent the instance of the entity that invoked the command. /// That method could be called on each messages sent in the in-game tchat. We will just check that it starts with our /// prefix ($). /// Then we will create a Context that will propagate onto the command. /// The CommandService will try to parse the message and find a command with the parsed arguments and will perform some /// checks, if necessary. /// /// It represents the already parsed command with its parameters. /// It represents the instance of the entity that performed the action of sending a message. /// public async Task HandleMessageAsync(string message, object entity, string prefix) { if (entity is not IClientSession player) { return; } if (!player.HasSelectedCharacter) { return; } if (!player.HasCurrentMapInstance) { return; } var ctx = new WingsEmuIngameCommandContext(message, player, prefix, _commands, Services); IResult result = await _commands.ExecuteAsync(ctx.Input, ctx); if (result.IsSuccessful) { ctx.Command = (result as CommandResult)?.Command; var authorityAttribute = (RequireAuthorityAttribute)ctx.Command?.Module?.Checks.FirstOrDefault(check => check is RequireAuthorityAttribute); if (authorityAttribute == null || authorityAttribute.Authority < AuthorityType.GameMaster) { ctx.Player.PlayerEntity.Session.EmitEvent(new PlayerCommandEvent { Command = ctx.Message }); return; } ctx.Player.PlayerEntity.Session.EmitEvent(new GmCommandEvent { Command = ctx.Message, PlayerAuthority = ctx.Player.Account.Authority, CommandAuthority = authorityAttribute.Authority }); return; } await HandleErrorAsync(result, ctx); } public void HandleCommand(string command, IClientSession sender, string prefix) { HandleCommandAsync(command, sender, prefix).ConfigureAwait(false).GetAwaiter().GetResult(); } public async Task HandleCommandAsync(string command, IClientSession sender, string prefix) { await HandleMessageAsync(command, sender, prefix); } /// /// This event is being invoked when the excecuted of a command threw an exception. /// Error results are handled by the result of CommandService#ExecuteAsync. /// /// Result with its associated exception. /// It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext) /// /// private Task _commands_CommandErrored(CommandExecutionFailedEventArgs e) { switch (e.Result.Exception) { default: Log.Debug($"{e.Result.Exception.GetType()} occured.\nError message: {e.Result.Exception.Message}.\nStack trace: {e.Result.Exception.StackTrace}"); break; } return Task.CompletedTask; } /// /// This event is being invoked when the execution of a command is over. When the command returned a result. /// It could be a custom result that we can cast from our instance of CommandResult. /// /// It represents the command that has been executed. /// /// It represents the returned result. It can an 'empty' result when the command returned a Task, or a /// custom result. /// /// It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext) /// private async Task _commands_CommandExecuted(CommandExecutedEventArgs e) { if (e.Context is not WingsEmuIngameCommandContext ctx) { Log.Debug($"Command context: {e.Context.GetType()}. This is bad. Please report this."); return; } Log.Debug($"The command {e.Context.Command.Name} (from player {ctx.Player.PlayerEntity.Name} [{ctx.Player.PlayerEntity.Id}]) has successfully been executed."); if (e.Result is SaltyCommandResult res && !string.IsNullOrWhiteSpace(res.Message)) { ctx.Player.SendChatMessage("[COMMAND] " + res.Message, e.Result.IsSuccessful ? ChatMessageColorType.Green : ChatMessageColorType.Red); } } /// /// This is being called when the CommandService returned an unsuccessfull result. /// /// This represents the generic result returned by the command service. We'll check what was wrong. /// This represents our context for this result. private async Task HandleErrorAsync(IResult result, WingsEmuIngameCommandContext ctx) { Log.Debug($"An error occured: {result}"); var errorBuilder = new StringBuilder(); bool help = false; switch (result) { case ChecksFailedResult ex: ctx.Command = ex.Command; Log.Debug("Some checks have failed: " + string.Join("\n", ex.FailedChecks.Select(x => x.Result))); break; case TypeParseFailedResult ex: ctx.Command = ex.Parameter.Command; errorBuilder.Append(ex.FailureReason); help = true; break; case CommandNotFoundResult ex: errorBuilder.Append($"The command was not found: {ctx.Input}"); break; case ArgumentParseFailedResult ex: ctx.Command = ex.Command; errorBuilder.Append(ex.Command.Parameters == null ? "Too many arguments." : $"The argument for the parameter {ex.Command.Name} was invalid."); help = true; break; case SaltyCommandResult ex: ctx.Command = ex.Command; errorBuilder.Append($"{ctx.Command.Name}: {ex.Message}"); break; case OverloadsFailedResult ex: ctx.Command = ex.FailedOverloads.Select(x => x.Key).FirstOrDefault(); Log.Debug($"Every overload failed: {string.Join("\n", ex.FailedOverloads.Select(x => x.Value.FailureReason))}"); errorBuilder.Append("Your command syntax was wrong."); help = true; break; } if (errorBuilder.Length == 0) { return; } ctx.Player.SendChatMessage(errorBuilder.ToString(), ChatMessageColorType.Red); if (help) { await _commands.ExecuteAsync($"help {ctx.Command.FullAliases[0]}", ctx); } } } }