diff --git a/Projects/Server/Console/ConsoleInputHandler.cs b/Projects/Server/Console/ConsoleInputHandler.cs new file mode 100644 index 000000000..ee655dcb1 --- /dev/null +++ b/Projects/Server/Console/ConsoleInputHandler.cs @@ -0,0 +1,197 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ConsoleInputHandler.cs * + * * + * This program 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. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Server; + +public static class ConsoleInputHandler +{ + private static readonly AutoResetEvent _receivedUserInput = new(false); + private static readonly AutoResetEvent _endUserInput = new(false); + private static bool _expectUserInput; + private static readonly Dictionary _inputCommands = new(); + private static string[] _commandDescriptions; + private static string _input; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RegisterCommand(string command, string description, Action function) => + RegisterCommand([command], description, function); + + // Note: Functions will be executed on a background thread!! + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RegisterCommand(string[] commands, string description, Action function) + { + if (commands is { Length: > 0 } && function != null) + { + var consoleCommand = new ConsoleCommand(commands, description, function); + lock (_inputCommands) + { + for (var i = 0; i < commands.Length; i++) + { + var command = commands[i].ToLower(); + _inputCommands[command] = consoleCommand; + } + } + + _commandDescriptions = null; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool UnregisterInputCommand(string command) + { + lock (_inputCommands) + { + var removed = _inputCommands.Remove(command); + _commandDescriptions = null; + return removed; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Action GetInputCommand(string command) + { + lock (_inputCommands) + { + var action = _inputCommands.GetValueOrDefault(command)?.Function; + _commandDescriptions = null; + return action; + } + } + + public static void Configure() + { + RegisterCommand(["help", "?"], "Displays this help screen.", DisplayHelp); + } + + public static void Initialize() + { + new Thread(ProcessConsoleInput) + { + IsBackground = true, + Name = "Console Input Handler" + }.Start(); + } + + private static string[] GetHelpDescriptions() + { + if (_commandDescriptions != null) + { + return _commandDescriptions; + } + + HashSet commands; + lock (_inputCommands) + { + commands = _inputCommands.Values.ToHashSet(); + } + + var longestCommand = 0; + var commandTuples = new (string Command, string Arguments)[commands.Count]; + + var index = 0; + foreach (var command in commands) + { + var commandAliases = string.Join("|", command.Commands); + longestCommand = Math.Max(longestCommand, commandAliases.Length); + commandTuples[index++] = (commandAliases, command.Description); + } + + Array.Sort(commandTuples, (a, b) => a.Command.CompareOrdinal(b.Command)); + + _commandDescriptions = new string[commandTuples.Length]; + + for (var i = 0; i < commandTuples.Length; i++) + { + var (commandAliases, description) = commandTuples[i]; + _commandDescriptions[i] = $"{commandAliases.PadRight(longestCommand + 1)} - {description}"; + } + + return _commandDescriptions; + } + + private static void DisplayHelp(string arguments) + { + var commandDescriptions = GetHelpDescriptions(); + if (commandDescriptions == null || commandDescriptions.Length == 0) + { + Console.WriteLine("No console commands registered."); + return; + } + + Console.WriteLine("Available Commands:"); + for (var i = 0; i < _commandDescriptions.Length; i++) + { + Console.WriteLine(_commandDescriptions[i]); + } + } + + private static async void ProcessConsoleInput() + { + var token = Core.ClosingTokenSource.Token; + + while (!token.IsCancellationRequested) + { + var input = Console.ReadLine()?.Trim(); + + if (Volatile.Read(ref _expectUserInput)) + { + _input = input; + _receivedUserInput.Set(); + _endUserInput.WaitOne(); + continue; + } + + if (string.IsNullOrEmpty(input)) + { + continue; + } + + var splitInput = input.Split(' ', 2); + var command = splitInput[0].ToLower(); + + Action action; + lock (_inputCommands) + { + action = _inputCommands.GetValueOrDefault(command)?.Function; + } + + action?.Invoke(splitInput.Length > 1 ? splitInput[1] : string.Empty); + } + } + + public static string ReadLine() + { + Volatile.Write(ref _expectUserInput, true); + _receivedUserInput.WaitOne(); + var line = _input; + Volatile.Write(ref _expectUserInput, false); + _endUserInput.Set(); + + return line; + } + + private class ConsoleCommand(string[] commands, string description, Action function) + { + public readonly string[] Commands = commands; + public readonly string Description = description; + public readonly Action Function = function; + } +} diff --git a/Projects/Server/Serialization/AdhocPersistence.cs b/Projects/Server/Serialization/AdhocPersistence.cs index 0e1e9ebb5..dd4161222 100644 --- a/Projects/Server/Serialization/AdhocPersistence.cs +++ b/Projects/Server/Serialization/AdhocPersistence.cs @@ -117,11 +117,9 @@ public static class AdhocPersistence Console.WriteLine($"***** Bad deserialize of {file.FullName} *****"); Console.WriteLine(error); - Console.WriteLine("Skip this file and continue? (y/n)"); + Console.Write("Skip this file and continue? (y/n): "); - var pressedKey = Console.ReadKey(true).Key; - - if (pressedKey != ConsoleKey.Y) + if (!ConsoleInputHandler.ReadLine().InsensitiveEquals("y")) { throw new Exception("Deserialization failed."); } diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs index 8564f0bd0..27a58fa1b 100644 --- a/Projects/Server/World/EntityPersistence.cs +++ b/Projects/Server/World/EntityPersistence.cs @@ -249,18 +249,16 @@ public static class EntityPersistence Console.WriteLine($"***** Bad deserialize of {t.GetType()} ({t.Serial}) *****"); Console.WriteLine(error); - ConsoleKey pressedKey; - if (!deleteAllFailures) { - Console.WriteLine("Delete the object and continue? (y/n/a)"); - pressedKey = Console.ReadKey(true).Key; + Console.Write("Delete the object and continue? (y/n/a): "); + var pressedKey = Console.ReadLine(); - if (pressedKey == ConsoleKey.A) + if (pressedKey.InsensitiveEquals("a")) { deleteAllFailures = true; } - else if (pressedKey != ConsoleKey.Y) + else if (pressedKey.InsensitiveEquals("y")) { throw new Exception("Deserialization failed."); } @@ -279,9 +277,9 @@ public static class EntityPersistence var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; - Console.WriteLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)"); + Console.Write($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n): "); - if (Console.ReadKey(true).Key == ConsoleKey.Y) + if (Console.ReadLine().InsensitiveEquals("y")) { Console.WriteLine("Loading..."); return null; diff --git a/Projects/UOContent/Console/ConsoleCommands.cs b/Projects/UOContent/Console/ConsoleCommands.cs new file mode 100644 index 000000000..af6807a6e --- /dev/null +++ b/Projects/UOContent/Console/ConsoleCommands.cs @@ -0,0 +1,27 @@ +using Server.Saves; + +namespace Server.Misc; + +public static class ConsoleCommands +{ + public static void Configure() + { + ConsoleInputHandler.RegisterCommand( + ["save", "s"], + "Saves the world", + _ => Core.LoopContext.Post(AutoSave.Save) + ); + + ConsoleInputHandler.RegisterCommand( + ["shutdown", "sh"], + "Shuts down the server.", + _ => Core.Kill() + ); + + ConsoleInputHandler.RegisterCommand( + ["restart", "r"], + "Restarts the server.", + _ => Core.Kill(true) + ); + } +} diff --git a/Projects/UOContent/Misc/AccountPrompt.cs b/Projects/UOContent/Misc/AccountPrompt.cs index 28f6d08d0..8d50739ea 100644 --- a/Projects/UOContent/Misc/AccountPrompt.cs +++ b/Projects/UOContent/Misc/AccountPrompt.cs @@ -16,7 +16,7 @@ public static class AccountPrompt Console.Write("Do you want to create the owner account now? (y/n): "); var answer = Console.ReadLine(); - if (answer is "y" or "Y") + if (answer.InsensitiveEquals("y")) { Console.WriteLine();