using System; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime; using System.Runtime.InteropServices; using System.Threading; using log4net; using log4net.Config; using ACE.Common; using ACE.Common.Extensions; using ACE.Database; using ACE.DatLoader; using ACE.Server.Command; using ACE.Server.Managers; using ACE.Server.Mods; using ACE.Server.Network.Managers; namespace ACE.Server { partial class Program { /// /// The timeBeginPeriod function sets the minimum timer resolution for an application or device driver. Used to manipulate the timer frequency. /// https://docs.microsoft.com/en-us/windows/desktop/api/timeapi/nf-timeapi-timebeginperiod /// Important note: This function affects a global Windows setting. Windows uses the lowest value (that is, highest resolution) requested by any process. /// [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] public static extern uint MM_BeginPeriod(uint uMilliseconds); /// /// The timeEndPeriod function clears a previously set minimum timer resolution /// https://docs.microsoft.com/en-us/windows/desktop/api/timeapi/nf-timeapi-timeendperiod /// [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] public static extern uint MM_EndPeriod(uint uMilliseconds); private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static readonly bool IsRunningInContainer = Convert.ToBoolean(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")); public static void Main(string[] args) { var consoleTitle = $"ACEmulator - v{ServerBuildInfo.FullVersion}"; Console.Title = consoleTitle; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.ProcessExit += new EventHandler(OnProcessExit); // Typically, you wouldn't force the current culture on an entire application unless you know sure your application is used in a specific region (which ACE is not) // We do this because almost all of the client/user input/output code does not take culture into account, and assumes en-US formatting. // Without this, many commands that require special characters like , and . will break Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); // Init our text encoding options. This will allow us to use more than standard ANSI text, which the client also supports. System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); // Look for the log4net.config first in the current environment directory, then in the ExecutingAssembly location var exeLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var containerConfigDirectory = "/ace/Config"; var log4netConfig = Path.Combine(exeLocation, "log4net.config"); var log4netConfigExample = Path.Combine(exeLocation, "log4net.config.example"); var log4netConfigContainer = Path.Combine(containerConfigDirectory, "log4net.config"); if (IsRunningInContainer && File.Exists(log4netConfigContainer)) File.Copy(log4netConfigContainer, log4netConfig, true); var log4netFileInfo = new FileInfo("log4net.config"); if (!log4netFileInfo.Exists) log4netFileInfo = new FileInfo(log4netConfig); if (!log4netFileInfo.Exists) { var exampleFile = new FileInfo(log4netConfigExample); if (!exampleFile.Exists) { Console.WriteLine("log4net Configuration file is missing. Please copy the file log4net.config.example to log4net.config and edit it to match your needs before running ACE."); throw new Exception("missing log4net configuration file"); } else { if (!IsRunningInContainer) { Console.WriteLine("log4net Configuration file is missing, cloning from example file."); File.Copy(log4netConfigExample, log4netConfig); } else { if (!File.Exists(log4netConfigContainer)) { Console.WriteLine("log4net Configuration file is missing, ACEmulator is running in a container, cloning from docker file."); var log4netConfigDocker = Path.Combine(exeLocation, "log4net.config.docker"); File.Copy(log4netConfigDocker, log4netConfig); File.Copy(log4netConfigDocker, log4netConfigContainer); } else { File.Copy(log4netConfigContainer, log4netConfig); } } } } var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.ConfigureAndWatch(logRepository, log4netFileInfo); if (Environment.ProcessorCount < 2) log.Warn("Only one vCPU was detected. ACE may run with limited performance. You should increase your vCPU count for anything more than a single player server."); // Do system specific initializations here try { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // On many windows systems, the default resolution for Thread.Sleep is 15.6ms. This allows us to command a tighter resolution MM_BeginPeriod(1); } } catch (Exception ex) { log.Error(ex.ToString()); } log.Info("Starting ACEmulator..."); if (IsRunningInContainer) log.Info("ACEmulator is running in a container..."); var configFile = Path.Combine(exeLocation, "Config.js"); var configConfigContainer = Path.Combine(containerConfigDirectory, "Config.js"); if (IsRunningInContainer && File.Exists(configConfigContainer)) File.Copy(configConfigContainer, configFile, true); if (!File.Exists(configFile)) { if (!IsRunningInContainer) DoOutOfBoxSetup(configFile); else { if (!File.Exists(configConfigContainer)) { DoOutOfBoxSetup(configFile); File.Copy(configFile, configConfigContainer); } else File.Copy(configConfigContainer, configFile); } } log.Info("Initializing ConfigManager..."); ConfigManager.Initialize(); log.Info("Initializing ModManager..."); ModManager.Initialize(); if (ConfigManager.Config.Server.WorldName != "ACEmulator") { consoleTitle = $"{ConfigManager.Config.Server.WorldName} | {consoleTitle}"; Console.Title = consoleTitle; } if (ConfigManager.Config.Offline.PurgeDeletedCharacters) { log.Info($"Purging deleted characters, and their possessions, older than {ConfigManager.Config.Offline.PurgeDeletedCharactersDays} days ({DateTime.Now.AddDays(-ConfigManager.Config.Offline.PurgeDeletedCharactersDays).ToCommonString()})..."); ShardDatabaseOfflineTools.PurgeCharactersInParallel(ConfigManager.Config.Offline.PurgeDeletedCharactersDays, out var charactersPurged, out var playerBiotasPurged, out var possessionsPurged); log.Info($"Purged {charactersPurged:N0} characters, {playerBiotasPurged:N0} player biotas and {possessionsPurged:N0} possessions."); } if (ConfigManager.Config.Offline.PurgeOrphanedBiotas) { log.Info($"Purging orphaned biotas..."); ShardDatabaseOfflineTools.PurgeOrphanedBiotasInParallel(out var numberOfBiotasPurged); log.Info($"Purged {numberOfBiotasPurged:N0} biotas."); } if (ConfigManager.Config.Offline.PruneDeletedCharactersFromFriendLists) { log.Info($"Pruning invalid friends from all friend lists..."); ShardDatabaseOfflineTools.PruneDeletedCharactersFromFriendLists(out var numberOfFriendsPruned); log.Info($"Pruned {numberOfFriendsPruned:N0} invalid friends found on friend lists."); } if (ConfigManager.Config.Offline.PruneDeletedObjectsFromShortcutBars) { log.Info($"Pruning invalid shortcuts from all shortcut bars..."); ShardDatabaseOfflineTools.PruneDeletedObjectsFromShortcutBars(out var numberOfShortcutsPruned); log.Info($"Pruned {numberOfShortcutsPruned:N0} deleted objects found on shortcut bars."); } if (ConfigManager.Config.Offline.PruneDeletedCharactersFromSquelchLists) { log.Info($"Pruning invalid squelches from all squelch lists..."); ShardDatabaseOfflineTools.PruneDeletedCharactersFromSquelchLists(out var numberOfSquelchesPruned); log.Info($"Pruned {numberOfSquelchesPruned:N0} invalid squelched characters found on squelch lists."); } if (ConfigManager.Config.Offline.AutoServerUpdateCheck) CheckForServerUpdate(); else log.Info($"AutoServerVersionCheck is disabled..."); if (ConfigManager.Config.Offline.AutoUpdateWorldDatabase) { CheckForWorldDatabaseUpdate(); if (ConfigManager.Config.Offline.AutoApplyWorldCustomizations) AutoApplyWorldCustomizations(); } else log.Info($"AutoUpdateWorldDatabase is disabled..."); if (ConfigManager.Config.Offline.AutoApplyDatabaseUpdates) AutoApplyDatabaseUpdates(); else log.Info($"AutoApplyDatabaseUpdates is disabled..."); // This should only be enabled manually. To enable it, simply uncomment this line //ACE.Database.OfflineTools.Shard.BiotaGuidConsolidator.ConsolidateBiotaGuids(0xA0000000, true, false, out int numberOfBiotasConsolidated, out int numberOfBiotasSkipped, out int numberOfErrors); //ACE.Database.OfflineTools.Shard.BiotaGuidConsolidator.ConsolidateBiotaGuids(0xD0000000, false, true, out int numberOfBiotasConsolidated2, out int numberOfBiotasSkipped2, out int numberOfErrors2); ShardDatabaseOfflineTools.CheckForBiotaPropertiesPaletteOrderColumnInShard(); // pre-load starterGear.json, abort startup if file is not found as it is required to create new characters. if (Factories.StarterGearFactory.GetStarterGearConfiguration() == null) { log.Fatal("Unable to load or parse starterGear.json. ACEmulator will now abort startup."); ServerManager.StartupAbort(); Environment.Exit(0); } log.Info("Initializing ServerManager..."); ServerManager.Initialize(); log.Info("Initializing DatManager..."); DatManager.Initialize(ConfigManager.Config.Server.DatFilesDirectory, true); if (ConfigManager.Config.DDD.EnableDATPatching) { log.Info("Initializing DDDManager..."); DDDManager.Initialize(); } else log.Info("DAT Patching Disabled..."); log.Info("Initializing DatabaseManager..."); DatabaseManager.Initialize(); if (DatabaseManager.InitializationFailure) { log.Fatal("DatabaseManager initialization failed. ACEmulator will now abort startup."); ServerManager.StartupAbort(); Environment.Exit(0); } log.Info("Starting DatabaseManager..."); DatabaseManager.Start(); log.Info("Starting PropertyManager..."); PropertyManager.Initialize(); log.Info("Initializing GuidManager..."); GuidManager.Initialize(); if (ConfigManager.Config.Server.ServerPerformanceMonitorAutoStart) { log.Info("Server Performance Monitor auto starting..."); ServerPerformanceMonitor.Start(); } if (ConfigManager.Config.Server.WorldDatabasePrecaching) { log.Info("Precaching Weenies..."); DatabaseManager.World.CacheAllWeenies(); log.Info("Precaching Cookbooks..."); DatabaseManager.World.CacheAllCookbooks(); log.Info("Precaching Events..."); DatabaseManager.World.GetAllEvents(); log.Info("Precaching House Portals..."); DatabaseManager.World.CacheAllHousePortals(); log.Info("Precaching Points Of Interest..."); DatabaseManager.World.CacheAllPointsOfInterest(); log.Info("Precaching Spells..."); DatabaseManager.World.CacheAllSpells(); log.Info("Precaching Treasures - Death..."); DatabaseManager.World.CacheAllTreasuresDeath(); log.Info("Precaching Treasures - Material Base..."); DatabaseManager.World.CacheAllTreasureMaterialBase(); log.Info("Precaching Treasures - Material Groups..."); DatabaseManager.World.CacheAllTreasureMaterialGroups(); log.Info("Precaching Treasures - Material Colors..."); DatabaseManager.World.CacheAllTreasureMaterialColor(); log.Info("Precaching Treasures - Wielded..."); DatabaseManager.World.CacheAllTreasureWielded(); } else log.Info("Precaching World Database Disabled..."); log.Info("Initializing PlayerManager..."); PlayerManager.Initialize(); log.Info("Initializing HouseManager..."); HouseManager.Initialize(); log.Info("Initializing InboundMessageManager..."); InboundMessageManager.Initialize(); log.Info("Initializing SocketManager..."); SocketManager.Initialize(); log.Info("Initializing WorldManager..."); WorldManager.Initialize(); log.Info("Initializing EventManager..."); EventManager.Initialize(); // Free up memory before the server goes online. This can free up 6 GB+ on larger servers. log.Info("Forcing .net garbage collection..."); for (int i = 0; i < 10; i++) { // https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals // https://learn.microsoft.com/en-us/dotnet/api/system.runtime.gcsettings.largeobjectheapcompactionmode GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); } // This should be last log.Info("Initializing CommandManager..."); CommandManager.Initialize(); //Register mod commands log.Info("Registering ModManager commands..."); ModManager.RegisterCommands(); ModManager.ListMods(); if (!PropertyManager.GetBool("world_closed", false).Item) { WorldManager.Open(null); } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { log.Error(e.ExceptionObject); } private static void OnProcessExit(object sender, EventArgs e) { if (!IsRunningInContainer) { if (!ServerManager.ShutdownInitiated) log.Warn("Unsafe server shutdown detected! Data loss is possible!"); PropertyManager.StopUpdating(); DatabaseManager.Stop(); // Do system specific cleanup here try { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { MM_EndPeriod(1); } } catch (Exception ex) { log.Error(ex.ToString()); } } else { ServerManager.DoShutdownNow(); DatabaseManager.Stop(); } } } }