diff --git a/Pal.Client/Commands/PalCommand.cs b/Pal.Client/Commands/PalCommand.cs deleted file mode 100644 index bd20305..0000000 --- a/Pal.Client/Commands/PalCommand.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Linq; -using Dalamud.Game.ClientState; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using ECommons.Schedulers; -using Pal.Client.Configuration; -using Pal.Client.DependencyInjection; -using Pal.Client.Extensions; -using Pal.Client.Properties; -using Pal.Client.Rendering; -using Pal.Client.Windows; - -namespace Pal.Client.Commands -{ - // should restructure this when more commands exist, if that ever happens - // this command is more-or-less a debug/troubleshooting command, if anything - internal sealed class PalCommand : IDisposable - { - private readonly IPalacePalConfiguration _configuration; - private readonly CommandManager _commandManager; - private readonly Chat _chat; - private readonly StatisticsService _statisticsService; - private readonly ConfigWindow _configWindow; - private readonly TerritoryState _territoryState; - private readonly FloorService _floorService; - private readonly ClientState _clientState; - - public PalCommand( - IPalacePalConfiguration configuration, - CommandManager commandManager, - Chat chat, - StatisticsService statisticsService, - ConfigWindow configWindow, - TerritoryState territoryState, - FloorService floorService, - ClientState clientState) - { - _configuration = configuration; - _commandManager = commandManager; - _chat = chat; - _statisticsService = statisticsService; - _configWindow = configWindow; - _territoryState = territoryState; - _floorService = floorService; - _clientState = clientState; - - _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) - { - HelpMessage = Localization.Command_pal_HelpText - }); - } - - public void Dispose() - { - _commandManager.RemoveHandler("/pal"); - } - - private void OnCommand(string command, string arguments) - { - if (_configuration.FirstUse) - { - _chat.Error(Localization.Error_FirstTimeSetupRequired); - return; - } - - try - { - arguments = arguments.Trim(); - switch (arguments) - { - case "stats": - _statisticsService.ShowGlobalStatistics(); - break; - - case "test-connection": - case "tc": - _configWindow.IsOpen = true; - var _ = new TickScheduler(() => _configWindow.TestConnection()); - break; - -#if DEBUG - case "update-saves": - LocalState.UpdateAll(); - _chat.Message(Localization.Command_pal_updatesaves); - break; -#endif - - case "": - case "config": - _configWindow.Toggle(); - break; - - case "near": - DebugNearest(_ => true); - break; - - case "tnear": - DebugNearest(m => m.Type == Marker.EType.Trap); - break; - - case "hnear": - DebugNearest(m => m.Type == Marker.EType.Hoard); - break; - - default: - _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, - command)); - break; - } - } - catch (Exception e) - { - _chat.Error(e.ToString()); - } - } - - private void DebugNearest(Predicate predicate) - { - if (!_territoryState.IsInDeepDungeon()) - return; - - var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); - var playerPosition = _clientState.LocalPlayer?.Position; - if (playerPosition == null) - return; - _chat.Message($"{playerPosition}"); - - var nearbyMarkers = state.Markers - .Where(m => predicate(m)) - .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) - .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) - .OrderBy(m => m.distance) - .Take(5) - .ToList(); - foreach (var nearbyMarker in nearbyMarkers) - _chat.UnformattedMessage( - $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); - } - } -} diff --git a/Pal.Client/Commands/PalConfigCommand.cs b/Pal.Client/Commands/PalConfigCommand.cs new file mode 100644 index 0000000..296392f --- /dev/null +++ b/Pal.Client/Commands/PalConfigCommand.cs @@ -0,0 +1,31 @@ +using Dalamud.Interface.Windowing; +using Pal.Client.Configuration; +using Pal.Client.Windows; + +namespace Pal.Client.Commands +{ + internal class PalConfigCommand + { + private readonly IPalacePalConfiguration _configuration; + private readonly AgreementWindow _agreementWindow; + private readonly ConfigWindow _configWindow; + + public PalConfigCommand( + IPalacePalConfiguration configuration, + AgreementWindow agreementWindow, + ConfigWindow configWindow) + { + _configuration = configuration; + _agreementWindow = agreementWindow; + _configWindow = configWindow; + } + + public void Execute() + { + if (_configuration.FirstUse) + _agreementWindow.IsOpen = true; + else + _configWindow.Toggle(); + } + } +} diff --git a/Pal.Client/Commands/PalNearCommand.cs b/Pal.Client/Commands/PalNearCommand.cs new file mode 100644 index 0000000..b24c959 --- /dev/null +++ b/Pal.Client/Commands/PalNearCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using Dalamud.Game.ClientState; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; +using Pal.Client.Rendering; + +namespace Pal.Client.Commands +{ + internal sealed class PalNearCommand + { + private readonly Chat _chat; + private readonly ClientState _clientState; + private readonly TerritoryState _territoryState; + private readonly FloorService _floorService; + + public PalNearCommand(Chat chat, ClientState clientState, TerritoryState territoryState, + FloorService floorService) + { + _chat = chat; + _clientState = clientState; + _territoryState = territoryState; + _floorService = floorService; + } + + public void Execute(string arguments) + { + switch (arguments) + { + default: + DebugNearest(_ => true); + break; + + case "tnear": + DebugNearest(m => m.Type == Marker.EType.Trap); + break; + + case "hnear": + DebugNearest(m => m.Type == Marker.EType.Hoard); + break; + } + } + + private void DebugNearest(Predicate predicate) + { + if (!_territoryState.IsInDeepDungeon()) + return; + + var state = _floorService.GetFloorMarkers(_clientState.TerritoryType); + var playerPosition = _clientState.LocalPlayer?.Position; + if (playerPosition == null) + return; + _chat.Message($"{playerPosition}"); + + var nearbyMarkers = state.Markers + .Where(m => predicate(m)) + .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) + .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) + .OrderBy(m => m.distance) + .Take(5) + .ToList(); + foreach (var nearbyMarker in nearbyMarkers) + _chat.UnformattedMessage( + $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); + } + } +} diff --git a/Pal.Client/Commands/PalStatsCommand.cs b/Pal.Client/Commands/PalStatsCommand.cs new file mode 100644 index 0000000..917efcc --- /dev/null +++ b/Pal.Client/Commands/PalStatsCommand.cs @@ -0,0 +1,18 @@ +using System; +using Pal.Client.DependencyInjection; + +namespace Pal.Client.Commands +{ + internal sealed class PalStatsCommand + { + private readonly StatisticsService _statisticsService; + + public PalStatsCommand(StatisticsService statisticsService) + { + _statisticsService = statisticsService; + } + + public void Execute() + => _statisticsService.ShowGlobalStatistics(); + } +} diff --git a/Pal.Client/Commands/PalTestConnectionCommand.cs b/Pal.Client/Commands/PalTestConnectionCommand.cs new file mode 100644 index 0000000..174330c --- /dev/null +++ b/Pal.Client/Commands/PalTestConnectionCommand.cs @@ -0,0 +1,20 @@ +using ECommons.Schedulers; +using Pal.Client.Windows; + +namespace Pal.Client.Commands +{ + internal sealed class PalTestConnectionCommand + { + private readonly ConfigWindow _configWindow; + + public PalTestConnectionCommand(ConfigWindow configWindow) + { + _configWindow = configWindow; + } + + public void Execute() + { + var _ = new TickScheduler(() => _configWindow.TestConnection()); + } + } +} diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index ea2f1ce..3ef0b58 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -52,7 +52,8 @@ namespace Pal.Client public string Name => Localization.Palace_Pal; - public DependencyInjectionContext(DalamudPluginInterface pluginInterface, + public DependencyInjectionContext( + DalamudPluginInterface pluginInterface, ClientState clientState, GameGui gameGui, ChatGui chatGui, @@ -70,7 +71,6 @@ namespace Pal.Client #pragma warning restore CS0612 // set up logging - CancellationToken token = _initCts.Token; IServiceCollection services = new ServiceCollection(); services.AddLogging(builder => builder.AddFilter("Pal", LogLevel.Trace) @@ -100,32 +100,37 @@ namespace Pal.Client services.AddTransient(); // plugin-specific - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService().Load()); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService().Load()); services.AddTransient(); - services.AddSingleton(); + + // commands + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // territory & marker related services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // windows & related services - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // these should maybe be scoped services.AddScoped(); services.AddScoped(); - services.AddSingleton(); + services.AddScoped(); // queue handling services.AddTransient, QueuedImport.Handler>(); @@ -161,63 +166,8 @@ namespace Pal.Client // 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(); - _plugin = new Plugin(pluginInterface, _serviceProvider); - - _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}"); - } - }); + _logger.LogInformation("Service container built, creating plugin"); + _plugin = new Plugin(pluginInterface, _serviceProvider, _initCts.Token); } public void Dispose() diff --git a/Pal.Client/DependencyInjectionLoader.cs b/Pal.Client/DependencyInjectionLoader.cs new file mode 100644 index 0000000..fce371c --- /dev/null +++ b/Pal.Client/DependencyInjectionLoader.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +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.Properties; +using Pal.Client.Windows; + +namespace Pal.Client +{ + /// + /// Takes care of async plugin init - this is mostly everything that requires either the config or the database to + /// be available. + /// + internal sealed class DependencyInjectionLoader + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public DependencyInjectionLoader(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public ELoadState LoadState { get; private set; } = ELoadState.Initializing; + + public event Action? InitCompleted; + + public async Task InitializeAsync(CancellationToken cancellationToken) + { + 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(); + + // takes 2-3 seconds with initializing connections, loading driver etc. + await dbContext.Database.MigrateAsync(cancellationToken); + _logger.LogInformation("Completed database migrations"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); + + cancellationToken.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(); + + // eager load any commands to find errors now, not when running them + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + + cancellationToken.ThrowIfCancellationRequested(); + + LoadState = ELoadState.Loaded; + InitCompleted?.Invoke(null); + _logger.LogInformation("Async init complete"); + } + catch (ObjectDisposedException) + { + InitCompleted?.Invoke(null); + LoadState = ELoadState.Error; + } + catch (Exception e) + { + _logger.LogError(e, "Async load failed"); + InitCompleted?.Invoke(() => chat?.Error(string.Format(Localization.Error_LoadFailed, $"{e.GetType()} - {e.Message}"))); + + LoadState = ELoadState.Error; + } + } + + public enum ELoadState + { + Initializing, + Loaded, + Error + } + } +} diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index f8fc54b..62c5adf 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -5,11 +5,17 @@ using Pal.Client.Windows; using System; using System.Globalization; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Pal.Client.Commands; using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client { @@ -17,51 +23,120 @@ namespace Pal.Client /// With all DI logic elsewhere, this plugin shell really only takes care of a few things around events that /// need to be sent to different receivers depending on priority or configuration . /// - /// + /// internal sealed class Plugin : IDisposable { private readonly DalamudPluginInterface _pluginInterface; - private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private readonly IPalacePalConfiguration _configuration; - private readonly RenderAdapter _renderAdapter; + private readonly CommandManager _commandManager; + private readonly Chat _chat; private readonly WindowSystem _windowSystem; + private readonly ClientState _clientState; + + private readonly IServiceScope _rootScope; + private readonly DependencyInjectionLoader _loader; + + private Action? _loginAction = null; public Plugin( DalamudPluginInterface pluginInterface, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + CancellationToken cancellationToken) { _pluginInterface = pluginInterface; - _serviceProvider = serviceProvider; - _logger = _serviceProvider.GetRequiredService>(); - _configuration = serviceProvider.GetRequiredService(); - _renderAdapter = serviceProvider.GetRequiredService(); + _logger = serviceProvider.GetRequiredService>(); + _commandManager = serviceProvider.GetRequiredService(); + _chat = serviceProvider.GetRequiredService(); _windowSystem = serviceProvider.GetRequiredService(); + _clientState = serviceProvider.GetRequiredService(); - LanguageChanged(pluginInterface.UiLanguage); + _rootScope = serviceProvider.CreateScope(); + _loader = _rootScope.ServiceProvider.GetRequiredService(); + _loader.InitCompleted += InitCompleted; + var _ = Task.Run(async () => await _loader.InitializeAsync(cancellationToken)); pluginInterface.UiBuilder.Draw += Draw; pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; pluginInterface.LanguageChanged += LanguageChanged; + _clientState.Login += Login; + + _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) + { + HelpMessage = Localization.Command_pal_HelpText + }); + } + + private void InitCompleted(Action? loginAction) + { + LanguageChanged(_pluginInterface.UiLanguage); + + if (_clientState.IsLoggedIn) + { + loginAction?.Invoke(); + _loginAction = null; + } + else + _loginAction = loginAction; + } + + private void Login(object? sender, EventArgs eventArgs) + { + _loginAction?.Invoke(); + _loginAction = null; + } + + private void OnCommand(string command, string arguments) + { + arguments = arguments.Trim(); + + IPalacePalConfiguration configuration = + _rootScope.ServiceProvider.GetRequiredService(); + if (configuration.FirstUse && arguments != "" && arguments != "config") + { + _chat.Error(Localization.Error_FirstTimeSetupRequired); + return; + } + + try + { + var sp = _rootScope.ServiceProvider; + + switch (arguments) + { + case "": + case "config": + sp.GetRequiredService().Execute(); + break; + + case "stats": + sp.GetRequiredService().Execute(); + break; + + case "tc": + case "test-connection": + sp.GetRequiredService().Execute(); + break; + + case "near": + case "tnear": + case "hnear": + sp.GetRequiredService().Execute(arguments); + break; + + default: + _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + command)); + break; + } + } + catch (Exception e) + { + _chat.Error(e.ToString()); + } } private void OpenConfigUi() - { - Window configWindow; - if (_configuration.FirstUse) - configWindow = _serviceProvider.GetRequiredService(); - else - configWindow = _serviceProvider.GetRequiredService(); - - configWindow.IsOpen = true; - } - - public void Dispose() - { - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - _pluginInterface.LanguageChanged -= LanguageChanged; - } + => _rootScope.ServiceProvider.GetRequiredService().Execute(); private void LanguageChanged(string languageCode) { @@ -74,8 +149,24 @@ namespace Pal.Client private void Draw() { - _renderAdapter.DrawLayers(); - _windowSystem.Draw(); + if (_loader.LoadState == DependencyInjectionLoader.ELoadState.Loaded) + { + _rootScope.ServiceProvider.GetRequiredService().DrawLayers(); + _windowSystem.Draw(); + } + } + + public void Dispose() + { + _commandManager.RemoveHandler("/pal"); + + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; + _clientState.Login -= Login; + + _loader.InitCompleted -= InitCompleted; + _rootScope.Dispose(); } } } diff --git a/Pal.Client/Properties/Localization.Designer.cs b/Pal.Client/Properties/Localization.Designer.cs index d15a42c..2c95215 100644 --- a/Pal.Client/Properties/Localization.Designer.cs +++ b/Pal.Client/Properties/Localization.Designer.cs @@ -149,15 +149,6 @@ namespace Pal.Client.Properties { } } - /// - /// Looks up a localized string similar to Updated all locally cached marker files to latest version.. - /// - internal static string Command_pal_updatesaves { - get { - return ResourceManager.GetString("Command_pal_updatesaves", resourceCulture); - } - } - /// /// Looks up a localized string similar to You are NOT in a deep dungeon.. /// @@ -664,6 +655,15 @@ namespace Pal.Client.Properties { } } + /// + /// Looks up a localized string similar to Plugin could not be loaded: {0}. + /// + internal static string Error_LoadFailed { + get { + return ResourceManager.GetString("Error_LoadFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please install this plugin from the official repository at {0} to continue using it.. /// diff --git a/Pal.Client/Properties/Localization.fr.resx b/Pal.Client/Properties/Localization.fr.resx index 0712d94..c634de2 100644 --- a/Pal.Client/Properties/Localization.fr.resx +++ b/Pal.Client/Properties/Localization.fr.resx @@ -61,10 +61,6 @@ Impossible de récupérer les statistiques. Shown when /pal stats produces a server-side error, and the statistics window can't be loaded. - - Mise à jour de tous les marqueurs du cache local vers la dernière version. - Shown after /pal update-saves was successful. - Connexion réussie. diff --git a/Pal.Client/Properties/Localization.ja.resx b/Pal.Client/Properties/Localization.ja.resx index 4dd5bbf..4c72606 100644 --- a/Pal.Client/Properties/Localization.ja.resx +++ b/Pal.Client/Properties/Localization.ja.resx @@ -61,10 +61,6 @@ 統計情報を取得できません。 Shown when /pal stats produces a server-side error, and the statistics window can't be loaded. - - 保存されたマーカーファイルを更新しました。 - Shown after /pal update-saves was successful. - 接続に成功しました。 diff --git a/Pal.Client/Properties/Localization.resx b/Pal.Client/Properties/Localization.resx index f9e3625..ddac1c3 100644 --- a/Pal.Client/Properties/Localization.resx +++ b/Pal.Client/Properties/Localization.resx @@ -46,6 +46,9 @@ Please finish the initial setup first. Before using any /pal command, the initial setup/agreeement needs to be completed. + + Plugin could not be loaded: {0} + Please install this plugin from the official repository at {0} to continue using it. @@ -66,11 +69,7 @@ Unable to fetch statistics. Shown when /pal stats produces a server-side error, and the statistics window can't be loaded. - - Updated all locally cached marker files to latest version. - Shown after /pal update-saves was successful. - - + Connection successful. @@ -322,6 +321,5 @@ This is not synchronized with other players and not saved between floors/runs. Import failed: Invalid file. -