From 54d093962db099af255fe9e751fee91922dfc7f1 Mon Sep 17 00:00:00 2001 From: Junior Date: Mon, 23 Sep 2024 21:59:21 -0400 Subject: [PATCH] Fix CommandHandler/Registry --- .../MinecraftTimeArgumentParser.cs | 43 +++++++ Obsidian.API/_Interfaces/IWorld.cs | 4 +- Obsidian.API/_Types/MinecraftTime.cs | 41 +++++++ Obsidian/Commands/Framework/CommandHandler.cs | 69 ++++++----- .../Commands/Framework/Entities/Command.cs | 7 +- .../{ => Modules}/MainCommandModule.cs | 23 +--- .../Commands/Modules/TimeCommandModule.cs | 78 ++++++++++++ .../Commands/Parsers/MinecraftTimeParser.cs | 18 +++ Obsidian/Obsidian.csproj | 7 +- Obsidian/Registries/CommandsRegistry.cs | 114 +++++++++++------- Obsidian/Server.cs | 8 +- Obsidian/Services/EventDispatcher.cs | 15 ++- Obsidian/WorldData/World.cs | 26 +++- 13 files changed, 348 insertions(+), 105 deletions(-) create mode 100644 Obsidian.API/Commands/ArgumentParsers/MinecraftTimeArgumentParser.cs create mode 100644 Obsidian.API/_Types/MinecraftTime.cs rename Obsidian/Commands/{ => Modules}/MainCommandModule.cs (96%) create mode 100644 Obsidian/Commands/Modules/TimeCommandModule.cs create mode 100644 Obsidian/Commands/Parsers/MinecraftTimeParser.cs diff --git a/Obsidian.API/Commands/ArgumentParsers/MinecraftTimeArgumentParser.cs b/Obsidian.API/Commands/ArgumentParsers/MinecraftTimeArgumentParser.cs new file mode 100644 index 00000000..6e97b794 --- /dev/null +++ b/Obsidian.API/Commands/ArgumentParsers/MinecraftTimeArgumentParser.cs @@ -0,0 +1,43 @@ +namespace Obsidian.API.Commands.ArgumentParsers; + +public sealed class MinecraftTimeArgumentParser : BaseArgumentParser +{ + public MinecraftTimeArgumentParser() : base(42, "minecraft:time") + { + } + + public override bool TryParseArgument(string input, CommandContext ctx, out MinecraftTime result) + { + var lastChar = input.LastOrDefault(); + var isSuccess = false; + + result = default; + + if (lastChar == 'd' && int.TryParse(input.TrimEnd('d'), out var dayTime)) + { + result = MinecraftTime.FromDay(dayTime); + + isSuccess = true; + } + else if (lastChar == 't' && int.TryParse(input.TrimEnd('t'), out var tickTime)) + { + result = MinecraftTime.FromTick(tickTime); + + isSuccess = true; + } + else if (lastChar == 's' && int.TryParse(input.TrimEnd('s'), out var secondsTime)) + { + result = MinecraftTime.FromSecond(secondsTime); + + isSuccess = true; + } + else if (int.TryParse(input, out var intValue)) + { + result = MinecraftTime.FromDay(intValue); + + isSuccess = true; + } + + return isSuccess; + } +} diff --git a/Obsidian.API/_Interfaces/IWorld.cs b/Obsidian.API/_Interfaces/IWorld.cs index 502fdf79..a75de4e3 100644 --- a/Obsidian.API/_Interfaces/IWorld.cs +++ b/Obsidian.API/_Interfaces/IWorld.cs @@ -8,8 +8,8 @@ public interface IWorld : IAsyncDisposable public string DimensionName { get; } - public long Time { get; } - + public long Time { get; set; } + public int DayTime { get; set; } public string Seed { get; } public Gamemode DefaultGamemode { get; } diff --git a/Obsidian.API/_Types/MinecraftTime.cs b/Obsidian.API/_Types/MinecraftTime.cs new file mode 100644 index 00000000..439bf2a6 --- /dev/null +++ b/Obsidian.API/_Types/MinecraftTime.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Obsidian.API; +public readonly struct MinecraftTime +{ + public int? Day { get; private init; } + + public int? Second { get; private init; } + + public int? Tick { get; private init; } + + public static MinecraftTime FromDay(int day) => new() { Day = day }; + + public static MinecraftTime FromSecond(int second) => new() { Second = second }; + + public static MinecraftTime FromTick(int tick) => new() { Tick = tick }; + + public bool ConvertServerTime(IServer server) + { + var success = false; + + if (this.Day.HasValue) + { + server.DefaultWorld.Time = this.Day.Value * 24000; + success = true; + } + else if (this.Second.HasValue) + { + server.DefaultWorld.Time = this.Second.Value * 20; + success = true; + } + else if (this.Tick.HasValue) + { + server.DefaultWorld.Time = this.Tick.Value; + success = true; + } + + return success; + } +} diff --git a/Obsidian/Commands/Framework/CommandHandler.cs b/Obsidian/Commands/Framework/CommandHandler.cs index 677c0c7c..ffd182dd 100644 --- a/Obsidian/Commands/Framework/CommandHandler.cs +++ b/Obsidian/Commands/Framework/CommandHandler.cs @@ -56,7 +56,7 @@ public BaseArgumentParser GetArgumentParser(Type argumentType) => public Command[] GetAllCommands() => _commands.ToArray(); - public void RegisterCommand(PluginContainer pluginContainer, string name, Delegate commandDelegate) + public void RegisterCommand(PluginContainer? pluginContainer, string name, Delegate commandDelegate) { var method = commandDelegate.Method; @@ -90,20 +90,56 @@ public void RegisterCommand(PluginContainer pluginContainer, string name, Delega public void RegisterCommandClass(PluginContainer? plugin, Type moduleType) { + if (moduleType.GetCustomAttribute() != null) + { + this.RegisterGroupCommand(moduleType, plugin, null); + return; + } + RegisterSubgroups(moduleType, plugin); RegisterSubcommands(moduleType, plugin); } - public void RegisterCommands(PluginContainer pluginContainer) + public void RegisterCommands(PluginContainer? pluginContainer = null) { - // Registering commands found in the plugin assembly - var commandRoots = pluginContainer.PluginAssembly.GetTypes().Where(x => x.IsSubclassOf(typeof(CommandModuleBase))); + var assembly = pluginContainer?.PluginAssembly ?? Assembly.GetExecutingAssembly(); + + var commandRoots = assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(CommandModuleBase))); + foreach (var root in commandRoots) { this.RegisterCommandClass(pluginContainer, root); } } + private void RegisterGroupCommand(Type moduleType, PluginContainer? pluginContainer, Command? parent = null) + { + var group = moduleType.GetCustomAttribute()!; + // Get command name from first constructor argument for command attribute. + var name = group.GroupName; + // Get aliases + var aliases = group.Aliases; + + var checks = moduleType.GetCustomAttributes(); + + var info = moduleType.GetCustomAttribute(); + var issuers = moduleType.GetCustomAttribute()?.Issuers ?? CommandHelpers.DefaultIssuerScope; + + var command = CommandBuilder.Create(name) + .WithDescription(info?.Description) + .WithParent(parent) + .WithUsage(info?.Usage) + .AddAliases(aliases) + .AddExecutionChecks(checks) + .CanIssueAs(issuers) + .Build(this, pluginContainer); + + RegisterSubgroups(moduleType, pluginContainer, command); + RegisterSubcommands(moduleType, pluginContainer, command); + + _commands.Add(command); + } + private void RegisterSubgroups(Type moduleType, PluginContainer? pluginContainer, Command? parent = null) { // find all command groups under this command @@ -112,30 +148,7 @@ private void RegisterSubgroups(Type moduleType, PluginContainer? pluginContainer foreach (var subModule in subModules) { - var group = subModule.GetCustomAttribute()!; - // Get command name from first constructor argument for command attribute. - var name = group.GroupName; - // Get aliases - var aliases = group.Aliases; - - var checks = subModule.GetCustomAttributes(); - - var info = subModule.GetCustomAttribute(); - var issuers = subModule.GetCustomAttribute()?.Issuers ?? CommandHelpers.DefaultIssuerScope; - - var command = CommandBuilder.Create(name) - .WithDescription(info?.Description) - .WithParent(parent) - .WithUsage(info?.Usage) - .AddAliases(aliases) - .AddExecutionChecks(checks) - .CanIssueAs(issuers) - .Build(this, pluginContainer); - - RegisterSubgroups(subModule, pluginContainer, command); - RegisterSubcommands(subModule, pluginContainer, command); - - _commands.Add(command); + this.RegisterGroupCommand(subModule, pluginContainer, parent); } } diff --git a/Obsidian/Commands/Framework/Entities/Command.cs b/Obsidian/Commands/Framework/Entities/Command.cs index 87adfa56..c5f5e0eb 100644 --- a/Obsidian/Commands/Framework/Entities/Command.cs +++ b/Obsidian/Commands/Framework/Entities/Command.cs @@ -82,7 +82,10 @@ public async Task ExecuteAsync(CommandContext context, string[] args) } if (!this.TryFindExecutor(executors, args, context, out var executor)) - throw new InvalidOperationException($"Failed to find valid executor for /{this.Name}"); + { + await context.Sender.SendMessageAsync(ChatMessage.Simple($"Correct usage: {Usage}", ChatColor.Red)); + return; + } await this.ExecuteAsync(executor, context, args); } @@ -92,7 +95,7 @@ private bool TryFindExecutor(IEnumerable> executors, s { executor = null; - var success = args.Length == 0; + var success = args.Length == 0 && executors.Any(x => x.GetParameters().Length == 0); if (success) { diff --git a/Obsidian/Commands/MainCommandModule.cs b/Obsidian/Commands/Modules/MainCommandModule.cs similarity index 96% rename from Obsidian/Commands/MainCommandModule.cs rename to Obsidian/Commands/Modules/MainCommandModule.cs index 0df371bb..6bbcc6c3 100644 --- a/Obsidian/Commands/MainCommandModule.cs +++ b/Obsidian/Commands/Modules/MainCommandModule.cs @@ -1,15 +1,16 @@ -using Obsidian.API.Commands; +using Obsidian.API.Commands; using Obsidian.API.Utilities; using Obsidian.Commands.Framework.Entities; using Obsidian.Entities; using Obsidian.Registries; using Obsidian.WorldData; -using System.Data; +using System.Collections.Frozen; using System.Diagnostics; -namespace Obsidian.Commands; +namespace Obsidian.Commands.Modules; -public class MainCommandModule : CommandModuleBase + +public sealed class MainCommandModule : CommandModuleBase { private const int CommandsPerPage = 15; @@ -370,20 +371,6 @@ public async Task StopAsync() await server.StopAsync(); } - [Command("time")] - [CommandInfo("Sets declared time", "/time ")] - public Task TimeAsync() => TimeAsync(1337); - - [CommandOverload] - public async Task TimeAsync(int time) - { - if (this.Player is Player player) - { - player.world.LevelData.DayTime = time; - await this.Player.SendMessageAsync($"Time set to {time}"); - } - } - [Command("toggleweather", "weather")] [RequirePermission(permissions: "obsidian.weather")] public async Task WeatherAsync() diff --git a/Obsidian/Commands/Modules/TimeCommandModule.cs b/Obsidian/Commands/Modules/TimeCommandModule.cs new file mode 100644 index 00000000..1e8b5cb1 --- /dev/null +++ b/Obsidian/Commands/Modules/TimeCommandModule.cs @@ -0,0 +1,78 @@ +using Obsidian.API.Commands; +using Obsidian.Entities; +using Obsidian.Net.Packets.Play.Clientbound; +using System.Collections.Frozen; + +namespace Obsidian.Commands.Modules; + +[CommandGroup("time")] +[CommandInfo("Sets declared time", "/time (value)")] +[RequirePermission(permissions: "time")] +public sealed class TimeCommandModule : CommandModuleBase +{ + private const int Mod = 24_000; + private static readonly FrozenDictionary TimeDictionary = new Dictionary() + { + { "day", 1000 }, + { "night", 13_000 }, + { "noon", 6000 }, + { "midnight", 18_000 } + }.ToFrozenDictionary(); + + [Command("query")] + [CommandInfo("Queries the time", "/time query ")] + public async Task Query(string value) + { + switch (value) + { + case "daytime": + await this.Sender.SendMessageAsync($"The time is {this.Server.DefaultWorld.DayTime}"); + break; + case "day": + await this.Sender.SendMessageAsync($"The time is {(int)(this.Server.DefaultWorld.Time / Mod)}"); + break; + case "gametime": + await this.Sender.SendMessageAsync($"The time is {this.Server.DefaultWorld.Time}"); + break; + default: + await this.Sender.SendMessageAsync("Invalid value."); + break; + } + } + + [Command("set")] + [CommandInfo("Sets declared time", "/time set <(d|t|s)>")] + public async Task SetTime(MinecraftTime time) + { + if (time.ConvertServerTime(this.Server)) + await this.Sender.SendMessageAsync($"Set the time to {this.Server.DefaultWorld.Time}"); + else + await this.Sender.SendMessageAsync("Failed to set the time."); + } + + //TODO: Command Suggestions + [Command("set")] + [CommandOverload] + [CommandInfo("Sets declared time", "/time set ")] + public async Task SetTime(string value) + { + if (TimeDictionary.TryGetValue(value, out int time)) + { + this.Server.DefaultWorld.DayTime = time; + + await this.Sender.SendMessageAsync($"Set the time to {value}"); + + return; + } + + await this.Sender.SendMessageAsync($"{value} is an invalid argument value."); + } + + [Command("add")] + public async Task AddTime(int timeToAdd) + { + this.Server.DefaultWorld.DayTime += timeToAdd; + + await this.Sender.SendMessageAsync($"Set the time to {this.Server.DefaultWorld.Time}"); + } +} diff --git a/Obsidian/Commands/Parsers/MinecraftTimeParser.cs b/Obsidian/Commands/Parsers/MinecraftTimeParser.cs new file mode 100644 index 00000000..09b2da0f --- /dev/null +++ b/Obsidian/Commands/Parsers/MinecraftTimeParser.cs @@ -0,0 +1,18 @@ +using Obsidian.Net; + +namespace Obsidian.Commands.Parsers; +public sealed class MinecraftTimeParser : CommandParser +{ + public int Min { get; set; } = 0; + + public MinecraftTimeParser() : base(42, "minecraft:time") + { + } + + public override void Write(MinecraftStream stream) + { + base.Write(stream); + + stream.WriteInt(Min); + } +} diff --git a/Obsidian/Obsidian.csproj b/Obsidian/Obsidian.csproj index e04108fa..be734e86 100644 --- a/Obsidian/Obsidian.csproj +++ b/Obsidian/Obsidian.csproj @@ -69,12 +69,18 @@ + + + + + + @@ -82,7 +88,6 @@ - diff --git a/Obsidian/Registries/CommandsRegistry.cs b/Obsidian/Registries/CommandsRegistry.cs index 33869fa2..daa309d5 100644 --- a/Obsidian/Registries/CommandsRegistry.cs +++ b/Obsidian/Registries/CommandsRegistry.cs @@ -1,6 +1,11 @@ using Obsidian.Commands; +using Obsidian.Commands.Framework.Entities; using Obsidian.Commands.Parsers; using Obsidian.Net.Packets.Play.Clientbound; +using Obsidian.Utilities.Interfaces; +using System.Xml; +using System; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Obsidian.Registries; public static class CommandsRegistry @@ -11,63 +16,82 @@ public static void Register(Server server) { Packet = new(); var index = 0; + var commands = server.CommandsHandler.GetAllCommands()!; - var node = new CommandNode() + var rootNode = new CommandNode() { Type = CommandNodeType.Root, Index = index }; - foreach (var cmd in server.CommandsHandler.GetAllCommands()) + foreach (var cmd in commands) { - var cmdNode = new CommandNode() + //Don't register commands alone that have parents + //This is very inconvenient and should be changed + if (cmd.Parent != null) + continue; + + Register(server, cmd, commands, rootNode, ref index); + } + + Packet.AddNode(rootNode); + } + + private static void Register(Server server, Command command, IEnumerable commands, CommandNode node, ref int index) + { + var nodeType = command.Overloads.Any(x => x.GetParameters().Length == 0) ? CommandNodeType.IsExecutable | CommandNodeType.Literal : CommandNodeType.Literal; + var parentNode = new CommandNode() + { + Index = ++index, + Name = command.Name, + Type = nodeType + }; + + foreach (var overload in command.Overloads) + RegisterChildNodeOverload(server, overload, parentNode, ref index); + + foreach (var childrenCommand in commands.Where(x => x.Parent == command)) + Register(server, childrenCommand, commands, parentNode, ref index); + + node.AddChild(parentNode); + } + + private static void RegisterChildNodeOverload(Server server, IExecutor overload, CommandNode cmdNode, ref int index) + { + var args = overload.GetParameters(); + if (args.Length == 0) + cmdNode.Type |= CommandNodeType.IsExecutable; + + var prev = cmdNode; + + foreach (var arg in args) + { + var argNode = new CommandNode() { Index = ++index, - Name = cmd.Name, - Type = CommandNodeType.Literal + Name = arg.Name, + Type = CommandNodeType.Argument | CommandNodeType.IsExecutable }; - foreach (var overload in cmd.Overloads) + var type = arg.ParameterType; + + var (id, mctype) = server.CommandsHandler.FindMinecraftType(type); + + //TODO make this better + argNode.Parser = mctype switch { - var args = overload.GetParameters(); - if (!args.Any()) - cmdNode.Type |= CommandNodeType.IsExecutable; - - var prev = cmdNode; - - foreach (var arg in args) - { - var argNode = new CommandNode() - { - Index = ++index, - Name = arg.Name, - Type = CommandNodeType.Argument | CommandNodeType.IsExecutable - }; - - var type = arg.ParameterType; - - var (id, mctype) = server.CommandsHandler.FindMinecraftType(type); - - //TODO make this better - argNode.Parser = mctype switch - { - "brigadier:string" => new StringCommandParser(arg.CustomAttributes.Any(x => x.AttributeType == typeof(RemainingAttribute)) ? StringType.GreedyPhrase : StringType.QuotablePhrase), - "brigadier:double" => new DoubleCommandParser(), - "brigadier:float" => new FloatCommandParser(), - "brigadier:integer" => new IntCommandParser(), - "brigadier:long" => new LongCommandParser(), - _ => new CommandParser(id, mctype), - }; - - prev.AddChild(argNode); - - prev = argNode; - } - } - - node.AddChild(cmdNode); - } + "brigadier:string" => new StringCommandParser(arg.CustomAttributes.Any(x => x.AttributeType == typeof(RemainingAttribute)) ? StringType.GreedyPhrase : StringType.QuotablePhrase), + "brigadier:double" => new DoubleCommandParser(), + "brigadier:float" => new FloatCommandParser(), + "brigadier:integer" => new IntCommandParser(), + "brigadier:long" => new LongCommandParser(), + "minecraft:time" => new MinecraftTimeParser(), + _ => new CommandParser(id, mctype), + }; - Packet.AddNode(node); + prev.AddChild(argNode); + + prev = argNode; + } } } diff --git a/Obsidian/Server.cs b/Obsidian/Server.cs index b6b69401..e27d94a1 100644 --- a/Obsidian/Server.cs +++ b/Obsidian/Server.cs @@ -143,11 +143,11 @@ public Server( PluginManager = new PluginManager(this.serviceProvider, this, eventDispatcher, CommandsHandler, loggerFactory.CreateLogger(), serviceProvider.GetRequiredService()); - _logger.LogDebug("Registering commands..."); - CommandsHandler.RegisterCommandClass(null); - eventDispatcher.RegisterEvents(null); + _logger.LogDebug("Registering events & commands..."); + + CommandsHandler.RegisterCommands(); + eventDispatcher.RegisterEvents(); - _logger.LogDebug("Registering command context type..."); _logger.LogDebug("Done registering commands."); this.userCache = playerCache; diff --git a/Obsidian/Services/EventDispatcher.cs b/Obsidian/Services/EventDispatcher.cs index cce468f3..38d20f2b 100644 --- a/Obsidian/Services/EventDispatcher.cs +++ b/Obsidian/Services/EventDispatcher.cs @@ -120,11 +120,20 @@ public void RegisterEvent(PluginContainer? pluginContainer, Delegate handler, Pr }); } - public void RegisterEvents(PluginContainer pluginContainer) + public void RegisterEvents(PluginContainer? pluginContainer = null) { - ArgumentNullException.ThrowIfNull(pluginContainer); + var assembly = pluginContainer?.PluginAssembly ?? Assembly.GetExecutingAssembly(); - var modules = pluginContainer.PluginAssembly.GetTypes().Where(x => x.IsSubclassOf(minecraftEventHandlerType)); + + var modules = assembly.GetTypes().Where(x => x.IsSubclassOf(minecraftEventHandlerType)); + + this.RegisterEventsInternal(modules, pluginContainer); + } + + private void RegisterEventsInternal(IEnumerable? modules, PluginContainer? pluginContainer = null) + { + if (modules == null) + return; foreach (var eventModule in modules) { diff --git a/Obsidian/WorldData/World.cs b/Obsidian/WorldData/World.cs index e7f623bc..f3077d88 100644 --- a/Obsidian/WorldData/World.cs +++ b/Obsidian/WorldData/World.cs @@ -50,7 +50,27 @@ public sealed class World : IWorld public bool Loaded { get; private set; } - public long Time => LevelData.Time; + public long Time + { + get => LevelData.Time; + set + { + LevelData.Time = value; + + this.BroadcastTime(); + } + } + + public int DayTime + { + get => LevelData.DayTime; + set + { + LevelData.DayTime = value; + + this.BroadcastTime(); + } + } public int RegionCount => this.Regions.Count; public int ChunksToGenCount => this.ChunksToGen.Count; @@ -400,7 +420,7 @@ public async Task DoWorldTickAsync() if (LevelData.Time % (20 * this.Configuration.TimeTickSpeedMultiplier) == 0) { // Update client time every second / 20 ticks - this.PacketBroadcaster.QueuePacketToWorld(this, new UpdateTimePacket(LevelData.Time, LevelData.Time % 24000)); + this.BroadcastTime(); } //Tick regions within the world manager @@ -931,6 +951,8 @@ private void LoadWorldGenSettings(NbtCompound levelCompound) } } + private void BroadcastTime() => this.PacketBroadcaster.QueuePacketToWorld(this, new UpdateTimePacket(LevelData.Time, LevelData.Time % 24000)); + private void WriteWorldGenSettings(NbtWriter writer) { if (!CodecRegistry.TryGetDimension(DimensionName, out var codec))