// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.Initialization; using System.ComponentModel.Design; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions; using MUnique.OpenMU.GameLogic.Resets; using MUnique.OpenMU.GameServer.MessageHandler; using MUnique.OpenMU.Network; using MUnique.OpenMU.Network.PlugIns; using MUnique.OpenMU.Persistence.Initialization.Updates; using MUnique.OpenMU.PlugIns; /// /// Class to manage data initialization. /// public abstract class DataInitializationBase : IDataInitializationPlugIn { private readonly IPersistenceContextProvider _persistenceContextProvider; private readonly ILoggerFactory _loggerFactory; private GameConfiguration? _gameConfiguration; private IContext? _context; /// /// Initializes a new instance of the class. /// /// The persistence context provider. /// The logger factory. protected DataInitializationBase(IPersistenceContextProvider persistenceContextProvider, ILoggerFactory loggerFactory) { this._persistenceContextProvider = persistenceContextProvider; this._loggerFactory = loggerFactory; } /// public abstract string Key { get; } /// public abstract string Caption { get; } /// /// Gets or sets the game configuration. /// /// not initialized yet. protected GameConfiguration GameConfiguration { get => this._gameConfiguration ?? throw new InvalidOperationException("not initialized yet."); set => this._gameConfiguration = value; } /// /// Gets or sets the context. /// /// not initialized yet. protected IContext Context { get => this._context ?? throw new InvalidOperationException("not initialized yet."); set => this._context = value; } /// /// Gets the game configuration initializer. /// protected abstract IInitializer GameConfigurationInitializer { get; } /// /// Gets the maps initializer. /// protected abstract IGameMapsInitializer GameMapsInitializer { get; } /// /// Gets the test accounts initializer. /// protected abstract IInitializer? TestAccountsInitializer { get; } /// /// Creates the initial data for a server. /// /// The number of game servers. /// If set to true, test accounts should be created. public async Task CreateInitialDataAsync(byte numberOfGameServers, bool createTestAccounts) { BaseMapInitializer.ClearDefaultDropItemGroups(); using (var temporaryContext = this._persistenceContextProvider.CreateNewContext()) { this.GameConfiguration = temporaryContext.CreateNew(); this.GameConfiguration.SetGuid(1); this.CreateSystemConfiguration(temporaryContext); using var tempSuspension = temporaryContext.SuspendChangeNotifications(); await temporaryContext.SaveChangesAsync().ConfigureAwait(false); } using var contextWithConfiguration = this._persistenceContextProvider.CreateNewContext(this.GameConfiguration); using var notificationSuspension = contextWithConfiguration.SuspendChangeNotifications(); this.Context = contextWithConfiguration; this.CreateGameClientDefinition(); await this.CreateChatServerDefinitionAsync().ConfigureAwait(false); this.GameConfigurationInitializer.Initialize(); var gameServerConfiguration = this.CreateGameServerConfiguration(this.GameConfiguration.Maps); await this.CreateGameServerDefinitionsAsync(gameServerConfiguration, numberOfGameServers).ConfigureAwait(false); await this.CreateConnectServerDefinitionAsync().ConfigureAwait(false); await this.Context.SaveChangesAsync().ConfigureAwait(false); this.GameMapsInitializer.SetSafezoneMaps(); if (createTestAccounts) { this.TestAccountsInitializer?.Initialize(); } if (!AppDomain.CurrentDomain.GetAssemblies().Contains(typeof(GameServer.GameServer).Assembly)) { // should never happen, but the access to the GameServer type is a trick to load the assembly into the current domain. } var dataSource = new GameConfigurationDataSource(this._loggerFactory.CreateLogger(), this._persistenceContextProvider); await dataSource.GetOwnerAsync(this.GameConfiguration.GetId()); var referenceHandler = new ByDataSourceReferenceHandler(dataSource); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IPersistenceContextProvider), this._persistenceContextProvider); var plugInManager = new PlugInManager(null, this._loggerFactory, serviceContainer, referenceHandler); plugInManager.DiscoverAndRegisterPlugIns(); plugInManager.KnownPlugInTypes.ForEach(plugInType => { var plugInConfiguration = this.Context.CreateNew(); plugInConfiguration.SetGuid(plugInType.GUID); plugInConfiguration.TypeId = plugInType.GUID; plugInConfiguration.IsActive = !plugInType.IsAssignableTo(typeof(IDisabledByDefault)); this.GameConfiguration.PlugInConfigurations.Add(plugInConfiguration); if (plugInType.GetInterfaces().Contains(typeof(ISupportDefaultCustomConfiguration))) { this.CreateDefaultPlugInConfiguration(plugInType, plugInConfiguration, referenceHandler); } if (plugInType == typeof(BlessJewelConsumeHandlerPlugIn)) { var config = new BlessJewelConsumeHandlerPlugInConfiguration { MaximumLevel = 5, MinimumLevel = 0, SuccessRatePercentage = 100, SuccessRateBonusWithLuckPercentage = 0, ResetToLevel0WhenFailMinLevel = 0, }; if (this.GameConfiguration.Items.FirstOrDefault(item => item is { Group: 13, Number: 37 }) is { } fenrir) { config.RepairTargetItems.Add(fenrir); } plugInConfiguration.SetConfiguration(config, referenceHandler); } // We don't move the player anymore by his request. This was usually requested after a player performed a skill. // However, it adds way for cheaters to move through the map. // The plugin is therefore deactivated by default. if (plugInType.IsAssignableTo(typeof(CharacterMoveBaseHandlerPlugIn))) { plugInConfiguration.IsActive = false; } // Disable plugins marked as disabled by default. if (plugInType.IsAssignableTo(typeof(IDisabledByDefault))) { plugInConfiguration.IsActive = false; } }); this.AddAllUpdateEntries(plugInManager); await this.Context.SaveChangesAsync().ConfigureAwait(false); } /// /// Creates the game client definition. /// protected abstract void CreateGameClientDefinition(); private void CreateDefaultPlugInConfiguration(Type plugInType, PlugInConfiguration plugInConfiguration, ReferenceHandler referenceHandler) { try { var plugin = (ISupportDefaultCustomConfiguration)Activator.CreateInstance(plugInType)!; var defaultConfig = plugin.CreateDefaultConfig(); plugInConfiguration.SetConfiguration(defaultConfig, referenceHandler); } catch (Exception ex) { this._loggerFactory.CreateLogger(this.GetType()).LogWarning(ex, "Could not create custom default configuration for plugin type {plugInType}", plugInType); } } private void AddAllUpdateEntries(PlugInManager plugInManager) { var updates = plugInManager.GetStrategyProvider() ?.AvailableStrategies.Where(up => up.DataInitializationKey == this.Key) .OrderBy(up => up.Version) .ToList(); if (updates is not { Count: > 0 }) { return; } foreach (var update in updates) { var entry = this.Context.CreateNew(); entry.Version = (int)update.Version; entry.Name = update.Name; entry.Description = update.Description; entry.CreatedAt = update.CreatedAt; entry.InstalledAt = DateTime.UtcNow; } var updateState = this.Context.CreateNew(); updateState.InitializationKey = this.Key; updateState.CurrentInstalledVersion = updates.Max(u => (int)u.Version); } private async ValueTask CreateConnectServerDefinitionAsync() { var port = 44405; var clients = await this.Context.GetAsync().ConfigureAwait(false); foreach (var client in clients.OrderBy(c => c.Season)) { var connectServer = this.Context.CreateNew(); connectServer.InitializeDefaults(); connectServer.SetGuid(client.Season, client.Episode); connectServer.Client = client; connectServer.ClientListenerPort = port; connectServer.Description = $"Connect Server ({client.Description})"; port++; } } private async ValueTask CreateGameServerDefinitionsAsync(GameServerConfiguration gameServerConfiguration, int numberOfServers) { var port = 55901; for (int i = 0; i < numberOfServers; i++) { var server = this.Context!.CreateNew(); server.SetGuid((short)i); server.ServerID = (byte)i; server.Description = $"Server {i}"; server.ExperienceRate = 1.0f; server.GameConfiguration = this.GameConfiguration; server.ServerConfiguration = gameServerConfiguration; foreach (var client in await this.Context.GetAsync().ConfigureAwait(false)) { var endPoint = this.Context.CreateNew(); endPoint.SetGuid((short)i, (short)server.Endpoints.Count); endPoint.Client = client; endPoint.NetworkPort = port; server.Endpoints.Add(endPoint); port++; } } } private async ValueTask CreateChatServerDefinitionAsync() { var server = this.Context!.CreateNew(); server.SetGuid(0); server.ServerId = 0; server.Description = "Chat Server"; var client = (await this.Context!.GetAsync().ConfigureAwait(false)).First(); var endPoint = this.Context.CreateNew(); server.SetGuid(0); endPoint.Client = client; endPoint.NetworkPort = 55980; server.Endpoints.Add(endPoint); } private GameServerConfiguration CreateGameServerConfiguration(ICollection maps) { var gameServerConfiguration = this.Context.CreateNew(); gameServerConfiguration.SetGuid(0); gameServerConfiguration.MaximumPlayers = 1000; // by default we add every map to a server configuration foreach (var map in maps) { gameServerConfiguration.Maps.Add(map); } return gameServerConfiguration; } private void CreateSystemConfiguration(IContext context) { var systemConfiguration = context.CreateNew(); systemConfiguration.SetGuid(0); systemConfiguration.AutoStart = true; systemConfiguration.AutoUpdateSchema = true; systemConfiguration.ReadConsoleInput = false; var (type, param) = IpAddressResolverFactory.DetermineBestFittingResolver(Environment.GetCommandLineArgs()); systemConfiguration.IpResolver = type; systemConfiguration.IpResolverParameter = param; } }