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();