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 +}