From 3fd4c7497b83f746d214d064b4fde7a2dc09fec6 Mon Sep 17 00:00:00 2001 From: akemimadoka Date: Tue, 29 Aug 2017 15:55:33 +0800 Subject: [PATCH] Fix bugs, add UnitTest --- src/MineCase.Nbt/Tags/NbtCompound.cs | 2 +- .../Game/GameSession.cs | 5 +- .../Game/Commands/CommandParser.cs | 20 +++--- .../Game/Commands/DataTagArgument.cs | 6 ++ .../Game/Commands/TargetSelector.cs | 54 ++++++++++----- .../Game/Commands/TildeNotationArgument.cs | 43 ++++++++++++ .../MineCase.Server.Interfaces.csproj | 1 - tests/UnitTest/CommandTest.cs | 65 +++++++++++++++++++ tests/UnitTest/MineCase.UnitTest.csproj | 1 + tests/UnitTest/NbtTest.cs | 2 +- 10 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 src/MineCase.Server.Interfaces/Game/Commands/TildeNotationArgument.cs create mode 100644 tests/UnitTest/CommandTest.cs diff --git a/src/MineCase.Nbt/Tags/NbtCompound.cs b/src/MineCase.Nbt/Tags/NbtCompound.cs index 58bbee9f..84c8ceb9 100644 --- a/src/MineCase.Nbt/Tags/NbtCompound.cs +++ b/src/MineCase.Nbt/Tags/NbtCompound.cs @@ -111,7 +111,7 @@ public void Add(NbtTag tag) if (_childTags.ContainsKey(tag.Name)) { - throw new ArgumentException($"试图加入具有名称{tag.Name}的 Tag,但因已有重名的子 Tag 而失败", nameof(tag)); + throw new ArgumentException($"试图加入具有名称 \"{tag.Name}\" 的 Tag,但因已有重名的子 Tag 而失败", nameof(tag)); } Contract.EndContractBlock(); diff --git a/src/MineCase.Server.Grains/Game/GameSession.cs b/src/MineCase.Server.Grains/Game/GameSession.cs index 559aedb2..3015b44d 100644 --- a/src/MineCase.Server.Grains/Game/GameSession.cs +++ b/src/MineCase.Server.Grains/Game/GameSession.cs @@ -70,7 +70,10 @@ public async Task SendChatMessage(IUser sender, string message) { if (!_commandMap.Dispatch(await sender.GetPlayer(), message)) { - // TODO: 处理命令未成功执行的情形 + await sender.SendChatMessage( + await CreateStandardChatMessage( + senderName, + $"试图执行指令 \"{command}\" 时被拒绝,请检查是否具有足够的权限以及指令语法是否正确"), 0); } return; diff --git a/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs b/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs index 0d913340..0aa7dd1b 100644 --- a/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs +++ b/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs @@ -39,11 +39,12 @@ public static class CommandParser /// /// 分析命令 /// - /// 输入,即作为命令被分析的文本 + /// 输入,即作为命令被分析的文本,应当不为 null、经过 处理且以 '/' 开头 /// 命令名及命令的参数 + /// 不合法 public static (string, IList) ParseCommand(string input) { - if (input == null || input.Length < 2) + if (input == null || input.Length < 2 || input[0] != '/') { throw new ArgumentException("输入不合法", nameof(input)); } @@ -51,30 +52,33 @@ public static (string, IList) ParseCommand(string input) var splitResult = input.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (splitResult.Length == 0) { - throw new ArgumentException($"输入 ({input}) 不合法"); + throw new ArgumentException($"输入 ({input}) 不合法", nameof(input)); } - return (splitResult[0], ParseCommandArgument(splitResult.Skip(1))); + return (splitResult[0].Substring(1), ParseCommandArgument(splitResult.Skip(1))); } - // 参数必须保持有序,因此返回值使用 IList 而不是 IEnumerable + // 参数必须保持原来的顺序,因此返回值使用 IList 而不是 IEnumerable private static IList ParseCommandArgument(IEnumerable input) { var result = new List(); foreach (var arg in input) { - Contract.Assert(arg != null && arg.Length > 1); + Contract.Assert(!string.IsNullOrWhiteSpace(arg)); // TODO: 使用更加具有可扩展性的方法 switch (arg[0]) { - case '@': + case TargetSelectorArgument.PrefixToken: result.Add(new TargetSelectorArgument(arg)); break; - case '{': + case DataTagArgument.PrefixToken: result.Add(new DataTagArgument(arg)); break; + case TildeNotationArgument.PrefixToken: + result.Add(new TildeNotationArgument(arg)); + break; default: result.Add(new UnresolvedArgument(arg)); break; diff --git a/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs b/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs index f35f6e6a..ed06c1f0 100644 --- a/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs +++ b/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs @@ -5,8 +5,14 @@ namespace MineCase.Server.Game.Commands { + /// + /// 数据标签参数 + /// + /// 表示一个 public class DataTagArgument : UnresolvedArgument { + internal const char PrefixToken = '{'; + public NbtCompound Tag { get; } public DataTagArgument(string rawContent) diff --git a/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs b/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs index 371ce394..d878e642 100644 --- a/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs +++ b/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs @@ -17,7 +17,7 @@ internal class TargetSelectorAliasAsAttribute : Attribute } /// - /// TargetSelector 类型 + /// 目标选择器类型 /// public enum TargetSelectorType { @@ -70,17 +70,17 @@ private enum ParseStatus Rejected } - private const char PrefixToken = '@'; + internal 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() + typeof(TargetSelectorType).GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static) .ToDictionary( - v => v.GetType().GetTypeInfo().GetCustomAttribute().Alias, - v => (TargetSelectorType)v.GetValue(null)); + v => v.GetCustomAttribute().Alias, + v => (TargetSelectorType)v.GetRawConstantValue()); /// /// Gets 指示选择了哪一类型的目标 @@ -91,11 +91,11 @@ private enum ParseStatus /// /// Initializes a new instance of the class. - /// 构造并分析一个 TargetSelector + /// 构造并分析一个目标选择器 /// - /// 作为 TargetSelector 的内容 + /// 作为目标选择器的内容 /// 为 null - /// 无法作为 TargetSelector 解析 + /// 无法作为目标选择器解析 public TargetSelectorArgument(string rawContent) : base(rawContent) { @@ -108,13 +108,20 @@ public TargetSelectorArgument(string rawContent) switch (status) { case ParseStatus.Prefix: - status = cur == PrefixToken ? ParseStatus.VariableTag : ParseStatus.Rejected; + if (cur == PrefixToken) + { + status = ParseStatus.VariableTag; + } + else + { + goto case ParseStatus.Rejected; + } + break; case ParseStatus.VariableTag: if (!TargetSelectorMap.TryGetValue(cur, out var type)) { - status = ParseStatus.Rejected; - break; + goto case ParseStatus.Rejected; } Type = type; @@ -123,11 +130,18 @@ public TargetSelectorArgument(string rawContent) case ParseStatus.OptionalArgumentListStart: if (cur != ArgumentListStartToken) { - status = ParseStatus.Rejected; + goto case ParseStatus.Rejected; } + status = ParseStatus.ArgumentElementName; break; case ParseStatus.ArgumentElementName: + if (char.IsWhiteSpace(cur) && tmpString.Length == 0) + { + // 略过开头的空白字符,但是这不可能发生。。。 + continue; + } + if (cur == ArgumentAssignmentToken) { argName = tmpString.ToString(); @@ -144,7 +158,15 @@ public TargetSelectorArgument(string rawContent) Contract.Assert(argName != null); _arguments.Add(argName, tmpString.ToString()); tmpString = new StringBuilder(); - status = cur == ArgumentSeparatorToken ? ParseStatus.ArgumentElementName : ParseStatus.ArgumentListEnd; + if (cur == ArgumentSeparatorToken) + { + status = ParseStatus.ArgumentElementName; + } + else + { + goto case ParseStatus.ArgumentListEnd; + } + break; } @@ -154,7 +176,9 @@ public TargetSelectorArgument(string rawContent) status = ParseStatus.Accepted; break; case ParseStatus.Accepted: - return; + // 尾部有多余的字符 + status = ParseStatus.Rejected; + break; case ParseStatus.Rejected: throw new ArgumentException($"\"{rawContent}\" 不能被解析为合法的 TargetSelector", nameof(rawContent)); default: @@ -163,7 +187,7 @@ public TargetSelectorArgument(string rawContent) } } - if (status != ParseStatus.Accepted || status != ParseStatus.OptionalArgumentListStart) + if (status != ParseStatus.Accepted && status != ParseStatus.OptionalArgumentListStart) { throw new ArgumentException($"在解析 \"{rawContent}\" 的过程中,解析被过早地中止"); } diff --git a/src/MineCase.Server.Interfaces/Game/Commands/TildeNotationArgument.cs b/src/MineCase.Server.Interfaces/Game/Commands/TildeNotationArgument.cs new file mode 100644 index 00000000..bea6f298 --- /dev/null +++ b/src/MineCase.Server.Interfaces/Game/Commands/TildeNotationArgument.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MineCase.Server.Game.Commands +{ + /// + /// 波浪号记号参数 + /// + /// 用于表示一个相对的值,具体含义由具体命令定义 + public class TildeNotationArgument : UnresolvedArgument + { + internal const char PrefixToken = '~'; + + /// + /// Gets 波浪号记号参数表示的偏移量 + /// + public int Offset { get; } + + /// + /// Initializes a new instance of the class. + /// 构造并分析一个波浪号记号参数 + /// + /// 作为波浪号记号的内容 + public TildeNotationArgument(string rawContent) + : base(rawContent) + { + if (rawContent.Length == 1) + { + Offset = 0; + return; + } + + var strToParse = rawContent.Substring(1); + if (!int.TryParse(strToParse, out int offset)) + { + throw new ArgumentException($"\"{strToParse}\" 不是合法的 offset 值", nameof(rawContent)); + } + + Offset = offset; + } + } +} diff --git a/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj b/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj index 23d4b9ea..fb7b5d5c 100644 --- a/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj +++ b/src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj @@ -12,7 +12,6 @@ - diff --git a/tests/UnitTest/CommandTest.cs b/tests/UnitTest/CommandTest.cs new file mode 100644 index 00000000..311b8167 --- /dev/null +++ b/tests/UnitTest/CommandTest.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MineCase.Server.Game; +using MineCase.Server.Game.Commands; +using Xunit; + +namespace MineCase.UnitTest +{ + public class CommandTest + { + private class OutputCommandSender : ICommandSender + { + private readonly TextWriter _tw; + + public OutputCommandSender(TextWriter tw) + { + _tw = tw; + } + + public Task HasPermission(Permission permission) + { + return Task.FromResult(true); + } + + public Task SendMessage(string msg) + { + _tw.WriteLine(msg); + return Task.CompletedTask; + } + } + + private class TestCommand : SimpleCommand + { + public TestCommand() + : base("test", null, null, null) + { + } + + public override bool Execute(ICommandSender commandSender, IList args) + { + commandSender.SendMessage(string.Join(", ", args.Select(arg => arg.ToString()))); + return true; + } + } + + [Fact] + public void Test1() + { + var commandMap = new CommandMap(); + commandMap.RegisterCommand(new TestCommand()); + + var sb = new StringBuilder(); + using (var sw = new StringWriter(sb)) + { + commandMap.Dispatch(new OutputCommandSender(sw), "/test 1 ~2 @p[arg1=233,arg2=]"); + var str = sb.ToString(); + Console.Write(str); + } + } + } +} diff --git a/tests/UnitTest/MineCase.UnitTest.csproj b/tests/UnitTest/MineCase.UnitTest.csproj index 04d14594..0b26526a 100644 --- a/tests/UnitTest/MineCase.UnitTest.csproj +++ b/tests/UnitTest/MineCase.UnitTest.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/UnitTest/NbtTest.cs b/tests/UnitTest/NbtTest.cs index ccb23da6..d84ea337 100644 --- a/tests/UnitTest/NbtTest.cs +++ b/tests/UnitTest/NbtTest.cs @@ -121,4 +121,4 @@ public void Test1() } } } -} \ No newline at end of file +}