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.Database.ModelExtensions;
using SCHALE.Common.FlatData; using SCHALE.Common.FlatData;
using SCHALE.GameServer.Controllers.Api.ProtocolHandlers;
using SCHALE.GameServer.Services;
using SCHALE.GameServer.Services.Irc; using SCHALE.GameServer.Services.Irc;
namespace SCHALE.GameServer.Commands namespace SCHALE.GameServer.Commands
{ {
[CommandHandler("character", "Unlock a character or all characters", "character unlock=all")] [CommandHandler("character", "Command to interact with user's characters", "/character <add|clear> [all|characterId]")]
public class CharacterCommand : Command internal class CharacterCommand : Command
{ {
[Argument("unlock")] public CharacterCommand(IrcConnection connection, string[] args, bool validate = true) : base(connection, args, validate) { }
public string? Unlock { get; set; }
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 switch (Op.ToLower())
if (Unlock is null)
{ {
connection.SendChatMessage($"Usage: /character unlock=<all|clear|shipId>"); case "add":
return; if (Target == "all")
}
if (Unlock.Equals("all", StringComparison.CurrentCultureIgnoreCase))
{ {
AddAllCharacters(connection); AddAllCharacters(connection);
connection.SendChatMessage("All Characters Added!"); 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))
{ {
connection.SendChatMessage($"{newChar.UniqueId} already exists!");
} else
{
connection.SendChatMessage($"Invalid Character Id: {characterId}");
return; 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(); connection.Context.SaveChanges();
base.NotifySuccess(connection);
} }
private void AddAllCharacters(IrcConnection connection) private void AddAllCharacters(IrcConnection connection)
@ -50,11 +74,20 @@ namespace SCHALE.GameServer.Commands
var characterExcel = connection.ExcelTableService.GetTable<CharacterExcelTable>().UnPack().DataList; 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 => 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() return new CharacterDB()
{ {
UniqueId = x.Id, UniqueId = characterId,
StarGrade = x.MaxStarGrade, StarGrade = 5,
Level = 90, Level = 90,
Exp = 0, Exp = 0,
PublicSkillLevel = 10, PublicSkillLevel = 10,
@ -68,10 +101,6 @@ namespace SCHALE.GameServer.Commands
PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } }, PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } },
EquipmentServerIds = [0, 0, 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 SCHALE.GameServer.Services.Irc;
using Serilog;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions;
namespace SCHALE.GameServer.Commands namespace SCHALE.GameServer.Commands
{ {
public abstract class Command
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CommandHandler : Attribute
{ {
public string Name { get; } protected IrcConnection connection;
public string Description { get; } protected string[] args;
public string Example { get; }
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; this.connection = connection;
Description = description; this.args = args;
Example = example;
} }
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)] [AttributeUsage(AttributeTargets.Property)]
public class ArgumentAttribute : Attribute 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 ArgumentFlags
public enum CommandUsage
{ {
None = 0, None = 0,
Console = 1, Optional = 1,
User = 2 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 Name = name;
Hint = hint;
Usage = usage;
}
}
public static class CommandFactory
{ {
var usage = CommandUsage.None; public static readonly Dictionary<string, Type> commands = new();
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;
return usage; public static void LoadCommands()
}
}
readonly Dictionary<string, PropertyInfo> argsProperties;
public Command()
{ {
argsProperties = GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) Log.Information("Loading commands...");
.Where(x => Attribute.IsDefined(x, typeof(ArgumentAttribute)))
.ToDictionary(x => ((ArgumentAttribute)Attribute.GetCustomAttribute(x, typeof(ArgumentAttribute))!).Key, StringComparer.OrdinalIgnoreCase);
}
public virtual void Execute(Dictionary<string, string> args) 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)
{ {
foreach (var arg in args) CommandHandlerAttribute nameAttr = command.GetCustomAttribute<CommandHandlerAttribute>()!;
commands.Add(nameAttr.Name, command);
#if DEBUG
Log.Information($"Loaded {nameAttr.Name} command");
#endif
}
Log.Information("Finished loading commands");
}
public static Command? CreateCommand(string name, IrcConnection connection, string[] args, bool validate = true)
{ {
if (argsProperties.TryGetValue(arg.Key, out var prop)) Type? command = commands.GetValueOrDefault(name);
prop.SetValue(this, arg.Value); if (command is null)
} return null;
}
public virtual void Execute(Dictionary<string, string> args, IrcConnection connection) var cmd = (Command)Activator.CreateInstance(command, new object[] { connection, args, validate })!;
{
Execute(args);
}
public virtual void NotifySuccess(IrcConnection connection) string? ret = cmd.Validate();
{ if (ret is not null && validate)
connection.SendChatMessage($"{GetType().Name} success! Please relog for it to take effect."); throw new ArgumentException(ret);
}
protected T Parse<T>(string? value, T fallback = default!) return cmd;
{
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;
}
}
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 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<Type, CommandHandler?> commandAttributes = new Dictionary<Type, CommandHandler?>(); public HelpCommand(IrcConnection connection, string[] args, bool validate = true) : base(connection, args, validate) { }
// doesnt support console yet public override void Execute()
public override void Execute(Dictionary<string, string> args) { // can't use newline, not gonna print args help for now
foreach (var command in CommandFactory.commands.Keys)
{ {
base.Execute(args); var cmdAtr = (CommandHandlerAttribute?)Attribute.GetCustomAttribute(CommandFactory.commands[command], typeof(CommandHandlerAttribute));
StringBuilder sb = new StringBuilder(); Command? cmd = CommandFactory.CreateCommand(command, connection, args, false);
sb.AppendLine("Available Commands: "); if (cmd is not null)
foreach (var command in CommandHandlerFactory.Commands.Where(x => x.Usage.HasFlag(CommandUsage.Console)))
{ {
if (!commandAttributes.TryGetValue(command.GetType(), out var attr)) connection.SendChatMessage($"{command} - {cmdAtr.Hint} (Usage: {cmdAtr.Usage})");
{
attr = command.GetType().GetCustomAttribute(typeof(CommandHandler)) as CommandHandler;
commandAttributes[command.GetType()] = attr;
} }
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; excelTableService = _excelTableService;
} }
// most handlers empty
[ProtocolHandler(Protocol.Account_CheckYostar)] [ProtocolHandler(Protocol.Account_CheckYostar)]
public ResponsePacket CheckYostarHandler(AccountCheckYostarRequest req) public ResponsePacket CheckYostarHandler(AccountCheckYostarRequest req)
{ {

View File

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

View File

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

View File

@ -150,7 +150,28 @@ namespace SCHALE.GameServer.Services.Irc
if (payload.Text.StartsWith('/')) 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);
}
} }
} }