From 3bdcfd22a011ac34a084bc899191b6775bd32713 Mon Sep 17 00:00:00 2001 From: raphaeIl Date: Tue, 14 May 2024 17:28:18 -0400 Subject: [PATCH] switch to ascnet's command system (no hate AL) --- .../Commands/CharacterCommand.cs | 113 +++++---- SCHALE.GameServer/Commands/Command.cs | 227 +++++++----------- SCHALE.GameServer/Commands/HelpCommand.cs | 53 +--- .../Api/ProtocolHandlers/Account.cs | 1 - .../Controllers/Api/ProtocolHandlers/Clan.cs | 3 +- SCHALE.GameServer/GameServer.cs | 4 + SCHALE.GameServer/Services/Irc/IrcServer.cs | 25 +- 7 files changed, 204 insertions(+), 222 deletions(-) diff --git a/SCHALE.GameServer/Commands/CharacterCommand.cs b/SCHALE.GameServer/Commands/CharacterCommand.cs index 8eb22b5..3d95342 100644 --- a/SCHALE.GameServer/Commands/CharacterCommand.cs +++ b/SCHALE.GameServer/Commands/CharacterCommand.cs @@ -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 [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 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="); - return; - } + case "add": + if (Target == "all") + { + AddAllCharacters(connection); - if (Unlock.Equals("all", StringComparison.CurrentCultureIgnoreCase)) - { - AddAllCharacters(connection); + connection.SendChatMessage("All Characters Added!"); + } + else if (uint.TryParse(Target, out uint characterId)) + { + var newChar = CreateMaxCharacterFromId(characterId); + + if (characterDB.Any(x => x.UniqueId == newChar.UniqueId)) + { + connection.SendChatMessage($"{newChar.UniqueId} already exists!"); + return; + } - connection.SendChatMessage("All Characters Added!"); - } else if (Unlock.Equals("clear", StringComparison.CurrentCultureIgnoreCase)) - { + connection.Account.AddCharacters(connection.Context, [newChar]); + connection.SendChatMessage($"{newChar.UniqueId} added!"); + } + else + { + throw new ArgumentException("Invalid Target / Amount!"); + } - } else if (uint.TryParse(Unlock, out uint characterId)) - { + break; + case "clear": + var defaultCharacters = connection.ExcelTableService.GetTable().UnPack().DataList.Select(x => x.CharacterId).ToList(); - } else - { - connection.SendChatMessage($"Invalid Character Id: {characterId}"); - return; + 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="); + throw new InvalidOperationException("Invalid operation!"); } connection.Context.SaveChanges(); - base.NotifySuccess(connection); } private void AddAllCharacters(IrcConnection connection) @@ -51,28 +75,33 @@ namespace SCHALE.GameServer.Commands var characterExcel = connection.ExcelTableService.GetTable().UnPack().DataList; var allCharacters = characterExcel.Where(x => x.IsPlayable && x.IsPlayableCharacter && x.CollectionVisible && !account.Characters.Any(c => c.UniqueId == x.Id)).Select(x => { - return new CharacterDB() - { - UniqueId = x.Id, - StarGrade = x.MaxStarGrade, - Level = 90, - Exp = 0, - PublicSkillLevel = 10, - ExSkillLevel = 5, - PassiveSkillLevel = 10, - ExtraPassiveSkillLevel = 10, - LeaderSkillLevel = 1, - FavorRank = 500, - IsNew = true, - IsLocked = true, - PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } }, - EquipmentServerIds = [0, 0, 0] - }; + return CreateMaxCharacterFromId(x.Id); }).ToList(); account.AddCharacters(context, [.. allCharacters]); connection.Context.SaveChanges(); } + private CharacterDB CreateMaxCharacterFromId(long characterId) + { + return new CharacterDB() + { + UniqueId = characterId, + StarGrade = 5, + Level = 90, + Exp = 0, + PublicSkillLevel = 10, + ExSkillLevel = 5, + PassiveSkillLevel = 10, + ExtraPassiveSkillLevel = 10, + LeaderSkillLevel = 1, + FavorRank = 500, + IsNew = true, + IsLocked = true, + PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } }, + EquipmentServerIds = [0, 0, 0] + }; + } + } } diff --git a/SCHALE.GameServer/Commands/Command.cs b/SCHALE.GameServer/Commands/Command.cs index 24e0c7d..714257a 100644 --- a/SCHALE.GameServer/Commands/Command.cs +++ b/SCHALE.GameServer/Commands/Command.cs @@ -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) + /// + /// + /// + /// + /// + 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 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 + public string Name { get; } + + public string Hint { get; } + + public string Usage { get; } + + public CommandHandlerAttribute(string name, string hint, string usage) { - get - { - var usage = CommandUsage.None; - if (GetType().GetMethod(nameof(Execute), [typeof(Dictionary), typeof(IrcConnection)])?.DeclaringType == GetType()) - usage |= CommandUsage.User; - if (GetType().GetMethod(nameof(Execute), [typeof(Dictionary)])?.DeclaringType == GetType()) - usage |= CommandUsage.Console; - - return usage; - } - } - - readonly Dictionary argsProperties; - - public Command() - { - 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 virtual void Execute(Dictionary args) - { - foreach (var arg in args) - { - if (argsProperties.TryGetValue(arg.Key, out var prop)) - prop.SetValue(this, arg.Value); - } - } - - public virtual void Execute(Dictionary args, IrcConnection connection) - { - Execute(args); - } - - public virtual void NotifySuccess(IrcConnection connection) - { - connection.SendChatMessage($"{GetType().Name} success! Please relog for it to take effect."); - } - - protected T Parse(string? value, T fallback = default!) - { - var tryParseMethod = typeof(T).GetMethod("TryParse", [typeof(string), typeof(T).MakeByRefType()]); - - if (tryParseMethod != null) - { - var parameters = new object[] { value!, null! }; - bool success = (bool)tryParseMethod.Invoke(null, parameters)!; - - if (success) - return (T)parameters[1]; - } - - return fallback; + Name = name; + Hint = hint; + Usage = usage; } } - public static class CommandHandlerFactory + public static class CommandFactory { - public static readonly List Commands = new List(); + public static readonly Dictionary commands = new(); - static readonly Dictionary>> commandFunctions; - static readonly Dictionary, IrcConnection>> commandFunctionsConn; - private static readonly char[] separator = new[] { ' ' }; - - static CommandHandlerFactory() + public static void LoadCommands() { - commandFunctions = new Dictionary>>(StringComparer.OrdinalIgnoreCase); - commandFunctionsConn = new Dictionary, IrcConnection>>(StringComparer.OrdinalIgnoreCase); + Log.Information("Loading commands..."); - RegisterCommands(Assembly.GetExecutingAssembly()); + IEnumerable classes = from t in Assembly.GetExecutingAssembly().GetTypes() + where t.IsClass && t.GetCustomAttribute() is not null + select t; + + foreach (var command in classes) + { + CommandHandlerAttribute nameAttr = command.GetCustomAttribute()!; + commands.Add(nameAttr.Name, command); +#if DEBUG + Log.Information($"Loaded {nameAttr.Name} command"); +#endif + } + + Log.Information("Finished loading commands"); } - public static void RegisterCommands(Assembly assembly) + public static Command? CreateCommand(string name, IrcConnection connection, string[] args, bool validate = true) { - var commandTypes = assembly.GetTypes() - .Where(t => Attribute.IsDefined(t, typeof(CommandHandler)) && typeof(Command).IsAssignableFrom(t)); + Type? command = commands.GetValueOrDefault(name); + if (command is null) + return null; - foreach (var commandType in commandTypes) - { - var commandAttribute = (CommandHandler?)Attribute.GetCustomAttribute(commandType, typeof(CommandHandler)); - if (commandAttribute != null) - { - var commandInstance = (Command)Activator.CreateInstance(commandType)!; + var cmd = (Command)Activator.CreateInstance(command, new object[] { connection, args, validate })!; - if (commandInstance.Usage.HasFlag(CommandUsage.Console)) - commandFunctions[commandAttribute.Name] = commandInstance.Execute; - if (commandInstance.Usage.HasFlag(CommandUsage.User)) - commandFunctionsConn[commandAttribute.Name] = commandInstance.Execute; + string? ret = cmd.Validate(); + if (ret is not null && validate) + throw new ArgumentException(ret); - 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(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); - } + return cmd; } } - } diff --git a/SCHALE.GameServer/Commands/HelpCommand.cs b/SCHALE.GameServer/Commands/HelpCommand.cs index 4e1ca81..fec0a6d 100644 --- a/SCHALE.GameServer/Commands/HelpCommand.cs +++ b/SCHALE.GameServer/Commands/HelpCommand.cs @@ -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 + + [CommandHandler("help", "Show this help.", "/help")] + internal class HelpCommand : Command { - static readonly Dictionary commandAttributes = new Dictionary(); + public HelpCommand(IrcConnection connection, string[] args, bool validate = true) : base(connection, args, validate) { } - // doesnt support console yet - public override void Execute(Dictionary args) - { - base.Execute(args); - - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("Available Commands: "); - foreach (var command in CommandHandlerFactory.Commands.Where(x => x.Usage.HasFlag(CommandUsage.Console))) + public override void Execute() + { // can't use newline, not gonna print args help for now + foreach (var command in CommandFactory.commands.Keys) { - if (!commandAttributes.TryGetValue(command.GetType(), out var attr)) + var cmdAtr = (CommandHandlerAttribute?)Attribute.GetCustomAttribute(CommandFactory.commands[command], typeof(CommandHandlerAttribute)); + + Command? cmd = CommandFactory.CreateCommand(command, connection, args, false); + + if (cmd is not null) { - 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 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); } } diff --git a/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Account.cs b/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Account.cs index 04aeb16..5764bcd 100644 --- a/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Account.cs +++ b/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Account.cs @@ -19,7 +19,6 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers excelTableService = _excelTableService; } - // most handlers empty [ProtocolHandler(Protocol.Account_CheckYostar)] public ResponsePacket CheckYostarHandler(AccountCheckYostarRequest req) { diff --git a/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Clan.cs b/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Clan.cs index 37b489f..784220b 100644 --- a/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Clan.cs +++ b/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Clan.cs @@ -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 = "" }, diff --git a/SCHALE.GameServer/GameServer.cs b/SCHALE.GameServer/GameServer.cs index 3fb92f5..cf62032 100644 --- a/SCHALE.GameServer/GameServer.cs +++ b/SCHALE.GameServer/GameServer.cs @@ -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(op => op.AllowSynchronousIO = true); diff --git a/SCHALE.GameServer/Services/Irc/IrcServer.cs b/SCHALE.GameServer/Services/Irc/IrcServer.cs index 71213f9..c6906a0 100644 --- a/SCHALE.GameServer/Services/Irc/IrcServer.cs +++ b/SCHALE.GameServer/Services/Irc/IrcServer.cs @@ -147,10 +147,31 @@ namespace SCHALE.GameServer.Services.Irc //logger.LogDebug("payload: " + payloadStr); var payload = JsonSerializer.Deserialize(payloadStr, typeof(IrcMessage)) as IrcMessage; - + 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); + } } }