diff --git a/.editorconfig b/.editorconfig
index 1b5926353..6a078547d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -66,12 +66,13 @@ csharp_style_var_for_built_in_types=true:silent
csharp_style_var_when_type_is_apparent=true:silent
csharp_style_var_elsewhere=true:silent
# Expression-bodied members
-csharp_style_expression_bodied_methods=false:silent
+csharp_style_expression_bodied_methods=true:silent
csharp_style_expression_bodied_constructors=false:silent
-csharp_style_expression_bodied_operators=false:silent
+csharp_style_expression_bodied_operators=true:silent
csharp_style_expression_bodied_properties=true:silent
csharp_style_expression_bodied_indexers=true:silent
csharp_style_expression_bodied_accessors=true:silent
+csharp_style_expression_bodied_local_functions = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check=true:suggestion
csharp_style_pattern_matching_over_as_with_null_check=true:suggestion
diff --git a/src/TShock/Commands/CommandHandlerAttribute.cs b/src/TShock/Commands/CommandHandlerAttribute.cs
index 4e5f39a21..444d1f1a5 100644
--- a/src/TShock/Commands/CommandHandlerAttribute.cs
+++ b/src/TShock/Commands/CommandHandlerAttribute.cs
@@ -16,37 +16,98 @@
// along with TShock. If not, see .
using System;
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using JetBrains.Annotations;
+using TShock.Utils;
namespace TShock.Commands {
///
- /// An attribute that can be applied to a method to indicate that it is a command handler. This can be applied to
- /// a method mutiple times to provide aliasing.
+ /// Specifies that a method is a command handler. This controls many aspects of the command, and can be applied
+ /// multiple times to provide aliasing.
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ [MeansImplicitUse]
public sealed class CommandHandlerAttribute : Attribute {
+ private readonly string _qualifiedName;
+
+ private string? _helpText;
+ private string? _usageText;
+ private Type? _resourceType;
+
+ ///
+ /// Gets the qualified name. This includes the namespace: e.g., tshock:kick .
+ ///
+ /// The qualified name.
+ public string QualifiedName => GetResourceStringMaybe(_qualifiedName);
+
+ ///
+ /// Gets or sets the help text. This will show up in the /help command. If , then no help
+ /// text exists.
+ ///
+ /// The help text.
+ /// is .
+ [DisallowNull]
+ public string? HelpText {
+ get => GetResourceStringMaybe(_helpText);
+ set => _helpText = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
///
- /// Gets the command's name. This includes the command's namespace: e.g., "tshock:kick" .
+ /// Gets or sets the usage text. This will show up in the /help command and when invalid syntax is used. If
+ /// , then no usage text exists.
///
- public string CommandName { get; }
+ /// The usage text.
+ /// is .
+ [DisallowNull]
+ public string? UsageText {
+ get => GetResourceStringMaybe(_usageText);
+ set => _usageText = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ ///
+ /// Gets or sets the resource type to load localizable strings from. If , then no
+ /// localization will occur.
+ ///
+ /// The resource type to load localizable strings from.
+ /// is .
+ [DisallowNull]
+ public Type? ResourceType {
+ get => _resourceType;
+ set => _resourceType = value ?? throw new ArgumentNullException(nameof(value));
+ }
///
- /// Gets the command's sub-names.
+ /// Gets or sets a value indicating whether the command should be logged.
///
- public IEnumerable CommandSubNames { get; }
+ /// if the command should be logged; otherwise, .
+ /// This property's value is useful for hiding, e.g., authentication commands.
+ public bool ShouldBeLogged { get; set; } = true;
///
- /// Initializes a new instance of the class with the given command name
- /// and sub-names.
+ /// Initializes a new instance of the class with the specified qualified
+ /// name.
///
- /// The command name.
- /// The command sub-names.
+ ///
+ /// The qualified name. This includes the namespace: e.g., tshock:kick .
+ ///
///
- /// or are null .
+ /// is .
///
- public CommandHandlerAttribute(string commandName, params string[] commandSubNames) {
- CommandName = commandName ?? throw new ArgumentNullException(nameof(commandName));
- CommandSubNames = commandSubNames ?? throw new ArgumentNullException(nameof(commandSubNames));
+ public CommandHandlerAttribute(string qualifiedName) {
+ if (qualifiedName is null) {
+ throw new ArgumentNullException(nameof(qualifiedName));
+ }
+
+ _qualifiedName = qualifiedName;
+ }
+
+ [return: NotNullIfNotNull("str")]
+ private string? GetResourceStringMaybe(string? str) {
+ if (str is null) {
+ return null;
+ }
+
+ return _resourceType != null ? ResourceHelper.LoadResource(_resourceType, str) : str;
}
}
}
diff --git a/src/TShock/Commands/ConsoleCommandSender.cs b/src/TShock/Commands/ConsoleCommandSender.cs
new file mode 100644
index 000000000..71ce00348
--- /dev/null
+++ b/src/TShock/Commands/ConsoleCommandSender.cs
@@ -0,0 +1,107 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Globalization;
+using System.Text;
+using Destructurama;
+using JetBrains.Annotations;
+using Microsoft.Xna.Framework;
+using Orion.Players;
+using Serilog;
+using Serilog.Events;
+using Serilog.Sinks.SystemConsole.Themes;
+
+namespace TShock.Commands {
+ ///
+ /// Represents a console-based command sender.
+ ///
+ public sealed class ConsoleCommandSender : ICommandSender {
+#if DEBUG
+ private const LogEventLevel LogLevel = LogEventLevel.Verbose;
+#else
+ private const LogEventLevel LogLevel = LogEventLevel.Error;
+#endif
+
+ ///
+ /// Gets the console-based command sender.
+ ///
+ /// The console-based command sender.
+ public static ConsoleCommandSender Instance { get; } = new ConsoleCommandSender();
+
+ ///
+ public string Name => "Console";
+
+ ///
+ public ILogger Log { get; }
+
+ ///
+ public IPlayer? Player => null;
+
+ [Pure]
+ private static string GetColorString(Color color) => $"\x1b[38;2;{color.R};{color.G};{color.B}m";
+
+ private ConsoleCommandSender() {
+ Log = new LoggerConfiguration()
+ .Destructure.UsingAttributes()
+ .WriteTo.Console(
+ outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}",
+ theme: AnsiConsoleTheme.Code)
+ .MinimumLevel.Is(LogLevel)
+ .CreateLogger();
+ }
+
+ ///
+ public void SendMessage(string message) => SendMessageImpl(message, string.Empty);
+
+ ///
+ public void SendMessage(string message, Color color) => SendMessageImpl(message, GetColorString(color));
+
+ private static void SendMessageImpl(ReadOnlySpan message, string colorString) {
+ var output = new StringBuilder(message.Length);
+ while (true) {
+ output.Append(colorString);
+ var leftBracket = message.IndexOf('[');
+ var rightBracket = leftBracket + 1 + message[(leftBracket + 1)..].IndexOf(']');
+ if (leftBracket < 0 || rightBracket < 0) {
+ break;
+ }
+
+ output.Append(message[..leftBracket]);
+ var inside = message[(leftBracket + 1)..rightBracket];
+ message = message[(rightBracket + 1)..];
+ var colon = inside.IndexOf(':');
+ var isValidColorTag = inside.StartsWith("c/", StringComparison.OrdinalIgnoreCase) && colon > 2;
+ if (!isValidColorTag) {
+ output.Append('[').Append(inside).Append(']');
+ continue;
+ }
+
+ if (int.TryParse(inside[2..colon], NumberStyles.AllowHexSpecifier,
+ CultureInfo.InvariantCulture, out var numberColor)) {
+ var tagColor = new Color((numberColor >> 16) & 255, (numberColor >> 8) & 255, numberColor & 255);
+ output.Append(GetColorString(tagColor));
+ }
+
+ output.Append(inside[(colon + 1)..]);
+ }
+
+ output.Append(message).Append("\x1b[0m");
+ Console.WriteLine(output);
+ }
+ }
+}
diff --git a/src/TShock/Commands/Exceptions/CommandExecuteException.cs b/src/TShock/Commands/Exceptions/CommandExecuteException.cs
new file mode 100644
index 000000000..2d2b4c2cc
--- /dev/null
+++ b/src/TShock/Commands/Exceptions/CommandExecuteException.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace TShock.Commands.Exceptions {
+ ///
+ /// The exception thrown when a command could not be executed.
+ ///
+ [ExcludeFromCodeCoverage]
+ public class CommandExecuteException : Exception {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommandExecuteException() { }
+
+ ///
+ /// Initializes a new instance of the class with the specified message.
+ ///
+ /// The message.
+ public CommandExecuteException(string message) : base(message) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified message
+ /// and inner exception.
+ ///
+ /// The message.
+ /// The inner exception.
+ public CommandExecuteException(string message, Exception innerException) : base(message, innerException) { }
+ }
+}
diff --git a/src/TShock/Commands/Exceptions/CommandNotFoundException.cs b/src/TShock/Commands/Exceptions/CommandNotFoundException.cs
new file mode 100644
index 000000000..5d58b2f25
--- /dev/null
+++ b/src/TShock/Commands/Exceptions/CommandNotFoundException.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace TShock.Commands.Exceptions {
+ ///
+ /// The exception thrown when a command could not be found.
+ ///
+ [ExcludeFromCodeCoverage]
+ public class CommandNotFoundException : Exception {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommandNotFoundException() { }
+
+ ///
+ /// Initializes a new instance of the class with the specified message.
+ ///
+ /// The message.
+ public CommandNotFoundException(string message) : base(message) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified message
+ /// and inner exception.
+ ///
+ /// The message.
+ /// The inner exception.
+ public CommandNotFoundException(string message, Exception innerException) : base(message, innerException) { }
+ }
+}
diff --git a/src/TShock/Commands/Exceptions/CommandParseException.cs b/src/TShock/Commands/Exceptions/CommandParseException.cs
new file mode 100644
index 000000000..31d9be897
--- /dev/null
+++ b/src/TShock/Commands/Exceptions/CommandParseException.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace TShock.Commands.Exceptions {
+ ///
+ /// The exception thrown when a command input cannot be parsed.
+ ///
+ [ExcludeFromCodeCoverage]
+ public class CommandParseException : Exception {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommandParseException() { }
+
+ ///
+ /// Initializes a new instance of the class with the specified message.
+ ///
+ /// The message.
+ public CommandParseException(string message) : base(message) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified message and
+ /// inner exception.
+ ///
+ /// The message.
+ /// The inner exception.
+ public CommandParseException(string message, Exception innerException) : base(message, innerException) { }
+ }
+}
diff --git a/src/TShock/Commands/ICommand.cs b/src/TShock/Commands/ICommand.cs
index ed63a1324..a052fdc7c 100644
--- a/src/TShock/Commands/ICommand.cs
+++ b/src/TShock/Commands/ICommand.cs
@@ -16,42 +16,48 @@
// along with TShock. If not, see .
using System;
-using System.Collections.Generic;
-using System.Reflection;
+using TShock.Commands.Exceptions;
namespace TShock.Commands {
///
- /// Represents a command.
+ /// Represents a command. Commands can be executed by command senders, and provide bits of functionality.
+ /// Implementations are thread-safe.
///
public interface ICommand {
///
- /// Gets the command's name.
+ /// Gets the command's qualified name. This includes the namespace: e.g., tshock:kick
///
- string Name { get; }
+ /// The command's qualified name.
+ string QualifiedName { get; }
///
- /// Gets the command's sub-names.
+ /// Gets the command's help text. This will show up in the /help command.
///
- IEnumerable SubNames { get; }
+ /// The command's help text.
+ string HelpText { get; }
///
- /// Gets the object associated with the command's handler. If null , then the command handler is static.
+ /// Gets the command's usage text. This will show up in the /help command and when invalid syntax is
+ /// used.
///
- object? HandlerObject { get; }
+ /// The command's usage text.
+ string UsageText { get; }
///
- /// Gets the command's handler.
+ /// Gets a value indicating whether the command should be logged.
///
- MethodBase Handler { get; }
+ /// if the command should be logged; otherwise .
+ /// This property's value is useful for hiding, e.g., authentication commands.
+ bool ShouldBeLogged { get; }
///
- /// Invokes the command as the given sender with the specified input.
+ /// Invokes the command as a with the specified .
///
/// The sender.
- /// The input. This does not include the command's name or sub-names.
- ///
- /// or are null .
- ///
+ /// The input. This does not include the command's name.
+ /// is .
+ /// The command could not be executed.
+ /// The command input could not be parsed.
void Invoke(ICommandSender sender, string input);
}
}
diff --git a/src/TShock/Commands/ICommandSender.cs b/src/TShock/Commands/ICommandSender.cs
index 67cf0293e..c923976e1 100644
--- a/src/TShock/Commands/ICommandSender.cs
+++ b/src/TShock/Commands/ICommandSender.cs
@@ -17,6 +17,7 @@
using System;
using Microsoft.Xna.Framework;
+using Orion.Players;
using Serilog;
namespace TShock.Commands {
@@ -27,19 +28,87 @@ public interface ICommandSender {
///
/// Gets the sender's name.
///
+ /// The sender's name.
string Name { get; }
///
/// Gets the sender's log.
///
+ /// The sender's log.
+ ///
+ /// This property's value can be used to log information to the sender. For example, you can pass debugging or
+ /// error messages and they will be formatted appropriately.
+ ///
ILogger Log { get; }
///
- /// Sends a message to the sender with the given color.
+ /// Gets the sender's player. If , then there is no associated player.
+ ///
+ /// The sender's player.
+ ///
+ /// This property's value can be used to modify the command sender's state. For example, you can heal or kill
+ /// the player.
+ ///
+ IPlayer? Player { get; }
+
+ ///
+ /// Sends a to the sender.
+ ///
+ /// The message.
+ /// is .
+ void SendMessage(string message);
+
+ ///
+ /// Sends a to the sender with the given .
///
/// The message.
/// The color.
- /// is null .
+ /// is .
void SendMessage(string message, Color color);
}
+
+ ///
+ /// Provides extensions for the interface.
+ ///
+ public static class CommandSenderExtensions {
+ ///
+ /// Sends an error to the .
+ ///
+ /// The sender.
+ /// The error message.
+ ///
+ /// or are .
+ ///
+ public static void SendErrorMessage(this ICommandSender sender, string message) {
+ if (sender is null) {
+ throw new ArgumentNullException(nameof(sender));
+ }
+
+ if (message is null) {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ sender.SendMessage(message, new Color(0xcc, 0x44, 0x44));
+ }
+
+ ///
+ /// Sends an informational to the .
+ ///
+ /// The sender.
+ /// The informational message.
+ ///
+ /// or are .
+ ///
+ public static void SendInfoMessage(this ICommandSender sender, string message) {
+ if (sender is null) {
+ throw new ArgumentNullException(nameof(sender));
+ }
+
+ if (message is null) {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ sender.SendMessage(message, new Color(0xff, 0xf0, 0x14));
+ }
+ }
}
diff --git a/src/TShock/Commands/ICommandService.cs b/src/TShock/Commands/ICommandService.cs
index 4d0d1e53a..e78e8313f 100644
--- a/src/TShock/Commands/ICommandService.cs
+++ b/src/TShock/Commands/ICommandService.cs
@@ -17,66 +17,53 @@
using System;
using System.Collections.Generic;
-using Orion;
-using Orion.Events;
-using TShock.Events.Commands;
+using TShock.Commands.Parsers;
namespace TShock.Commands {
///
- /// Represents a service that manages commands. Provides command-related hooks and methods.
+ /// Represents a service that manages commands. Provides command-related properties and methods.
///
- public interface ICommandService : IService {
+ public interface ICommandService {
///
- /// Gets or sets the event handlers that occur when registering a command.
+ /// Gets a read-only mapping from qualified command names to commands.
///
- EventHandlerCollection? CommandRegister { get; set; }
+ /// A read-only mapping from qualified command names to commands.
+ IReadOnlyDictionary Commands { get; }
///
- /// Gets or sets the event handlers that occur when executing a command.
+ /// Gets a read-only mapping from types to parsers.
///
- EventHandlerCollection? CommandExecute { get; set; }
+ /// A read-only mapping from types to parsers.
+ IReadOnlyDictionary Parsers { get; }
///
- /// Gets or sets the event handlers that occur when unregistering a command.
+ /// Registers and returns the commands defined with the 's command handlers.
+ /// Command handlers are specified using the attribute.
///
- EventHandlerCollection? CommandUnregister { get; set; }
-
- ///
- /// Registers and returns the commands defined with the given object's command handlers.
- ///
- /// The object.
- /// The resulting commands.
- /// is null .
- IReadOnlyCollection RegisterCommands(object obj);
-
- ///
- /// Registers and returns the commands defined with the given type's static command handlers.
- ///
- /// The type.
+ /// The object.
/// The resulting commands.
- /// is null .
- IReadOnlyCollection RegisterCommands(Type type);
+ ///
+ /// is .
+ ///
+ IReadOnlyCollection RegisterCommands(object handlerObject);
///
- /// Returns all commands with the given command name and sub-names.
+ /// Registers as the parser for . This will allow
+ /// to be parsed in command handlers.
///
- /// The command name.
- /// The command sub-names.
- /// The commands with the given command name.
- ///
- /// An element of is null .
- ///
- ///
- /// or are null .
- ///
- IReadOnlyCollection FindCommands(string commandName, params string[] commandSubNames);
+ /// The parse type.
+ /// The parser.
+ /// is .
+ void RegisterParser(IArgumentParser parser);
///
- /// Unregisters the given command and returns a value indicating success.
+ /// Unregisters the and returns a value indicating success.
///
/// The command.
- /// A value indicating whether the command was successfully unregistered.
- /// is null .
+ ///
+ /// if was unregistered; otherwise, .
+ ///
+ /// is .
bool UnregisterCommand(ICommand command);
}
}
diff --git a/src/TShock/Commands/Parsers/Attributes/FlagAttribute.cs b/src/TShock/Commands/Parsers/Attributes/FlagAttribute.cs
new file mode 100644
index 000000000..12ee5ae2f
--- /dev/null
+++ b/src/TShock/Commands/Parsers/Attributes/FlagAttribute.cs
@@ -0,0 +1,65 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace TShock.Commands.Parsers.Attributes {
+ ///
+ /// Specifies that a parameter should have flag-based parsing.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter)]
+ public sealed class FlagAttribute : Attribute {
+ ///
+ /// Gets a read-only view of the flags. Flags with length 1 are treated as short flags.
+ ///
+ /// A read-only view of the flags.
+ public IReadOnlyCollection Flags { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified flags. Flags with
+ /// length 1 are treated as short flags.
+ ///
+ /// The flag.
+ /// The alternate flags. This may be empty.
+ ///
+ /// contains a element.
+ ///
+ ///
+ /// or are .
+ ///
+ [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters",
+ Justification = "strings are not user-facing")]
+ public FlagAttribute(string flag, params string[] alternateFlags) {
+ if (flag is null) {
+ throw new ArgumentNullException(nameof(flag));
+ }
+
+ if (alternateFlags is null) {
+ throw new ArgumentNullException(nameof(alternateFlags));
+ }
+
+ if (alternateFlags.Any(f => f == null)) {
+ throw new ArgumentException("Array contains null element.", nameof(alternateFlags));
+ }
+
+ Flags = new[] { flag }.Concat(alternateFlags).ToList();
+ }
+ }
+}
diff --git a/src/TShock/Commands/Parsers/Attributes/RestOfInputAttribute.cs b/src/TShock/Commands/Parsers/Attributes/RestOfInputAttribute.cs
new file mode 100644
index 000000000..f0433aee2
--- /dev/null
+++ b/src/TShock/Commands/Parsers/Attributes/RestOfInputAttribute.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+
+namespace TShock.Commands.Parsers.Attributes {
+ ///
+ /// Specifies that a parameter should parse to the rest of the input.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter)]
+ public sealed class RestOfInputAttribute : Attribute { }
+}
diff --git a/src/TShock/Commands/Parsers/DoubleParser.cs b/src/TShock/Commands/Parsers/DoubleParser.cs
new file mode 100644
index 000000000..4e1abf715
--- /dev/null
+++ b/src/TShock/Commands/Parsers/DoubleParser.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using TShock.Commands.Exceptions;
+using TShock.Properties;
+using TShock.Utils.Extensions;
+
+namespace TShock.Commands.Parsers {
+ ///
+ /// Parses a Double.
+ ///
+ public sealed class DoubleParser : IArgumentParser {
+ ///
+ public double Parse(ref ReadOnlySpan input, ISet attributes) {
+ var end = input.IndexOfOrEnd(' ');
+ var parse = input[..end];
+
+ // Calling Parse here instead of TryParse allows us to give better error messages.
+ try {
+ var value = double.Parse(parse, NumberStyles.Float, CultureInfo.InvariantCulture);
+ input = input[end..];
+ return value;
+ } catch (FormatException ex) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.DoubleParser_InvalidDouble,
+ parse.ToString()), ex);
+ }
+ }
+ }
+}
diff --git a/src/TShock/Commands/Parsers/IArgumentParser.cs b/src/TShock/Commands/Parsers/IArgumentParser.cs
new file mode 100644
index 000000000..59afd2932
--- /dev/null
+++ b/src/TShock/Commands/Parsers/IArgumentParser.cs
@@ -0,0 +1,58 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using TShock.Commands.Exceptions;
+
+namespace TShock.Commands.Parsers {
+ ///
+ /// Provides parsing support.
+ ///
+ public interface IArgumentParser {
+ ///
+ /// Parses the and returns a corresponding object using the specified
+ /// . will be consumed as necessary.
+ ///
+ /// The input. This is guaranteed to start with a non-whitespace character.
+ /// The attributes.
+ /// A corresponding object.
+ /// The input could not be parsed properly.
+ object? Parse(ref ReadOnlySpan input, ISet attributes);
+ }
+
+ ///
+ /// Provides type-safe parsing support.
+ ///
+ /// The parse type.
+ public interface IArgumentParser : IArgumentParser {
+ ///
+ /// Parses the and returns a corresponding instance using
+ /// the specified . will be consumed as necessary.
+ ///
+ /// The input. This is guaranteed to start with a non-whitespace character.
+ /// The attributes.
+ /// A corresponding instance.
+ /// The input could not be parsed properly.
+ new TParse Parse(ref ReadOnlySpan input, ISet attributes);
+
+ [ExcludeFromCodeCoverage]
+ object? IArgumentParser.Parse(ref ReadOnlySpan input, ISet attributes) =>
+ Parse(ref input, attributes);
+ }
+}
diff --git a/src/TShock/Commands/Parsers/Int32Parser.cs b/src/TShock/Commands/Parsers/Int32Parser.cs
new file mode 100644
index 000000000..4e5755f98
--- /dev/null
+++ b/src/TShock/Commands/Parsers/Int32Parser.cs
@@ -0,0 +1,51 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using TShock.Commands.Exceptions;
+using TShock.Properties;
+using TShock.Utils.Extensions;
+
+namespace TShock.Commands.Parsers {
+ ///
+ /// Parses an Int32.
+ ///
+ public sealed class Int32Parser : IArgumentParser {
+ ///
+ public int Parse(ref ReadOnlySpan input, ISet attributes) {
+ var end = input.IndexOfOrEnd(' ');
+ var parse = input[..end];
+
+ // Calling Parse here instead of TryParse allows us to give better error messages.
+ try {
+ var value = int.Parse(parse, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ input = input[end..];
+ return value;
+ } catch (FormatException ex) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.Int32Parser_InvalidInteger,
+ parse.ToString()), ex);
+ } catch (OverflowException ex) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.Int32Parser_IntegerOutOfRange,
+ parse.ToString()), ex);
+ }
+ }
+ }
+}
diff --git a/src/TShock/Commands/Parsers/StringParser.cs b/src/TShock/Commands/Parsers/StringParser.cs
new file mode 100644
index 000000000..cb48057fb
--- /dev/null
+++ b/src/TShock/Commands/Parsers/StringParser.cs
@@ -0,0 +1,93 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using TShock.Commands.Exceptions;
+using TShock.Commands.Parsers.Attributes;
+using TShock.Properties;
+
+namespace TShock.Commands.Parsers {
+ ///
+ /// Parses a string.
+ ///
+ public sealed class StringParser : IArgumentParser {
+ ///
+ public string Parse(ref ReadOnlySpan input, ISet attributes) {
+ if (attributes.Any(a => a is RestOfInputAttribute)) {
+ var result = input.ToString();
+ input = default;
+ return result;
+ }
+
+ // Begin building our string character-by-character.
+ var builder = new StringBuilder();
+ var end = 0;
+ var isInQuotes = false;
+ while (end < input.Length) {
+ var c = input[end];
+
+ // Handle quotes.
+ if (c == '"') {
+ ++end;
+ if (isInQuotes) {
+ break;
+ }
+
+ isInQuotes = true;
+ continue;
+ }
+
+ // Handle escape characters.
+ if (c == '\\') {
+ if (++end >= input.Length) {
+ throw new CommandParseException(Resources.StringParser_InvalidBackslash);
+ }
+
+ var nextC = input[end];
+ if (nextC == '"' || nextC == '\\' || char.IsWhiteSpace(nextC)) {
+ builder.Append(nextC);
+ } else if (nextC == 't') {
+ builder.Append('\t');
+ } else if (nextC == 'n') {
+ builder.Append('\n');
+ } else {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.StringParser_UnrecognizedEscape,
+ nextC));
+ }
+
+ ++end;
+ continue;
+ }
+
+ if (char.IsWhiteSpace(c) && !isInQuotes) {
+ break;
+ }
+
+ builder.Append(c);
+ ++end;
+ }
+
+ input = input[end..];
+ return builder.ToString();
+ }
+ }
+}
diff --git a/src/TShock/Commands/PlayerCommandSender.cs b/src/TShock/Commands/PlayerCommandSender.cs
new file mode 100644
index 000000000..63087e370
--- /dev/null
+++ b/src/TShock/Commands/PlayerCommandSender.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using Destructurama;
+using Microsoft.Xna.Framework;
+using Orion.Players;
+using Serilog;
+using Serilog.Events;
+using TShock.Logging;
+
+namespace TShock.Commands {
+ ///
+ /// Represents a player-based command sender.
+ ///
+ public sealed class PlayerCommandSender : ICommandSender {
+#if DEBUG
+ private const LogEventLevel LogLevel = LogEventLevel.Verbose;
+#else
+ private const LogEventLevel LogLevel = LogEventLevel.Error;
+#endif
+
+ ///
+ public string Name => Player.Name;
+
+ ///
+ public ILogger Log { get; }
+
+ ///
+ public IPlayer Player { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified player.
+ ///
+ /// The player.
+ /// is .
+ public PlayerCommandSender(IPlayer player) {
+ Player = player ?? throw new ArgumentNullException(nameof(player));
+ Log = new LoggerConfiguration()
+ .Destructure.UsingAttributes()
+ .MinimumLevel.Is(LogLevel)
+ .WriteTo.Player(player)
+ .CreateLogger();
+ }
+
+ ///
+ public void SendMessage(string message) => SendMessage(message, Color.White);
+
+ ///
+ public void SendMessage(string message, Color color) => Player.SendMessage(message, color);
+ }
+}
diff --git a/src/TShock/Commands/TShockCommand.cs b/src/TShock/Commands/TShockCommand.cs
new file mode 100644
index 000000000..708509c97
--- /dev/null
+++ b/src/TShock/Commands/TShockCommand.cs
@@ -0,0 +1,271 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Orion.Events;
+using TShock.Commands.Exceptions;
+using TShock.Commands.Parsers.Attributes;
+using TShock.Events.Commands;
+using TShock.Properties;
+using TShock.Utils.Extensions;
+
+namespace TShock.Commands {
+ internal class TShockCommand : ICommand {
+ private readonly TShockCommandService _commandService;
+ private readonly object _handlerObject;
+ private readonly MethodInfo _handler;
+ private readonly CommandHandlerAttribute _attribute;
+
+ private readonly ISet _validShortFlags = new HashSet();
+ private readonly ISet _validLongFlags = new HashSet();
+ private readonly IDictionary _validOptionals = new Dictionary();
+
+ public string QualifiedName => _attribute.QualifiedName;
+ public string HelpText => _attribute.HelpText ?? Resources.Command_MissingHelpText;
+ public string UsageText => _attribute.UsageText ?? Resources.Command_MissingUsageText;
+ public bool ShouldBeLogged => _attribute.ShouldBeLogged;
+
+ // We need to inject TShockCommandService so that we can raise a CommandExecuteEvent.
+ public TShockCommand(
+ TShockCommandService commandService, object handlerObject, MethodInfo handler,
+ CommandHandlerAttribute attribute) {
+ Debug.Assert(commandService != null, "command service should not be null");
+ Debug.Assert(handlerObject != null, "handler object should not be null");
+ Debug.Assert(handler != null, "handler should not be null");
+ Debug.Assert(attribute != null, "attribute should not be null");
+
+ _commandService = commandService;
+ _handlerObject = handlerObject;
+ _handler = handler;
+ _attribute = attribute;
+
+ // Preprocessing parameters in the constructor allows us to learn the command's flags and optionals.
+ void PreprocessParameter(ParameterInfo parameterInfo) {
+ var parameterType = parameterInfo.ParameterType;
+
+ // If the parameter is a bool and it is marked with FlagAttribute, we'll note it.
+ if (parameterType == typeof(bool)) {
+ var attribute = parameterInfo.GetCustomAttribute();
+ foreach (var flag in attribute?.Flags ?? Enumerable.Empty()) {
+ if (flag.Length == 1) {
+ _validShortFlags.Add(flag[0]);
+ } else {
+ _validLongFlags.Add(flag);
+ }
+ }
+ }
+
+ // If the parameter is optional, we'll note it. We replace underscores with hyphens here since hyphens
+ // aren't valid in C# identifiers.
+ if (parameterInfo.IsOptional) {
+ _validOptionals.Add(parameterInfo.Name.Replace('_', '-'), parameterInfo);
+ }
+ }
+
+ foreach (var parameterInfo in _handler.GetParameters()) {
+ PreprocessParameter(parameterInfo);
+ }
+ }
+
+ public void Invoke(ICommandSender sender, string input) {
+ if (sender is null) {
+ throw new ArgumentNullException(nameof(sender));
+ }
+
+ var e = new CommandExecuteEvent(this, sender, input);
+ _commandService.Kernel.RaiseEvent(e, _commandService.Log);
+ if (e.IsCanceled()) {
+ return;
+ }
+
+ var shortFlags = new HashSet();
+ var longFlags = new HashSet();
+ var optionals = new Dictionary();
+
+ object? ParseArgument(ref ReadOnlySpan input, ParameterInfo parameterInfo, Type? typeHint = null) {
+ var parameterType = typeHint ?? parameterInfo.ParameterType;
+ if (!_commandService.Parsers.TryGetValue(parameterType, out var parser)) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandParse_UnrecognizedArgType,
+ parameterType));
+ }
+
+ var attributes = parameterInfo.GetCustomAttributes().ToHashSet();
+ if (input.IsEmpty) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandParse_MissingArg,
+ parameterInfo));
+ }
+
+ return parser.Parse(ref input, attributes);
+ }
+
+ void ParseShortFlags(ref ReadOnlySpan input, int space) {
+ if (space <= 1) {
+ throw new CommandParseException(Resources.CommandParse_InvalidHyphenatedArg);
+ }
+
+ foreach (var c in input[1..space]) {
+ if (!_validShortFlags.Contains(c)) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandParse_UnrecognizedShortFlag,
+ c));
+ }
+
+ shortFlags.Add(c);
+ }
+
+ input = input[space..];
+ }
+
+ void ParseLongFlag(ref ReadOnlySpan input, int space) {
+ if (space <= 2) {
+ throw new CommandParseException(Resources.CommandParse_InvalidHyphenatedArg);
+ }
+
+ var longFlag = input[2..space].ToString();
+ if (!_validLongFlags.Contains(longFlag)) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandParse_UnrecognizedLongFlag,
+ longFlag));
+ }
+
+ longFlags.Add(longFlag);
+ input = input[space..];
+ }
+
+ void ParseOptional(ref ReadOnlySpan input, int equals) {
+ if (equals <= 2) {
+ throw new CommandParseException(Resources.CommandParse_InvalidHyphenatedArg);
+ }
+
+ var optional = input[2..equals].ToString();
+ if (!_validOptionals.TryGetValue(optional, out var parameterInfo)) {
+ throw new CommandParseException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandParse_UnrecognizedOptional,
+ optional));
+ }
+
+ input = input[(equals + 1)..];
+ input = input.TrimStart();
+ optionals[optional] = ParseArgument(ref input, parameterInfo);
+ }
+
+ // Parse all hyphenated arguments:
+ // 1) Short flags are single-character flags and use one hyphen: "-f".
+ // 2) Long flags are string flags and use two hyphens: "--force".
+ // 3) Optionals specify values with two hyphens: "--depth=10".
+ void ParseHyphenatedArguments(ref ReadOnlySpan input) {
+ input = input.TrimStart();
+ while (input.StartsWith("-")) {
+ var space = input.IndexOfOrEnd(' ');
+ if (input.StartsWith("--")) {
+ var equals = input.IndexOfOrEnd('=');
+ if (equals < space) {
+ ParseOptional(ref input, equals);
+ } else {
+ ParseLongFlag(ref input, space);
+ }
+ } else {
+ ParseShortFlags(ref input, space);
+ }
+
+ input = input.TrimStart();
+ }
+ }
+
+ // Parse a parameter:
+ // 1) If the parameter is an ICommandSender, then inject sender.
+ // 2) If the parameter is a bool and is marked with FlagAttribute, then inject the flag.
+ // 3) If the parameter is optional, then inject the optional or else the default value.
+ // 4) Otherwise, we parse the argument directly.
+ object? ParseParameter(ParameterInfo parameterInfo, ref ReadOnlySpan input) {
+ var parameterType = parameterInfo.ParameterType;
+ if (parameterType == typeof(ICommandSender)) {
+ return sender;
+ }
+
+ if (parameterType == typeof(bool)) {
+ var attribute = parameterInfo.GetCustomAttribute();
+ if (attribute != null) {
+ return attribute.Flags.Any(
+ f => f.Length == 1 && shortFlags.Contains(f[0]) || longFlags.Contains(f));
+ }
+ }
+
+ if (parameterInfo.GetCustomAttribute() != null) {
+ var elementType = parameterType.GetElementType();
+
+ var list = new List();
+ input = input.TrimStart();
+ while (!input.IsEmpty) {
+ list.Add(ParseArgument(ref input, parameterInfo, elementType));
+ input = input.TrimStart();
+ }
+
+ var array = Array.CreateInstance(elementType, list.Count);
+ for (var i = 0; i < list.Count; ++i) {
+ array.SetValue(list[i], i);
+ }
+
+ return array;
+ }
+
+ input = input.TrimStart();
+ if (parameterInfo.IsOptional) {
+ var optional = parameterInfo.Name.Replace('_', '-');
+ if (optionals.TryGetValue(optional, out var value)) {
+ return value;
+ }
+
+ if (input.IsEmpty) {
+ return parameterInfo.DefaultValue;
+ }
+ }
+
+ return ParseArgument(ref input, parameterInfo);
+ }
+
+ var inputSpan = e.Input.AsSpan();
+ if (_validShortFlags.Count + _validLongFlags.Count + _validOptionals.Count > 0) {
+ ParseHyphenatedArguments(ref inputSpan);
+ }
+
+ var parameterInfos = _handler.GetParameters();
+ var parameters = new object?[parameterInfos.Length];
+ for (var i = 0; i < parameters.Length; ++i) {
+ parameters[i] = ParseParameter(parameterInfos[i], ref inputSpan);
+ }
+
+ // Ensure that we've consumed all of the useful parts of the input.
+ if (!inputSpan.IsWhiteSpace()) {
+ throw new CommandParseException(Resources.CommandParse_TooManyArgs);
+ }
+
+ try {
+ _handler.Invoke(_handlerObject, parameters);
+ } catch (TargetInvocationException ex) {
+ throw new CommandExecuteException(Resources.CommandExecute_Exception, ex.InnerException);
+ }
+ }
+ }
+}
diff --git a/src/TShock/Commands/TShockCommandService.cs b/src/TShock/Commands/TShockCommandService.cs
new file mode 100644
index 000000000..d83e68d2a
--- /dev/null
+++ b/src/TShock/Commands/TShockCommandService.cs
@@ -0,0 +1,76 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Orion;
+using Orion.Events;
+using Serilog;
+using TShock.Commands.Parsers;
+using TShock.Events.Commands;
+
+namespace TShock.Commands {
+ internal sealed class TShockCommandService : OrionService, ICommandService {
+ private readonly Dictionary _commands = new Dictionary();
+ private readonly Dictionary _parsers = new Dictionary();
+
+ public IReadOnlyDictionary Commands => _commands;
+ public IReadOnlyDictionary Parsers => _parsers;
+
+ public TShockCommandService(OrionKernel kernel, ILogger log) : base(kernel, log) {
+ Debug.Assert(log != null, "log should not be null");
+ }
+
+ public IReadOnlyCollection RegisterCommands(object handlerObject) {
+ if (handlerObject is null) {
+ throw new ArgumentNullException(nameof(handlerObject));
+ }
+
+ ICommand? RegisterCommand(ICommand command) {
+ var e = new CommandRegisterEvent(command);
+ Kernel.RaiseEvent(e, Log);
+ if (e.IsCanceled()) {
+ return null;
+ }
+
+ _commands.Add(command.QualifiedName, command);
+ return command;
+ }
+
+ return handlerObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .SelectMany(m => m.GetCustomAttributes(),
+ (m, a) => RegisterCommand(new TShockCommand(this, handlerObject, m, a)))
+ .Where(c => c != null).ToList()!;
+ }
+
+ public void RegisterParser(IArgumentParser parser) =>
+ _parsers[typeof(TParse)] = parser ?? throw new ArgumentNullException(nameof(parser));
+
+ public bool UnregisterCommand(ICommand command) {
+ if (command is null) {
+ throw new ArgumentNullException(nameof(command));
+ }
+
+ var e = new CommandUnregisterEvent(command);
+ Kernel.RaiseEvent(e, Log);
+ return !e.IsCanceled() && _commands.Remove(command.QualifiedName);
+ }
+ }
+}
diff --git a/src/TShock/Events/Commands/CommandEvent.cs b/src/TShock/Events/Commands/CommandEvent.cs
new file mode 100644
index 000000000..277da4214
--- /dev/null
+++ b/src/TShock/Events/Commands/CommandEvent.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using Orion.Events;
+using TShock.Commands;
+
+namespace TShock.Events.Commands {
+ ///
+ /// Represents a command-related event.
+ ///
+ public abstract class CommandEvent : Event {
+ private ICommand _command;
+
+ ///
+ /// Gets or sets the command.
+ ///
+ /// The command.
+ /// is .
+ public ICommand Command {
+ get => _command;
+ set => _command = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified command.
+ ///
+ /// The command.
+ /// is .
+ protected CommandEvent(ICommand command) {
+ _command = command ?? throw new ArgumentNullException(nameof(command));
+ }
+ }
+}
diff --git a/src/TShock/Events/Commands/CommandExecuteEventArgs.cs b/src/TShock/Events/Commands/CommandExecuteEvent.cs
similarity index 64%
rename from src/TShock/Events/Commands/CommandExecuteEventArgs.cs
rename to src/TShock/Events/Commands/CommandExecuteEvent.cs
index 1d531befd..875835699 100644
--- a/src/TShock/Events/Commands/CommandExecuteEventArgs.cs
+++ b/src/TShock/Events/Commands/CommandExecuteEvent.cs
@@ -16,38 +16,54 @@
// along with TShock. If not, see .
using System;
+using Destructurama.Attributed;
+using Orion.Events;
+using Orion.Utils;
using TShock.Commands;
namespace TShock.Events.Commands {
///
- /// Provides data for the event.
+ /// An event that occurs when a command executes. This event can be canceled and modified.
///
- public sealed class CommandExecuteEventArgs : CommandEventArgs {
+ [Event("command-execute")]
+ public sealed class CommandExecuteEvent : CommandEvent, ICancelable, IDirtiable {
private string _input;
+ ///
+ [NotLogged]
+ public string? CancellationReason { get; set; }
+
+ ///
+ [NotLogged]
+ public bool IsDirty { get; private set; }
+
///
/// Gets the command sender.
///
public ICommandSender Sender { get; }
///
- /// Gets or sets the input.
+ /// Gets or sets the input. This does not include the command's name.
///
+ /// is .
public string Input {
get => _input;
set => _input = value ?? throw new ArgumentNullException(nameof(value));
}
///
- /// Initializes a new instance of the class with the specified command,
+ /// Initializes a new instance of the class with the specified command,
/// command sender, and input.
///
/// The command.
/// The command sender.
- /// The input. This does not include the command's name or sub-names.
- public CommandExecuteEventArgs(ICommand command, ICommandSender sender, string input) : base(command) {
+ /// The input. This does not include the command's name.
+ public CommandExecuteEvent(ICommand command, ICommandSender sender, string input) : base(command) {
Sender = sender ?? throw new ArgumentNullException(nameof(sender));
_input = input ?? throw new ArgumentNullException(nameof(input));
}
+
+ ///
+ public void Clean() => IsDirty = false;
}
}
diff --git a/src/TShock/Events/Commands/CommandRegisterEventArgs.cs b/src/TShock/Events/Commands/CommandRegisterEvent.cs
similarity index 67%
rename from src/TShock/Events/Commands/CommandRegisterEventArgs.cs
rename to src/TShock/Events/Commands/CommandRegisterEvent.cs
index 766bbd6b1..21cc97b8c 100644
--- a/src/TShock/Events/Commands/CommandRegisterEventArgs.cs
+++ b/src/TShock/Events/Commands/CommandRegisterEvent.cs
@@ -16,18 +16,25 @@
// along with TShock. If not, see .
using System;
+using Destructurama.Attributed;
+using Orion.Events;
using TShock.Commands;
namespace TShock.Events.Commands {
///
- /// Provides data for the event.
+ /// An event that occurs when a command is registered. This event can be canceled.
///
- public sealed class CommandRegisterEventArgs : CommandEventArgs {
+ [Event("command-register")]
+ public sealed class CommandRegisterEvent : CommandEvent, ICancelable {
+ ///
+ [NotLogged]
+ public string? CancellationReason { get; set; }
+
///
- /// Initializes a new instance of the class with the specified command.
+ /// Initializes a new instance of the class with the specified command.
///
/// The command.
- /// is null .
- public CommandRegisterEventArgs(ICommand command) : base(command) { }
+ /// is .
+ public CommandRegisterEvent(ICommand command) : base(command) { }
}
}
diff --git a/src/TShock/Events/Commands/CommandUnregisterEventArgs.cs b/src/TShock/Events/Commands/CommandUnregisterEvent.cs
similarity index 66%
rename from src/TShock/Events/Commands/CommandUnregisterEventArgs.cs
rename to src/TShock/Events/Commands/CommandUnregisterEvent.cs
index 02f54b9c9..1e6c345dd 100644
--- a/src/TShock/Events/Commands/CommandUnregisterEventArgs.cs
+++ b/src/TShock/Events/Commands/CommandUnregisterEvent.cs
@@ -16,18 +16,25 @@
// along with TShock. If not, see .
using System;
+using Destructurama.Attributed;
+using Orion.Events;
using TShock.Commands;
namespace TShock.Events.Commands {
///
- /// Provides data for the event.
+ /// An event that occurs when a command is unregistered. This event can be canceled.
///
- public sealed class CommandUnregisterEventArgs : CommandEventArgs {
+ [Event("command-unregister")]
+ public sealed class CommandUnregisterEvent : CommandEvent, ICancelable {
+ ///
+ [NotLogged]
+ public string? CancellationReason { get; set; }
+
///
- /// Initializes a new instance of the class with the specified command.
+ /// Initializes a new instance of the class with the specified command.
///
/// The command.
- /// is null .
- public CommandUnregisterEventArgs(ICommand command) : base(command) { }
+ /// is .
+ public CommandUnregisterEvent(ICommand command) : base(command) { }
}
}
diff --git a/src/TShock/Logging/Formatting/ExceptionFormatter.cs b/src/TShock/Logging/Formatting/ExceptionFormatter.cs
new file mode 100644
index 000000000..ee8c65c33
--- /dev/null
+++ b/src/TShock/Logging/Formatting/ExceptionFormatter.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class ExceptionFormatter : ITextFormatter {
+ private readonly PlayerLogTheme _theme;
+
+ public ExceptionFormatter(PlayerLogTheme theme) {
+ Debug.Assert(theme != null, "theme should not be null");
+
+ _theme = theme;
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ var exception = logEvent.Exception;
+ if (exception is null) {
+ return;
+ }
+
+ output.Write(_theme.Stylize(exception.ToString(), PlayerLogThemeStyle.Exception));
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/LevelFormatter.cs b/src/TShock/Logging/Formatting/LevelFormatter.cs
new file mode 100644
index 000000000..f62f22a3c
--- /dev/null
+++ b/src/TShock/Logging/Formatting/LevelFormatter.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using TShock.Logging.Themes;
+using TShock.Utils.Extensions;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class LevelFormatter : ITextFormatter {
+ private static readonly IDictionary _levelToText =
+ new Dictionary {
+ [LogEventLevel.Verbose] = "VRB",
+ [LogEventLevel.Debug] = "DBG",
+ [LogEventLevel.Information] = "INF",
+ [LogEventLevel.Warning] = "WRN",
+ [LogEventLevel.Error] = "ERR",
+ [LogEventLevel.Fatal] = "FTL"
+ };
+
+ private static readonly IDictionary _levelToStyle =
+ new Dictionary {
+ [LogEventLevel.Verbose] = PlayerLogThemeStyle.VerboseLevel,
+ [LogEventLevel.Debug] = PlayerLogThemeStyle.DebugLevel,
+ [LogEventLevel.Information] = PlayerLogThemeStyle.InformationLevel,
+ [LogEventLevel.Warning] = PlayerLogThemeStyle.WarningLevel,
+ [LogEventLevel.Error] = PlayerLogThemeStyle.ErrorLevel,
+ [LogEventLevel.Fatal] = PlayerLogThemeStyle.FatalLevel
+ };
+
+ private readonly PlayerLogTheme _theme;
+
+ public LevelFormatter(PlayerLogTheme theme) {
+ Debug.Assert(theme != null, "theme should not be null");
+
+ _theme = theme;
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ var level = logEvent.Level;
+ var text = _levelToText.GetValueOrDefault(level, () => "???");
+ var style = _levelToStyle.GetValueOrDefault(level, () => PlayerLogThemeStyle.Invalid);
+ output.Write(_theme.Stylize(text, style));
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/MessageTemplateFormatter.cs b/src/TShock/Logging/Formatting/MessageTemplateFormatter.cs
new file mode 100644
index 000000000..0bf34d986
--- /dev/null
+++ b/src/TShock/Logging/Formatting/MessageTemplateFormatter.cs
@@ -0,0 +1,58 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class MessageTemplateFormatter : ITextFormatter {
+ private readonly PlayerLogTheme _theme;
+ private readonly PlayerLogValueVisitor _visitor;
+
+ public MessageTemplateFormatter(PlayerLogTheme theme, IFormatProvider? formatProvider) {
+ Debug.Assert(theme != null, "theme should not be null");
+
+ _theme = theme;
+ _visitor = new PlayerLogValueVisitor(theme, formatProvider);
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ foreach (var token in logEvent.MessageTemplate.Tokens) {
+ if (token is TextToken textToken) {
+ output.Write(_theme.Stylize(textToken.Text, PlayerLogThemeStyle.Text));
+ continue;
+ }
+
+ var propertyToken = (PropertyToken)token;
+ if (!logEvent.Properties.TryGetValue(propertyToken.PropertyName, out var propertyValue)) {
+ output.Write(_theme.Stylize(propertyToken.ToString(), PlayerLogThemeStyle.Invalid));
+ continue;
+ }
+
+ _visitor.Format(propertyValue, output);
+ }
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/NewLineFormatter.cs b/src/TShock/Logging/Formatting/NewLineFormatter.cs
new file mode 100644
index 000000000..a79b9c7ea
--- /dev/null
+++ b/src/TShock/Logging/Formatting/NewLineFormatter.cs
@@ -0,0 +1,32 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class NewLineFormatter : ITextFormatter {
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ output.Write('\n');
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/PlayerLogFormatter.cs b/src/TShock/Logging/Formatting/PlayerLogFormatter.cs
new file mode 100644
index 000000000..075624619
--- /dev/null
+++ b/src/TShock/Logging/Formatting/PlayerLogFormatter.cs
@@ -0,0 +1,71 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using Serilog.Formatting.Display;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class PlayerLogFormatter : ITextFormatter {
+ // Cache the MessageTemplateParser because why not?
+ private static readonly MessageTemplateParser _templateParser = new MessageTemplateParser();
+
+ private readonly IList _formatters = new List();
+
+ public PlayerLogFormatter(PlayerLogTheme theme, string outputTemplate, IFormatProvider? formatProvider) {
+ Debug.Assert(theme != null, "theme should not be null");
+ Debug.Assert(outputTemplate != null, "output template should not be null");
+
+ // The general strategy is to parse the template, and create a list of formatters in the same order as the
+ // properties found within the template. We can then run the formatters in order on each log event.
+ var template = _templateParser.Parse(outputTemplate);
+ foreach (var token in template.Tokens) {
+ if (token is TextToken textToken) {
+ _formatters.Add(new TextFormatter(theme, textToken.Text));
+ continue;
+ }
+
+ var propertyToken = (PropertyToken)token;
+ _formatters.Add(propertyToken.PropertyName switch {
+ OutputProperties.MessagePropertyName => new MessageTemplateFormatter(theme, formatProvider),
+ OutputProperties.TimestampPropertyName =>
+ new TimestampFormatter(theme, propertyToken.Format, formatProvider),
+ OutputProperties.LevelPropertyName => new LevelFormatter(theme),
+ OutputProperties.NewLinePropertyName => new NewLineFormatter(),
+ OutputProperties.ExceptionPropertyName => new ExceptionFormatter(theme),
+ OutputProperties.PropertiesPropertyName => new PropertiesFormatter(theme, template, formatProvider),
+ _ => new PropertyFormatter(theme, propertyToken, formatProvider)
+ });
+ }
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ foreach (var formatter in _formatters) {
+ formatter.Format(logEvent, output);
+ }
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/PropertiesFormatter.cs b/src/TShock/Logging/Formatting/PropertiesFormatter.cs
new file mode 100644
index 000000000..2ead9beef
--- /dev/null
+++ b/src/TShock/Logging/Formatting/PropertiesFormatter.cs
@@ -0,0 +1,55 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using Serilog.Events;
+using Serilog.Formatting;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class PropertiesFormatter : ITextFormatter {
+ private readonly MessageTemplate _outputTemplate;
+ private readonly PlayerLogValueVisitor _visitor;
+
+ public PropertiesFormatter(PlayerLogTheme theme, MessageTemplate outputTemplate,
+ IFormatProvider? formatProvider) {
+ Debug.Assert(theme != null, "theme should not be null");
+ Debug.Assert(outputTemplate != null, "output template should not be null");
+
+ _outputTemplate = outputTemplate;
+ _visitor = new PlayerLogValueVisitor(theme, formatProvider);
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ bool IsExcluded(string propertyName) => _outputTemplate.Tokens
+ .Concat(logEvent.MessageTemplate.Tokens)
+ .All(t => !(t is PropertyToken p) || p.PropertyName != propertyName);
+
+ var properties = logEvent.Properties
+ .Where(kvp => IsExcluded(kvp.Key))
+ .Select(kvp => new LogEventProperty(kvp.Key, kvp.Value));
+ _visitor.Format(new StructureValue(properties), output);
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/PropertyFormatter.cs b/src/TShock/Logging/Formatting/PropertyFormatter.cs
new file mode 100644
index 000000000..b8f19f9ba
--- /dev/null
+++ b/src/TShock/Logging/Formatting/PropertyFormatter.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class PropertyFormatter : ITextFormatter {
+ private readonly PlayerLogTheme _theme;
+ private readonly PropertyToken _propertyToken;
+ private readonly PlayerLogValueVisitor _visitor;
+
+ public PropertyFormatter(PlayerLogTheme theme, PropertyToken propertyToken, IFormatProvider? formatProvider) {
+ Debug.Assert(theme != null, "theme should not be null");
+ Debug.Assert(propertyToken != null, "property token should not be null");
+
+ _theme = theme;
+ _propertyToken = propertyToken;
+ _visitor = new PlayerLogValueVisitor(theme, formatProvider);
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ if (!logEvent.Properties.TryGetValue(_propertyToken.PropertyName, out var propertyValue)) {
+ output.Write(_theme.Stylize(_propertyToken.ToString(), PlayerLogThemeStyle.Invalid));
+ } else {
+ _visitor.Format(propertyValue, output);
+ }
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/TextFormatter.cs b/src/TShock/Logging/Formatting/TextFormatter.cs
new file mode 100644
index 000000000..6122fa4be
--- /dev/null
+++ b/src/TShock/Logging/Formatting/TextFormatter.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class TextFormatter : ITextFormatter {
+ private readonly PlayerLogTheme _theme;
+ private readonly string _text;
+
+ public TextFormatter(PlayerLogTheme theme, string text) {
+ Debug.Assert(theme != null, "theme should not be null");
+ Debug.Assert(text != null, "text should not be null");
+
+ _theme = theme;
+ _text = text;
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ output.Write(_theme.Stylize(_text, PlayerLogThemeStyle.Text));
+ }
+ }
+}
diff --git a/src/TShock/Logging/Formatting/TimestampFormatter.cs b/src/TShock/Logging/Formatting/TimestampFormatter.cs
new file mode 100644
index 000000000..5e05631a0
--- /dev/null
+++ b/src/TShock/Logging/Formatting/TimestampFormatter.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using Serilog.Events;
+using Serilog.Formatting;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging.Formatting {
+ internal sealed class TimestampFormatter : ITextFormatter {
+ private readonly PlayerLogTheme _theme;
+ private readonly string _format;
+ private readonly IFormatProvider? _formatProvider;
+
+ public TimestampFormatter(PlayerLogTheme theme, string format, IFormatProvider? formatProvider) {
+ Debug.Assert(theme != null, "theme should not be null");
+ Debug.Assert(format != null, "format should not be null");
+
+ _theme = theme;
+ _format = format;
+ _formatProvider = formatProvider;
+ }
+
+ public void Format(LogEvent logEvent, TextWriter output) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ var timestamp = logEvent.Timestamp.ToString(_format, _formatProvider);
+ output.Write(_theme.Stylize(timestamp, PlayerLogThemeStyle.Timestamp));
+ }
+ }
+}
diff --git a/src/TShock/Logging/PlayerLogSink.cs b/src/TShock/Logging/PlayerLogSink.cs
new file mode 100644
index 000000000..66a9666eb
--- /dev/null
+++ b/src/TShock/Logging/PlayerLogSink.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Diagnostics;
+using System.IO;
+using Microsoft.Xna.Framework;
+using Orion.Players;
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Formatting;
+
+namespace TShock.Logging {
+ internal sealed class PlayerLogSink : ILogEventSink {
+ private readonly IPlayer _player;
+ private readonly ITextFormatter _formatter;
+
+ public PlayerLogSink(IPlayer player, ITextFormatter formatter) {
+ Debug.Assert(player != null, "player should not be null");
+ Debug.Assert(formatter != null, "formatter should not be null");
+
+ _player = player;
+ _formatter = formatter;
+ }
+
+ public void Emit(LogEvent logEvent) {
+ Debug.Assert(logEvent != null, "log event should not be null");
+
+ var output = new StringWriter();
+ _formatter.Format(logEvent, output);
+
+ // When sending the text, we don't want any newlines if possible. So we strip the ending newline.
+ var text = output.ToString();
+ if (text.EndsWith('\n')) {
+ text = text[0..^1];
+ }
+
+ _player.SendMessage(text, Color.White);
+ }
+ }
+}
diff --git a/src/TShock/Logging/PlayerLogValueVisitor.cs b/src/TShock/Logging/PlayerLogValueVisitor.cs
new file mode 100644
index 000000000..f1c58d307
--- /dev/null
+++ b/src/TShock/Logging/PlayerLogValueVisitor.cs
@@ -0,0 +1,126 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Serilog.Data;
+using Serilog.Events;
+using TShock.Logging.Themes;
+using Unit = System.ValueTuple;
+
+namespace TShock.Logging {
+ [SuppressMessage(
+ "Globalization", "CA1303:Do not pass literals as localized parameters",
+ Justification = "symbols should not be localized")]
+ [SuppressMessage(
+ "Design", "CA1062:Validate arguments of public methods",
+ Justification = "validation has already occurred")]
+ internal sealed class PlayerLogValueVisitor : LogEventPropertyValueVisitor {
+ private readonly PlayerLogTheme _theme;
+ private readonly IFormatProvider? _formatProvider;
+
+ public PlayerLogValueVisitor(PlayerLogTheme theme, IFormatProvider? formatProvider = null) {
+ Debug.Assert(theme != null, "theme should not be null");
+
+ _theme = theme ?? throw new ArgumentNullException(nameof(theme));
+ _formatProvider = formatProvider;
+ }
+
+ public Unit Format(LogEventPropertyValue value, TextWriter output) {
+ Debug.Assert(value != null, "value should not be null");
+ Debug.Assert(output != null, "output should not be null");
+
+ return Visit(output, value);
+ }
+
+ protected override Unit VisitScalarValue(TextWriter output, ScalarValue scalar) {
+ var value = scalar.Value;
+ var text = value is IFormattable f ? f.ToString(null, _formatProvider) : value?.ToString() ?? "null";
+ var style = scalar.Value switch {
+ null => PlayerLogThemeStyle.Null,
+ string _ => PlayerLogThemeStyle.String,
+ bool _ => PlayerLogThemeStyle.Boolean,
+ char _ => PlayerLogThemeStyle.Character,
+ var n when n.GetType().IsPrimitive || n is decimal => PlayerLogThemeStyle.Number,
+ _ => PlayerLogThemeStyle.Scalar
+ };
+ output.Write(_theme.Stylize(text, style));
+ return default;
+ }
+
+ protected override Unit VisitSequenceValue(TextWriter output, SequenceValue sequence) {
+ var includeSeparator = false;
+ output.Write(_theme.Stylize("[", PlayerLogThemeStyle.Text));
+ foreach (var element in sequence.Elements) {
+ if (includeSeparator) {
+ output.Write(_theme.Stylize(", ", PlayerLogThemeStyle.Separator));
+ }
+
+ Visit(output, element);
+ includeSeparator = true;
+ }
+
+ output.Write(_theme.Stylize("]", PlayerLogThemeStyle.Text));
+ return default;
+ }
+
+ protected override Unit VisitStructureValue(TextWriter output, StructureValue structure) {
+ var typeTag = structure.TypeTag;
+ if (typeTag != null) {
+ output.Write(_theme.Stylize(typeTag, PlayerLogThemeStyle.Type));
+ }
+
+ var includeSeparator = false;
+ output.Write(_theme.Stylize("{", PlayerLogThemeStyle.Text));
+ foreach (var property in structure.Properties) {
+ if (includeSeparator) {
+ output.Write(_theme.Stylize(", ", PlayerLogThemeStyle.Separator));
+ }
+
+ output.Write(_theme.Stylize(property.Name, PlayerLogThemeStyle.Identifier));
+ output.Write(_theme.Stylize("=", PlayerLogThemeStyle.Separator));
+ Visit(output, property.Value);
+ includeSeparator = true;
+ }
+
+ output.Write(_theme.Stylize("}", PlayerLogThemeStyle.Text));
+ return default;
+ }
+
+ protected override Unit VisitDictionaryValue(TextWriter output, DictionaryValue dictionary) {
+ var includeSeparator = false;
+ output.Write(_theme.Stylize("{", PlayerLogThemeStyle.Text));
+ foreach (var kvp in dictionary.Elements) {
+ if (includeSeparator) {
+ output.Write(_theme.Stylize(", ", PlayerLogThemeStyle.Separator));
+ }
+
+ output.Write(_theme.Stylize("[", PlayerLogThemeStyle.Text));
+ Visit(output, kvp.Key);
+ output.Write(_theme.Stylize("]", PlayerLogThemeStyle.Text));
+ output.Write(_theme.Stylize("=", PlayerLogThemeStyle.Separator));
+ Visit(output, kvp.Value);
+ includeSeparator = true;
+ }
+
+ output.Write(_theme.Stylize("}", PlayerLogThemeStyle.Text));
+ return default;
+ }
+ }
+}
diff --git a/src/TShock/Logging/PlayerLoggerConfigurationExtensions.cs b/src/TShock/Logging/PlayerLoggerConfigurationExtensions.cs
new file mode 100644
index 000000000..3fbf4b8de
--- /dev/null
+++ b/src/TShock/Logging/PlayerLoggerConfigurationExtensions.cs
@@ -0,0 +1,74 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using Orion.Players;
+using Serilog;
+using Serilog.Configuration;
+using Serilog.Core;
+using Serilog.Events;
+using TShock.Logging.Formatting;
+using TShock.Logging.Themes;
+
+namespace TShock.Logging {
+ ///
+ /// Extends with a WriteTo.Player() method.
+ ///
+ public static class PlayerLoggerConfigurationExtensions {
+ private const string DefaultOutputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {Message}{NewLine}{Exception}";
+
+ ///
+ /// Writes log events to the given with optional settings.
+ ///
+ /// The logger sink configuration.
+ /// The player.
+ /// The minimum level for events to be logged.
+ /// The output template.
+ /// The format provider, or for a default one.
+ /// The level switch, or for none.
+ /// The theme, or for a default one.
+ /// A logger configuration.
+ ///
+ /// , , or are
+ /// .
+ ///
+ public static LoggerConfiguration Player(
+ this LoggerSinkConfiguration configuration, IPlayer player,
+ LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
+ string outputTemplate = DefaultOutputTemplate,
+ IFormatProvider? formatProvider = null,
+ LoggingLevelSwitch? levelSwitch = null,
+ PlayerLogTheme? theme = null) {
+ if (configuration is null) {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ if (player is null) {
+ throw new ArgumentNullException(nameof(player));
+ }
+
+ if (outputTemplate is null) {
+ throw new ArgumentNullException(nameof(outputTemplate));
+ }
+
+ var formatter = new PlayerLogFormatter(
+ theme ?? PlayerLogThemes.VisualStudio, outputTemplate, formatProvider);
+ var sink = new PlayerLogSink(player, formatter);
+ return configuration.Sink(sink, restrictedToMinimumLevel, levelSwitch);
+ }
+ }
+}
diff --git a/src/TShock/Logging/Themes/PlayerLogTheme.cs b/src/TShock/Logging/Themes/PlayerLogTheme.cs
new file mode 100644
index 000000000..a76889243
--- /dev/null
+++ b/src/TShock/Logging/Themes/PlayerLogTheme.cs
@@ -0,0 +1,42 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+using TShock.Utils.Extensions;
+
+namespace TShock.Logging.Themes {
+ ///
+ /// Represents a player log theme.
+ ///
+ public sealed class PlayerLogTheme {
+ private readonly IDictionary _styleToColor;
+
+ internal PlayerLogTheme(IDictionary styleToColor) {
+ Debug.Assert(styleToColor != null, "style to color map should not be null");
+
+ _styleToColor = styleToColor;
+ }
+
+ internal string Stylize(string text, PlayerLogThemeStyle style) {
+ Debug.Assert(text != null, "Text should not be null");
+
+ return _styleToColor.TryGetValue(style, out var color) ? text.WithColor(color) : text;
+ }
+ }
+}
diff --git a/src/TShock/Logging/Themes/PlayerLogThemeStyle.cs b/src/TShock/Logging/Themes/PlayerLogThemeStyle.cs
new file mode 100644
index 000000000..aafc20801
--- /dev/null
+++ b/src/TShock/Logging/Themes/PlayerLogThemeStyle.cs
@@ -0,0 +1,123 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace TShock.Logging.Themes {
+ ///
+ /// Specifies an entity styled by a player log theme.
+ ///
+ [SuppressMessage(
+ "Naming", "CA1720:Identifier contains type name",
+ Justification = "String is only a keyword in Visual Basic")]
+ public enum PlayerLogThemeStyle {
+ ///
+ /// Represents text.
+ ///
+ Text,
+
+ ///
+ /// Represents a separator in, e.g., a sequence or structure.
+ ///
+ Separator,
+
+ ///
+ /// Represents .
+ ///
+ Null,
+
+ ///
+ /// Represents a .
+ ///
+ Boolean,
+
+ ///
+ /// Represents a .
+ ///
+ String,
+
+ ///
+ /// Represents a .
+ ///
+ Character,
+
+ ///
+ /// Represents a number.
+ ///
+ Number,
+
+ ///
+ /// Represents all other scalar values.
+ ///
+ Scalar,
+
+ ///
+ /// Represents a property identifier.
+ ///
+ Identifier,
+
+ ///
+ /// Represents a structure type.
+ ///
+ Type,
+
+ ///
+ /// Represents a timestamp.
+ ///
+ Timestamp,
+
+ ///
+ /// Represents an exception.
+ ///
+ Exception,
+
+ ///
+ /// Represents the verbose level indicator.
+ ///
+ VerboseLevel,
+
+ ///
+ /// Represents the debug level indicator.
+ ///
+ DebugLevel,
+
+ ///
+ /// Represents the information level indicator.
+ ///
+ InformationLevel,
+
+ ///
+ /// Represents the warning level indicator.
+ ///
+ WarningLevel,
+
+ ///
+ /// Represents the error level indicator.
+ ///
+ ErrorLevel,
+
+ ///
+ /// Represents the fatal level indicator.
+ ///
+ FatalLevel,
+
+ ///
+ /// Represents something invalid: e.g., an unknown level indicator or a missing property.
+ ///
+ Invalid
+ }
+}
diff --git a/src/TShock/Logging/Themes/PlayerLogThemes.cs b/src/TShock/Logging/Themes/PlayerLogThemes.cs
new file mode 100644
index 000000000..28718a97d
--- /dev/null
+++ b/src/TShock/Logging/Themes/PlayerLogThemes.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace TShock.Logging.Themes {
+ ///
+ /// Provides instances.
+ ///
+ public static class PlayerLogThemes {
+ ///
+ /// Gets a theme that uses Visual Studio's colors wherever possible.
+ ///
+ /// A theme that uses Visual Studio's colors wherever possible.
+ public static PlayerLogTheme VisualStudio { get; } =
+ new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.Text] = new Color(0xdc, 0xdc, 0xdc),
+ [PlayerLogThemeStyle.Separator] = new Color(0x9b, 0x9b, 0x9b),
+ [PlayerLogThemeStyle.Null] = new Color(0x56, 0x9c, 0xd6),
+ [PlayerLogThemeStyle.Boolean] = new Color(0x56, 0x9c, 0xd6),
+ [PlayerLogThemeStyle.String] = new Color(0xd6, 0x9d, 0x85),
+ [PlayerLogThemeStyle.Character] = new Color(0xd6, 0x9d, 0x85),
+ [PlayerLogThemeStyle.Number] = new Color(0xb5, 0xce, 0xa8),
+ [PlayerLogThemeStyle.Scalar] = new Color(0x86, 0xc6, 0x91),
+ [PlayerLogThemeStyle.Identifier] = new Color(0x9c, 0xdc, 0xfe),
+ [PlayerLogThemeStyle.Type] = new Color(0x4e, 0xc9, 0xb0),
+
+ // The below don't really have any analogs, so I made some colors up.
+ [PlayerLogThemeStyle.Timestamp] = new Color(0x86, 0xc6, 0x91),
+ [PlayerLogThemeStyle.Exception] = new Color(0xcc, 0x44, 0x44),
+ [PlayerLogThemeStyle.VerboseLevel] = new Color(0xcc, 0xcc, 0xcc),
+ [PlayerLogThemeStyle.DebugLevel] = new Color(0xff, 0xff, 0x44),
+ [PlayerLogThemeStyle.WarningLevel] = new Color(0xff, 0x88, 0x44),
+ [PlayerLogThemeStyle.ErrorLevel] = new Color(0xcc, 0x44, 0x44),
+ [PlayerLogThemeStyle.FatalLevel] = new Color(0xff, 0x00, 0x00),
+ [PlayerLogThemeStyle.Invalid] = new Color(0xcc, 0x44, 0x44),
+ });
+ }
+}
diff --git a/src/TShock/Modules/CommandModule.cs b/src/TShock/Modules/CommandModule.cs
new file mode 100644
index 000000000..f04a67be4
--- /dev/null
+++ b/src/TShock/Modules/CommandModule.cs
@@ -0,0 +1,319 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Orion;
+using Orion.Events;
+using Orion.Events.Players;
+using Orion.Events.Server;
+using Orion.Players;
+using Serilog;
+using TShock.Commands;
+using TShock.Commands.Exceptions;
+using TShock.Commands.Parsers;
+using TShock.Commands.Parsers.Attributes;
+using TShock.Events.Commands;
+using TShock.Properties;
+using TShock.Utils.Extensions;
+
+namespace TShock.Modules {
+ ///
+ /// Represents TShock's command module. Provides command functionality and core commands.
+ ///
+ internal sealed class CommandModule : TShockModule {
+ private readonly OrionKernel _kernel;
+ private readonly ILogger _log;
+ private readonly IPlayerService _playerService;
+ private readonly ICommandService _commandService;
+
+ private readonly IDictionary _terrariaCommandToCommand = new Dictionary {
+ ["Say"] = string.Empty,
+ ["Emote"] = Resources.TerrariaCommand_Me,
+ ["Party"] = Resources.TerrariaCommand_P,
+ ["Playing"] = Resources.TerrariaCommand_Playing,
+ ["Roll"] = Resources.TerrariaCommand_Roll
+ };
+
+ private readonly IDictionary> _nameToQualifiedName
+ = new Dictionary>();
+
+ private readonly Random _rand = new Random();
+
+ public CommandModule(
+ OrionKernel kernel, ILogger log, IPlayerService playerService, ICommandService commandService) {
+ Debug.Assert(kernel != null, "kernel should not be null");
+ Debug.Assert(log != null, "log should not be null");
+ Debug.Assert(playerService != null, "player service should not be null");
+ Debug.Assert(commandService != null, "command service should not be null");
+
+ _kernel = kernel;
+ _log = log;
+ _playerService = playerService;
+ _commandService = commandService;
+
+ _kernel.RegisterHandlers(this, _log);
+ }
+
+ public override void Dispose() => _kernel.UnregisterHandlers(this, _log);
+
+ public override void Initialize() {
+ _commandService.RegisterParser(new Int32Parser());
+ _commandService.RegisterParser(new DoubleParser());
+ _commandService.RegisterParser(new StringParser());
+ _commandService.RegisterCommands(this);
+ }
+
+ // Executes a command. input should not have the leading /.
+ public void ExecuteCommand(ICommandSender commandSender, string input) {
+ var space = input.IndexOf(' ', StringComparison.Ordinal);
+ if (space < 0) {
+ space = input.Length;
+ }
+
+ var commandName = input.Substring(0, space);
+ ICommand command;
+ try {
+ var qualifiedName = GetQualifiedName(commandName);
+ command = _commandService.Commands[qualifiedName];
+ } catch (CommandNotFoundException ex) {
+ commandSender.SendErrorMessage(ex.Message);
+ return;
+ }
+
+ if (command.ShouldBeLogged) {
+ Log.Information(Resources.Log_ExecutingCommand, commandSender.Name, input);
+ }
+
+ try {
+ command.Invoke(commandSender, input.Substring(space));
+ } catch (CommandParseException ex) {
+ commandSender.SendErrorMessage(ex.Message);
+ commandSender.SendInfoMessage(
+ string.Format(CultureInfo.InvariantCulture, command.UsageText, commandName));
+ } catch (CommandExecuteException ex) {
+ commandSender.SendErrorMessage(ex.Message);
+ }
+ }
+
+ // Gets the qualified command name for a possibly-qualified command name.
+ public string GetQualifiedName(string maybeQualifiedName) {
+ if (string.IsNullOrWhiteSpace(maybeQualifiedName)) {
+ throw new CommandNotFoundException(Resources.CommandNotFound_MissingCommand);
+ }
+
+ var isQualifiedName = maybeQualifiedName.IndexOf(':', StringComparison.Ordinal) >= 0;
+ if (isQualifiedName) {
+ if (!_commandService.Commands.ContainsKey(maybeQualifiedName)) {
+ throw new CommandNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandNotFound_UnrecognizedCommand,
+ maybeQualifiedName));
+ }
+
+ return maybeQualifiedName;
+ }
+
+ var qualifiedNames = _nameToQualifiedName.GetValueOrDefault(maybeQualifiedName, () => new HashSet());
+ if (qualifiedNames.Count == 0) {
+ throw new CommandNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandNotFound_UnrecognizedCommand,
+ maybeQualifiedName));
+ }
+
+ if (qualifiedNames.Count > 1) {
+ throw new CommandNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, Resources.CommandNotFound_AmbiguousName,
+ maybeQualifiedName, string.Join(", ", qualifiedNames.Select(n => $"\"/{n}\""))));
+ }
+
+ return qualifiedNames.Single();
+ }
+
+ [EventHandler(Name = "tshock")]
+ [SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "Implicit usage")]
+ private void ServerCommandHandler(ServerCommandEvent e) {
+ if (e.IsCanceled()) {
+ return;
+ }
+
+ e.Cancel("tshock: command executing");
+
+ var input = e.Input;
+ if (input.StartsWith('/')) {
+ input = input.Substring(1);
+ }
+
+ ExecuteCommand(ConsoleCommandSender.Instance, input);
+ }
+
+ [EventHandler(Name = "tshock")]
+ [SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "Implicit usage")]
+ private void PlayerChatHandler(PlayerChatEvent e) {
+ if (e.IsCanceled()) {
+ return;
+ }
+
+ var chatCommand = e.Command;
+ if (!_terrariaCommandToCommand.TryGetValue(chatCommand, out var canonicalCommand)) {
+ e.Cancel("tshock: Terraria command is invalid");
+ return;
+ }
+
+ var chat = canonicalCommand + e.Text;
+ if (chat.StartsWith('/')) {
+ e.Cancel("tshock: command executing");
+
+ ICommandSender commandSender = e.Player.GetAnnotationOrDefault(
+ "tshock:CommandSender",
+ () => new PlayerCommandSender(e.Player), true);
+ var input = chat.Substring(1);
+ ExecuteCommand(commandSender, input);
+ }
+ }
+
+ [EventHandler(EventPriority.Monitor, Name = "tshock")]
+ [SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "Implicit usage")]
+ private void CommandRegisterHandler(CommandRegisterEvent e) {
+ var qualifiedName = e.Command.QualifiedName;
+ var name = qualifiedName.Substring(qualifiedName.IndexOf(':', StringComparison.Ordinal) + 1);
+ _nameToQualifiedName
+ .GetValueOrDefault(name, () => new HashSet(), true)
+ .Add(qualifiedName);
+ }
+
+ [EventHandler(EventPriority.Monitor, Name = "tshock")]
+ [SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "Implicit usage")]
+ private void CommandUnregisterHandler(CommandUnregisterEvent e) {
+ var qualifiedName = e.Command.QualifiedName;
+ var name = qualifiedName.Substring(qualifiedName.IndexOf(':', StringComparison.Ordinal) + 1);
+ _nameToQualifiedName
+ .GetValueOrDefault(name, () => new HashSet(), true)
+ .Remove(qualifiedName);
+ }
+
+ // =============================================================================================================
+ // Core command implementations below:
+ //
+
+ [CommandHandler(nameof(Resources.Command_Help),
+ HelpText = nameof(Resources.Command_Help_HelpText),
+ UsageText = nameof(Resources.Command_Help_UsageText),
+ ResourceType = typeof(Resources))]
+ public void Help(ICommandSender sender, string? command_name = null) {
+ if (command_name is null) {
+ string FormatCommandName(ICommand command) {
+ var qualifiedName = command.QualifiedName;
+ var name = qualifiedName.Substring(qualifiedName.IndexOf(':', StringComparison.Ordinal) + 1);
+ return _nameToQualifiedName[name].Count > 1 ? qualifiedName : name;
+ }
+
+ sender.SendInfoMessage(Resources.Command_Help_Header);
+ sender.SendInfoMessage(
+ string.Join(", ", _commandService.Commands.Values.Select(c => $"/{FormatCommandName(c)}")));
+ return;
+ }
+
+ if (command_name.StartsWith('/')) {
+ command_name = command_name.Substring(1);
+ }
+
+ string qualifiedName;
+ try {
+ qualifiedName = GetQualifiedName(command_name);
+ } catch (CommandNotFoundException ex) {
+ sender.SendErrorMessage(ex.Message);
+ return;
+ }
+
+ var command = _commandService.Commands[qualifiedName];
+ var usageText = string.Format(CultureInfo.InvariantCulture, command.UsageText, command_name);
+ sender.SendInfoMessage($"/{command_name}:\n{command.HelpText}\n{usageText}");
+ }
+
+ [CommandHandler(nameof(Resources.Command_Playing),
+ HelpText = nameof(Resources.Command_Playing_HelpText),
+ UsageText = nameof(Resources.Command_Playing_UsageText),
+ ResourceType = typeof(Resources))]
+ public void Playing(ICommandSender sender, [Flag("i")] bool showIds) {
+ var onlinePlayers = _playerService.Players.Where(p => p.IsActive).ToList();
+ if (onlinePlayers.Count == 0) {
+ sender.SendInfoMessage(Resources.Command_Playing_NoPlayers);
+ return;
+ }
+
+ sender.SendInfoMessage(Resources.Command_Playing_Header);
+ if (showIds) {
+ sender.SendInfoMessage(string.Join(", ", onlinePlayers.Select(p => $"[{p.Index}] {p.Name}")));
+ } else {
+ sender.SendInfoMessage(string.Join(", ", onlinePlayers.Select(p => p.Name)));
+ }
+ }
+
+ [CommandHandler(nameof(Resources.Command_Me),
+ HelpText = nameof(Resources.Command_Me_HelpText),
+ UsageText = nameof(Resources.Command_Me_UsageText),
+ ResourceType = typeof(Resources))]
+ public void Me(ICommandSender sender, [RestOfInput] string text) {
+ _playerService.BroadcastMessage(
+ string.Format(CultureInfo.InvariantCulture, Resources.Command_Me_Message, sender.Name, text),
+ new Color(0xc8, 0x64, 0x00));
+ Log.Information(Resources.Log_Command_Me_Message, sender.Name, text);
+ }
+
+ [CommandHandler(nameof(Resources.Command_Roll),
+ HelpText = nameof(Resources.Command_Roll_HelpText),
+ UsageText = nameof(Resources.Command_Roll_UsageText),
+ ResourceType = typeof(Resources))]
+ public void Roll(ICommandSender sender) {
+ var num = _rand.Next(1, 101);
+ _playerService.BroadcastMessage(
+ string.Format(CultureInfo.InvariantCulture, Resources.Command_Roll_Message, sender.Name, num),
+ new Color(0xff, 0xf0, 0x14));
+ Log.Information(Resources.Log_Command_Roll_Message, sender.Name, num);
+ }
+
+ [CommandHandler(nameof(Resources.Command_P),
+ HelpText = nameof(Resources.Command_P_HelpText),
+ UsageText = nameof(Resources.Command_P_UsageText),
+ ResourceType = typeof(Resources))]
+ public void P(ICommandSender sender, [RestOfInput] string text) {
+ var player = sender.Player;
+ if (player is null) {
+ sender.SendErrorMessage(Resources.Command_P_NotAPlayer);
+ return;
+ }
+
+ var team = player.Team;
+ if (team == PlayerTeam.None) {
+ sender.SendErrorMessage(Resources.Command_P_NotInTeam);
+ return;
+ }
+
+ var teamColor = team.Color();
+ var teamPlayers = _playerService.Players.Where(p => p.IsActive && p.Team == team);
+ foreach (var teamPlayer in teamPlayers) {
+ teamPlayer.SendMessageFrom(player, text, teamColor);
+ }
+ Log.Information(Resources.Log_Command_P_Message, player.Name, team, text);
+ }
+ }
+}
diff --git a/src/TShock/Modules/TShockModule.cs b/src/TShock/Modules/TShockModule.cs
new file mode 100644
index 000000000..4d3582a51
--- /dev/null
+++ b/src/TShock/Modules/TShockModule.cs
@@ -0,0 +1,42 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace TShock.Modules {
+ ///
+ /// Represents a module of TShock's functionality.
+ ///
+ [SuppressMessage(
+ "Design", "CA1063:Implement IDisposable Correctly",
+ Justification = "IDisposable pattern makes no sense")]
+ public abstract class TShockModule : IDisposable {
+ ///
+ /// Disposes the service, releasing any resources associated with it.
+ ///
+ [SuppressMessage(
+ "Usage", "CA1816:Dispose methods should call SuppressFinalize",
+ Justification = "IDisposable pattern makes no sense")]
+ public virtual void Dispose() { }
+
+ ///
+ /// Initializes the module. Typically, commands should be registered here.
+ ///
+ public abstract void Initialize();
+ }
+}
diff --git a/src/TShock/Properties/Resources.Designer.cs b/src/TShock/Properties/Resources.Designer.cs
index fb0ec952f..489da05a4 100644
--- a/src/TShock/Properties/Resources.Designer.cs
+++ b/src/TShock/Properties/Resources.Designer.cs
@@ -59,5 +59,437 @@ internal Resources() {
resourceCulture = value;
}
}
+
+ ///
+ /// Looks up a localized string similar to tshock:help.
+ ///
+ internal static string Command_Help {
+ get {
+ return ResourceManager.GetString("Command_Help", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Commands:.
+ ///
+ internal static string Command_Help_Header {
+ get {
+ return ResourceManager.GetString("Command_Help_Header", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Shows available commands and provides information about specific commands..
+ ///
+ internal static string Command_Help_HelpText {
+ get {
+ return ResourceManager.GetString("Command_Help_HelpText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Usage: /{0} [command-name].
+ ///
+ internal static string Command_Help_UsageText {
+ get {
+ return ResourceManager.GetString("Command_Help_UsageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to tshock:me.
+ ///
+ internal static string Command_Me {
+ get {
+ return ResourceManager.GetString("Command_Me", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Sends a message in the third person..
+ ///
+ internal static string Command_Me_HelpText {
+ get {
+ return ResourceManager.GetString("Command_Me_HelpText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to *{0} {1}.
+ ///
+ internal static string Command_Me_Message {
+ get {
+ return ResourceManager.GetString("Command_Me_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Usage: /{0} <text>.
+ ///
+ internal static string Command_Me_UsageText {
+ get {
+ return ResourceManager.GetString("Command_Me_UsageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing command help text..
+ ///
+ internal static string Command_MissingHelpText {
+ get {
+ return ResourceManager.GetString("Command_MissingHelpText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing command usage text..
+ ///
+ internal static string Command_MissingUsageText {
+ get {
+ return ResourceManager.GetString("Command_MissingUsageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to tshock:p.
+ ///
+ internal static string Command_P {
+ get {
+ return ResourceManager.GetString("Command_P", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Sends a message to your party members..
+ ///
+ internal static string Command_P_HelpText {
+ get {
+ return ResourceManager.GetString("Command_P_HelpText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to You must be a player to party chat..
+ ///
+ internal static string Command_P_NotAPlayer {
+ get {
+ return ResourceManager.GetString("Command_P_NotAPlayer", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to You are not in a team!.
+ ///
+ internal static string Command_P_NotInTeam {
+ get {
+ return ResourceManager.GetString("Command_P_NotInTeam", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Usage: /{0} <text>.
+ ///
+ internal static string Command_P_UsageText {
+ get {
+ return ResourceManager.GetString("Command_P_UsageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to tshock:playing.
+ ///
+ internal static string Command_Playing {
+ get {
+ return ResourceManager.GetString("Command_Playing", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Players online:.
+ ///
+ internal static string Command_Playing_Header {
+ get {
+ return ResourceManager.GetString("Command_Playing_Header", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Shows the current players..
+ ///
+ internal static string Command_Playing_HelpText {
+ get {
+ return ResourceManager.GetString("Command_Playing_HelpText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No players online..
+ ///
+ internal static string Command_Playing_NoPlayers {
+ get {
+ return ResourceManager.GetString("Command_Playing_NoPlayers", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Usage: /{0} [-i].
+ ///
+ internal static string Command_Playing_UsageText {
+ get {
+ return ResourceManager.GetString("Command_Playing_UsageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to tshock:roll.
+ ///
+ internal static string Command_Roll {
+ get {
+ return ResourceManager.GetString("Command_Roll", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Rolls a random number from 1 to 100..
+ ///
+ internal static string Command_Roll_HelpText {
+ get {
+ return ResourceManager.GetString("Command_Roll_HelpText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to *{0} rolls a {1}.
+ ///
+ internal static string Command_Roll_Message {
+ get {
+ return ResourceManager.GetString("Command_Roll_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Usage: /{0}.
+ ///
+ internal static string Command_Roll_UsageText {
+ get {
+ return ResourceManager.GetString("Command_Roll_UsageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Exception occurred while executing command..
+ ///
+ internal static string CommandExecute_Exception {
+ get {
+ return ResourceManager.GetString("CommandExecute_Exception", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to "/{0}" is ambiguous and can refer to {1}..
+ ///
+ internal static string CommandNotFound_AmbiguousName {
+ get {
+ return ResourceManager.GetString("CommandNotFound_AmbiguousName", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing command name. Type /help for a list of commands..
+ ///
+ internal static string CommandNotFound_MissingCommand {
+ get {
+ return ResourceManager.GetString("CommandNotFound_MissingCommand", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Command "/{0}" is not recognized. Type /help for a list of commands..
+ ///
+ internal static string CommandNotFound_UnrecognizedCommand {
+ get {
+ return ResourceManager.GetString("CommandNotFound_UnrecognizedCommand", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid hyphenated argument..
+ ///
+ internal static string CommandParse_InvalidHyphenatedArg {
+ get {
+ return ResourceManager.GetString("CommandParse_InvalidHyphenatedArg", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing argument "{0}"..
+ ///
+ internal static string CommandParse_MissingArg {
+ get {
+ return ResourceManager.GetString("CommandParse_MissingArg", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Too many arguments were provided..
+ ///
+ internal static string CommandParse_TooManyArgs {
+ get {
+ return ResourceManager.GetString("CommandParse_TooManyArgs", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Argument type "{0}" not recognized..
+ ///
+ internal static string CommandParse_UnrecognizedArgType {
+ get {
+ return ResourceManager.GetString("CommandParse_UnrecognizedArgType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Long flag "--{0}" not recognized..
+ ///
+ internal static string CommandParse_UnrecognizedLongFlag {
+ get {
+ return ResourceManager.GetString("CommandParse_UnrecognizedLongFlag", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Optional argument "{0}" not recognized..
+ ///
+ internal static string CommandParse_UnrecognizedOptional {
+ get {
+ return ResourceManager.GetString("CommandParse_UnrecognizedOptional", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Short flag "-{0}" not recognized..
+ ///
+ internal static string CommandParse_UnrecognizedShortFlag {
+ get {
+ return ResourceManager.GetString("CommandParse_UnrecognizedShortFlag", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to "{0}" is an invalid number..
+ ///
+ internal static string DoubleParser_InvalidDouble {
+ get {
+ return ResourceManager.GetString("DoubleParser_InvalidDouble", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to "{0}" is a number that is out of range of an integer..
+ ///
+ internal static string Int32Parser_IntegerOutOfRange {
+ get {
+ return ResourceManager.GetString("Int32Parser_IntegerOutOfRange", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to "{0}" is an invalid integer..
+ ///
+ internal static string Int32Parser_InvalidInteger {
+ get {
+ return ResourceManager.GetString("Int32Parser_InvalidInteger", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to *{Name} {Text}.
+ ///
+ internal static string Log_Command_Me_Message {
+ get {
+ return ResourceManager.GetString("Log_Command_Me_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to <{Player} (to {Team} team)> {Text}.
+ ///
+ internal static string Log_Command_P_Message {
+ get {
+ return ResourceManager.GetString("Log_Command_P_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to *{Name} rolls a {Num}.
+ ///
+ internal static string Log_Command_Roll_Message {
+ get {
+ return ResourceManager.GetString("Log_Command_Roll_Message", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {Sender} is executing command /{Command}.
+ ///
+ internal static string Log_ExecutingCommand {
+ get {
+ return ResourceManager.GetString("Log_ExecutingCommand", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid backslash..
+ ///
+ internal static string StringParser_InvalidBackslash {
+ get {
+ return ResourceManager.GetString("StringParser_InvalidBackslash", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Escape character "\{0}" not recognized..
+ ///
+ internal static string StringParser_UnrecognizedEscape {
+ get {
+ return ResourceManager.GetString("StringParser_UnrecognizedEscape", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /me .
+ ///
+ internal static string TerrariaCommand_Me {
+ get {
+ return ResourceManager.GetString("TerrariaCommand_Me", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /p .
+ ///
+ internal static string TerrariaCommand_P {
+ get {
+ return ResourceManager.GetString("TerrariaCommand_P", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /playing .
+ ///
+ internal static string TerrariaCommand_Playing {
+ get {
+ return ResourceManager.GetString("TerrariaCommand_Playing", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /roll .
+ ///
+ internal static string TerrariaCommand_Roll {
+ get {
+ return ResourceManager.GetString("TerrariaCommand_Roll", resourceCulture);
+ }
+ }
}
}
diff --git a/src/TShock/Properties/Resources.resx b/src/TShock/Properties/Resources.resx
index 1af7de150..299a9715f 100644
--- a/src/TShock/Properties/Resources.resx
+++ b/src/TShock/Properties/Resources.resx
@@ -117,4 +117,148 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Exception occurred while executing command.
+
+
+ "/{0}" is ambiguous and can refer to {1}.
+
+
+ Missing command name. Type /help for a list of commands.
+
+
+ Command "/{0}" is not recognized. Type /help for a list of commands.
+
+
+ Invalid hyphenated argument.
+
+
+ Missing argument "{0}".
+
+
+ Too many arguments were provided.
+
+
+ Argument type "{0}" not recognized.
+
+
+ Long flag "--{0}" not recognized.
+
+
+ Optional argument "{0}" not recognized.
+
+
+ Short flag "-{0}" not recognized.
+
+
+ tshock:help
+
+
+ Commands:
+
+
+ Shows available commands and provides information about specific commands.
+
+
+ Usage: /{0} [command-name]
+
+
+ tshock:me
+
+
+ Sends a message in the third person.
+
+
+ *{0} {1}
+
+
+ Usage: /{0} <text>
+
+
+ Missing command help text.
+
+
+ Missing command usage text.
+
+
+ tshock:p
+
+
+ tshock:playing
+
+
+ Players online:
+
+
+ Shows the current players.
+
+
+ No players online.
+
+
+ Usage: /{0} [-i]
+
+
+ Sends a message to your party members.
+
+
+ You must be a player to party chat.
+
+
+ You are not in a team!
+
+
+ Usage: /{0} <text>
+
+
+ tshock:roll
+
+
+ Rolls a random number from 1 to 100.
+
+
+ *{0} rolls a {1}
+
+
+ Usage: /{0}
+
+
+ "{0}" is an invalid number.
+
+
+ "{0}" is a number that is out of range of an integer.
+
+
+ "{0}" is an invalid integer.
+
+
+ *{Name} {Text}
+
+
+ <{Player} (to {Team} team)> {Text}
+
+
+ *{Name} rolls a {Num}
+
+
+ {Sender} is executing command /{Command}
+
+
+ Invalid backslash.
+
+
+ Escape character "\{0}" not recognized.
+
+
+ /me
+
+
+ /p
+
+
+ /playing
+
+
+ /roll
+
\ No newline at end of file
diff --git a/src/TShock/TShock.csproj b/src/TShock/TShock.csproj
index 2441febd0..95386279d 100644
--- a/src/TShock/TShock.csproj
+++ b/src/TShock/TShock.csproj
@@ -14,10 +14,13 @@
git
en-US
COPYING
+ OnBuildSuccess
bin\$(Configuration)\TShock.xml
+ true
+
@@ -32,6 +35,12 @@
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -52,7 +61,7 @@
-
+
diff --git a/src/TShock/TShockPlugin.cs b/src/TShock/TShockPlugin.cs
index a3ee02ea8..7c7cf9db0 100644
--- a/src/TShock/TShockPlugin.cs
+++ b/src/TShock/TShockPlugin.cs
@@ -16,51 +16,90 @@
// along with TShock. If not, see .
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Orion;
using Orion.Events;
-using Orion.Events.Packets;
+using Orion.Events.Server;
using Orion.Players;
+using Serilog;
+using TShock.Commands;
+using TShock.Modules;
namespace TShock {
///
/// Represents the TShock plugin.
///
+ [Service("tshock")]
public sealed class TShockPlugin : OrionPlugin {
private readonly Lazy _playerService;
+ private readonly Lazy _commandService;
- ///
- [ExcludeFromCodeCoverage]
- public override string Author => "Pryaxis";
-
- ///
- [ExcludeFromCodeCoverage]
- public override string Name => "TShock";
+ private readonly ISet _modules = new HashSet();
///
- /// Initializes a new instance of the class with the specified Orion kernel and
- /// services.
+ /// Initializes a new instance of the class with the specified
+ /// instance, log, and services.
///
- /// The Orion kernel.
+ /// The instance.
+ /// The log.
/// The player service.
- /// Any of the services are null .
- public TShockPlugin(OrionKernel kernel, Lazy playerService) : base(kernel) {
+ /// The command service.
+ ///
+ /// , , or any of the services are .
+ ///
+ public TShockPlugin(
+ OrionKernel kernel, ILogger log, Lazy playerService,
+ Lazy commandService) : base(kernel, log) {
+ Kernel.Bind().To();
+
_playerService = playerService ?? throw new ArgumentNullException(nameof(playerService));
+ _commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
}
- ///
- protected override void Initialize() {
- _playerService.Value.PacketReceive += PacketReceiveHandler;
+ ///
+ /// Registers the given .
+ ///
+ /// The module.
+ /// is .
+ public void RegisterModule(TShockModule module) {
+ if (module is null) {
+ throw new ArgumentNullException(nameof(module));
+ }
+
+ _modules.Add(module);
}
- ///
- protected override void Dispose(bool disposeManaged) {
- if (!disposeManaged) return;
+ ///
+ public override void Initialize() {
+ Kernel.RegisterHandlers(this, Log);
+ RegisterModule(new CommandModule(Kernel, Log, _playerService.Value, _commandService.Value));
+ }
- _playerService.Value.PacketReceive -= PacketReceiveHandler;
+ ///
+ public override void Dispose() {
+ Kernel.UnregisterHandlers(this, Log);
+ foreach (var module in _modules) {
+ module.Dispose();
+ }
}
- [EventHandler(EventPriority.Lowest)]
- private void PacketReceiveHandler(object sender, PacketReceiveEventArgs args) { }
+ [EventHandler]
+ [SuppressMessage("Usage", "CA1801:Review unused parameters:", Justification = "Parameter is required")]
+ [SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "Implicit usage")]
+ [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Parameter is required")]
+ private void OnServerInitialize(ServerInitializeEvent e) {
+ // The reason why we want to wait for ServerInitialize to initialize the modules is because we need three
+ // three stages of initialization:
+ //
+ // 1) Plugin constructors, which use lazy services.
+ // 2) Plugin Initialize, which can set up service event handlers,
+ // 3) ServerInitialize, which can use any service.
+ //
+ // This way, we can have service rebinding and service event handlers that always run.
+ foreach (var module in _modules) {
+ module.Initialize();
+ }
+ }
}
}
diff --git a/src/TShock/Utils/Extensions/ColorExtensions.cs b/src/TShock/Utils/Extensions/ColorExtensions.cs
new file mode 100644
index 000000000..1bc1b0158
--- /dev/null
+++ b/src/TShock/Utils/Extensions/ColorExtensions.cs
@@ -0,0 +1,32 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using Microsoft.Xna.Framework;
+
+namespace TShock.Utils.Extensions {
+ ///
+ /// Provides extensions for the structure.
+ ///
+ public static class ColorExtensions {
+ ///
+ /// Converts the to a hex string representation.
+ ///
+ /// The color.
+ /// The hex string representation.
+ public static string ToHexString(this Color color) => $"{color.R:x2}{color.G:x2}{color.B:x2}";
+ }
+}
diff --git a/src/TShock/Utils/Extensions/DictionaryExtensions.cs b/src/TShock/Utils/Extensions/DictionaryExtensions.cs
new file mode 100644
index 000000000..c9305cdb0
--- /dev/null
+++ b/src/TShock/Utils/Extensions/DictionaryExtensions.cs
@@ -0,0 +1,63 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+
+namespace TShock.Utils.Extensions {
+ ///
+ /// Provides extensions for the interface.
+ ///
+ public static class DictionaryExtensions {
+ ///
+ /// Gets the value corresponding to the given , using the
+ /// if does not exist.
+ ///
+ /// The type of key.
+ /// The type of value.
+ /// The dictionary.
+ /// The key.
+ ///
+ /// The default value provider. If , then the provider will return a default instance of
+ /// .
+ ///
+ ///
+ /// to create the annotation if it does not exist; otherwise, .
+ ///
+ ///
+ /// The value, or a default instance provided by if
+ /// does not exist in the dictionary.
+ ///
+ ///
+ /// or are .
+ ///
+ public static TValue GetValueOrDefault(
+ this IDictionary dictionary, TKey key, Func? defaultValueProvider = null,
+ bool createIfNotExists = false) {
+ if (dictionary is null) {
+ throw new ArgumentNullException(nameof(dictionary));
+ }
+
+ if (dictionary.TryGetValue(key, out var value)) {
+ return value;
+ }
+
+ var provider = defaultValueProvider ?? (() => default!);
+ return createIfNotExists ? (dictionary[key] = provider())! : provider();
+ }
+ }
+}
diff --git a/src/TShock/Utils/Extensions/ReadOnlySpanExtensions.cs b/src/TShock/Utils/Extensions/ReadOnlySpanExtensions.cs
new file mode 100644
index 000000000..e99e311ba
--- /dev/null
+++ b/src/TShock/Utils/Extensions/ReadOnlySpanExtensions.cs
@@ -0,0 +1,38 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+
+namespace TShock.Utils.Extensions {
+ ///
+ /// Provides extensions for the structure.
+ ///
+ public static class ReadOnlySpanExtensions {
+ ///
+ /// Searches for and returns the first index of its occurrence, or the length of the
+ /// span if it is not found.
+ ///
+ /// The type of span.
+ /// The span.
+ /// The value.
+ /// The index of the first occurrence, or the length of the span if it is not found.
+ public static int IndexOfOrEnd(this ReadOnlySpan span, T value) where T : IEquatable {
+ var index = span.IndexOf(value);
+ return index >= 0 ? index : span.Length;
+ }
+ }
+}
diff --git a/src/TShock/Utils/Extensions/StringExtensions.cs b/src/TShock/Utils/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..d463db3e5
--- /dev/null
+++ b/src/TShock/Utils/Extensions/StringExtensions.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using Microsoft.Xna.Framework;
+
+namespace TShock.Utils.Extensions {
+ ///
+ /// Provides extensions for the class.
+ ///
+ public static class StringExtensions {
+ ///
+ /// Returns a string with the given .
+ ///
+ /// The string.
+ /// The color.
+ /// A string with the given .
+ /// is .
+ public static string WithColor(this string str, Color color) {
+ if (str is null) {
+ throw new ArgumentNullException(nameof(str));
+ }
+
+ return $"[c/{color.ToHexString()}:{str}]";
+ }
+ }
+}
diff --git a/src/TShock/Utils/ResourceHelper.cs b/src/TShock/Utils/ResourceHelper.cs
new file mode 100644
index 000000000..de49b0d4e
--- /dev/null
+++ b/src/TShock/Utils/ResourceHelper.cs
@@ -0,0 +1,61 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace TShock.Utils {
+ ///
+ /// Provides helper methods for loading resources.
+ ///
+ public static class ResourceHelper {
+ ///
+ /// Loads a resource from the with the given .
+ ///
+ /// The type of resource.
+ /// The resource type.
+ /// The name.
+ /// The resource.
+ ///
+ /// is not a property of .
+ ///
+ ///
+ /// or are .
+ ///
+ [SuppressMessage(
+ "Globalization", "CA1303:Do not pass literals as localized parameters",
+ Justification = "strings are not user-facing")]
+ public static T LoadResource(Type resourceType, string name) {
+ if (resourceType is null) {
+ throw new ArgumentNullException(nameof(resourceType));
+ }
+
+ if (name is null) {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var property = resourceType.GetProperty(name,
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
+ if (property is null) {
+ throw new ArgumentException("Property does not exist.", nameof(name));
+ }
+
+ return (T)property.GetValue(null);
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/CommandHandlerAttributeTests.cs b/tests/TShock.Tests/Commands/CommandHandlerAttributeTests.cs
index 11c080428..87e08a2f4 100644
--- a/tests/TShock.Tests/Commands/CommandHandlerAttributeTests.cs
+++ b/tests/TShock.Tests/Commands/CommandHandlerAttributeTests.cs
@@ -22,31 +22,76 @@
namespace TShock.Commands {
public class CommandHandlerAttributeTests {
[Fact]
- public void Ctor_NullCommandName_ThrowsArgumentNullException() {
+ public void Ctor_NullQualifiedName_ThrowsArgumentNullException() {
Func func = () => new CommandHandlerAttribute(null);
func.Should().Throw();
}
[Fact]
- public void Ctor_NullCommandSubNames_ThrowsArgumentNullException() {
- Func func = () => new CommandHandlerAttribute("", null);
+ public void HelpText_GetWithResourceType() {
+ var attribute = new CommandHandlerAttribute("tshock_test:test") {
+ HelpText = nameof(TestClass.HelpText),
+ ResourceType = typeof(TestClass)
+ };
- func.Should().Throw();
+ attribute.HelpText.Should().Be(TestClass.HelpText);
+ }
+
+ [Fact]
+ public void QualifiedName_Get() {
+ var attribute = new CommandHandlerAttribute("tshock_test:test");
+
+ attribute.QualifiedName.Should().Be("tshock_test:test");
}
[Fact]
- public void CommandName_Get_IsCorrect() {
- var attribute = new CommandHandlerAttribute("test");
+ public void QualifiedName_GetWithResourceType() {
+ var attribute = new CommandHandlerAttribute(nameof(TestClass.QualifiedName)) {
+ ResourceType = typeof(TestClass)
+ };
- attribute.CommandName.Should().Be("test");
+ attribute.QualifiedName.Should().Be(TestClass.QualifiedName);
}
[Fact]
- public void CommandSubNames_Get_IsCorrect() {
- var attribute = new CommandHandlerAttribute("", "test1", "test2");
+ public void HelpText_SetNullValue_ThrowsArgumentNullException() {
+ var attribute = new CommandHandlerAttribute("tshock_test:test");
+ Action action = () => attribute.HelpText = null;
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void UsageText_GetWithResourceType() {
+ var attribute = new CommandHandlerAttribute("tshock_test:test") {
+ UsageText = nameof(TestClass.UsageText),
+ ResourceType = typeof(TestClass)
+ };
+
+ attribute.UsageText.Should().Be(TestClass.UsageText);
+ }
+
+ [Fact]
+ public void UsageText_SetNullValue_ThrowsArgumentNullException() {
+ var attribute = new CommandHandlerAttribute("tshock_test:test");
+ Action action = () => attribute.UsageText = null;
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void ResourceType_SetNullValue_ThrowsArgumentNullException() {
+ var attribute = new CommandHandlerAttribute("tshock_test:test");
+ Action action = () => attribute.ResourceType = null;
+
+ action.Should().Throw();
+ }
- attribute.CommandSubNames.Should().BeEquivalentTo("test1", "test2");
+ private class TestClass {
+ public static string QualifiedName => "tshock:qualified_name_test";
+ public static string HelpText => "HelpText test";
+ public static string UsageText => "UsageText test";
}
}
}
diff --git a/tests/TShock.Tests/Commands/CommandSenderTests.cs b/tests/TShock.Tests/Commands/CommandSenderTests.cs
new file mode 100644
index 000000000..e596d5268
--- /dev/null
+++ b/tests/TShock.Tests/Commands/CommandSenderTests.cs
@@ -0,0 +1,76 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Moq;
+using Xunit;
+
+namespace TShock.Commands {
+ public class CommandSenderTests {
+ [Fact]
+ public void SendErrorMessage() {
+ var mockSender = new Mock();
+
+ mockSender.Object.SendErrorMessage("test");
+
+ mockSender.Verify(s => s.SendMessage("test", It.IsAny()));
+ mockSender.VerifyNoOtherCalls();
+ }
+
+ [Fact]
+ public void SendErrorMessage_NullSender_ThrowsArgumentNullException() {
+ Action action = () => CommandSenderExtensions.SendErrorMessage(null, "");
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void SendErrorMessage_NullMessage_ThrowsArgumentNullException() {
+ var sender = new Mock().Object;
+ Action action = () => sender.SendErrorMessage(null);
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void SendInfoMessage() {
+ var mockSender = new Mock();
+
+ mockSender.Object.SendInfoMessage("test");
+
+ mockSender.Verify(s => s.SendMessage("test", It.IsAny()));
+ mockSender.VerifyNoOtherCalls();
+ }
+
+ [Fact]
+ public void SendInfoMessage_NullSender_ThrowsArgumentNullException() {
+ Action action = () => CommandSenderExtensions.SendInfoMessage(null, "");
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void SendInfoMessage_NullMessage_ThrowsArgumentNullException() {
+ var sender = new Mock().Object;
+ Action action = () => sender.SendInfoMessage(null);
+
+ action.Should().Throw();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/ConsoleCommandSenderTests.cs b/tests/TShock.Tests/Commands/ConsoleCommandSenderTests.cs
new file mode 100644
index 000000000..d980977c5
--- /dev/null
+++ b/tests/TShock.Tests/Commands/ConsoleCommandSenderTests.cs
@@ -0,0 +1,37 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using FluentAssertions;
+using Xunit;
+
+namespace TShock.Commands {
+ public class ConsoleCommandSenderTests {
+ [Fact]
+ public void Name_Get() {
+ ICommandSender sender = ConsoleCommandSender.Instance;
+
+ sender.Name.Should().Be("Console");
+ }
+
+ [Fact]
+ public void Player_Get() {
+ ICommandSender sender = ConsoleCommandSender.Instance;
+
+ sender.Player.Should().BeNull();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/Parsers/ArgumentParserTests.cs b/tests/TShock.Tests/Commands/Parsers/ArgumentParserTests.cs
new file mode 100644
index 000000000..16b0eaf45
--- /dev/null
+++ b/tests/TShock.Tests/Commands/Parsers/ArgumentParserTests.cs
@@ -0,0 +1,39 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using Xunit;
+
+namespace TShock.Commands.Parsers {
+ public class ArgumentParserTests {
+ [Fact]
+ public void Parse_NonGeneric() {
+ IArgumentParser parser = new TestParser();
+ var input = "".AsSpan();
+
+ parser.Parse(ref input, new HashSet()).Should().NotBeNull().And.BeOfType();
+ }
+
+ private class TestParser : IArgumentParser {
+ public TestClass Parse(ref ReadOnlySpan input, ISet attributes) => new TestClass();
+ }
+
+ private class TestClass { }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/Parsers/Attributes/FlagAttributeTests.cs b/tests/TShock.Tests/Commands/Parsers/Attributes/FlagAttributeTests.cs
new file mode 100644
index 000000000..707f560f0
--- /dev/null
+++ b/tests/TShock.Tests/Commands/Parsers/Attributes/FlagAttributeTests.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace TShock.Commands.Parsers.Attributes {
+ public class FlagAttributeTests {
+ [Fact]
+ public void Ctor_NullFlag_ThrowsArgumentNullException() {
+ Func func = () => new FlagAttribute(null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void Ctor_NullAlternateFlags_ThrowsArgumentNullException() {
+ Func func = () => new FlagAttribute("", null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void Ctor_AlternateFlagsNullElement_ThrowsArgumentException() {
+ Func func = () => new FlagAttribute("", "test", null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void Flags_Get() {
+ var attribute = new FlagAttribute("test1", "test2", "test3");
+
+ attribute.Flags.Should().BeEquivalentTo("test1", "test2", "test3");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/Parsers/DoubleParserTests.cs b/tests/TShock.Tests/Commands/Parsers/DoubleParserTests.cs
new file mode 100644
index 000000000..f05b5d701
--- /dev/null
+++ b/tests/TShock.Tests/Commands/Parsers/DoubleParserTests.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using TShock.Commands.Exceptions;
+using Xunit;
+
+namespace TShock.Commands.Parsers {
+ public class DoubleParserTests {
+ [Theory]
+ [InlineData("1.234", 1.234, "")]
+ [InlineData("+1.234", 1.234, "")]
+ [InlineData("000", 0, "")]
+ [InlineData("-1.234", -1.234, "")]
+ [InlineData("1.23 test", 1.23, " test")]
+ [InlineData("1E6", 1E6, "")]
+ public void Parse(string inputString, double expected, string expectedNextInput) {
+ var parser = new DoubleParser();
+ var input = inputString.AsSpan();
+
+ parser.Parse(ref input, new HashSet()).Should().Be(expected);
+
+ input.ToString().Should().Be(expectedNextInput);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Parse_MissingDouble_ThrowsParseException(string inputString) {
+ var parser = new DoubleParser();
+ Func func = () => {
+ var input = inputString.AsSpan();
+ return parser.Parse(ref input, new HashSet());
+ };
+
+ func.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("aaa")]
+ [InlineData("123a")]
+ public void Parse_InvalidDouble_ThrowsParseException(string inputString) {
+ var parser = new DoubleParser();
+ Func func = () => {
+ var input = inputString.AsSpan();
+ return parser.Parse(ref input, new HashSet());
+ };
+ func.Should().Throw().WithInnerException();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/Parsers/Int32ParserTests.cs b/tests/TShock.Tests/Commands/Parsers/Int32ParserTests.cs
new file mode 100644
index 000000000..ff9751c2c
--- /dev/null
+++ b/tests/TShock.Tests/Commands/Parsers/Int32ParserTests.cs
@@ -0,0 +1,80 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using TShock.Commands.Exceptions;
+using Xunit;
+
+namespace TShock.Commands.Parsers {
+ public class Int32ParserTests {
+ [Theory]
+ [InlineData("1234", 1234, "")]
+ [InlineData("+1234", 1234, "")]
+ [InlineData("000", 0, "")]
+ [InlineData("-1234", -1234, "")]
+ [InlineData("123 test", 123, " test")]
+ public void Parse(string inputString, int expected, string expectedNextInput) {
+ var parser = new Int32Parser();
+ var input = inputString.AsSpan();
+
+ parser.Parse(ref input, new HashSet()).Should().Be(expected);
+
+ input.ToString().Should().Be(expectedNextInput);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Parse_MissingInteger_ThrowsParseException(string inputString) {
+ var parser = new Int32Parser();
+ Func func = () => {
+ var input = inputString.AsSpan();
+ return parser.Parse(ref input, new HashSet());
+ };
+
+ func.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("2147483648")]
+ [InlineData("-2147483649")]
+ public void Parse_IntegerOutOfRange_ThrowsParseException(string inputString) {
+ var parser = new Int32Parser();
+ Func func = () => {
+ var input = inputString.AsSpan();
+ return parser.Parse(ref input, new HashSet());
+ };
+
+ func.Should().Throw().WithInnerException();
+ }
+
+ [Theory]
+ [InlineData("aaa")]
+ [InlineData("123a")]
+ [InlineData("123.0")]
+ public void Parse_InvalidInteger_ThrowsParseException(string inputString) {
+ var parser = new Int32Parser();
+ Func func = () => {
+ var input = inputString.AsSpan();
+ return parser.Parse(ref input, new HashSet());
+ };
+ func.Should().Throw().WithInnerException();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/Parsers/StringParserTests.cs b/tests/TShock.Tests/Commands/Parsers/StringParserTests.cs
new file mode 100644
index 000000000..bad2e4ac6
--- /dev/null
+++ b/tests/TShock.Tests/Commands/Parsers/StringParserTests.cs
@@ -0,0 +1,72 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using TShock.Commands.Exceptions;
+using TShock.Commands.Parsers.Attributes;
+using Xunit;
+
+namespace TShock.Commands.Parsers {
+ public class StringParserTests {
+ [Theory]
+ [InlineData("test", "test", "")]
+ [InlineData("test abc", "test", " abc")]
+ [InlineData(@"""test""", "test", "")]
+ [InlineData(@"""test abc def""", "test abc def", "")]
+ [InlineData(@"""test abc"" def ghi", "test abc", " def ghi")]
+ [InlineData(@"test\ abc", "test abc", "")]
+ [InlineData(@"test\ abc def", "test abc", " def")]
+ [InlineData(@"\\", @"\", "")]
+ [InlineData(@"\""", @"""", "")]
+ [InlineData(@"\t", "\t", "")]
+ [InlineData(@"\n", "\n", "")]
+ public void Parse(string inputString, string expected, string expectedNextInput) {
+ var parser = new StringParser();
+ var input = inputString.AsSpan();
+
+ parser.Parse(ref input, new HashSet()).Should().Be(expected);
+
+ input.ToString().Should().Be(expectedNextInput);
+ }
+
+ [Theory]
+ [InlineData(@"\")]
+ [InlineData(@"\a")]
+ public void Parse_EscapeReachesEnd_ThrowsParseException(string inputString) {
+ var parser = new StringParser();
+ Func func = () => {
+ var input = inputString.AsSpan();
+ return parser.Parse(ref input, new HashSet());
+ };
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void Parse_RestOfInput() {
+ var parser = new StringParser();
+ var input = @"blah blah ""test"" blah blah".AsSpan();
+
+ parser.Parse(ref input, new HashSet { new RestOfInputAttribute() })
+ .Should().Be(@"blah blah ""test"" blah blah");
+
+ input.ToString().Should().BeEmpty();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/PlayerCommandSenderTests.cs b/tests/TShock.Tests/Commands/PlayerCommandSenderTests.cs
new file mode 100644
index 000000000..aa73386ae
--- /dev/null
+++ b/tests/TShock.Tests/Commands/PlayerCommandSenderTests.cs
@@ -0,0 +1,76 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Moq;
+using Orion.Packets.World;
+using Orion.Players;
+using Xunit;
+
+namespace TShock.Commands {
+ public class PlayerCommandSenderTests {
+ private readonly Mock _mockPlayer = new Mock();
+ private readonly ICommandSender _sender;
+
+ public PlayerCommandSenderTests() {
+ _sender = new PlayerCommandSender(_mockPlayer.Object);
+ }
+
+ [Fact]
+ public void Ctor_NullPlayer_ThrowsArgumentNullException() {
+ Func func = () => new PlayerCommandSender(null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void Name_Get() {
+ _mockPlayer.SetupGet(p => p.Name).Returns("test");
+
+ _sender.Name.Should().Be("test");
+
+ _mockPlayer.VerifyGet(p => p.Name);
+ _mockPlayer.VerifyNoOtherCalls();
+ }
+
+ [Fact]
+ public void Player_Get() {
+ _sender.Player.Should().NotBeNull();
+ _sender.Player.Should().Be(_mockPlayer.Object);
+ }
+
+ [Fact]
+ public void SendMessage() {
+ _sender.SendMessage("test");
+
+ _mockPlayer.Verify(p => p.SendPacket(
+ It.Is(cp => cp.Color == Color.White && cp.Text == "test")));
+ _mockPlayer.VerifyNoOtherCalls();
+ }
+
+ [Fact]
+ public void SendMessage_WithColor() {
+ _sender.SendMessage("test", Color.Orange);
+
+ _mockPlayer.Verify(p => p.SendPacket(
+ It.Is(cp => cp.Color == Color.Orange && cp.Text == "test")));
+ _mockPlayer.VerifyNoOtherCalls();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/TShockCommandServiceTests.cs b/tests/TShock.Tests/Commands/TShockCommandServiceTests.cs
new file mode 100644
index 000000000..11fe07564
--- /dev/null
+++ b/tests/TShock.Tests/Commands/TShockCommandServiceTests.cs
@@ -0,0 +1,190 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using Moq;
+using Orion;
+using Orion.Events;
+using Serilog.Core;
+using TShock.Commands.Parsers;
+using TShock.Events.Commands;
+using Xunit;
+
+namespace TShock.Commands {
+ public class TShockCommandServiceTests {
+ [Fact]
+ public void Commands_Get() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+
+ var commands = commandService.RegisterCommands(testClass).ToList();
+
+ commandService.Commands.Keys.Should().Contain(
+ new[] { "tshock_tests:test", "tshock_tests:test2", "tshock_tests2:test" });
+ commandService.Commands.Values.Should().Contain(commands);
+ }
+
+ [Fact]
+ public void Parsers_Get() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var parser = new Mock>().Object;
+ commandService.RegisterParser(parser);
+
+ commandService.Parsers.Should().Contain(new KeyValuePair(typeof(object), parser));
+ }
+
+ [Fact]
+ public void RegisterCommands() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+
+ var commands = commandService.RegisterCommands(testClass).ToList();
+
+ commands.Should().HaveCount(3);
+ foreach (var command in commands) {
+ command.QualifiedName.Should().BeOneOf("tshock_tests:test", "tshock_tests:test2", "tshock_tests2:test");
+ }
+ }
+
+ [Fact]
+ public void RegisterCommands_NullObj_ThrowsArgumentNullException() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ Func> func = () => commandService.RegisterCommands(null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void RegisterParser_NullParser_ThrowsArgumentNullException() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ Action action = () => commandService.RegisterParser(null);
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void UnregisterCommand() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var commands = commandService.RegisterCommands(testClass).ToList();
+ var command = commands[0];
+
+ commandService.UnregisterCommand(command).Should().BeTrue();
+
+ commandService.Commands.Keys.Should().NotContain(command.QualifiedName);
+ commandService.Commands.Values.Should().NotContain(command);
+ }
+
+ [Fact]
+ public void UnregisterCommand_CommandDoesntExist_ReturnsFalse() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var mockCommand = new Mock();
+ mockCommand.SetupGet(c => c.QualifiedName).Returns("test");
+
+ commandService.UnregisterCommand(mockCommand.Object).Should().BeFalse();
+ }
+
+ [Fact]
+ public void UnregisterCommand_NullCommand_ThrowsArgumentNullException() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ Func func = () => commandService.UnregisterCommand(null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void CommandRegister_IsTriggered() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var isRun = false;
+ var testClass = new TestClass();
+ kernel.RegisterHandler(e => {
+ isRun = true;
+ e.Command.QualifiedName.Should().BeOneOf(
+ "tshock_tests:test", "tshock_tests:test2", "tshock_tests2:test");
+ }, Logger.None);
+
+ commandService.RegisterCommands(testClass);
+
+ isRun.Should().BeTrue();
+ }
+
+ [Fact]
+ public void CommandRegister_Canceled() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ kernel.RegisterHandler(e => e.Cancel(), Logger.None);
+
+ commandService.RegisterCommands(testClass).Should().BeEmpty();
+ }
+
+ [Fact]
+ public void CommandUnregister_IsTriggered() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var isRun = false;
+ var testClass = new TestClass();
+ var commands = commandService.RegisterCommands(testClass).ToList();
+ var command = commands[0];
+ kernel.RegisterHandler(e => {
+ isRun = true;
+ e.Command.Should().BeSameAs(command);
+ }, Logger.None);
+
+ commandService.UnregisterCommand(command);
+
+ isRun.Should().BeTrue();
+ }
+
+ [Fact]
+ public void CommandUnregister_Canceled() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var commands = commandService.RegisterCommands(testClass).ToList();
+ var command = commands[0];
+ kernel.RegisterHandler(e => e.Cancel(), Logger.None);
+
+ commandService.UnregisterCommand(command).Should().BeFalse();
+
+ commandService.Commands.Values.Should().Contain(command);
+ }
+
+ private class TestClass {
+ [CommandHandler("tshock_tests:test")]
+ public void TestCommand() { }
+
+ [CommandHandler("tshock_tests:test2")]
+ public void TestCommand2() { }
+
+ [CommandHandler("tshock_tests2:test")]
+ public void TestCommandTest() { }
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Commands/TShockCommandTests.cs b/tests/TShock.Tests/Commands/TShockCommandTests.cs
new file mode 100644
index 000000000..70dbcfb6c
--- /dev/null
+++ b/tests/TShock.Tests/Commands/TShockCommandTests.cs
@@ -0,0 +1,495 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using FluentAssertions;
+using Moq;
+using Orion;
+using Orion.Events;
+using Serilog.Core;
+using TShock.Commands.Exceptions;
+using TShock.Commands.Parsers;
+using TShock.Commands.Parsers.Attributes;
+using TShock.Events.Commands;
+using TShock.Properties;
+using Xunit;
+
+namespace TShock.Commands {
+ public class TShockCommandTests {
+ [Fact]
+ public void QualifiedName_Get() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var attribute = new CommandHandlerAttribute("test");
+ var command = new TShockCommand(
+ commandService, "",
+ typeof(TShockCommandTests).GetMethod(nameof(QualifiedName_Get)), attribute);
+
+ command.QualifiedName.Should().Be("test");
+ }
+
+ [Fact]
+ public void HelpText_Get() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var attribute = new CommandHandlerAttribute("test") { HelpText = "HelpTest" };
+ var command = new TShockCommand(
+ commandService, "",
+ typeof(TShockCommandTests).GetMethod(nameof(HelpText_Get)), attribute);
+
+ command.HelpText.Should().Be("HelpTest");
+ }
+
+ [Fact]
+ public void HelpText_GetMissing() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var attribute = new CommandHandlerAttribute("test");
+ var command = new TShockCommand(
+ commandService, "",
+ typeof(TShockCommandTests).GetMethod(nameof(HelpText_GetMissing)), attribute);
+
+ command.HelpText.Should().Be(Resources.Command_MissingHelpText);
+ }
+
+ [Fact]
+ public void UsageText_Get() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var attribute = new CommandHandlerAttribute("test") { UsageText = "UsageTest" };
+ var command = new TShockCommand(
+ commandService, "",
+ typeof(TShockCommandTests).GetMethod(nameof(UsageText_Get)), attribute);
+
+ command.UsageText.Should().Be("UsageTest");
+ }
+
+ [Fact]
+ public void UsageText_GetMissing() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var attribute = new CommandHandlerAttribute("test");
+ var command = new TShockCommand(
+ commandService, "",
+ typeof(TShockCommandTests).GetMethod(nameof(UsageText_GetMissing)), attribute);
+
+ command.UsageText.Should().Be(Resources.Command_MissingUsageText);
+ }
+
+ [Fact]
+ public void ShouldBeLogged_Get() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var attribute = new CommandHandlerAttribute("test") { ShouldBeLogged = false };
+ var command = new TShockCommand(
+ commandService, "",
+ typeof(TShockCommandTests).GetMethod(nameof(ShouldBeLogged_Get)), attribute);
+
+ command.ShouldBeLogged.Should().Be(false);
+ }
+
+ [Fact]
+ public void Invoke_Sender() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, "");
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ }
+
+ [Theory]
+ [InlineData("1 test", 1, "test")]
+ [InlineData(@"-56872 ""test abc\"" def""", -56872, "test abc\" def")]
+ public void Invoke_SenderIntString(string input, int expectedInt, string expectedString) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ commandService.RegisterParser(new Int32Parser());
+ commandService.RegisterParser(new StringParser());
+
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Int_String));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, input);
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ testClass.Int.Should().Be(expectedInt);
+ testClass.String.Should().Be(expectedString);
+ }
+
+ [Theory]
+ [InlineData("", false, false)]
+ [InlineData(" -x", true, false)]
+ [InlineData("--xxx ", true, false)]
+ [InlineData("-y ", false, true)]
+ [InlineData(" --yyy", false, true)]
+ [InlineData(" -xy ", true, true)]
+ [InlineData(" -x --yyy", true, true)]
+ [InlineData("--xxx --yyy", true, true)]
+ public void Invoke_Flags(string input, bool expectedX, bool expectedY) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ commandService.RegisterParser(new Int32Parser());
+ commandService.RegisterParser(new StringParser());
+
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Flags));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, input);
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ testClass.X.Should().Be(expectedX);
+ testClass.Y.Should().Be(expectedY);
+ }
+
+ [Theory]
+ [InlineData("1", 1, 1234, 5678)]
+ [InlineData(" 1 ", 1, 1234, 5678)]
+ [InlineData("1 2", 1, 2, 5678)]
+ [InlineData("1 2 3", 1, 2, 3)]
+ [InlineData("--val=9001 1", 1, 9001, 5678)]
+ [InlineData(" --val=9001 1", 1, 9001, 5678)]
+ [InlineData("--val2=5678 1", 1, 1234, 5678)]
+ [InlineData(" --val2=5678 1", 1, 1234, 5678)]
+ public void Invoke_Optionals(string input, int expectedRequired, int expectedVal, int expectedVal2) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ commandService.RegisterParser(new Int32Parser());
+ commandService.RegisterParser(new StringParser());
+
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Optionals));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, input);
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ testClass.Required.Should().Be(expectedRequired);
+ testClass.Val.Should().Be(expectedVal);
+ testClass.Val2.Should().Be(expectedVal2);
+ }
+
+ [Theory]
+ [InlineData("", false, false, 10)]
+ [InlineData("-f", true, false, 10)]
+ [InlineData("--force", true, false, 10)]
+ [InlineData("-r", false, true, 10)]
+ [InlineData("--recursive", false, true, 10)]
+ [InlineData("-fr", true, true, 10)]
+ [InlineData("-rf", true, true, 10)]
+ [InlineData("-f --recursive", true, true, 10)]
+ [InlineData("-r --force", true, true, 10)]
+ [InlineData("--recursive --force", true, true, 10)]
+ [InlineData("--depth=1 --recursive -f", true, true, 1)]
+ [InlineData("--force -r --depth=100 ", true, true, 100)]
+ [InlineData("--force -r --depth= 100 ", true, true, 100)]
+ public void Invoke_FlagsAndOptionals(
+ string input, bool expectedForce, bool expectedRecursive, int expectedDepth) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ commandService.RegisterParser(new Int32Parser());
+ commandService.RegisterParser(new StringParser());
+
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_FlagsAndOptionals));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, input);
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ testClass.Force.Should().Be(expectedForce);
+ testClass.Recursive.Should().Be(expectedRecursive);
+ testClass.Depth.Should().Be(expectedDepth);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("1", 1)]
+ [InlineData("1 2", 1, 2)]
+ [InlineData(" -1 2 -5", -1, 2, -5)]
+ public void Invoke_Params(string input, params int[] expectedInts) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ commandService.RegisterParser(new Int32Parser());
+ commandService.RegisterParser(new StringParser());
+
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Params));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, input);
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ testClass.Ints.Should().BeEquivalentTo(expectedInts);
+ }
+
+ [Fact]
+ public void Invoke_OptionalGetsRenamed() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ commandService.RegisterParser(new Int32Parser());
+ commandService.RegisterParser(new StringParser());
+
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_OptionalRename));
+ var commandSender = new Mock().Object;
+
+ command.Invoke(commandSender, "--hyphenated-optional-is-long=60");
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ testClass.HyphenatedOptionalIsLong.Should().Be(60);
+ }
+
+ [Fact]
+ public void Invoke_TriggersCommandExecute() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand));
+ var commandSender = new Mock().Object;
+
+ var isRun = false;
+ kernel.RegisterHandler(e => {
+ isRun = true;
+ e.Command.Should().Be(command);
+ e.Input.Should().BeEmpty();
+ }, Logger.None);
+
+ command.Invoke(commandSender, "");
+
+ testClass.Sender.Should().BeSameAs(commandSender);
+ isRun.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Invoke_CommandExecuteCanceled_IsCanceled() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand));
+ var commandSender = new Mock().Object;
+ kernel.RegisterHandler(e => e.Cancel(), Logger.None);
+
+ command.Invoke(commandSender, "failing input");
+ }
+
+ [Theory]
+ [InlineData("1 ")]
+ [InlineData("-7345734 ")]
+ public void Invoke_MissingArg_ThrowsCommandParseException(string input) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Int_String));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, input);
+
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("-xyz")]
+ [InlineData("-z")]
+ public void Invoke_UnexpectedShortFlag_ThrowsCommandParseException(string input) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Flags));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, input);
+
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("--this-is-not-ok")]
+ [InlineData("--neither-is-this")]
+ public void Invoke_UnexpectedLongFlag_ThrowsCommandParseException(string input) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Flags));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, input);
+
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("--required=123")]
+ [InlineData("--not-ok=test")]
+ public void Invoke_UnexpectedOptional_ThrowsCommandParseException(string input) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Optionals));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, input);
+
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("-")]
+ [InlineData("- ")]
+ [InlineData("--")]
+ [InlineData("-- ")]
+ [InlineData("--= ")]
+ public void Invoke_InvalidHyphenatedArgs_ThrowsCommandParseException(string input) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_FlagsAndOptionals));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, input);
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void Invoke_UnexpectedArgType_ThrowsCommandParseException() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_NoTestClass));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, "");
+
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [InlineData("a")]
+ [InlineData("bcd")]
+ public void Invoke_TooManyArguments_ThrowsCommandParseException(string input) {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, input);
+
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void Invoke_ThrowsException_ThrowsCommandException() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand_Exception));
+ var commandSender = new Mock().Object;
+ Action action = () => command.Invoke(commandSender, "");
+
+ action.Should().Throw().WithInnerException();
+ }
+
+ [Fact]
+ public void Invoke_NullSender_ThrowsArgumentNullException() {
+ using var kernel = new OrionKernel(Logger.None);
+ using var commandService = new TShockCommandService(kernel, Logger.None);
+ var testClass = new TestClass();
+ var command = GetCommand(commandService, testClass, nameof(TestClass.TestCommand));
+ Action action = () => command.Invoke(null, "");
+
+ action.Should().Throw();
+ }
+
+ private ICommand GetCommand(TShockCommandService commandService, TestClass testClass, string methodName) {
+ var handler = typeof(TestClass).GetMethod(methodName);
+ var attribute = handler.GetCustomAttribute();
+ return new TShockCommand(commandService, testClass, handler, attribute);
+ }
+
+ [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Testing")]
+ private class TestClass {
+ public ICommandSender Sender { get; private set; }
+ public int Int { get; private set; }
+ public string String { get; private set; }
+ public bool X { get; private set; }
+ public bool Y { get; private set; }
+ public int Required { get; private set; }
+ public int Val { get; private set; }
+ public int Val2 { get; private set; }
+ public bool Force { get; private set; }
+ public bool Recursive { get; private set; }
+ public int Depth { get; private set; }
+ public int HyphenatedOptionalIsLong { get; private set; }
+ public int[] Ints { get; private set; }
+
+ [CommandHandler("tshock_tests:test")]
+ public void TestCommand(ICommandSender sender) => Sender = sender;
+
+ [CommandHandler("tshock_tests:test_int_string")]
+ public void TestCommand_Int_String(ICommandSender sender, int @int, string @string) {
+ Sender = sender;
+ Int = @int;
+ String = @string;
+ }
+
+ [CommandHandler("tshock_tests:test_flags")]
+ public void TestCommand_Flags(ICommandSender sender, [Flag("x", "xxx")] bool x, [Flag("y", "yyy")] bool y) {
+ Sender = sender;
+ X = x;
+ Y = y;
+ }
+
+ [CommandHandler("tshock_tests:test_optionals")]
+ public void TestCommand_Optionals(ICommandSender sender, int required, int val = 1234, int val2 = 5678) {
+ Sender = sender;
+ Required = required;
+ Val = val;
+ Val2 = val2;
+ }
+
+ [CommandHandler("tshock_tests:test_flags_and_optionals")]
+ public void TestCommand_FlagsAndOptionals(
+ ICommandSender sender, [Flag("f", "force")] bool force, [Flag("r", "recursive")] bool recursive,
+ int depth = 10) {
+ Sender = sender;
+ Force = force;
+ Recursive = recursive;
+ Depth = depth;
+ }
+
+ [CommandHandler("tshock_tests:test_optional_rename")]
+ public void TestCommand_OptionalRename(ICommandSender sender, int hyphenated_optional_is_long = 100) {
+ Sender = sender;
+ HyphenatedOptionalIsLong = hyphenated_optional_is_long;
+ }
+
+ [CommandHandler("tshock_tests:test_params")]
+ public void TestCommand_Params(ICommandSender sender, params int[] ints) {
+ Sender = sender;
+ Ints = ints;
+ }
+
+ [CommandHandler("tshock_tests:exception")]
+ public void TestCommand_Exception(ICommandSender sender) => throw new NotImplementedException();
+
+ [CommandHandler("tshock_tests:test_no_testclass")]
+ public void TestCommand_NoTestClass(ICommandSender sender, TestClass testClass) { }
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Events/Commands/CommandEventTests.cs b/tests/TShock.Tests/Events/Commands/CommandEventTests.cs
new file mode 100644
index 000000000..89c4262cd
--- /dev/null
+++ b/tests/TShock.Tests/Events/Commands/CommandEventTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using FluentAssertions;
+using Moq;
+using TShock.Commands;
+using Xunit;
+
+namespace TShock.Events.Commands {
+ public class CommandEventTests {
+ [Fact]
+ public void Ctor_NullCommand_ThrowsArgumentNullException() {
+ Func func = () => new TestCommandEvent(null);
+
+ func.Should().Throw();
+ }
+
+ [Fact]
+ public void Command_Get() {
+ var command = new Mock().Object;
+ var e = new TestCommandEvent(command);
+
+ e.Command.Should().BeSameAs(command);
+ }
+
+ [Fact]
+ public void Command_SetNullValue_ThrowsArgumentNullException() {
+ var command = new Mock().Object;
+ var e = new TestCommandEvent(command);
+ Action action = () => e.Command = null;
+
+ action.Should().Throw();
+ }
+
+ private class TestCommandEvent : CommandEvent {
+ public TestCommandEvent(ICommand command) : base(command) { }
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Events/Commands/CommandExecuteEventArgsTests.cs b/tests/TShock.Tests/Events/Commands/CommandExecuteEventTests.cs
similarity index 66%
rename from tests/TShock.Tests/Events/Commands/CommandExecuteEventArgsTests.cs
rename to tests/TShock.Tests/Events/Commands/CommandExecuteEventTests.cs
index 8f67dfc1e..f2995dae4 100644
--- a/tests/TShock.Tests/Events/Commands/CommandExecuteEventArgsTests.cs
+++ b/tests/TShock.Tests/Events/Commands/CommandExecuteEventTests.cs
@@ -22,11 +22,19 @@
using Xunit;
namespace TShock.Events.Commands {
- public class CommandExecuteEventArgsTests {
+ public class CommandExecuteEventTests {
+ [Fact]
+ public void Ctor_NullCommand_ThrowsArgumentNullException() {
+ var sender = new Mock().Object;
+ Func func = () => new CommandExecuteEvent(null, sender, "");
+
+ func.Should().Throw();
+ }
+
[Fact]
public void Ctor_NullSender_ThrowsArgumentNullException() {
var command = new Mock().Object;
- Func func = () => new CommandExecuteEventArgs(command, null, "");
+ Func func = () => new CommandExecuteEvent(command, null, "");
func.Should().Throw();
}
@@ -35,35 +43,35 @@ public void Ctor_NullSender_ThrowsArgumentNullException() {
public void Ctor_NullInput_ThrowsArgumentNullException() {
var command = new Mock().Object;
var sender = new Mock().Object;
- Func func = () => new CommandExecuteEventArgs(command, sender, null);
+ Func func = () => new CommandExecuteEvent(command, sender, null);
func.Should().Throw();
}
[Fact]
- public void Sender_Get_IsCorrect() {
+ public void Sender_Get() {
var command = new Mock().Object;
var sender = new Mock().Object;
- var args = new CommandExecuteEventArgs(command, sender, "");
+ var e = new CommandExecuteEvent(command, sender, "");
- args.Sender.Should().BeSameAs(sender);
+ e.Sender.Should().BeSameAs(sender);
}
[Fact]
- public void Input_Get_IsCorrect() {
+ public void Input_Get() {
var command = new Mock().Object;
var sender = new Mock().Object;
- var args = new CommandExecuteEventArgs(command, sender, "test");
+ var e = new CommandExecuteEvent(command, sender, "test");
- args.Input.Should().Be("test");
+ e.Input.Should().Be("test");
}
[Fact]
public void Input_SetNullValue_ThrowsArgumentNullException() {
var command = new Mock().Object;
var sender = new Mock().Object;
- var args = new CommandExecuteEventArgs(command, sender, "");
- Action action = () => args.Input = null;
+ var e = new CommandExecuteEvent(command, sender, "");
+ Action action = () => e.Input = null;
action.Should().Throw();
}
diff --git a/tests/TShock.Tests/Events/Commands/CommandEventArgsTests.cs b/tests/TShock.Tests/Events/Commands/CommandRegisterEventTests.cs
similarity index 78%
rename from tests/TShock.Tests/Events/Commands/CommandEventArgsTests.cs
rename to tests/TShock.Tests/Events/Commands/CommandRegisterEventTests.cs
index fb4d6b856..6400887e3 100644
--- a/tests/TShock.Tests/Events/Commands/CommandEventArgsTests.cs
+++ b/tests/TShock.Tests/Events/Commands/CommandRegisterEventTests.cs
@@ -17,20 +17,18 @@
using System;
using FluentAssertions;
+using Moq;
using TShock.Commands;
using Xunit;
namespace TShock.Events.Commands {
- public class CommandEventArgsTests {
+ public class CommandRegisterEventTests {
[Fact]
public void Ctor_NullCommand_ThrowsArgumentNullException() {
- Func func = () => new TestCommandEventArgs(null);
+ var sender = new Mock().Object;
+ Func func = () => new CommandRegisterEvent(null);
func.Should().Throw();
}
-
- private class TestCommandEventArgs : CommandEventArgs {
- public TestCommandEventArgs(ICommand command) : base(command) { }
- }
}
}
diff --git a/src/TShock/Events/Commands/CommandEventArgs.cs b/tests/TShock.Tests/Events/Commands/CommandUnregisterEventTests.cs
similarity index 60%
rename from src/TShock/Events/Commands/CommandEventArgs.cs
rename to tests/TShock.Tests/Events/Commands/CommandUnregisterEventTests.cs
index a95c8ab35..8f088e939 100644
--- a/src/TShock/Events/Commands/CommandEventArgs.cs
+++ b/tests/TShock.Tests/Events/Commands/CommandUnregisterEventTests.cs
@@ -16,24 +16,19 @@
// along with TShock. If not, see .
using System;
-using Orion.Events;
+using FluentAssertions;
+using Moq;
using TShock.Commands;
+using Xunit;
namespace TShock.Events.Commands {
- ///
- /// Provides data for command-related events.
- ///
- public abstract class CommandEventArgs : EventArgs, ICancelable {
- ///
- public string? CancellationReason { get; set; }
+ public class CommandUnregisterEventTests {
+ [Fact]
+ public void Ctor_NullCommand_ThrowsArgumentNullException() {
+ var sender = new Mock().Object;
+ Func func = () => new CommandUnregisterEvent(null);
- ///
- /// Gets or sets the command.
- ///
- public ICommand Command { get; set; }
-
- private protected CommandEventArgs(ICommand command) {
- Command = command ?? throw new ArgumentNullException(nameof(command));
+ func.Should().Throw();
}
}
}
diff --git a/tests/TShock.Tests/Logging/Formatting/ExceptionFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/ExceptionFormatterTests.cs
new file mode 100644
index 000000000..b36dee8ff
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/ExceptionFormatterTests.cs
@@ -0,0 +1,61 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class ExceptionFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.Exception] = new Color(0xff, 0x00, 0x00)
+ });
+
+ [Fact]
+ public void Format() {
+ var formatter = new ExceptionFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug,
+ new Exception("TEST"),
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ff0000:System.Exception: TEST]");
+ }
+
+ [Fact]
+ public void Format_NoException() {
+ var formatter = new ExceptionFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().BeEmpty();
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/LevelFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/LevelFormatterTests.cs
new file mode 100644
index 000000000..a11b3355a
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/LevelFormatterTests.cs
@@ -0,0 +1,131 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class LevelFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.VerboseLevel] = new Color(0x00, 0x00, 0xff),
+ [PlayerLogThemeStyle.DebugLevel] = new Color(0x00, 0xff, 0x00),
+ [PlayerLogThemeStyle.InformationLevel] = new Color(0x00, 0xff, 0xff),
+ [PlayerLogThemeStyle.WarningLevel] = new Color(0xff, 0x00, 0x00),
+ [PlayerLogThemeStyle.ErrorLevel] = new Color(0xff, 0x00, 0xff),
+ [PlayerLogThemeStyle.FatalLevel] = new Color(0xff, 0xff, 0x00),
+ [PlayerLogThemeStyle.Invalid] = new Color(0x00, 0x00, 0x00)
+ });
+
+ [Fact]
+ public void Format_Verbose() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Verbose, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/0000ff:VRB]");
+ }
+
+ [Fact]
+ public void Format_Debug() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/00ff00:DBG]");
+ }
+
+ [Fact]
+ public void Format_Information() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Information, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/00ffff:INF]");
+ }
+
+ [Fact]
+ public void Format_Warning() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Warning, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ff0000:WRN]");
+ }
+
+ [Fact]
+ public void Format_Error() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Error, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ff00ff:ERR]");
+ }
+
+ [Fact]
+ public void Format_Fatal() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Fatal, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ffff00:FTL]");
+ }
+
+ [Fact]
+ public void Format_Unknown() {
+ var formatter = new LevelFormatter(_theme);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, (LogEventLevel)(-1), null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/000000:???]");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/MessageTemplateFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/MessageTemplateFormatterTests.cs
new file mode 100644
index 000000000..1e2d5a0a0
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/MessageTemplateFormatterTests.cs
@@ -0,0 +1,78 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class MessageTemplateFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.Invalid] = new Color(0xff, 0x00, 0x00)
+ });
+
+ [Fact]
+ public void Format_Text() {
+ var formatter = new MessageTemplateFormatter(_theme, CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ new MessageTemplate(new[] { new TextToken("Test") }),
+ Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("Test");
+ }
+
+ [Fact]
+ public void Format_Property() {
+ var formatter = new MessageTemplateFormatter(_theme, CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ new MessageTemplate(new[] { new PropertyToken("Test", "{Test}") }),
+ new[] { new LogEventProperty("Test", new ScalarValue(1)) });
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("1");
+ }
+
+ [Fact]
+ public void Format_InvalidProperty() {
+ var formatter = new MessageTemplateFormatter(_theme, CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ new MessageTemplate(new[] { new PropertyToken("Test", "{Test}") }),
+ Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ff0000:{Test}]");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/NewLineFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/NewLineFormatterTests.cs
new file mode 100644
index 000000000..05b1a1e3a
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/NewLineFormatterTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Serilog.Events;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class NewLineFormatterTests {
+ [Fact]
+ public void Format() {
+ var formatter = new NewLineFormatter();
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("\n");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/PlayerLogFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/PlayerLogFormatterTests.cs
new file mode 100644
index 000000000..6dafb1e82
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/PlayerLogFormatterTests.cs
@@ -0,0 +1,51 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class PlayerLogFormatterTests {
+ [Fact]
+ public void Format() {
+ var theme = new PlayerLogTheme(new Dictionary());
+ var formatter = new PlayerLogFormatter(theme,
+ "[{Timestamp:HH:mm:ss} {Level}] {Property1}{Message}{NewLine}{Exception}",
+ CultureInfo.InvariantCulture);
+ var logEvent = new LogEvent(
+ new DateTimeOffset(new DateTime(2019, 10, 10, 9, 59, 59)), LogEventLevel.Debug,
+ new Exception("TEST"),
+ new MessageTemplate(new[] { new TextToken("my message") }),
+ new[] {
+ new LogEventProperty("Property1", new ScalarValue(1))
+ });
+ var writer = new StringWriter();
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[09:59:59 DBG] 1my message\nSystem.Exception: TEST");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/PropertiesFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/PropertiesFormatterTests.cs
new file mode 100644
index 000000000..dc864d6d0
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/PropertiesFormatterTests.cs
@@ -0,0 +1,85 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class PropertiesFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary());
+
+ [Fact]
+ public void Format() {
+ var formatter = new PropertiesFormatter(_theme, MessageTemplate.Empty, CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty,
+ new[] {
+ new LogEventProperty("Test", new ScalarValue(1)),
+ new LogEventProperty("Test2", new ScalarValue(2)),
+ });
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("{Test=1, Test2=2}");
+ }
+
+ [Fact]
+ public void Format_ExistsInOutputTemplate() {
+ var formatter = new PropertiesFormatter(_theme,
+ new MessageTemplate(new[] { new PropertyToken("Test2", "{Test2}") }), CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty,
+ new[] {
+ new LogEventProperty("Test", new ScalarValue(1)),
+ new LogEventProperty("Test2", new ScalarValue(2)),
+ });
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("{Test=1}");
+ }
+
+ [Fact]
+ public void Format_ExistsInTemplate() {
+ var formatter = new PropertiesFormatter(_theme, MessageTemplate.Empty, CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ new MessageTemplate(new[] { new PropertyToken("Test", "{Test}") }),
+ new[] {
+ new LogEventProperty("Test", new ScalarValue(1)),
+ new LogEventProperty("Test2", new ScalarValue(2)),
+ });
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("{Test2=2}");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/PropertyFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/PropertyFormatterTests.cs
new file mode 100644
index 000000000..5c940b376
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/PropertyFormatterTests.cs
@@ -0,0 +1,64 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using Serilog.Parsing;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class PropertyFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.Invalid] = new Color(0xff, 0x00, 0x00)
+ });
+
+ [Fact]
+ public void Format() {
+ var formatter = new PropertyFormatter(_theme, new PropertyToken("Test", "{Test}"),
+ CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null, MessageTemplate.Empty,
+ new[] { new LogEventProperty("Test", new ScalarValue(1)) });
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("1");
+ }
+
+ [Fact]
+ public void Format_InvalidProperty() {
+ var formatter = new PropertyFormatter(_theme, new PropertyToken("Test", "{Test}"),
+ CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ff0000:{Test}]");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/TextFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/TextFormatterTests.cs
new file mode 100644
index 000000000..b23dad195
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/TextFormatterTests.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class TextFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.Text] = new Color(0xff, 0xff, 0x00)
+ });
+
+ [Fact]
+ public void Format_Text() {
+ var formatter = new TextFormatter(_theme, "test");
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ DateTimeOffset.Now, LogEventLevel.Debug, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/ffff00:test]");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/Formatting/TimestampFormatterTests.cs b/tests/TShock.Tests/Logging/Formatting/TimestampFormatterTests.cs
new file mode 100644
index 000000000..73d8435a1
--- /dev/null
+++ b/tests/TShock.Tests/Logging/Formatting/TimestampFormatterTests.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using Microsoft.Xna.Framework;
+using Serilog.Events;
+using TShock.Logging.Themes;
+using Xunit;
+
+namespace TShock.Logging.Formatting {
+ public class TimestampFormatterTests {
+ private readonly PlayerLogTheme _theme = new PlayerLogTheme(new Dictionary {
+ [PlayerLogThemeStyle.Timestamp] = new Color(0xfa, 0xdf, 0xad)
+ });
+
+ [Fact]
+ public void Format() {
+ var formatter = new TimestampFormatter(_theme, "yyyyMMdd", CultureInfo.InvariantCulture);
+ var writer = new StringWriter();
+ var logEvent = new LogEvent(
+ new DateTimeOffset(new DateTime(2019, 10, 10)), LogEventLevel.Debug, null,
+ MessageTemplate.Empty, Enumerable.Empty());
+
+ formatter.Format(logEvent, writer);
+
+ writer.ToString().Should().Be("[c/fadfad:20191010]");
+ }
+ }
+}
diff --git a/tests/TShock.Tests/Logging/PlayerLogSinkTests.cs b/tests/TShock.Tests/Logging/PlayerLogSinkTests.cs
new file mode 100644
index 000000000..0fbb3996a
--- /dev/null
+++ b/tests/TShock.Tests/Logging/PlayerLogSinkTests.cs
@@ -0,0 +1,65 @@
+// Copyright (c) 2019 Pryaxis & TShock Contributors
+//
+// This file is part of TShock.
+//
+// TShock is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// TShock is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with TShock. If not, see .
+
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Moq;
+using Orion.Packets.World;
+using Orion.Players;
+using Serilog.Events;
+using Serilog.Formatting;
+using Xunit;
+
+namespace TShock.Logging {
+ public class PlayerLogSinkTests {
+ [Fact]
+ public void Emit() {
+ var mockPlayer = new Mock();
+ var mockFormatter = new Mock();
+ mockFormatter
+ .Setup(f => f.Format(It.IsAny(), It.IsAny