forked from Raphael/SCHALE.GameServer
switch to ascnet's command system (no hate AL)
This commit is contained in:
parent
1f697b42d5
commit
3bdcfd22a0
|
@ -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;
|
||||
}
|
||||
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);
|
||||
|
||||
connection.SendChatMessage("All Characters Added!");
|
||||
} else if (Unlock.Equals("clear", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
if (characterDB.Any(x => x.UniqueId == newChar.UniqueId))
|
||||
{
|
||||
connection.SendChatMessage($"{newChar.UniqueId} already exists!");
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (uint.TryParse(Unlock, out uint characterId))
|
||||
{
|
||||
connection.Account.AddCharacters(connection.Context, [newChar]);
|
||||
connection.SendChatMessage($"{newChar.UniqueId} added!");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid Target / Amount!");
|
||||
}
|
||||
|
||||
} else
|
||||
{
|
||||
connection.SendChatMessage($"Invalid Character Id: {characterId}");
|
||||
return;
|
||||
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)
|
||||
|
@ -51,28 +75,33 @@ 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 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]
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
readonly Dictionary<string, PropertyInfo> 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<string, string> args)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (argsProperties.TryGetValue(arg.Key, out var prop))
|
||||
prop.SetValue(this, arg.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Execute(Dictionary<string, string> 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<T>(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<Command> Commands = new List<Command>();
|
||||
public static readonly Dictionary<string, Type> commands = new();
|
||||
|
||||
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()
|
||||
public static void LoadCommands()
|
||||
{
|
||||
commandFunctions = new Dictionary<string, Action<Dictionary<string, string>>>(StringComparer.OrdinalIgnoreCase);
|
||||
commandFunctionsConn = new Dictionary<string, Action<Dictionary<string, string>, IrcConnection>>(StringComparer.OrdinalIgnoreCase);
|
||||
Log.Information("Loading commands...");
|
||||
|
||||
RegisterCommands(Assembly.GetExecutingAssembly());
|
||||
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
|
||||
}
|
||||
|
||||
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<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);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<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(Dictionary<string, string> 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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
|
|||
excelTableService = _excelTableService;
|
||||
}
|
||||
|
||||
// most handlers empty
|
||||
[ProtocolHandler(Protocol.Account_CheckYostar)]
|
||||
public ResponsePacket CheckYostarHandler(AccountCheckYostarRequest req)
|
||||
{
|
||||
|
|
|
@ -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 = ""
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue