Adds configuration system. (#125)

This commit is contained in:
Kamron Batman 2020-05-02 16:44:05 -07:00 • committed by GitHub
parent 2b4a6e1de1
commit 5b01754e3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 409 additions and 420 deletions

View file

@ -1,133 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Configuration.cs - Created: 2019/10/04 - Updated: 2020/01/19 *
* *
* 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. *
* *
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Server
{
public class Configuration
{
private static Configuration m_Configuration;
public static Configuration Instance => m_Configuration ??= ReadConfiguration();
[JsonPropertyName("dataDirectories")]
public List<string> DataDirectories { get; set; } = new List<string>();
[JsonPropertyName("emailSettings")]
public EmailSettings emailSettings { get; set; } = new EmailSettings();
private static string FilePath => Path.Join(Core.BaseDirectory, "Data/modernuo.json");
private static void PromptDataDirectories(Configuration config)
{
Console.WriteLine("Please enter the Ultima Online directory:");
string directory;
do
{
Console.Write("> ");
directory = Console.ReadLine();
} while (!Directory.Exists(directory));
config.DataDirectories.Add(directory);
}
private static Configuration ReadConfiguration()
{
var relPath = new Uri($"{Core.BaseDirectory}/").MakeRelativeUri(new Uri(FilePath)).ToString();
Console.Write($"Reading configuration from {relPath}...");
Configuration config;
if (File.Exists(FilePath))
{
using var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
Span<byte> configBytes = stackalloc byte[(int)fs.Length];
fs.Read(configBytes);
config = JsonSerializer.Deserialize<Configuration>(Utility.UTF8WithEncoding.GetString(configBytes));
Console.WriteLine("done");
}
else
{
Console.WriteLine("not found");
config = new Configuration();
}
// TODO: Extend with a config read verification function that can be extended.
if (config.DataDirectories.Count == 0)
{
PromptDataDirectories(config);
config.Flush();
}
return config;
}
public void Flush()
{
using var fs = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write);
var configJson = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
Span<byte> data = stackalloc byte[Utility.UTF8WithEncoding.GetMaxByteCount(configJson.Length)];
var bytesWritten = Utility.UTF8WithEncoding.GetBytes(configJson, data);
fs.Write(data.Slice(0, bytesWritten));
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Configuration saved to {FilePath}");
Console.ResetColor();
}
}
// TODO: Make configuration pluggable. Move this to scripts
public class EmailSettings
{
[JsonPropertyName("fromAddress")]
public string FromAddress { get; set; }
[JsonPropertyName("fromName")]
public string FromName { get; set; }
[JsonPropertyName("crashAddress")]
public string crashAddress { get; set; }
[JsonPropertyName("crashName")]
public string crashName { get; set; }
[JsonPropertyName("speechLogPageAddress")]
public string speechLogPageAddress { get; set; }
[JsonPropertyName("speechLogPageName")]
public string speechLogPageName { get; set; }
[JsonPropertyName("emailServer")]
public string emailServer { get; set; }
[JsonPropertyName("emailPort")]
public int emailPort { get; set; }
[JsonPropertyName("emailUsername")]
public string emailUsername { get; set; }
[JsonPropertyName("emailPassword")]
public string emailPassword { get; set; }
}
}

View file

@ -0,0 +1,110 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Configuration.cs - Created: 2019/10/04 - Updated: 2020/01/19 *
* *
* 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. *
* *
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
using Server.Json;
namespace Server
{
public static class ServerConfiguration
{
private const string m_RelPath = "Configuration/modernuo.json";
private static readonly string m_FilePath = Path.Join(Core.BaseDirectory, m_RelPath);
private static ServerSettings m_Settings;
public static List<string> DataDirectories => m_Settings.dataDirectories;
public static Dictionary<string, string> Settings => m_Settings.settings;
public static Dictionary<string, object> Metadata => m_Settings.metadata;
public static void LoadConfiguration()
{
bool updated = false;
if (File.Exists(m_FilePath))
{
Console.Write($"Core: Reading configuration from {m_RelPath}...");
m_Settings = JsonConfig.Deserialize<ServerSettings>(m_FilePath);
if (m_Settings == null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("failed");
Console.ResetColor();
throw new Exception("Core: Server configuration failed to deserialize.");
}
Console.WriteLine("done");
Console.WriteLine("Settings: {0}", m_Settings.settings["stuff"]);
}
else
{
updated = true;
m_Settings = new ServerSettings();
}
if (m_Settings.dataDirectories.Count == 0)
{
updated = true;
Console.WriteLine("Core: Server configuration is missing data directories.");
m_Settings.dataDirectories.Add(GetDataDirectory());
}
if (updated)
SaveConfiguration();
}
internal class ServerSettings
{
[JsonPropertyName("dataDirectories")]
public List<string> dataDirectories { get; set; } = new List<string>();
[JsonPropertyName("settings")]
public Dictionary<string, string> settings { get; set; } = new Dictionary<string, string>();
[JsonExtensionData]
public Dictionary<string, object> metadata { get; set; } = new Dictionary<string, object>();
}
private static string GetDataDirectory()
{
Console.WriteLine("Please enter the Ultima Online directory:");
string directory;
do
{
Console.Write("> ");
directory = Console.ReadLine();
} while (!Directory.Exists(directory));
return directory;
}
public static void SaveConfiguration()
{
JsonConfig.Serialize(m_FilePath, m_Settings);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Core: Configuration saved to {m_RelPath}.");
Console.ResetColor();
}
}
}

View file

@ -0,0 +1,35 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: MapConverter.cs - Created: 2020/04/12 - Updated: 2020/05/02 *
* *
* 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. *
* *
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Server.Json
{
public class MapConverter : JsonConverter<Map>
{
public override Map Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Map.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, Map value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);
}
}

View file

@ -0,0 +1,67 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: MapConverter.cs - Created: 2020/04/12 - Updated: 2020/05/02 *
* *
* 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. *
* *
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Server.Json
{
public class Point3dConverter : JsonConverter<Point3D>
{
public override Point3D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException("Point3d must be an array of x, y, z");
var data = new int[3];
var count = 0;
while (true)
{
reader.Read();
if (reader.TokenType == JsonTokenType.EndArray)
break;
if (reader.TokenType == JsonTokenType.Number)
{
if (count < 3)
data[count] = reader.GetInt32();
count++;
}
}
if (count < 2 || count > 3)
throw new JsonException("Point3d must be an array of x, y, z");
return new Point3D(data[0], data[1], data[2]);
}
public override void Write(Utf8JsonWriter writer, Point3D value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteNumberValue(value.X);
writer.WriteNumberValue(value.Y);
writer.WriteNumberValue(value.Z);
writer.WriteEndArray();
}
}
}

View file

@ -0,0 +1,49 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: JsonConfig.cs - Created: 2020/05/02 - Updated: 2020/05/02 *
* *
* 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. *
* *
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System.IO;
using System.Text.Json;
namespace Server.Json
{
public static class JsonConfig
{
public static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip,
WriteIndented = true,
AllowTrailingCommas = true,
};
public static T Deserialize<T>(string filePath, JsonSerializerOptions options = null)
{
if (!File.Exists(filePath)) return default;
string text = File.ReadAllText(filePath, Utility.UTF8);
return JsonSerializer.Deserialize<T>(text, options ?? Options);
}
public static void Serialize(string filePath, object value, JsonSerializerOptions options = null)
{
if (File.Exists(filePath)) File.Delete(filePath);
File.WriteAllText(filePath, JsonSerializer.Serialize(value, options ?? Options));
}
}
}

View file

@ -1,15 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Server.Json
{
public class MapConverter : JsonConverter<Map>
{
public override Map Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Map.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, Map value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);
}
}

View file

@ -1,47 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Server.Json
{
public class Point3dConverter : JsonConverter<Point3D>
{
public override Point3D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException("Point3d must be an array of x, y, z");
var data = new int[3];
var count = 0;
while (true)
{
reader.Read();
if (reader.TokenType == JsonTokenType.EndArray)
break;
if (reader.TokenType == JsonTokenType.Number)
{
if (count < 3)
data[count] = reader.GetInt32();
count++;
}
}
if (count < 2 || count > 3)
throw new JsonException("Point3d must be an array of x, y, z");
return new Point3D(data[0], data[1], data[2]);
}
public override void Write(Utf8JsonWriter writer, Point3D value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteNumberValue(value.X);
writer.WriteNumberValue(value.Y);
writer.WriteNumberValue(value.Z);
writer.WriteEndArray();
}
}
}

View file

@ -45,8 +45,6 @@ namespace Server
private static string m_BaseDirectory;
private static string m_ExePath;
private static bool m_Cache = true;
private static bool m_Profiling;
private static DateTime m_ProfileStart;
private static TimeSpan m_ProfileTime;
@ -69,9 +67,6 @@ namespace Server
private static readonly double m_HighFrequency = 1000.0 / Stopwatch.Frequency;
private static readonly double m_LowFrequency = 1000.0 / TimeSpan.TicksPerSecond;
private static bool m_UseHRT;
public static readonly bool Is64Bit = Environment.Is64BitProcess;
internal static ConsoleEventHandler m_ConsoleEventHandler;
private static int m_CycleIndex = 1;
@ -111,8 +106,6 @@ namespace Server
}
}
public static bool Service { get; private set; }
public static bool Debug { get; private set; }
internal static bool HaltOnWarning { get; private set; }
@ -126,7 +119,7 @@ namespace Server
public static MultiTextWriter MultiConsoleOut { get; private set; }
public static bool UsingHighResolutionTiming => m_UseHRT && m_HighRes && !Unix;
public static bool UsingHighResolutionTiming => m_HighRes && !Unix;
public static long TickCount => (long)Ticks;
@ -134,7 +127,7 @@ namespace Server
{
get
{
if (m_UseHRT && m_HighRes && !Unix) return Stopwatch.GetTimestamp() * m_HighFrequency;
if (m_HighRes && !Unix) return Stopwatch.GetTimestamp() * m_HighFrequency;
return DateTime.UtcNow.Ticks * m_LowFrequency;
}
@ -188,21 +181,12 @@ namespace Server
if (Debug)
Utility.Separate(sb, "-debug", " ");
if (Service)
Utility.Separate(sb, "-service", " ");
if (m_Profiling)
Utility.Separate(sb, "-profile", " ");
if (!m_Cache)
Utility.Separate(sb, "-nocache", " ");
if (HaltOnWarning)
Utility.Separate(sb, "-haltonwarning", " ");
if (m_UseHRT)
Utility.Separate(sb, "-usehrt", " ");
return sb.ToString();
}
}
@ -216,14 +200,9 @@ namespace Server
public static string FindDataFile(string path)
{
var config = Configuration.Instance;
if (config.DataDirectories.Count == 0)
throw new InvalidOperationException(
"Attempted to FindDataFile before DataDirectories list has been filled.");
string fullPath = null;
foreach (var p in config.DataDirectories)
foreach (var p in ServerConfiguration.DataDirectories)
{
fullPath = Path.Combine(p, path);
@ -262,7 +241,7 @@ namespace Server
// ignored
}
if (!close && !Service)
if (!close)
{
try
{
@ -283,7 +262,7 @@ namespace Server
private static bool OnConsoleEvent(ConsoleEventType type)
{
if (World.Saving || (Service && type == ConsoleEventType.CTRL_LOGOFF_EVENT))
if (World.Saving || type == ConsoleEventType.CTRL_LOGOFF_EVENT)
return true;
Kill(); // Kill -> HandleClosed will handle waiting for the completion of flushing to disk
@ -338,47 +317,25 @@ namespace Server
foreach (var a in args)
if (Insensitive.Equals(a, "-debug"))
Debug = true;
else if (Insensitive.Equals(a, "-service"))
Service = true;
else if (Insensitive.Equals(a, "-profile"))
Profiling = true;
else if (Insensitive.Equals(a, "-nocache"))
m_Cache = false;
else if (Insensitive.Equals(a, "-haltonwarning"))
HaltOnWarning = true;
else if (Insensitive.Equals(a, "-usehrt"))
m_UseHRT = true;
try
{
if (Service)
{
if (!Directory.Exists("Logs"))
Directory.CreateDirectory("Logs");
Console.SetOut(MultiConsoleOut = new MultiTextWriter(new FileLogger("Logs/Console.log")));
}
else
{
Console.SetOut(MultiConsoleOut = new MultiTextWriter(Console.Out));
}
}
catch
{
// ignored
}
Thread = Thread.CurrentThread;
Process = Process.GetCurrentProcess();
Assembly = Assembly.GetEntryAssembly();
if (Assembly == null)
throw new Exception("Core: Assembly entry is missing.");
if (Thread != null)
Thread.Name = "Core Thread";
if (BaseDirectory.Length > 0)
Directory.SetCurrentDirectory(BaseDirectory);
var ver = Assembly.GetName().Version;
var ver = Assembly.GetName().Version ?? new Version();
Console.ForegroundColor = ConsoleColor.Green;
// Added to help future code support on forums, as a 'check' people can ask for to it see if they recompiled core or not
@ -389,14 +346,6 @@ namespace Server
Console.ResetColor();
Console.WriteLine();
var config = Configuration.Instance;
foreach (var dir in config.DataDirectories.Where(dir => !Directory.Exists(dir)))
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("Core: Config directory {0} does not exist.", dir);
Console.ResetColor();
}
var ttObj = new Timer.TimerThread();
timerThread = new Thread(ttObj.TimerMain)
{
@ -413,9 +362,8 @@ namespace Server
if (ProcessorCount > 1)
MultiProcessor = true;
if (MultiProcessor || Is64Bit)
Console.WriteLine("Core: Optimizing for {0} {2}processor{1}", ProcessorCount, ProcessorCount == 1 ? "" : "s",
Is64Bit ? "64-bit " : "");
if (MultiProcessor)
Console.WriteLine("Core: Optimizing for {0} processor{1}", ProcessorCount, ProcessorCount == 1 ? "" : "s");
if (IsWindows)
{
@ -426,13 +374,14 @@ namespace Server
if (GCSettings.IsServerGC)
Console.WriteLine("Core: Server garbage collection mode enabled");
if (m_UseHRT)
Console.WriteLine("Core: Requested high resolution timing ({0})",
UsingHighResolutionTiming ? "Supported" : "Unsupported");
Console.WriteLine("Core: High resolution timing ({0})",
UsingHighResolutionTiming ? "Supported" : "Unsupported");
Console.WriteLine("SecureRandomImpl: {0} ({1})", SecureRandomImpl.Name,
SecureRandomImpl.IsHardwareRNG ? "Hardware" : "Software");
ServerConfiguration.LoadConfiguration();
// Load Assembly Scripts.CS.dll
AssemblyHandler.LoadScripts();

View file

@ -208,7 +208,7 @@ namespace Server
for (var i = 0; i < 0x4000; ++i)
{
if (i == 1 || (i > 0 && (i & 0x1F) == 0)) bin.ReadInt32(); // header
if (i == 1 || i > 0 && (i & 0x1F) == 0) bin.ReadInt32(); // header
var flags = (TileFlag)bin.ReadInt64();
bin.ReadInt16(); // skip 2 bytes -- textureID

View file

@ -130,24 +130,17 @@ namespace Server
{
Console.WriteLine("failed");
if (!Core.Service)
{
Console.WriteLine("Error: Type '{0}' was not found. Delete all of those types? (y/n)", typeName);
Console.WriteLine("Error: Type '{0}' was not found. Delete all of those types? (y/n)", typeName);
if (Console.ReadKey(true).Key == ConsoleKey.Y)
{
types.Add(null);
Console.Write("World: Loading...");
continue;
}
Console.WriteLine("Types will not be deleted. An exception will be thrown.");
}
else
if (Console.ReadKey(true).Key == ConsoleKey.Y)
{
Console.WriteLine("Error: Type '{0}' was not found.", typeName);
types.Add(null);
Console.Write("World: Loading...");
continue;
}
Console.WriteLine("Types will not be deleted. An exception will be thrown.");
throw new Exception($"Bad type '{typeName}'");
}
@ -454,46 +447,39 @@ namespace Server
Console.WriteLine(" - Type: {0}", failedType);
Console.WriteLine(" - Serial: {0}", failedSerial);
if (!Core.Service)
Console.WriteLine("Delete the object? (y/n)");
if (Console.ReadKey(true).Key == ConsoleKey.Y)
{
Console.WriteLine("Delete the object? (y/n)");
if (Console.ReadKey(true).Key == ConsoleKey.Y)
if (failedType != typeof(BaseGuild))
{
if (failedType != typeof(BaseGuild))
Console.WriteLine("Delete all objects of that type? (y/n)");
if (Console.ReadKey(true).Key == ConsoleKey.Y)
{
Console.WriteLine("Delete all objects of that type? (y/n)");
if (Console.ReadKey(true).Key == ConsoleKey.Y)
{
if (failedMobiles)
for (var i = 0; i < mobiles.Count;)
if (mobiles[i].TypeID == failedTypeID)
mobiles.RemoveAt(i);
else
++i;
else if (failedItems)
for (var i = 0; i < items.Count;)
if (items[i].TypeID == failedTypeID)
items.RemoveAt(i);
else
++i;
}
if (failedMobiles)
for (var i = 0; i < mobiles.Count;)
if (mobiles[i].TypeID == failedTypeID)
mobiles.RemoveAt(i);
else
++i;
else if (failedItems)
for (var i = 0; i < items.Count;)
if (items[i].TypeID == failedTypeID)
items.RemoveAt(i);
else
++i;
}
SaveIndex(mobiles, MobileIndexPath);
SaveIndex(items, ItemIndexPath);
SaveIndex(guilds, GuildIndexPath);
}
Console.WriteLine("After pressing return an exception will be thrown and the server will terminate.");
Console.ReadLine();
}
else
{
Console.WriteLine("An exception will be thrown and the server will terminate.");
SaveIndex(mobiles, MobileIndexPath);
SaveIndex(items, ItemIndexPath);
SaveIndex(guilds, GuildIndexPath);
}
Console.WriteLine("After pressing return an exception will be thrown and the server will terminate.");
Console.ReadLine();
throw new Exception(
$"Load failed (items={failedItems}, mobiles={failedMobiles}, guilds={failedGuilds}, type={failedType}, serial={failedSerial})",
failed);