/************************************************************************* * ModernUO * * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Main.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.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Server.Compression; using Server.Json; using Server.Logging; using Server.Network; using Server.Text; namespace Server; public static class Core { private static readonly ILogger logger = LogFactory.GetLogger(typeof(Core)); private static bool _performProcessKill; private static bool _restartOnKill; private static bool _performSnapshot; private static string _snapshotPath; private static bool _crashed; private static string _baseDirectory; private static bool? _isRunningFromXUnit; private static int _itemCount; private static int _mobileCount; public static EventLoopContext LoopContext { get; set; } private static readonly Type[] _serialTypeArray = { typeof(Serial) }; public static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public static readonly bool IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); public static readonly bool IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); public static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || IsFreeBSD; public static readonly bool IsBSD = IsDarwin || IsFreeBSD; public static readonly bool Unix = IsBSD || IsLinux; private const string AssembliesConfiguration = "Data/assemblies.json"; #nullable enable // TODO: Find a way to get rid of this public static bool IsRunningFromXUnit { get { if (_isRunningFromXUnit != null) { return _isRunningFromXUnit.Value; } foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) { if (a.FullName.InsensitiveStartsWith("xunit")) { _isRunningFromXUnit = true; return true; } } _isRunningFromXUnit = false; return false; } } #nullable restore public static Assembly ApplicationAssembly { get; set; } public static Assembly Assembly { get; set; } // Assembly file version public static Version Version => new(ThisAssembly.AssemblyFileVersion); public static Process Process { get; private set; } public static Thread Thread { get; private set; } private static long _firstTick; private static long _tickCount; private static DateTime _now; public static long TickCount => _tickCount; public static DateTime Now => _now; public static long Uptime => TickCount - _firstTick; private static long _cycleIndex; private static readonly double[] _cyclesPerSecond = new double[128]; public static double CyclesPerSecond => _cyclesPerSecond[_cycleIndex]; public static double AverageCPS => _cyclesPerSecond.Average(); public static string BaseDirectory { get { if (_baseDirectory == null) { try { _baseDirectory = ApplicationAssembly.Location; if (_baseDirectory.Length > 0) { _baseDirectory = Path.GetDirectoryName(_baseDirectory); } } catch { _baseDirectory = ""; } } return _baseDirectory; } } public static CancellationTokenSource ClosingTokenSource { get; } = new(); public static bool Closing => ClosingTokenSource.IsCancellationRequested; public static int GlobalUpdateRange { get; set; } = 18; public static int GlobalMaxUpdateRange { get; set; } = 24; public static int ScriptItems => _itemCount; public static int ScriptMobiles => _mobileCount; public static Expansion Expansion { get; set; } public static bool T2A => Expansion >= Expansion.T2A; public static bool UOR => Expansion >= Expansion.UOR; public static bool UOTD => Expansion >= Expansion.UOTD; public static bool LBR => Expansion >= Expansion.LBR; public static bool AOS => Expansion >= Expansion.AOS; public static bool SE => Expansion >= Expansion.SE; public static bool ML => Expansion >= Expansion.ML; public static bool SA => Expansion >= Expansion.SA; public static bool HS => Expansion >= Expansion.HS; public static bool TOL => Expansion >= Expansion.TOL; public static bool EJ => Expansion >= Expansion.EJ; public static string FindDataFile(string path, bool throwNotFound = true) { string fullPath = null; foreach (var p in ServerConfiguration.DataDirectories) { fullPath = Path.Combine(p, path); if (IsLinux && !File.Exists(fullPath)) { var fi = new FileInfo(fullPath); if (fi.Directory != null && Directory.Exists(fi.Directory.FullName)) { fullPath = fi.Directory.EnumerateFiles( fi.Name, new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive } ).FirstOrDefault()?.FullName; } } if (File.Exists(fullPath)) { break; } fullPath = null; } if (fullPath == null && throwNotFound) { throw new FileNotFoundException($"Data: {path} was not found"); } return fullPath; } public static IEnumerable FindDataFileByPattern(string pattern) { var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; foreach (var p in ServerConfiguration.DataDirectories) { if (Directory.Exists(p)) { foreach (var file in Directory.EnumerateFiles(p, pattern, options)) { yield return file; } } } } public static void Kill(bool restart = false) { _restartOnKill = restart; _performProcessKill = true; } public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine(e.IsTerminating ? "Error:" : "Warning:"); Console.WriteLine(e.ExceptionObject); if (e.IsTerminating) { _crashed = true; var close = false; try { var args = new ServerCrashedEventArgs(e.ExceptionObject as Exception); EventSink.InvokeServerCrashed(args); close = args.Close; } catch { // ignored } if (!close) { Console.WriteLine("This exception is fatal, press return to exit"); ConsoleInputHandler.ReadLine(); } DoKill(); } } private static void CurrentDomain_ProcessExit(object sender, EventArgs e) { if (!Closing) { HandleClosed(); } } private static void Console_CancelKeyPressed(object sender, ConsoleCancelEventArgs e) { var keypress = e.SpecialKey switch { ConsoleSpecialKey.ControlBreak => "CTRL+BREAK", _ => "CTRL+C" }; logger.Information("Detected {Key} pressed.", keypress); e.Cancel = true; Kill(); } internal static void DoKill(bool restart = false) { if (Closing) { return; } HandleClosed(); if (restart) { try { logger.Information("Restarting"); if (IsWindows) { using var process = Process.Start("dotnet", $"{ApplicationAssembly.Location}"); } else { using var process = new Process(); process.StartInfo = new ProcessStartInfo { FileName = "dotnet", Arguments = $"{ApplicationAssembly.Location}", UseShellExecute = true }; process.Start(); } logger.Information("Restart done"); } catch (Exception e) { logger.Error(e, "Restart failed"); } } Environment.Exit(0); } private static void HandleClosed() { ClosingTokenSource.Cancel(); logger.Information("Shutting down"); World.WaitForWriteCompletion(); World.ExitSerializationThreads(); PingServer.Shutdown(); TcpServer.Shutdown(); if (!_crashed) { EventSink.InvokeShutdown(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long GetTimestamp() => 1000L * Stopwatch.GetTimestamp() / Stopwatch.Frequency; public static void Setup(Assembly applicationAssembly, Process process) { CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; Process = process; ApplicationAssembly = applicationAssembly; Assembly = Assembly.GetAssembly(typeof(Core)); Thread = Thread.CurrentThread; LoopContext = new EventLoopContext(); SynchronizationContext.SetSynchronizationContext(LoopContext); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; AppDomain.CurrentDomain.AssemblyResolve += AssemblyHandler.AssemblyResolver; Console.OutputEncoding = Encoding.UTF8; Thread.Name = "Core Thread"; if (BaseDirectory.Length > 0) { Directory.SetCurrentDirectory(BaseDirectory); } Utility.PushColor(ConsoleColor.Green); Console.WriteLine( "ModernUO - [https://github.com/modernuo/modernuo] Version {0}.{1}.{2}.{3}", Version.Major, Version.Minor, Version.Build, Version.Revision ); Utility.PopColor(); Utility.PushColor(ConsoleColor.DarkGray); Console.WriteLine(@"Copyright 2019-2023 ModernUO Development Team This program comes with ABSOLUTELY NO WARRANTY; This is free software, and you are welcome to redistribute it under certain conditions. You should have received a copy of the GNU General Public License along with this program. If not, see . ".TrimMultiline()); Utility.PopColor(); Console.CancelKeyPress += Console_CancelKeyPressed; // LibDeflate is not thread safe, so we need to create a new instance for each thread var standard = Deflate.Standard; AppDomain.CurrentDomain.ProcessExit += (_, _) => standard.Dispose(); ServerConfiguration.Load(); logger.Information("Running on {Framework}", RuntimeInformation.FrameworkDescription); var assemblyPath = Path.Join(BaseDirectory, AssembliesConfiguration); // Load UOContent.dll var assemblyFiles = JsonConfig.Deserialize>(assemblyPath)?.ToArray(); if (assemblyFiles == null) { throw new JsonException($"Failed to deserialize {assemblyPath}."); } for (var i = 0; i < assemblyFiles.Length; i++) { assemblyFiles[i] = Path.Join(BaseDirectory, "Assemblies", assemblyFiles[i]); } AssemblyHandler.LoadAssemblies(assemblyFiles); VerifySerialization(); _now = DateTime.UtcNow; _firstTick = _tickCount = GetTimestamp(); Timer.Init(_tickCount); AssemblyHandler.Invoke("Configure"); TileMatrixLoader.LoadTileMatrix(); RegionJsonSerializer.LoadRegions(); World.Load(); AssemblyHandler.Invoke("Initialize"); TcpServer.Start(); PingServer.Start(); EventSink.InvokeServerStarted(); RunEventLoop(); } public static void RunEventLoop() { try { #if DEBUG const bool idleCPU = true; #else var idleCPU = ServerConfiguration.GetOrUpdateSetting("core.enableIdleCPU", false); #endif var cycleCount = _cyclesPerSecond.Length; long last = _tickCount; const int interval = 100; double frequency = Stopwatch.Frequency * interval; int sample = 0; while (!Closing) { _tickCount = GetTimestamp(); _now = DateTime.UtcNow; Mobile.ProcessDeltaQueue(); Item.ProcessDeltaQueue(); Timer.Slice(_tickCount); // Handle networking NetState.Slice(); // Execute captured post-await methods (like Timer.Pause) LoopContext.ExecuteTasks(); Timer.CheckTimerPool(); // Check for pool depletion so we can async refill it. if (_performSnapshot) { // Return value is the offset that can be used to fix timers that should drift World.Snapshot(_snapshotPath); _performSnapshot = false; } if (_performProcessKill) { World.WaitForWriteCompletion(); break; } if (sample++ == interval) { sample = 0; var now = GetTimestamp(); var cyclesPerSecond = frequency / (now - last); _cyclesPerSecond[_cycleIndex++] = cyclesPerSecond; if (_cycleIndex == cycleCount) { _cycleIndex = 0; } last = now; if (idleCPU && cyclesPerSecond > 125) { Thread.Sleep(2); } } } } catch (Exception e) { CurrentDomain_UnhandledException(null, new UnhandledExceptionEventArgs(e, true)); return; } DoKill(_restartOnKill); } internal static void RequestSnapshot(string snapshotPath) { _performSnapshot = true; _snapshotPath = snapshotPath; } public static void VerifySerialization() { _itemCount = 0; _mobileCount = 0; var callingAssembly = Assembly.GetCallingAssembly(); VerifySerialization(callingAssembly); foreach (var assembly in AssemblyHandler.Assemblies) { if (assembly != callingAssembly) { VerifySerialization(assembly); } } } private static void VerifyType(Type type) { if (!type.IsAssignableTo(typeof(ISerializable)) || type.IsInterface || type.IsAbstract) { return; } if (type.IsSubclassOf(typeof(Item))) { Interlocked.Increment(ref _itemCount); } else if (type.IsSubclassOf(typeof(Mobile))) { Interlocked.Increment(ref _mobileCount); } using var errors = ValueStringBuilder.CreateMT(); try { if (World.DirtyTrackingEnabled) { var manualDirtyCheckingAttribute = type.GetCustomAttribute(false); var codeGennedAttribute = type.GetCustomAttribute(false); if (manualDirtyCheckingAttribute == null && codeGennedAttribute == null) { errors.AppendLine(" - No property tracking (dirty checking)"); } } if (type.GetConstructor(_serialTypeArray) == null) { errors.AppendLine(" - No serialization constructor"); } const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; var hasSerializeMethod = false; var hasDeserializeMethod = false; foreach (var method in type.GetMethods(bindingFlags)) { if (method.Name == "Serialize") { hasSerializeMethod = true; } if (method.Name == "Deserialize") { var parameters = method.GetParameters(); if (parameters.Length == 1 && parameters[0].ParameterType == typeof(IGenericReader)) { hasDeserializeMethod = true; } } } if (!hasSerializeMethod) { errors.AppendLine(" - No Serialize() method"); } if (!hasDeserializeMethod) { errors.AppendLine(" - No Deserialize() method"); } if (errors.Length > 0) { Utility.PushColor(ConsoleColor.Red); Console.WriteLine($"{type}{Environment.NewLine}{errors.ToString()}"); Utility.PopColor(); } } catch (AmbiguousMatchException e) { // ignored } catch { Console.WriteLine("Warning: Exception in serialization verification of type {0}", type); } } private static void VerifySerialization(Assembly assembly) { if (assembly != null) { Parallel.ForEach(assembly.GetTypes(), VerifyType); } } }