diff --git a/build.bat b/build.bat
new file mode 100644
index 00000000..32c93163
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,6 @@
+@echo off
+cd src
+dotnet restore
+dotnet build
+
+pause
diff --git a/src/MineCase.Core/Minecase.Core.csproj b/src/MineCase.Core/Minecase.Core.csproj
index 50806e6f..f3141288 100644
--- a/src/MineCase.Core/Minecase.Core.csproj
+++ b/src/MineCase.Core/Minecase.Core.csproj
@@ -9,10 +9,6 @@
MineCase.Core
-
-
-
-
@@ -25,4 +21,8 @@
+
+
+
+
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.Commands/MineCase.Server.Commands.csproj b/src/MineCase.Server.Commands/MineCase.Server.Commands.csproj
new file mode 100644
index 00000000..ddde8657
--- /dev/null
+++ b/src/MineCase.Server.Commands/MineCase.Server.Commands.csproj
@@ -0,0 +1,12 @@
+
+
+
+ netcoreapp2.0
+
+
+
+
+
+
+
+
diff --git a/src/MineCase.Server.Commands/Vanilla/GameModeCommand.cs b/src/MineCase.Server.Commands/Vanilla/GameModeCommand.cs
new file mode 100644
index 00000000..dfc0cdba
--- /dev/null
+++ b/src/MineCase.Server.Commands/Vanilla/GameModeCommand.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Commands;
+
+namespace MineCase.Server.Commands.Vanilla
+{
+ public class GameModeCommand
+ : SimpleCommand
+ {
+ public GameModeCommand()
+ : base("gamemode", "Changes the player to a specific game mode")
+ {
+ }
+
+ public override async Task Execute(ICommandSender commandSender, IReadOnlyList args)
+ {
+ var player = (IPlayer)commandSender;
+
+ if (args.Count < 1)
+ {
+ throw new CommandWrongUsageException(this, "Invalid arguments");
+ }
+
+ GameMode.Class gameModeClass;
+
+ switch (((UnresolvedArgument)args[0]).RawContent)
+ {
+ case "survival":
+ case "s":
+ case "0":
+ gameModeClass = GameMode.Class.Survival;
+ break;
+ case "creative":
+ case "c":
+ case "1":
+ gameModeClass = GameMode.Class.Creative;
+ break;
+ case "adventure":
+ case "ad":
+ case "2":
+ gameModeClass = GameMode.Class.Adventure;
+ break;
+ case "spectator":
+ case "sp":
+ case "3":
+ gameModeClass = GameMode.Class.Spectator;
+ break;
+ default:
+ throw new CommandWrongUsageException(this);
+ }
+
+ var targets = new List();
+
+ if (args.Count == 2)
+ {
+ var rawArg = (UnresolvedArgument)args[1];
+
+ if (rawArg is TargetSelectorArgument targetSelector)
+ {
+ switch (targetSelector.Type)
+ {
+ case TargetSelectorType.NearestPlayer:
+ break;
+ case TargetSelectorType.RandomPlayer:
+ break;
+ case TargetSelectorType.AllPlayers:
+ break;
+ case TargetSelectorType.AllEntites:
+ break;
+ case TargetSelectorType.Executor:
+ break;
+ default:
+ throw new CommandException(this);
+ }
+
+ throw new CommandException(this, "Sorry, this feature has not been implemented.");
+ }
+ else
+ {
+ var user = await player.GetUser();
+ var session = await user.GetGameSession();
+
+ var targetPlayer = await (await session.FindUserByName(rawArg.RawContent)).GetPlayer();
+
+ if (targetPlayer == null)
+ {
+ throw new CommandException(this, $"Player \"{rawArg.RawContent}\" not found, may be offline or not existing.");
+ }
+
+ targets.Add(targetPlayer);
+ }
+ }
+
+ foreach (var target in targets)
+ {
+ var targetDesc = await target.GetDescription();
+ var mode = targetDesc.GameMode;
+ mode.ModeClass = gameModeClass;
+ targetDesc.GameMode = mode;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs b/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs
index 08886cb4..c927ffb5 100644
--- a/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs
+++ b/src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs
@@ -336,5 +336,17 @@ private class WindowContext
public IWindow Window;
public short ActionNumber;
}
+
+ public Task HasPermission(Permission permission)
+ {
+ // TODO: 临时提供所有权限,需要实现权限管理
+ return Task.FromResult(true);
+ }
+
+ public Task SendMessage(string msg)
+ {
+ // TODO: 向玩家发送信息
+ return Task.CompletedTask;
+ }
}
}
diff --git a/src/MineCase.Server.Grains/Game/GameSession.cs b/src/MineCase.Server.Grains/Game/GameSession.cs
index 07dae81e..ecf49f24 100644
--- a/src/MineCase.Server.Grains/Game/GameSession.cs
+++ b/src/MineCase.Server.Grains/Game/GameSession.cs
@@ -23,6 +23,8 @@ internal class GameSession : Grain, IGameSession
private HashSet _tickables;
+ private readonly Commands.CommandMap _commandMap = new Commands.CommandMap();
+
public override async Task OnActivateAsync()
{
_world = await GrainFactory.GetGrain(0).GetWorld(this.GetPrimaryKeyString());
@@ -32,6 +34,24 @@ public override async Task OnActivateAsync()
_gameTick = RegisterTimer(OnGameTick, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(50));
}
+ public Task> GetUsers()
+ {
+ return Task.FromResult((IEnumerable)_users.Keys);
+ }
+
+ public async Task FindUserByName(string name)
+ {
+ foreach (var user in _users)
+ {
+ if (await user.Key.GetName() == name)
+ {
+ return user.Key;
+ }
+ }
+
+ return null;
+ }
+
public async Task JoinGame(IUser user)
{
var sink = await user.GetClientPacketSink();
@@ -64,10 +84,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 (!await _commandMap.Dispatch(await sender.GetPlayer(), message))
+ {
+ await SendSystemMessage(sender, $"试图执行指令 \"{command}\" 时被拒绝,请检查是否具有足够的权限以及指令语法是否正确");
+ }
+
+ 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);
@@ -79,8 +112,8 @@ public async Task SendChatMessage(IUser sender, IUser receiver, string message)
var senderName = await sender.GetName();
var receiverName = await receiver.GetName();
- 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)
{
if (await item.GetName() == receiverName ||
@@ -89,6 +122,24 @@ await item.GetName() == senderName)
}
}
+ public async Task SendSystemMessage(IUser receiver, string message)
+ {
+ var receiverName = await receiver.GetName();
+
+ var jsonData = await CreateStandardSystemMessage(message);
+ const byte position = 1; // It represents user message as system message
+ await receiver.SendChatMessage(jsonData, position);
+ }
+
+ public async Task SendGameInfoMessage(IUser receiver, string message)
+ {
+ var receiverName = await receiver.GetName();
+
+ var jsonData = await CreateStandardGameInfoMessage(message);
+ const byte position = 2; // It represents user message as system message
+ await receiver.SendChatMessage(jsonData, position);
+ }
+
private async Task OnGameTick(object state)
{
var now = DateTime.UtcNow;
@@ -105,20 +156,35 @@ await Task.WhenAll(from u in _tickables
private Task CreateStandardChatMessage(string name, string message)
{
- StringComponent nameComponent = new StringComponent(name);
- nameComponent.ClickEvent = new ChatClickEvent(ClickEventType.SuggestCommand, "/msg " + name);
- nameComponent.HoverEvent = new ChatHoverEvent(HoverEventType.ShowEntity, name);
- nameComponent.Insertion = name;
+ var nameComponent = new StringComponent(name)
+ {
+ ClickEvent = new ChatClickEvent(ClickEventType.SuggestCommand, "/msg " + name),
+ HoverEvent = new ChatHoverEvent(HoverEventType.ShowEntity, name),
+ Insertion = name
+ };
// construct message
- StringComponent messageComponent = new StringComponent(message);
+ var messageComponent = new StringComponent(message);
// list
- List list = new List();
- list.Add(nameComponent);
- list.Add(messageComponent);
+ var list = new List
+ {
+ nameComponent, messageComponent
+ };
- Chat jsonData = new Chat(new TranslationComponent("chat.type.text", list));
+ var jsonData = new Chat(new TranslationComponent("chat.type.text", list));
+ return Task.FromResult(jsonData);
+ }
+
+ private Task CreateStandardSystemMessage(string message)
+ {
+ var jsonData = new Chat(new StringComponent(message));
+ return Task.FromResult(jsonData);
+ }
+
+ private Task CreateStandardGameInfoMessage(string message)
+ {
+ var jsonData = new Chat(new StringComponent(message));
return Task.FromResult(jsonData);
}
diff --git a/src/MineCase.Server.Grains/World/WorldGrain.cs b/src/MineCase.Server.Grains/World/WorldGrain.cs
index e05b3365..5809719a 100644
--- a/src/MineCase.Server.Grains/World/WorldGrain.cs
+++ b/src/MineCase.Server.Grains/World/WorldGrain.cs
@@ -43,6 +43,11 @@ public Task FindEntity(uint eid)
return Task.FromException(new EntityNotFoundException());
}
+ public Task> GetEntities()
+ {
+ return Task.FromResult((IEnumerable)_entities.Values);
+ }
+
public Task<(long age, long timeOfDay)> GetTime()
{
return Task.FromResult((_worldAge, _worldAge % 24000));
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..dcbb2f1b
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/CommandMap.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MineCase.Server.Game.Commands
+{
+ ///
+ /// 命令 Map
+ ///
+ public class CommandMap
+ {
+ private readonly Dictionary _commandMap = new Dictionary();
+
+ ///
+ /// 注册一个命令
+ ///
+ /// 要注册的命令
+ /// 为 null
+ /// 已有重名的 command 被注册
+ public void RegisterCommand(ICommand command)
+ {
+ if (command == null)
+ {
+ throw new ArgumentNullException(nameof(command));
+ }
+
+ Contract.EndContractBlock();
+
+ _commandMap.Add(command.Name, command);
+ }
+
+ ///
+ /// 分派命令
+ ///
+ /// 命令的发送者
+ /// 命令的内容
+ public Task Dispatch(ICommandSender sender, string commandContent)
+ {
+ var (commandName, args) = CommandParser.ParseCommand(commandContent);
+
+ try
+ {
+ if (_commandMap.TryGetValue(commandName, out var command) &&
+ (command.NeededPermission == null || sender.HasPermission(command.NeededPermission).Result))
+ {
+ return command.Execute(sender, args);
+ }
+
+ return Task.FromResult(false);
+ }
+ catch (CommandException e)
+ {
+ sender.SendMessage($"在执行命令 {commandName} 之时发生命令相关的异常 {e}");
+ return Task.FromResult(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..8c106e02
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/CommandParser.cs
@@ -0,0 +1,92 @@
+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
+ {
+ ///
+ /// 分析命令
+ ///
+ /// 输入,即作为命令被分析的文本,应当不为 null、经过 处理且以 '/' 开头
+ /// 命令名及命令的参数
+ /// 不合法
+ public static (string, IReadOnlyList) ParseCommand(string input)
+ {
+ if (input == null || input.Length < 2 || input[0] != '/')
+ {
+ throw new ArgumentException("输入不合法", nameof(input));
+ }
+
+ var splitResult = input.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+ if (splitResult.Length == 0)
+ {
+ throw new ArgumentException($"输入 ({input}) 不合法", nameof(input));
+ }
+
+ return (splitResult[0].Substring(1), ParseCommandArgument(splitResult.Skip(1)));
+ }
+
+ // 参数必须保持原来的顺序,因此返回值使用 IReadOnlyList 而不是 IEnumerable
+ private static IReadOnlyList ParseCommandArgument(IEnumerable input)
+ {
+ var result = new List();
+
+ foreach (var arg in input)
+ {
+ Contract.Assert(!string.IsNullOrWhiteSpace(arg));
+
+ // TODO: 使用更加具有可扩展性的方法
+ switch (arg[0])
+ {
+ case TargetSelectorArgument.PrefixToken:
+ result.Add(new TargetSelectorArgument(arg));
+ break;
+ 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;
+ }
+ }
+
+ 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..9e50c96e
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/DataTagArgument.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MineCase.Nbt.Tags;
+
+namespace MineCase.Server.Game.Commands
+{
+ ///
+ ///
+ /// 数据标签参数
+ ///
+ /// 表示一个
+ public class DataTagArgument : UnresolvedArgument
+ {
+ internal const char PrefixToken = '{';
+
+ 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..e21b97ba
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/ICommand.cs
@@ -0,0 +1,93 @@
+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
+ /// 可能抛出派生自 的异常
+ Task Execute(ICommandSender commandSender, IReadOnlyList 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/Commands/SimpleCommand.cs b/src/MineCase.Server.Interfaces/Game/Commands/SimpleCommand.cs
new file mode 100644
index 00000000..3bf1c8b4
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/SimpleCommand.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MineCase.Server.Game.Commands
+{
+ ///
+ ///
+ /// 简单指令
+ ///
+ /// 用于无复杂的名称、描述、权限及别名机制的指令
+ public abstract class SimpleCommand : ICommand
+ {
+ public string Name { get; }
+
+ public string Description { get; }
+
+ public Permission NeededPermission { get; }
+
+ private readonly HashSet _aliases;
+
+ public IEnumerable Aliases => _aliases;
+
+ protected SimpleCommand(string name, string description = null, Permission neededPermission = null, IEnumerable aliases = null)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ Description = description;
+ NeededPermission = neededPermission;
+ if (aliases != null)
+ {
+ _aliases = new HashSet(aliases);
+ }
+ }
+
+ public abstract Task Execute(ICommandSender commandSender, IReadOnlyList 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..ae040b3f
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/TargetSelector.cs
@@ -0,0 +1,222 @@
+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;
+ }
+
+ ///
+ /// 目标选择器类型
+ ///
+ 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
+ }
+
+ 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(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static)
+ .ToDictionary(
+ v => v.GetCustomAttribute().Alias,
+ v => (TargetSelectorType)v.GetRawConstantValue());
+
+ ///
+ /// Gets 指示选择了哪一类型的目标
+ ///
+ public TargetSelectorType Type { get; }
+
+ private readonly Dictionary _arguments = new Dictionary();
+
+ ///
+ /// Initializes a new instance of the class.
+ /// 构造并分析一个目标选择器
+ ///
+ /// 作为目标选择器的内容
+ /// 为 null
+ /// 无法作为目标选择器解析
+ 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:
+ if (cur == PrefixToken)
+ {
+ status = ParseStatus.VariableTag;
+ }
+ else
+ {
+ goto case ParseStatus.Rejected;
+ }
+
+ break;
+ case ParseStatus.VariableTag:
+ if (!TargetSelectorMap.TryGetValue(cur, out var type))
+ {
+ goto case ParseStatus.Rejected;
+ }
+
+ Type = type;
+ status = ParseStatus.OptionalArgumentListStart;
+ break;
+ case ParseStatus.OptionalArgumentListStart:
+ if (cur != ArgumentListStartToken)
+ {
+ 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();
+ 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();
+ if (cur == ArgumentSeparatorToken)
+ {
+ status = ParseStatus.ArgumentElementName;
+ }
+ else
+ {
+ goto case ParseStatus.ArgumentListEnd;
+ }
+
+ break;
+ }
+
+ tmpString.Append(cur);
+ break;
+ case ParseStatus.ArgumentListEnd:
+ status = ParseStatus.Accepted;
+ break;
+ case ParseStatus.Accepted:
+ // 尾部有多余的字符
+ status = ParseStatus.Rejected;
+ break;
+ 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/Commands/TildeNotationArgument.cs b/src/MineCase.Server.Interfaces/Game/Commands/TildeNotationArgument.cs
new file mode 100644
index 00000000..28ba2938
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Commands/TildeNotationArgument.cs
@@ -0,0 +1,44 @@
+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/Game/Entities/IPlayer.cs b/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs
index e3b1b93d..3e950024 100644
--- a/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs
+++ b/src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs
@@ -5,6 +5,7 @@
using System.Threading.Tasks;
using MineCase.Protocol.Play;
+using MineCase.Server.Game.Commands;
using MineCase.Server.Game.Windows;
using MineCase.Server.Network;
using MineCase.Server.User;
@@ -12,7 +13,7 @@
namespace MineCase.Server.Game.Entities
{
- public interface IPlayer : IEntity
+ public interface IPlayer : IEntity, ICommandSender
{
Task GetName();
diff --git a/src/MineCase.Server.Interfaces/Game/IGameSession.cs b/src/MineCase.Server.Interfaces/Game/IGameSession.cs
index fac3b822..8822af00 100644
--- a/src/MineCase.Server.Interfaces/Game/IGameSession.cs
+++ b/src/MineCase.Server.Interfaces/Game/IGameSession.cs
@@ -11,6 +11,10 @@ namespace MineCase.Server.Game
{
public interface IGameSession : IGrainWithStringKey
{
+ Task> GetUsers();
+
+ Task FindUserByName(string name);
+
Task JoinGame(IUser player);
Task LeaveGame(IUser player);
diff --git a/src/MineCase.Server.Interfaces/Game/IPermissible.cs b/src/MineCase.Server.Interfaces/Game/IPermissible.cs
new file mode 100644
index 00000000..21c583ec
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/IPermissible.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MineCase.Server.Game
+{
+ ///
+ /// 具有权限者接口
+ ///
+ public interface IPermissible
+ {
+ ///
+ /// 判断是否具有特定的权限
+ ///
+ /// 要判断的权限
+ Task HasPermission(Permission permission);
+ }
+}
diff --git a/src/MineCase.Server.Interfaces/Game/Permission.cs b/src/MineCase.Server.Interfaces/Game/Permission.cs
new file mode 100644
index 00000000..76522cd7
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/Permission.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MineCase.Server.Game
+{
+ ///
+ /// 权限
+ ///
+ public class Permission : IEnumerable
+ {
+ ///
+ /// Gets 该权限的名称
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets 该权限的描述,可为 null
+ ///
+ public string Description { get; }
+
+ ///
+ /// Gets or sets 该权限的默认值
+ ///
+ public PermissionDefaultValue DefaultValue { get; set; }
+
+ private readonly Dictionary _children;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// 以指定的名称、描述、默认值及子权限构造
+ ///
+ /// 名称
+ /// 描述,可为 null
+ /// 默认值
+ /// 子权限,可为 null,为 null 时表示不存在子权限
+ /// 为 null
+ public Permission(
+ string name,
+ string description = null,
+ PermissionDefaultValue permissionDefaultValue = PermissionDefaultValue.True,
+ IEnumerable children = null)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ Description = description;
+ DefaultValue = permissionDefaultValue;
+ _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/Game/PermissionDefaultValue.cs b/src/MineCase.Server.Interfaces/Game/PermissionDefaultValue.cs
new file mode 100644
index 00000000..4af9aa62
--- /dev/null
+++ b/src/MineCase.Server.Interfaces/Game/PermissionDefaultValue.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Server.Game
+{
+ ///
+ /// 权限 () 的默认值
+ ///
+ public enum PermissionDefaultValue
+ {
+ ///
+ /// 对所有人都启用
+ ///
+ True,
+
+ ///
+ /// 对 Operator 启用
+ ///
+ TrueIfOp,
+
+ ///
+ /// 对所有人都禁用
+ ///
+ False
+ }
+}
diff --git a/src/MineCase.Server.Interfaces/World/IWorld.cs b/src/MineCase.Server.Interfaces/World/IWorld.cs
index 441d20d6..addf0ef7 100644
--- a/src/MineCase.Server.Interfaces/World/IWorld.cs
+++ b/src/MineCase.Server.Interfaces/World/IWorld.cs
@@ -17,6 +17,8 @@ public interface IWorld : IGrainWithStringKey
Task FindEntity(uint eid);
+ Task> GetEntities();
+
Task<(long age, long timeOfDay)> GetTime();
Task GetAge();
diff --git a/src/MineCase.sln b/src/MineCase.sln
index 235dc028..fdae31f2 100644
--- a/src/MineCase.sln
+++ b/src/MineCase.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26730.12
+VisualStudioVersion = 15.0.26730.15
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.Server", "MineCase.Server\MineCase.Server.csproj", "{8E71CBEC-5804-4125-B651-C78426E57C8C}"
EndProject
@@ -38,6 +38,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "data", "data", "{7DC8CDBD-0
..\data\furnace.txt = ..\data\furnace.txt
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.Server.Commands", "MineCase.Server.Commands\MineCase.Server.Commands.csproj", "{FA7A21DF-BC1B-4DD4-A7CF-0F023FE709F3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -80,6 +82,10 @@ Global
{B7B1A959-72F3-42C6-B138-93C8D654F139}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7B1A959-72F3-42C6-B138-93C8D654F139}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7B1A959-72F3-42C6-B138-93C8D654F139}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FA7A21DF-BC1B-4DD4-A7CF-0F023FE709F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FA7A21DF-BC1B-4DD4-A7CF-0F023FE709F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FA7A21DF-BC1B-4DD4-A7CF-0F023FE709F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FA7A21DF-BC1B-4DD4-A7CF-0F023FE709F3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/tests/UnitTest/CommandTest.cs b/tests/UnitTest/CommandTest.cs
new file mode 100644
index 00000000..30e9722f
--- /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")
+ {
+ }
+
+ public override Task Execute(ICommandSender commandSender, IReadOnlyList args)
+ {
+ commandSender.SendMessage(string.Join(", ", args.Select(arg => arg.ToString())));
+ return Task.FromResult(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/NbtTest.cs b/tests/UnitTest/NbtTest.cs
index 907f141e..d84ea337 100644
--- a/tests/UnitTest/NbtTest.cs
+++ b/tests/UnitTest/NbtTest.cs
@@ -98,6 +98,7 @@ public void Test1()
testList.Add(new NbtInt(2));
testList.Add(new NbtInt(4));
testCompound.Add(new NbtLong(0x000000FFFFFFFFFF, "testLong"));
+ testCompound.Add(new NbtDouble(3.1415926, "testDouble"));
using (var sw = new StringWriter())
{
@@ -120,4 +121,4 @@ public void Test1()
}
}
}
-}
\ No newline at end of file
+}