switch to ascnet's command system (no hate AL)

This commit is contained in:
raphaeIl 2024-05-14 17:28:18 -04:00
parent 1f697b42d5
commit 3bdcfd22a0
7 changed files with 204 additions and 222 deletions

View File

@ -1,46 +1,70 @@
using SCHALE.Common.Database;
using Microsoft.AspNetCore.Http.Connections;
using SCHALE.Common.Database;
using SCHALE.Common.Database.ModelExtensions;
using SCHALE.Common.FlatData;
using SCHALE.GameServer.Controllers.Api.ProtocolHandlers;
using SCHALE.GameServer.Services;
using SCHALE.GameServer.Services.Irc;
namespace SCHALE.GameServer.Commands
{
[CommandHandler("character", "Unlock a character or all characters", "character unlock=all")]
public class CharacterCommand : Command
[CommandHandler("character", "Command to interact with user's characters", "/character <add|clear> [all|characterId]")]
internal class CharacterCommand : Command
{
[Argument("unlock")]
public string? Unlock { get; set; }
public CharacterCommand(IrcConnection connection, string[] args, bool validate = true) : base(connection, args, validate) { }
public override void Execute(Dictionary<string, string> args, IrcConnection connection)
[Argument(0, @"^add$|^clear$", "The operation selected (add, clear)", ArgumentFlags.IgnoreCase)]
public string Op { get; set; } = string.Empty;
[Argument(1, @"^[0-9]+$|^all$", "The target character, value is item id or 'all'", ArgumentFlags.Optional)]
public string Target { get; set; } = string.Empty;
public override void Execute()
{
base.Execute(args);
var characterDB = connection.Context.Characters;
// TODO: finish this
if (Unlock is null)
switch (Op.ToLower())
{
connection.SendChatMessage($"Usage: /character unlock=<all|clear|shipId>");
return;
}
if (Unlock.Equals("all", StringComparison.CurrentCultureIgnoreCase))
case "add":
if (Target == "all")
{
AddAllCharacters(connection);
connection.SendChatMessage("All Characters Added!");
} else if (Unlock.Equals("clear", StringComparison.CurrentCultureIgnoreCase))
}
else if (uint.TryParse(Target, out uint characterId))
{
var newChar = CreateMaxCharacterFromId(characterId);
} else if (uint.TryParse(Unlock, out uint characterId))
if (characterDB.Any(x => x.UniqueId == newChar.UniqueId))
{
} else
{
connection.SendChatMessage($"Invalid Character Id: {characterId}");
connection.SendChatMessage($"{newChar.UniqueId} already exists!");
return;
}
connection.Account.AddCharacters(connection.Context, [newChar]);
connection.SendChatMessage($"{newChar.UniqueId} added!");
}
else
{
throw new ArgumentException("Invalid Target / Amount!");
}
break;
case "clear":
var defaultCharacters = connection.ExcelTableService.GetTable<DefaultCharacterExcelTable>().UnPack().DataList.Select(x => x.CharacterId).ToList();
var removed = characterDB.Where(x => x.AccountServerId == connection.AccountServerId && !defaultCharacters.Contains(x.UniqueId));
characterDB.RemoveRange(removed);
connection.SendChatMessage($"Removed {removed.Count()} characters!");
break;
default:
connection.SendChatMessage($"Usage: /character unlock=<all|clear|characterId>");
throw new InvalidOperationException("Invalid operation!");
}
connection.Context.SaveChanges();
base.NotifySuccess(connection);
}
private void AddAllCharacters(IrcConnection connection)
@ -50,11 +74,20 @@ namespace SCHALE.GameServer.Commands
var characterExcel = connection.ExcelTableService.GetTable<CharacterExcelTable>().UnPack().DataList;
var allCharacters = characterExcel.Where(x => x.IsPlayable && x.IsPlayableCharacter && x.CollectionVisible && !account.Characters.Any(c => c.UniqueId == x.Id)).Select(x =>
{
return CreateMaxCharacterFromId(x.Id);
}).ToList();
account.AddCharacters(context, [.. allCharacters]);
connection.Context.SaveChanges();
}
private CharacterDB CreateMaxCharacterFromId(long characterId)
{
return new CharacterDB()
{
UniqueId = x.Id,
StarGrade = x.MaxStarGrade,
UniqueId = characterId,
StarGrade = 5,
Level = 90,
Exp = 0,
PublicSkillLevel = 10,
@ -68,10 +101,6 @@ namespace SCHALE.GameServer.Commands
PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } },
EquipmentServerIds = [0, 0, 0]
};
}).ToList();
account.AddCharacters(context, [.. allCharacters]);
connection.Context.SaveChanges();
}
}

