using System; using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Commands; using Pal.Client.Configuration; using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection.Logging; using Pal.Client.Extensions; using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Client.Windows; namespace Pal.Client { /// /// DI-aware Plugin. /// // ReSharper disable once UnusedType.Global internal sealed class DependencyInjectionContext : IDalamudPlugin { public static DalamudLoggerProvider LoggerProvider { get; } = new(); /// /// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. /// private readonly ILogger _logger = LoggerProvider.CreateLogger(); private readonly string _sqliteConnectionString; private readonly CancellationTokenSource _initCts = new(); private ServiceProvider? _serviceProvider; public string Name => Localization.Palace_Pal; public DependencyInjectionContext(DalamudPluginInterface pluginInterface, ClientState clientState, GameGui gameGui, ChatGui chatGui, ObjectTable objectTable, Framework framework, Condition condition, CommandManager commandManager, DataManager dataManager) { _logger.LogInformation("Building service container"); // set up legacy services #pragma warning disable CS0612 JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory()); #pragma warning restore CS0612 // set up logging CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); services.AddLogging(builder => builder.AddFilter("Pal", LogLevel.Trace) .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) .AddFilter("Grpc", LogLevel.Debug) .ClearProviders() .AddProvider(LoggerProvider)); // dalamud services.AddSingleton(this); services.AddSingleton(pluginInterface); services.AddSingleton(clientState); services.AddSingleton(gameGui); services.AddSingleton(chatGui); services.AddSingleton(); services.AddSingleton(objectTable); services.AddSingleton(framework); services.AddSingleton(condition); services.AddSingleton(commandManager); services.AddSingleton(dataManager); services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); // EF core _sqliteConnectionString = $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); services.AddTransient(); // plugin-specific services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService().Load()); services.AddTransient(); services.AddSingleton(); // territory & marker related services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // windows & related services services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddSingleton(); // these should maybe be scoped services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // queue handling services.AddTransient, QueuedImport.Handler>(); services.AddTransient, QueuedUndoImport.Handler>(); services.AddTransient, QueuedConfigUpdate.Handler>(); services.AddTransient, QueuedSyncResponse.Handler>(); // set up the current UI language before creating anything Localization.Culture = new CultureInfo(pluginInterface.UiLanguage); // build _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, }); #if RELEASE // You're welcome to remove this code in your fork, but please make sure that: // - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and // - you host your own server instance // // This is mainly to avoid this plugin being included in 'mega-repos' that, for whatever reason, decide // that collecting all plugins is a good idea (and break half in the process). _serviceProvider.GetService(); #endif // This is not ideal as far as loading the plugin goes, because there's no way to check for errors and // tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized, // even if it's not. // // There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for // config stuff. _logger = _serviceProvider.GetRequiredService>(); _logger.LogInformation("Service container built, triggering async init"); Task.Run(async () => { using IDisposable? logScope = _logger.BeginScope("AsyncInit"); Chat? chat = null; try { _logger.LogInformation("Starting async init"); chat = _serviceProvider.GetService(); // initialize database await using (var scope = _serviceProvider.CreateAsyncScope()) { _logger.LogInformation("Loading database & running migrations"); await using var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(token); _logger.LogInformation("Completed database migrations"); } token.ThrowIfCancellationRequested(); // v1 migration: config migration for import history, json migration for markers _serviceProvider.GetRequiredService().Migrate(); await _serviceProvider.GetRequiredService().MigrateAsync(token); token.ThrowIfCancellationRequested(); // windows that have logic to open on startup _serviceProvider.GetRequiredService(); // initialize components that are mostly self-contained/self-registered _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); _serviceProvider.GetRequiredService(); token.ThrowIfCancellationRequested(); _serviceProvider.GetRequiredService(); _logger.LogInformation("Async init complete"); } catch (ObjectDisposedException) { } catch (TaskCanceledException e) { _logger.LogError(e, "Task cancelled"); chat?.Error("Plugin was unloaded before it finished loading."); } catch (Exception e) { _logger.LogError(e, "Async load failed"); chat?.Error($"Async loading failed: {e.GetType()}: {e.Message}"); } }); } public void Dispose() { _initCts.Cancel(); // ensure we're not calling dispose recursively on ourselves if (_serviceProvider != null) { ServiceProvider serviceProvider = _serviceProvider; _serviceProvider = null; serviceProvider.Dispose(); // ensure we're not keeping the file open longer than the plugin is loaded using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) SqliteConnection.ClearPool(sqliteConnection); } } } }