From 414db37a35fcb1ccfe4cad0f1289ff8221ed75a0 Mon Sep 17 00:00:00 2001 From: akemimadoka Date: Mon, 28 Aug 2017 22:08:22 +0800 Subject: [PATCH] Add some class for parsing command arguments --- .../Game/Entities/PlayerGrain.cs | 7 +- .../Game/GameSession.cs | 21 +- .../Game/Commands/CommandMap.cs | 33 +++ .../Game/Commands/CommandParser.cs | 87 ++++++++ .../Game/Commands/DataTagArgument.cs | 18 ++ .../Game/Commands/ICommand.cs | 90 ++++++++ .../Game/{ => Commands}/SimpleCommand.cs | 9 +- .../Game/Commands/TargetSelector.cs | 197 ++++++++++++++++++ .../Game/Entities/IPlayer.cs | 1 + .../Game/ICommand.cs | 46 ---- .../Game/IPermissible.cs | 3 +- .../Game/Permission.cs | 40 +++- .../MineCase.Server.Interfaces.csproj | 3 + 13 files changed, 498 insertions(+), 57 deletions(-) create mode 100644 src/MineCase.Server.Interfaces/Game/Commands/CommandMap.cs create mode 100644 src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs create mode 100644 src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs create mode 100644 src/MineCase.Server.Interfaces/Game/Commands/ICommand.cs rename src/MineCase.Server.Interfaces/Game/{ => Commands}/SimpleCommand.cs (79%) create mode 100644 src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs delete mode 100644 src/MineCase.Server.Interfaces/Game/ICommand.cs diff --git a/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs b/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs index 8d5fa47d..44eb3a1e 100644 --- a/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs +++ b/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs @@ -133,7 +133,12 @@ public Task SetLook(float yaw, float pitch, bool onGround) return Task.CompletedTask; } - public bool HasPermission(Permission permission) + public Task HasPermission(Permission permission) + { + throw new NotImplementedException(); + } + + public Task SendMessage(string msg) { throw new NotImplementedException(); } diff --git a/src/MineCase.Server.Grains/Game/GameSession.cs b/src/MineCase.Server.Grains/Game/GameSession.cs index 0656d453..559aedb2 100644 --- a/src/MineCase.Server.Grains/Game/GameSession.cs +++ b/src/MineCase.Server.Grains/Game/GameSession.cs @@ -21,6 +21,8 @@ internal class GameSession : Grain, IGameSession private IDisposable _gameTick; private DateTime _lastGameTickTime; + private readonly Commands.CommandMap _commandMap = new Commands.CommandMap(); + public override async Task OnActivateAsync() { _world = await GrainFactory.GetGrain(0).GetWorld(this.GetPrimaryKeyString()); @@ -61,10 +63,23 @@ public async Task SendChatMessage(IUser sender, string message) { var senderName = await sender.GetName(); - // TODO command parser + if (!string.IsNullOrWhiteSpace(message)) + { + var command = message.Trim(); + if (command[0] == '/') + { + if (!_commandMap.Dispatch(await sender.GetPlayer(), message)) + { + // TODO: 处理命令未成功执行的情形 + } + + return; + } + } + // construct name - Chat jsonData = await CreateStandardChatMessage(senderName, message); - byte position = 0; // It represents user message in chat box + var jsonData = await CreateStandardChatMessage(senderName, message); + const byte position = 0; // It represents user message in chat box foreach (var item in _users.Keys) { await item.SendChatMessage(jsonData, position); diff --git a/src/MineCase.Server.Interfaces/Game/Commands/CommandMap.cs b/src/MineCase.Server.Interfaces/Game/Commands/CommandMap.cs new file mode 100644 index 00000000..00b4dd2d --- /dev/null +++ b/src/MineCase.Server.Interfaces/Game/Commands/CommandMap.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MineCase.Server.Game.Commands +{ + public class CommandMap + { + private readonly Dictionary _commandMap = new Dictionary(); + + public void RegisterCommand(ICommand command) + { + _commandMap.Add(command.Name, command); + } + + public bool Dispatch(ICommandSender sender, string commandContent) + { + var (commandName, args) = CommandParser.ParseCommand(commandContent); + + try + { + return _commandMap.TryGetValue(commandName, out var command) && + (command.NeededPermission == null || sender.HasPermission(command.NeededPermission).Result) && + command.Execute(sender, args); + } + catch (CommandException e) + { + sender.SendMessage($"在执行指令 {commandName} 之时发生指令相关的异常 {e}"); + return false; + } + } + } +} diff --git a/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs b/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs new file mode 100644 index 00000000..0d913340 --- /dev/null +++ b/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; + +namespace MineCase.Server.Game.Commands +{ + /// + /// 未解析参数 + /// + /// 用于参数无法解析或当前不需要已解析的形式的情形 + public class UnresolvedArgument : ICommandArgument + { + public string RawContent { get; } + + public UnresolvedArgument(string rawContent) + { + if (rawContent == null) + { + throw new ArgumentNullException(nameof(rawContent)); + } + + if (rawContent.Length <= 0) + { + throw new ArgumentException($"{nameof(rawContent)} 不得为空", nameof(rawContent)); + } + + RawContent = rawContent; + } + } + + /// + /// 命令分析器 + /// + public static class CommandParser + { + /// + /// 分析命令 + /// + /// 输入,即作为命令被分析的文本 + /// 命令名及命令的参数 + public static (string, IList) ParseCommand(string input) + { + if (input == null || input.Length < 2) + { + throw new ArgumentException("输入不合法", nameof(input)); + } + + var splitResult = input.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (splitResult.Length == 0) + { + throw new ArgumentException($"输入 ({input}) 不合法"); + } + + return (splitResult[0], ParseCommandArgument(splitResult.Skip(1))); + } + + // 参数必须保持有序,因此返回值使用 IList 而不是 IEnumerable + private static IList ParseCommandArgument(IEnumerable input) + { + var result = new List(); + + foreach (var arg in input) + { + Contract.Assert(arg != null && arg.Length > 1); + + // TODO: 使用更加具有可扩展性的方法 + switch (arg[0]) + { + case '@': + result.Add(new TargetSelectorArgument(arg)); + break; + case '{': + result.Add(new DataTagArgument(arg)); + break; + default: + result.Add(new UnresolvedArgument(arg)); + break; + } + } + + return result; + } + } +} diff --git a/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs b/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs new file mode 100644 index 00000000..f35f6e6a --- /dev/null +++ b/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MineCase.Nbt.Tags; + +namespace MineCase.Server.Game.Commands +{ + public class DataTagArgument : UnresolvedArgument + { + public NbtCompound Tag { get; } + + public DataTagArgument(string rawContent) + : base(rawContent) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/MineCase.Server.Interfaces/Game/Commands/ICommand.cs b/src/MineCase.Server.Interfaces/Game/Commands/ICommand.cs new file mode 100644 index 00000000..e807242b --- /dev/null +++ b/src/MineCase.Server.Interfaces/Game/Commands/ICommand.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MineCase.Server.Game.Commands +{ + /// + /// 命令参数接口 + /// + public interface ICommandArgument + { + /// + /// Gets 命令参数的原本内容 + /// + string RawContent { get; } + } + + /// + /// 命令接口 + /// + public interface ICommand + { + /// + /// Gets 该命令的名称 + /// + string Name { get; } + + /// + /// Gets 该命令的描述,可为 null + /// + string Description { get; } + + /// + /// Gets 要执行此命令需要的权限,可为 null + /// + Permission NeededPermission { get; } + + /// + /// Gets 该命令的别名,可为 null + /// + IEnumerable Aliases { get; } + + /// + /// 执行该命令 + /// + /// 发送命令者 + /// 命令的参数 + /// 执行是否成功,如果成功则返回 true + /// 可能抛出派生自 的异常 + bool Execute(ICommandSender commandSender, IList args); + } + + /// + /// 命令执行过程中可能发生的异常的基类 + /// + /// 派生自此类的异常在 中将会被吃掉,不会传播到外部 + public class CommandException : Exception + { + public ICommand Command { get; } + + public CommandException(ICommand command = null, string content = null, Exception innerException = null) + : base(content, innerException) + { + Command = command; + } + } + + /// + /// 表示命令的使用方式错误的异常 + /// + public class CommandWrongUsageException : CommandException + { + public CommandWrongUsageException(ICommand command, string content = null, Exception innerException = null) + : base(command, content, innerException) + { + } + } + + /// + /// 可发送命令者接口 + /// + public interface ICommandSender : IPermissible + { + /// + /// 向可发送命令者发送(一般为反馈)特定的信息 + /// + /// 要发送的信息 + Task SendMessage(string msg); + } +} diff --git a/src/MineCase.Server.Interfaces/Game/SimpleCommand.cs b/src/MineCase.Server.Interfaces/Game/Commands/SimpleCommand.cs similarity index 79% rename from src/MineCase.Server.Interfaces/Game/SimpleCommand.cs rename to src/MineCase.Server.Interfaces/Game/Commands/SimpleCommand.cs index 9486203b..f409492d 100644 --- a/src/MineCase.Server.Interfaces/Game/SimpleCommand.cs +++ b/src/MineCase.Server.Interfaces/Game/Commands/SimpleCommand.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; -using System.Text; -namespace MineCase.Server.Game +namespace MineCase.Server.Game.Commands { + /// + /// 简单指令 + /// + /// 用于无复杂的名称、描述、权限及别名机制的指令 public abstract class SimpleCommand : ICommand { public string Name { get; } @@ -27,6 +30,6 @@ protected SimpleCommand(string name, string description = null, Permission neede } } - public abstract bool Execute(ICommandSender commandSender, IEnumerable args); + public abstract bool Execute(ICommandSender commandSender, IList args); } } diff --git a/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs b/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs new file mode 100644 index 00000000..371ce394 --- /dev/null +++ b/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace MineCase.Server.Game.Commands +{ + [AttributeUsage(AttributeTargets.Field)] + internal class TargetSelectorAliasAsAttribute : Attribute + { + public char Alias { get; } + + public TargetSelectorAliasAsAttribute(char alias) => Alias = alias; + } + + /// + /// TargetSelector 类型 + /// + public enum TargetSelectorType + { + /// + /// 选择最近的玩家为目标 + /// + [TargetSelectorAliasAs('p')] + NearestPlayer, + + /// + /// 选择随机的玩家为目标 + /// + [TargetSelectorAliasAs('r')] + RandomPlayer, + + /// + /// 选择所有玩家为目标 + /// + [TargetSelectorAliasAs('a')] + AllPlayers, + + /// + /// 选择所有实体为目标 + /// + [TargetSelectorAliasAs('e')] + AllEntites, + + /// + /// 选择命令的执行者为目标 + /// + [TargetSelectorAliasAs('s')] + Executor + } + + /// + /// 用于选择目标的 + /// + public class TargetSelectorArgument : UnresolvedArgument, IEnumerable> + { + private enum ParseStatus + { + Prefix, + VariableTag, + OptionalArgumentListStart, + ArgumentElementName, + ArgumentElementValue, + ArgumentListEnd, + + Accepted, + Rejected + } + + private const char PrefixToken = '@'; + private const char ArgumentListStartToken = '['; + private const char ArgumentListEndToken = ']'; + private const char ArgumentAssignmentToken = '='; + private const char ArgumentSeparatorToken = ','; + + private static readonly Dictionary TargetSelectorMap = + typeof(TargetSelectorType).GetFields() + .ToDictionary( + v => v.GetType().GetTypeInfo().GetCustomAttribute().Alias, + v => (TargetSelectorType)v.GetValue(null)); + + /// + /// Gets 指示选择了哪一类型的目标 + /// + public TargetSelectorType Type { get; } + + private readonly Dictionary _arguments = new Dictionary(); + + /// + /// Initializes a new instance of the class. + /// 构造并分析一个 TargetSelector + /// + /// 作为 TargetSelector 的内容 + /// 为 null + /// 无法作为 TargetSelector 解析 + public TargetSelectorArgument(string rawContent) + : base(rawContent) + { + var status = ParseStatus.Prefix; + string argName = null; + var tmpString = new StringBuilder(); + + foreach (var cur in rawContent) + { + switch (status) + { + case ParseStatus.Prefix: + status = cur == PrefixToken ? ParseStatus.VariableTag : ParseStatus.Rejected; + break; + case ParseStatus.VariableTag: + if (!TargetSelectorMap.TryGetValue(cur, out var type)) + { + status = ParseStatus.Rejected; + break; + } + + Type = type; + status = ParseStatus.OptionalArgumentListStart; + break; + case ParseStatus.OptionalArgumentListStart: + if (cur != ArgumentListStartToken) + { + status = ParseStatus.Rejected; + } + + break; + case ParseStatus.ArgumentElementName: + if (cur == ArgumentAssignmentToken) + { + argName = tmpString.ToString(); + tmpString = new StringBuilder(); + status = ParseStatus.ArgumentElementValue; + break; + } + + tmpString.Append(cur); + break; + case ParseStatus.ArgumentElementValue: + if (cur == ArgumentSeparatorToken || cur == ArgumentListEndToken) + { + Contract.Assert(argName != null); + _arguments.Add(argName, tmpString.ToString()); + tmpString = new StringBuilder(); + status = cur == ArgumentSeparatorToken ? ParseStatus.ArgumentElementName : ParseStatus.ArgumentListEnd; + break; + } + + tmpString.Append(cur); + break; + case ParseStatus.ArgumentListEnd: + status = ParseStatus.Accepted; + break; + case ParseStatus.Accepted: + return; + case ParseStatus.Rejected: + throw new ArgumentException($"\"{rawContent}\" 不能被解析为合法的 TargetSelector", nameof(rawContent)); + default: + // 任何情况下都不应当发生 + throw new ArgumentOutOfRangeException(nameof(status)); + } + } + + if (status != ParseStatus.Accepted || status != ParseStatus.OptionalArgumentListStart) + { + throw new ArgumentException($"在解析 \"{rawContent}\" 的过程中,解析被过早地中止"); + } + } + + /// + public string this[string name] => GetArgumentValue(name); + + /// + /// 获得具有指定名称的参数值 + /// + /// 要查找的名称 + /// 无法找到具有指定名称的参数 + public string GetArgumentValue(string name) => _arguments[name]; + + /// + /// 判断是否存在具有指定名称的参数 + /// + /// 要判断的名称 + public bool ContainsArgument(string name) => _arguments.ContainsKey(name); + + /// + /// Gets 具有的参数数量 + /// + public int ArgumentCount => _arguments.Count; + + public IEnumerator> GetEnumerator() => _arguments.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs b/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs index 2e824adb..1a56c86e 100644 --- a/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs +++ b/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using MineCase.Server.Game.Commands; using MineCase.Server.Game.Windows; using MineCase.Server.Network; using MineCase.Server.User; diff --git a/src/MineCase.Server.Interfaces/Game/ICommand.cs b/src/MineCase.Server.Interfaces/Game/ICommand.cs deleted file mode 100644 index e443bf5e..00000000 --- a/src/MineCase.Server.Interfaces/Game/ICommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MineCase.Server.Game -{ - /// - /// 命令接口 - /// - public interface ICommand - { - /// - /// Gets 该命令的名称 - /// - string Name { get; } - - /// - /// Gets 该命令的描述,可为 null - /// - string Description { get; } - - /// - /// Gets 要执行此命令需要的权限,可为 null - /// - Permission NeededPermission { get; } - - /// - /// Gets 该命令的别名,可为 null - /// - IEnumerable Aliases { get; } - - /// - /// 执行该命令 - /// - /// 发送命令者 - /// 命令的参数 - /// 执行是否成功,如果成功则返回 true - bool Execute(ICommandSender commandSender, IEnumerable args); - } - - /// - /// 可发送命令者接口 - /// - public interface ICommandSender : IPermissible - { - } -} diff --git a/src/MineCase.Server.Interfaces/Game/IPermissible.cs b/src/MineCase.Server.Interfaces/Game/IPermissible.cs index 8960deae..21c583ec 100644 --- a/src/MineCase.Server.Interfaces/Game/IPermissible.cs +++ b/src/MineCase.Server.Interfaces/Game/IPermissible.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace MineCase.Server.Game { @@ -13,6 +14,6 @@ public interface IPermissible /// 判断是否具有特定的权限 /// /// 要判断的权限 - bool HasPermission(Permission permission); + Task HasPermission(Permission permission); } } diff --git a/src/MineCase.Server.Interfaces/Game/Permission.cs b/src/MineCase.Server.Interfaces/Game/Permission.cs index c188e0e2..76522cd7 100644 --- a/src/MineCase.Server.Interfaces/Game/Permission.cs +++ b/src/MineCase.Server.Interfaces/Game/Permission.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,7 +9,7 @@ namespace MineCase.Server.Game /// /// 权限 /// - public class Permission + public class Permission : IEnumerable { /// /// Gets 该权限的名称 @@ -27,6 +28,15 @@ public class Permission private readonly Dictionary _children; + /// + /// Initializes a new instance of the class. + /// 以指定的名称、描述、默认值及子权限构造 + /// + /// 名称 + /// 描述,可为 null + /// 默认值 + /// 子权限,可为 null,为 null 时表示不存在子权限 + /// 为 null public Permission( string name, string description = null, @@ -36,9 +46,33 @@ public class Permission Name = name ?? throw new ArgumentNullException(nameof(name)); Description = description; DefaultValue = permissionDefaultValue; - _children = children.ToDictionary( - p => p?.Name ?? throw new ArgumentException("子权限不得为 null", nameof(children))) ?? + _children = children?.Where(p => p != null).Distinct().ToDictionary(p => p.Name) ?? new Dictionary(); } + + /// + public Permission this[string name] => GetChild(name); + + /// + /// 以指定的名称获取子权限 + /// + /// 要查找的名称 + public Permission GetChild(string name) => _children[name]; + + /// + /// 判断是否包含指定名称的子权限 + /// + /// 要判断的名称 + public bool ContainsChild(string name) => _children.ContainsKey(name); + + public IEnumerator GetEnumerator() + { + return _children.Select(p => p.Value).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } } diff --git a/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj b/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj index b82e6b11..a6fe45a9 100644 --- a/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj +++ b/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj @@ -11,15 +11,18 @@ + + +