Quellcode durchsuchen

Fix bugs, add UnitTest

akemimadoka vor 8 Jahren
Ursprung
Commit
8ad2b5fde9

+ 1 - 1
src/MineCase.Nbt/Tags/NbtCompound.cs

@@ -111,7 +111,7 @@ namespace MineCase.Nbt.Tags
 
             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();

+ 4 - 1
src/MineCase.Server.Grains/Game/GameSession.cs

@@ -73,7 +73,10 @@ namespace MineCase.Server.Game
                 {
                     if (!_commandMap.Dispatch(await sender.GetPlayer(), message))
                     {
-                        // TODO: 处理命令未成功执行的情形
+                        await sender.SendChatMessage(
+                            await CreateStandardChatMessage(
+                                senderName,
+                                $"试图执行指令 \"{command}\" 时被拒绝,请检查是否具有足够的权限以及指令语法是否正确"), 0);
                     }
 
                     return;

+ 12 - 8
src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs

@@ -39,11 +39,12 @@ namespace MineCase.Server.Game.Commands
         /// <summary>
         /// 分析命令
         /// </summary>
-        /// <param name="input">输入,即作为命令被分析的文本</param>
+        /// <param name="input">输入,即作为命令被分析的文本,应当不为 null、经过 <see cref="string.Trim()"/> 处理且以 '/' 开头</param>
         /// <returns>命令名及命令的参数</returns>
+        /// <exception cref="ArgumentException"><paramref name="input"/> 不合法</exception>
         public static (string, IList<ICommandArgument>) 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 @@ namespace MineCase.Server.Game.Commands
             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<ICommandArgument> ParseCommandArgument(IEnumerable<string> input)
         {
             var result = new List<ICommandArgument>();
 
             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;

+ 6 - 0
src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs

@@ -5,8 +5,14 @@ using MineCase.Nbt.Tags;
 
 namespace MineCase.Server.Game.Commands
 {
+    /// <summary>
+    /// 数据标签参数
+    /// </summary>
+    /// <remarks>表示一个 <see cref="NbtTag"/></remarks>
     public class DataTagArgument : UnresolvedArgument
     {
+        internal const char PrefixToken = '{';
+
         public NbtCompound Tag { get; }
 
         public DataTagArgument(string rawContent)

+ 39 - 15
src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs

@@ -17,7 +17,7 @@ namespace MineCase.Server.Game.Commands
     }
 
     /// <summary>
-    /// TargetSelector 类型
+    /// 目标选择器类型
     /// </summary>
     public enum TargetSelectorType
     {
@@ -70,17 +70,17 @@ namespace MineCase.Server.Game.Commands
             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<char, TargetSelectorType> TargetSelectorMap =
-            typeof(TargetSelectorType).GetFields()
+            typeof(TargetSelectorType).GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static)
                 .ToDictionary(
-                    v => v.GetType().GetTypeInfo().GetCustomAttribute<TargetSelectorAliasAsAttribute>().Alias,
-                    v => (TargetSelectorType)v.GetValue(null));
+                    v => v.GetCustomAttribute<TargetSelectorAliasAsAttribute>().Alias,
+                    v => (TargetSelectorType)v.GetRawConstantValue());
 
         /// <summary>
         /// Gets 指示选择了哪一类型的目标
@@ -91,11 +91,11 @@ namespace MineCase.Server.Game.Commands
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TargetSelectorArgument"/> class.<para />
-        /// 构造并分析一个 TargetSelector
+        /// 构造并分析一个目标选择器
         /// </summary>
-        /// <param name="rawContent">作为 TargetSelector 的内容</param>
+        /// <param name="rawContent">作为目标选择器的内容</param>
         /// <exception cref="ArgumentNullException"><paramref name="rawContent"/> 为 null</exception>
-        /// <exception cref="ArgumentException"><paramref name="rawContent"/> 无法作为 TargetSelector 解析</exception>
+        /// <exception cref="ArgumentException"><paramref name="rawContent"/> 无法作为目标选择器解析</exception>
         public TargetSelectorArgument(string rawContent)
             : base(rawContent)
         {
@@ -108,13 +108,20 @@ namespace MineCase.Server.Game.Commands
                 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 @@ namespace MineCase.Server.Game.Commands
                     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 @@ namespace MineCase.Server.Game.Commands
                             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 @@ namespace MineCase.Server.Game.Commands
                         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 @@ namespace MineCase.Server.Game.Commands
                 }
             }
 
-            if (status != ParseStatus.Accepted || status != ParseStatus.OptionalArgumentListStart)
+            if (status != ParseStatus.Accepted && status != ParseStatus.OptionalArgumentListStart)
             {
                 throw new ArgumentException($"在解析 \"{rawContent}\" 的过程中,解析被过早地中止");
             }

+ 43 - 0
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
+{
+    /// <summary>
+    /// 波浪号记号参数
+    /// </summary>
+    /// <remarks>用于表示一个相对的值,具体含义由具体命令定义</remarks>
+    public class TildeNotationArgument : UnresolvedArgument
+    {
+        internal const char PrefixToken = '~';
+
+        /// <summary>
+        /// Gets 波浪号记号参数表示的偏移量
+        /// </summary>
+        public int Offset { get; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TildeNotationArgument"/> class.
+        /// 构造并分析一个波浪号记号参数
+        /// </summary>
+        /// <param name="rawContent">作为波浪号记号的内容</param>
+        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;
+        }
+    }
+}

+ 0 - 1
src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj

@@ -12,7 +12,6 @@
     <PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0-preview2-20170724" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
     <PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />
-    <PackageReference Include="System.ValueTuple" Version="4.4.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 65 - 0
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<bool> 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<ICommandArgument> 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);
+            }
+        }
+    }
+}

+ 1 - 1
tests/UnitTest/NbtTest.cs

@@ -121,4 +121,4 @@ namespace MineCase.UnitTest
             }
         }
     }
-}
+}