View File

@ -1,177 +1,134 @@
using SCHALE.GameServer.Services.Irc;
using Serilog;
using System.Reflection;
using System.Text.RegularExpressions;
namespace SCHALE.GameServer.Commands
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CommandHandler : Attribute
public abstract class Command
{
public string Name { get; }
public string Description { get; }
public string Example { get; }
protected IrcConnection connection;
protected string[] args;
public CommandHandler(string commandName, string description, string example)
/// <summary>
/// </summary>
/// <param name="connection"></param>
/// <param name="args"></param>
/// <exception cref="ArgumentException"></exception>
public Command(IrcConnection connection, string[] args, bool validate = true)
{
Name = commandName;
Description = description;
Example = example;
this.connection = connection;
this.args = args;
}
public string? Validate()
{
List<PropertyInfo> argsProperties = GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute(typeof(ArgumentAttribute)) is not null).ToList();
if (argsProperties.Where(x => (((ArgumentAttribute)x.GetCustomAttribute(typeof(ArgumentAttribute))!).Flags & ArgumentFlags.Optional) != ArgumentFlags.Optional).Count() > args.Length)
return "Invalid args length!";
foreach (var argProp in argsProperties)
{
ArgumentAttribute attr = (ArgumentAttribute)argProp.GetCustomAttribute(typeof(ArgumentAttribute))!;
if (attr.Position + 1 > args.Length && (attr.Flags & ArgumentFlags.Optional) != ArgumentFlags.Optional)
return $"Argument {argProp.Name} is required!";
else if (attr.Position + 1 > args.Length)
return null;
if (!attr.Pattern.IsMatch(args[attr.Position]))
return $"Argument {argProp.Name} is invalid!";
argProp.SetValue(this, args[attr.Position]);
}
return null;
}
public abstract void Execute();
}
[AttributeUsage(AttributeTargets.Property)]
public class ArgumentAttribute : Attribute
{
public string Key { get; }
public int Position { get; }
public Regex Pattern { get; set; }
public string? Description { get; }
public ArgumentFlags Flags { get; }
public ArgumentAttribute(string key)
public ArgumentAttribute(int position, string pattern, string? description = null, ArgumentFlags flags = ArgumentFlags.None)
{
Key = key;
Position = position;
if ((flags & ArgumentFlags.IgnoreCase) != ArgumentFlags.IgnoreCase)
Pattern = new(pattern);
else
Pattern = new(pattern, RegexOptions.IgnoreCase);
Description = description;
Flags = flags;
}
}
[Flags]
public enum CommandUsage
public enum ArgumentFlags
{
None = 0,
Console = 1,
User = 2
Optional = 1,
IgnoreCase = 2
}
public abstract class Command
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CommandHandlerAttribute : Attribute
{
public virtual CommandUsage Usage
{
get
{
var usage = CommandUsage.None;
if (GetType().GetMethod(nameof(Execute), [typeof(Dictionary<string, string>), typeof(IrcConnection)])?.DeclaringType == GetType())
usage |= CommandUsage.User;
if (GetType().GetMethod(nameof(Execute), [typeof(Dictionary<string, string>)])?.DeclaringType == GetType())
usage |= CommandUsage.Console;
public string Name { get; }
return usage;
public string Hint { get; }
public string Usage { get; }
public CommandHandlerAttribute(string name, string hint, string usage)
{
Name = name;
Hint = hint;
Usage = usage;
}
}
readonly Dictionary<string, PropertyInfo> argsProperties;
public Command()
public static class CommandFactory
{
argsProperties = GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(x => Attribute.IsDefined(x, typeof(ArgumentAttribute)))
.ToDictionary(x => ((ArgumentAttribute)Attribute.GetCustomAttribute(x, typeof(ArgumentAttribute))!).Key, StringComparer.OrdinalIgnoreCase);
public static readonly Dictionary<string, Type> commands = new();
public static void LoadCommands()
{
Log.Information("Loading commands...");
IEnumerable<Type> classes = from t in Assembly.GetExecutingAssembly().GetTypes()
where t.IsClass && t.GetCustomAttribute<CommandHandlerAttribute>() is not null
select t;
foreach (var command in classes)
{
CommandHandlerAttribute nameAttr = command.GetCustomAttribute<CommandHandlerAttribute>()!;
commands.Add(nameAttr.Name, command);
#if DEBUG
Log.Information($"Loaded {nameAttr.Name} command");
#endif
}
public virtual void Execute(Dictionary<string, string> args)
{
foreach (var arg in args)
{
if (argsProperties.TryGetValue(arg.Key, out var prop))
prop.SetValue(this, arg.Value);
}
Log.Information("Finished loading commands");
}
public virtual void Execute(Dictionary<string, string> args, IrcConnection connection)
public static Command? CreateCommand(string name, IrcConnection connection, string[] args, bool validate = true)
{
Execute(args);
}
Type? command = commands.GetValueOrDefault(name);
if (command is null)
return null;
public virtual void NotifySuccess(IrcConnection connection)
{
connection.SendChatMessage($"{GetType().Name} success! Please relog for it to take effect.");
}
var cmd = (Command)Activator.CreateInstance(command, new object[] { connection, args, validate })!;
protected T Parse<T>(string? value, T fallback = default!)
{
var tryParseMethod = typeof(T).GetMethod("TryParse", [typeof(string), typeof(T).MakeByRefType()]);
string? ret = cmd.Validate();
if (ret is not null && validate)
throw new ArgumentException(ret);
if (tryParseMethod != null)
{
var parameters = new object[] { value!, null! };
bool success = (bool)tryParseMethod.Invoke(null, parameters)!;
if (success)
return (T)parameters[1];
}
return fallback;
return cmd;
}
}
public static class CommandHandlerFactory
{
public static readonly List<Command> Commands = new List<Command>();
static readonly Dictionary<string, Action<Dictionary<string, string>>> commandFunctions;
static readonly Dictionary<string, Action<Dictionary<string, string>, IrcConnection>> commandFunctionsConn;
private static readonly char[] separator = new[] { ' ' };
static CommandHandlerFactory()
{
commandFunctions = new Dictionary<string, Action<Dictionary<string, string>>>(StringComparer.OrdinalIgnoreCase);
commandFunctionsConn = new Dictionary<string, Action<Dictionary<string, string>, IrcConnection>>(StringComparer.OrdinalIgnoreCase);
RegisterCommands(Assembly.GetExecutingAssembly());
}
public static void RegisterCommands(Assembly assembly)
{
var commandTypes = assembly.GetTypes()
.Where(t => Attribute.IsDefined(t, typeof(CommandHandler)) && typeof(Command).IsAssignableFrom(t));
foreach (var commandType in commandTypes)
{
var commandAttribute = (CommandHandler?)Attribute.GetCustomAttribute(commandType, typeof(CommandHandler));
if (commandAttribute != null)
{
var commandInstance = (Command)Activator.CreateInstance(commandType)!;
if (commandInstance.Usage.HasFlag(CommandUsage.Console))
commandFunctions[commandAttribute.Name] = commandInstance.Execute;
if (commandInstance.Usage.HasFlag(CommandUsage.User))
commandFunctionsConn[commandAttribute.Name] = commandInstance.Execute;
Commands.Add(commandInstance);
}
}
}
public static void HandleCommand(string commandLine, IrcConnection? connection = null)
{
var parts = commandLine.Split(separator, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
return;
var arguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for (var i = 1; i < parts.Length; i++)
{
var argParts = parts[i].Split('=', 2);
if (argParts.Length == 2)
arguments[argParts[0]] = argParts[1];
}
if (connection is not null)
{
if (!(commandFunctionsConn).TryGetValue(parts[0], out var command))
{
connection.SendChatMessage($"Unknown command: {parts[0]}");
return;
}
command(arguments, connection);
} else
{
if (!(commandFunctions).TryGetValue(parts[0], out var command))
{
connection.SendChatMessage($"Unknown command: {parts[0]}");
return;
}
command(arguments);
}
}
}
}

View File

@ -6,52 +6,25 @@ using System.Text;
namespace SCHALE.GameServer.Commands
{
[CommandHandler("help", "Print out all commands with their description and example", "help")]
public class HelpCommand : Command
{
static readonly Dictionary<Type, CommandHandler?> commandAttributes = new Dictionary<Type, CommandHandler?>();
// doesnt support console yet
public override void Execute(Dictionary<string, string> args)
[CommandHandler("help", "Show this help.", "/help")]
internal class HelpCommand : Command
{
base.Execute(args);
public HelpCommand(IrcConnection connection, string[] args, bool validate = true) : base(connection, args, validate) { }
StringBuilder sb = new StringBuilder();
public override void Execute()
{ // can't use newline, not gonna print args help for now
foreach (var command in CommandFactory.commands.Keys)
{
var cmdAtr = (CommandHandlerAttribute?)Attribute.GetCustomAttribute(CommandFactory.commands[command], typeof(CommandHandlerAttribute));
sb.AppendLine("Available Commands: ");
foreach (var command in CommandHandlerFactory.Commands.Where(x => x.Usage.HasFlag(CommandUsage.Console)))
Command? cmd = CommandFactory.CreateCommand(command, connection, args, false);
if (cmd is not null)
{
if (!commandAttributes.TryGetValue(command.GetType(), out var attr))
{
attr = command.GetType().GetCustomAttribute(typeof(CommandHandler)) as CommandHandler;
commandAttributes[command.GetType()] = attr;
connection.SendChatMessage($"{command} - {cmdAtr.Hint} (Usage: {cmdAtr.Usage})");
}
if (attr != null)
sb.AppendLine($"{attr.Name} - {attr.Description} (Example: {attr.Example})");
}
Console.Write(sb.ToString());
}
public override void Execute(Dictionary<string, string> args, IrcConnection connection)
{
base.Execute(args);
connection.SendChatMessage("Available Commands: ");
foreach (var command in CommandHandlerFactory.Commands.Where(x => x.Usage.HasFlag(CommandUsage.User)))
{
if (!commandAttributes.TryGetValue(command.GetType(), out var attr))
{
attr = command.GetType().GetCustomAttribute(typeof(CommandHandler)) as CommandHandler;
commandAttributes[command.GetType()] = attr;
}
if (attr != null)
connection.SendChatMessage($"{attr.Name} - {attr.Description} (Example: {attr.Example})");
}
base.NotifySuccess(connection);
}
}

View File

@ -19,7 +19,6 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
excelTableService = _excelTableService;
}
// most handlers empty
[ProtocolHandler(Protocol.Account_CheckYostar)]
public ResponsePacket CheckYostarHandler(AccountCheckYostarRequest req)
{

View File

@ -26,8 +26,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{
IrcConfig = new()
{
//HostAddress = "10.0.0.149",
HostAddress = "192.168.86.35",
HostAddress = REPLACE WITH YOUR IP,
Port = 6667,
Password = ""
},

View File

@ -7,6 +7,7 @@ using SCHALE.GameServer.Controllers.Api.ProtocolHandlers;
using SCHALE.GameServer.Services;
using Microsoft.EntityFrameworkCore;
using SCHALE.GameServer.Services.Irc;
using SCHALE.GameServer.Commands;
namespace SCHALE.GameServer
{
@ -43,6 +44,9 @@ namespace SCHALE.GameServer
Log.Information("Starting...");
try
{
// Load Commands
CommandFactory.LoadCommands();
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<KestrelServerOptions>(op => op.AllowSynchronousIO = true);

View File

@ -150,7 +150,28 @@ namespace SCHALE.GameServer.Services.Irc
if (payload.Text.StartsWith('/'))
{
CommandHandlerFactory.HandleCommand(payload.Text.Substring(1), clients[client]);
var cmdStrings = payload.Text.Split(" ");
var connection = clients[client];
var cmdStr = cmdStrings.First().Split('/').Last();
try
{
Command? cmd = CommandFactory.CreateCommand(cmdStr, connection, cmdStrings[1..]);
if (cmd is null)
{
connection.SendChatMessage($"Invalid command {cmdStr}, try /help");
return;
}
cmd?.Execute();
connection.SendChatMessage($"Command {cmdStr} executed sucessfully! Please relog for it to take effect.");
}
catch (Exception ex)
{
connection.SendChatMessage($"Command {cmdStr} failed to execute!, " + ex.Message);
}
}
}