server-master/srcs/WingsAPI.Commands/CommandHandler.cs
2026-02-10 18:21:30 +01:00

263 lines
No EOL
11 KiB
C#

// 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;
/// <summary>
/// This class should be instanciated with our Container.
/// </summary>
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<T>() where T : SaltyModuleBase
{
Log.Info($"[ADD_MODULE] {typeof(T).Name}");
_commands.AddModule<T>();
IReadOnlyList<Command> 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<T>() 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<T>(TypeParser<T> typeParser)
{
_commands.AddTypeParser(typeParser);
Log.Info($"[ADD_TYPE_PARSER] {typeParser.GetType().Name}");
}
/// <inheritdoc />
/// <summary>
/// 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.
/// </summary>
/// <param name="message">It represents the already parsed command with its parameters.</param>
/// <param name="entity">It represents the instance of the entity that performed the action of sending a message.</param>
/// <param name="prefix"></param>
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);
}
/// <summary>
/// This event is being invoked when the excecuted of a command threw an exception.
/// Error results are handled by the result of CommandService#ExecuteAsync.
/// </summary>
/// <param name="result">Result with its associated exception.</param>
/// <param name="context">It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext)</param>
/// <param name="services"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="command">It represents the command that has been executed.</param>
/// <param name="result">
/// It represents the returned result. It can an 'empty' result when the command returned a Task, or a
/// custom result.
/// </param>
/// <param name="context">It represents the context. Must be casted to our custom context (WingsEmuIngameCommandContext)</param>
/// <returns></returns>
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);
}
}
/// <summary>
/// This is being called when the CommandService returned an unsuccessfull result.
/// </summary>
/// <param name="result">This represents the generic result returned by the command service. We'll check what was wrong.</param>
/// <param name="ctx">This represents our context for this result.</param>
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);
}
}
}
}