From c52341eb0d05863af927c752808cf3af83ea6b2b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 15 Feb 2023 23:17:19 +0100 Subject: [PATCH] DI: Initial Draft --- Pal.Client/Commands/PalCommand.cs | 141 ++++ .../Configuration/ConfigurationManager.cs | 23 +- Pal.Client/Configuration/ConfigurationV1.cs | 7 +- Pal.Client/Configuration/ConfigurationV7.cs | 10 + .../Configuration/IPalacePalConfiguration.cs | 2 + Pal.Client/DependencyInjection/ChatService.cs | 109 +++ Pal.Client/DependencyInjection/DIPlugin.cs | 61 +- Pal.Client/DependencyInjection/DebugState.cs | 15 + .../DependencyInjection/FloorService.cs | 15 + .../DependencyInjection/FrameworkService.cs | 392 ++++++++++ .../DependencyInjection/RepoVerification.cs | 25 + .../DependencyInjection/StatisticsService.cs | 65 ++ .../DependencyInjection/TerritoryState.cs | 38 + Pal.Client/Hooks.cs | 22 +- Pal.Client/Net/RemoteApi.AccountService.cs | 20 +- Pal.Client/Net/RemoteApi.PalaceService.cs | 1 - Pal.Client/Net/RemoteApi.Utils.cs | 9 - Pal.Client/Net/RemoteApi.cs | 24 +- Pal.Client/Plugin.cs | 694 +----------------- Pal.Client/Rendering/MarkerConfig.cs | 6 +- Pal.Client/Rendering/RenderAdapter.cs | 33 + Pal.Client/Rendering/RenderData.cs | 7 + Pal.Client/Rendering/SimpleRenderer.cs | 153 ++-- Pal.Client/Rendering/SplatoonRenderer.cs | 61 +- .../Scheduled/IQueueOnFrameworkThread.cs | 9 +- Pal.Client/Scheduled/QueueHandler.cs | 203 +++++ Pal.Client/Scheduled/QueuedConfigUpdate.cs | 17 +- Pal.Client/Scheduled/QueuedImport.cs | 84 +-- Pal.Client/Scheduled/QueuedSyncResponse.cs | 75 +- Pal.Client/Scheduled/QueuedUndoImport.cs | 29 +- Pal.Client/Service.cs | 24 +- Pal.Client/Windows/AgreementWindow.cs | 38 +- Pal.Client/Windows/ConfigWindow.cs | 200 +++-- Pal.Client/Windows/StatisticsWindow.cs | 33 +- 34 files changed, 1557 insertions(+), 1088 deletions(-) create mode 100644 Pal.Client/Commands/PalCommand.cs create mode 100644 Pal.Client/DependencyInjection/ChatService.cs create mode 100644 Pal.Client/DependencyInjection/DebugState.cs create mode 100644 Pal.Client/DependencyInjection/FloorService.cs create mode 100644 Pal.Client/DependencyInjection/FrameworkService.cs create mode 100644 Pal.Client/DependencyInjection/RepoVerification.cs create mode 100644 Pal.Client/DependencyInjection/StatisticsService.cs create mode 100644 Pal.Client/DependencyInjection/TerritoryState.cs create mode 100644 Pal.Client/Rendering/RenderAdapter.cs create mode 100644 Pal.Client/Rendering/RenderData.cs create mode 100644 Pal.Client/Scheduled/QueueHandler.cs diff --git a/Pal.Client/Commands/PalCommand.cs b/Pal.Client/Commands/PalCommand.cs new file mode 100644 index 0000000..4d9ff97 --- /dev/null +++ b/Pal.Client/Commands/PalCommand.cs @@ -0,0 +1,141 @@ +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 ChatGui _chatGui; + 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, + ChatGui chatGui, + StatisticsService statisticsService, + ConfigWindow configWindow, + TerritoryState territoryState, + FloorService floorService, + ClientState clientState) + { + _configuration = configuration; + _commandManager = commandManager; + _chatGui = chatGui; + _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) + { + _chatGui.PalError(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(); + Service.Chat.Print(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: + _chatGui.PalError(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + command)); + break; + } + } + catch (Exception e) + { + _chatGui.PalError(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; + _chatGui.Print($"[Palace Pal] {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) + _chatGui.Print( + $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); + } + } +} diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 86757a7..1dfdbd7 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Text; using System.Text.Encodings.Web; @@ -6,6 +7,8 @@ using System.Text.Json; using Dalamud.Logging; using Dalamud.Plugin; using ImGuiNET; +using Pal.Client.DependencyInjection; +using Pal.Client.Scheduled; using NJson = Newtonsoft.Json; namespace Pal.Client.Configuration @@ -14,12 +17,16 @@ namespace Pal.Client.Configuration { private readonly DalamudPluginInterface _pluginInterface; + public event EventHandler? Saved; + public ConfigurationManager(DalamudPluginInterface pluginInterface) { _pluginInterface = pluginInterface; + + Migrate(); } - public string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); + private string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); public IPalacePalConfiguration Load() { @@ -27,16 +34,20 @@ namespace Pal.Client.Configuration new ConfigurationV7(); } - public void Save(IConfigurationInConfigDirectory config) + public void Save(IConfigurationInConfigDirectory config, bool queue = true) { File.WriteAllText(ConfigPath, - JsonSerializer.Serialize(config, config.GetType(), new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), + JsonSerializer.Serialize(config, config.GetType(), + new JsonSerializerOptions + { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), Encoding.UTF8); + if (queue && config is ConfigurationV7 v7) + Saved?.Invoke(this, v7); } #pragma warning disable CS0612 #pragma warning disable CS0618 - public void Migrate() + private void Migrate() { if (_pluginInterface.ConfigFile.Exists) { @@ -49,7 +60,7 @@ namespace Pal.Client.Configuration configurationV1.Save(); var v7 = MigrateToV7(configurationV1); - Save(v7); + Save(v7, queue: false); File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); } diff --git a/Pal.Client/Configuration/ConfigurationV1.cs b/Pal.Client/Configuration/ConfigurationV1.cs index ba47de8..7128f52 100644 --- a/Pal.Client/Configuration/ConfigurationV1.cs +++ b/Pal.Client/Configuration/ConfigurationV1.cs @@ -90,7 +90,7 @@ namespace Pal.Client.Configuration { // 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up. // Not a problem for online players, but offline players might be fucked. - bool changedAnyFile = false; + //bool changedAnyFile = false; LocalState.ForEach(s => { foreach (var marker in s.Markers) @@ -104,7 +104,7 @@ namespace Pal.Client.Configuration s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == Marker.EType.Hoard || m.WasImported)); s.Save(); - changedAnyFile = true; + //changedAnyFile = true; } else { @@ -113,6 +113,7 @@ namespace Pal.Client.Configuration } }); + /* // Only notify offline users - we can just re-download the backup markers from the server seamlessly. if (Mode == EMode.Offline && changedAnyFile) { @@ -123,6 +124,7 @@ namespace Pal.Client.Configuration Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023."); }, 2500); } + */ Version = 5; Save(); @@ -144,7 +146,6 @@ namespace Pal.Client.Configuration TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameHandling = TypeNameHandling.Objects })); - Service.Plugin.EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); } public class AccountInfo diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index 0d2aa52..3fc1dbf 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Pal.Client.Net; namespace Pal.Client.Configuration; @@ -45,4 +46,13 @@ public class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDi { Accounts.RemoveAll(a => a.Server == server && a.IsUsable); } + + public bool HasRoleOnCurrentServer(string role) + { + if (Mode != EMode.Online) + return false; + + var account = FindAccount(RemoteApi.RemoteUrl); + return account == null || account.CachedRoles.Contains(role); + } } diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index 8b10b13..1c6a747 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -29,6 +29,8 @@ namespace Pal.Client.Configuration IAccountConfiguration CreateAccount(string server, Guid accountId); IAccountConfiguration? FindAccount(string server); void RemoveAccount(string server); + + bool HasRoleOnCurrentServer(string role); } public class DeepDungeonConfiguration diff --git a/Pal.Client/DependencyInjection/ChatService.cs b/Pal.Client/DependencyInjection/ChatService.cs new file mode 100644 index 0000000..2dcfeb2 --- /dev/null +++ b/Pal.Client/DependencyInjection/ChatService.cs @@ -0,0 +1,109 @@ +using System; +using System.Text.RegularExpressions; +using Dalamud.Data; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Lumina.Excel.GeneratedSheets; +using Pal.Client.Configuration; + +namespace Pal.Client.DependencyInjection +{ + internal sealed class ChatService : IDisposable + { + private readonly ChatGui _chatGui; + private readonly TerritoryState _territoryState; + private readonly IPalacePalConfiguration _configuration; + private readonly DataManager _dataManager; + private readonly LocalizedChatMessages _localizedChatMessages; + + public ChatService(ChatGui chatGui, TerritoryState territoryState, IPalacePalConfiguration configuration, + DataManager dataManager) + { + _chatGui = chatGui; + _territoryState = territoryState; + _configuration = configuration; + _dataManager = dataManager; + + _localizedChatMessages = LoadLanguageStrings(); + + _chatGui.ChatMessage += OnChatMessage; + } + + public void Dispose() + => _chatGui.ChatMessage -= OnChatMessage; + + private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, + ref bool isHandled) + { + if (_configuration.FirstUse) + return; + + if (type != (XivChatType)2105) + return; + + string message = seMessage.ToString(); + if (_localizedChatMessages.FloorChanged.IsMatch(message)) + { + _territoryState.PomanderOfSight = PomanderState.Inactive; + + if (_territoryState.PomanderOfIntuition == PomanderState.FoundOnCurrentFloor) + _territoryState.PomanderOfIntuition = PomanderState.Inactive; + } + else if (message.EndsWith(_localizedChatMessages.MapRevealed)) + { + _territoryState.PomanderOfSight = PomanderState.Active; + } + else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved)) + { + _territoryState.PomanderOfSight = PomanderState.PomanderOfSafetyUsed; + } + else if (message.EndsWith(_localizedChatMessages.HoardNotOnCurrentFloor) || + message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor)) + { + // There is no functional difference between these - if you don't open the marked coffer, + // going to higher floors will keep the pomander active. + _territoryState.PomanderOfIntuition = PomanderState.Active; + } + else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened)) + { + _territoryState.PomanderOfIntuition = PomanderState.FoundOnCurrentFloor; + } + } + + private LocalizedChatMessages LoadLanguageStrings() + { + return new LocalizedChatMessages + { + MapRevealed = GetLocalizedString(7256), + AllTrapsRemoved = GetLocalizedString(7255), + HoardOnCurrentFloor = GetLocalizedString(7272), + HoardNotOnCurrentFloor = GetLocalizedString(7273), + HoardCofferOpened = GetLocalizedString(7274), + FloorChanged = + new Regex("^" + GetLocalizedString(7270).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") + + "$"), + }; + } + + private string GetLocalizedString(uint id) + { + return _dataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; + } + + private class LocalizedChatMessages + { + public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!"; + public string AllTrapsRemoved { get; init; } = "???"; // "All the traps on this floor have disappeared!"; + public string HoardOnCurrentFloor { get; init; } = "???"; // "You sense the Accursed Hoard calling you..."; + + public string HoardNotOnCurrentFloor { get; init; } = + "???"; // "You do not sense the call of the Accursed Hoard on this floor..."; + + public string HoardCofferOpened { get; init; } = "???"; // "You discover a piece of the Accursed Hoard!"; + + public Regex FloorChanged { get; init; } = + new(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$"); + } + } +} diff --git a/Pal.Client/DependencyInjection/DIPlugin.cs b/Pal.Client/DependencyInjection/DIPlugin.cs index d64368b..e77e056 100644 --- a/Pal.Client/DependencyInjection/DIPlugin.cs +++ b/Pal.Client/DependencyInjection/DIPlugin.cs @@ -1,13 +1,21 @@ -using Dalamud.Data; +using System.Globalization; +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.Plugin; using Microsoft.Extensions.DependencyInjection; +using Pal.Client.Commands; +using Pal.Client.Configuration; +using Pal.Client.Net; using Pal.Client.Properties; +using Pal.Client.Rendering; +using Pal.Client.Scheduled; +using Pal.Client.Windows; namespace Pal.Client.DependencyInjection { @@ -35,6 +43,7 @@ namespace Pal.Client.DependencyInjection // dalamud services.AddSingleton(this); services.AddSingleton(pluginInterface); + services.AddSingleton(clientState); services.AddSingleton(gameGui); services.AddSingleton(chatGui); services.AddSingleton(objectTable); @@ -42,9 +51,38 @@ namespace Pal.Client.DependencyInjection services.AddSingleton(condition); services.AddSingleton(commandManager); services.AddSingleton(dataManager); + services.AddSingleton(new WindowSystem(typeof(DIPlugin).AssemblyQualifiedName)); - // palace pal + // plugin-specific services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService().Load()); + services.AddTransient(); + services.AddSingleton(); + + // territory handling + 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(); + + // set up the current UI language before creating anything + Localization.Culture = new CultureInfo(pluginInterface.UiLanguage); // build _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions @@ -54,6 +92,24 @@ namespace Pal.Client.DependencyInjection }); // initialize plugin +#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 + + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); } @@ -67,7 +123,6 @@ namespace Pal.Client.DependencyInjection serviceProvider.Dispose(); } - } } } diff --git a/Pal.Client/DependencyInjection/DebugState.cs b/Pal.Client/DependencyInjection/DebugState.cs new file mode 100644 index 0000000..0a632a3 --- /dev/null +++ b/Pal.Client/DependencyInjection/DebugState.cs @@ -0,0 +1,15 @@ +using System; + +namespace Pal.Client.DependencyInjection +{ + internal class DebugState + { + public string? DebugMessage { get; set; } + + public void SetFromException(Exception e) + => DebugMessage = $"{DateTime.Now}\n{e}"; + + public void Reset() + => DebugMessage = null; + } +} diff --git a/Pal.Client/DependencyInjection/FloorService.cs b/Pal.Client/DependencyInjection/FloorService.cs new file mode 100644 index 0000000..8cdcb98 --- /dev/null +++ b/Pal.Client/DependencyInjection/FloorService.cs @@ -0,0 +1,15 @@ +using System.Collections.Concurrent; + +namespace Pal.Client.DependencyInjection +{ + internal sealed class FloorService + { + public ConcurrentDictionary FloorMarkers { get; } = new(); + public ConcurrentBag EphemeralMarkers { get; set; } = new(); + + public LocalState GetFloorMarkers(ushort territoryType) + { + return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt)); + } + } +} diff --git a/Pal.Client/DependencyInjection/FrameworkService.cs b/Pal.Client/DependencyInjection/FrameworkService.cs new file mode 100644 index 0000000..f3adaee --- /dev/null +++ b/Pal.Client/DependencyInjection/FrameworkService.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; +using ImGuiNET; +using Pal.Client.Configuration; +using Pal.Client.Extensions; +using Pal.Client.Net; +using Pal.Client.Rendering; +using Pal.Client.Scheduled; + +namespace Pal.Client.DependencyInjection +{ + internal class FrameworkService : IDisposable + { + private readonly Framework _framework; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private readonly ClientState _clientState; + private readonly TerritoryState _territoryState; + private readonly FloorService _floorService; + private readonly DebugState _debugState; + private readonly RenderAdapter _renderAdapter; + private readonly QueueHandler _queueHandler; + private readonly ObjectTable _objectTable; + private readonly RemoteApi _remoteApi; + + internal Queue EarlyEventQueue { get; } = new(); + internal Queue LateEventQueue { get; } = new(); + internal ConcurrentQueue NextUpdateObjects { get; } = new(); + + public FrameworkService(Framework framework, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + ClientState clientState, + TerritoryState territoryState, + FloorService floorService, + DebugState debugState, + RenderAdapter renderAdapter, + QueueHandler queueHandler, + ObjectTable objectTable, + RemoteApi remoteApi) + { + _framework = framework; + _configurationManager = configurationManager; + _configuration = configuration; + _clientState = clientState; + _territoryState = territoryState; + _floorService = floorService; + _debugState = debugState; + _renderAdapter = renderAdapter; + _queueHandler = queueHandler; + _objectTable = objectTable; + _remoteApi = remoteApi; + + _framework.Update += OnUpdate; + _configurationManager.Saved += OnSaved; + } + + public void Dispose() + { + _framework.Update -= OnUpdate; + _configurationManager.Saved -= OnSaved; + } + + private void OnSaved(object? sender, IPalacePalConfiguration? config) + => EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); + + private void OnUpdate(Framework framework) + { + if (_configuration.FirstUse) + return; + + try + { + bool recreateLayout = false; + bool saveMarkers = false; + + while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) + _queueHandler.Handle(queued, ref recreateLayout, ref saveMarkers); + + if (_territoryState.LastTerritory != _clientState.TerritoryType) + { + _territoryState.LastTerritory = _clientState.TerritoryType; + _territoryState.TerritorySyncState = SyncState.NotAttempted; + NextUpdateObjects.Clear(); + + if (_territoryState.IsInDeepDungeon()) + _floorService.GetFloorMarkers(_territoryState.LastTerritory); + _floorService.EphemeralMarkers.Clear(); + _territoryState.PomanderOfSight = PomanderState.Inactive; + _territoryState.PomanderOfIntuition = PomanderState.Inactive; + recreateLayout = true; + _debugState.Reset(); + } + + if (!_territoryState.IsInDeepDungeon()) + return; + + if (_configuration.Mode == EMode.Online && _territoryState.TerritorySyncState == SyncState.NotAttempted) + { + _territoryState.TerritorySyncState = SyncState.Started; + Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory)); + } + + while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) + _queueHandler.Handle(queued, ref recreateLayout, ref saveMarkers); + + var currentFloor = _floorService.GetFloorMarkers(_territoryState.LastTerritory); + + IList visibleMarkers = GetRelevantGameObjects(); + HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout); + HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + #region Render Markers + private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, bool recreateLayout) + { + var currentFloorMarkers = currentFloor.Markers; + + bool updateSeenMarkers = false; + var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + foreach (var visibleMarker in visibleMarkers) + { + Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); + if (knownMarker != null) + { + if (!knownMarker.Seen) + { + knownMarker.Seen = true; + saveMarkers = true; + } + + // This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, + // markers discovered afterwards are automatically marked seen. + if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && !knownMarker.RemoteSeenOn.Contains(partialAccountId)) + updateSeenMarkers = true; + + continue; + } + + currentFloorMarkers.Add(visibleMarker); + recreateLayout = true; + saveMarkers = true; + } + + if (!recreateLayout && currentFloorMarkers.Count > 0 && (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) + { + + try + { + foreach (var marker in currentFloorMarkers) + { + uint desiredColor = DetermineColor(marker, visibleMarkers); + if (marker.RenderElement == null || !marker.RenderElement.IsValid) + { + recreateLayout = true; + break; + } + + if (marker.RenderElement.Color != desiredColor) + marker.RenderElement.Color = desiredColor; + } + } + catch (Exception e) + { + _debugState.SetFromException(e); + recreateLayout = true; + } + } + + if (updateSeenMarkers && partialAccountId != null) + { + var markersToUpdate = currentFloorMarkers.Where(x => x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); + foreach (var marker in markersToUpdate) + marker.RemoteSeenRequested = true; + Task.Run(async () => await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, markersToUpdate)); + } + + if (saveMarkers) + { + currentFloor.Save(); + + if (_territoryState.TerritorySyncState == SyncState.Complete) + { + var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); + if (markersToUpload.Count > 0) + { + foreach (var marker in markersToUpload) + marker.UploadRequested = true; + Task.Run(async () => await UploadMarkersForTerritory(_territoryState.LastTerritory, markersToUpload)); + } + } + } + + if (recreateLayout) + { + _renderAdapter.ResetLayer(ELayer.TrapHoard); + + List elements = new(); + foreach (var marker in currentFloorMarkers) + { + if (marker.Seen || _configuration.Mode == EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) + { + if (marker.Type == Marker.EType.Trap) + { + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.Traps); + } + else if (marker.Type == Marker.EType.Hoard) + { + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.HoardCoffers); + } + } + } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.TrapHoard, elements); + } + } + + private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) + { + recreateLayout |= _floorService.EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); + recreateLayout |= visibleMarkers.Any(visibleMarker => _floorService.EphemeralMarkers.All(x => x != visibleMarker)); + + if (recreateLayout) + { + _renderAdapter.ResetLayer(ELayer.RegularCoffers); + _floorService.EphemeralMarkers.Clear(); + + List elements = new(); + foreach (var marker in visibleMarkers) + { + _floorService.EphemeralMarkers.Add(marker); + + if (marker.Type == Marker.EType.SilverCoffer && _configuration.DeepDungeons.SilverCoffers.Show) + { + CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), _configuration.DeepDungeons.SilverCoffers); + } + } + + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); + } + } + + private uint DetermineColor(Marker marker, IList visibleMarkers) + { + switch (marker.Type) + { + case Marker.EType.Trap when _territoryState.PomanderOfSight == PomanderState.Inactive || !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + return _configuration.DeepDungeons.Traps.Color; + case Marker.EType.Hoard when _territoryState.PomanderOfIntuition == PomanderState.Inactive || !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): + return _configuration.DeepDungeons.HoardCoffers.Color; + case Marker.EType.SilverCoffer: + return _configuration.DeepDungeons.SilverCoffers.Color; + case Marker.EType.Trap: + case Marker.EType.Hoard: + return RenderData.ColorInvisible; + default: + return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); + } + } + + private void CreateRenderElement(Marker marker, List elements, uint color, MarkerConfiguration config) + { + if (!config.Show) + return; + + var element = _renderAdapter.CreateElement(marker.Type, marker.Position, color, config.Fill); + marker.RenderElement = element; + elements.Add(element); + } + #endregion + + #region Up-/Download + private async Task DownloadMarkersForTerritory(ushort territoryId) + { + try + { + var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId); + LateEventQueue.Enqueue(new QueuedSyncResponse + { + Type = SyncType.Download, + TerritoryType = territoryId, + Success = success, + Markers = downloadedMarkers + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) + { + try + { + var (success, uploadedMarkers) = await _remoteApi.UploadMarker(territoryId, markersToUpload); + LateEventQueue.Enqueue(new QueuedSyncResponse + { + Type = SyncType.Upload, + TerritoryType = territoryId, + Success = success, + Markers = uploadedMarkers + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) + { + try + { + var success = await _remoteApi.MarkAsSeen(territoryId, markersToUpdate); + LateEventQueue.Enqueue(new QueuedSyncResponse + { + Type = SyncType.MarkSeen, + TerritoryType = territoryId, + Success = success, + Markers = markersToUpdate, + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + #endregion + + private IList GetRelevantGameObjects() + { + List result = new(); + for (int i = 246; i < _objectTable.Length; i++) + { + GameObject? obj = _objectTable[i]; + if (obj == null) + continue; + + switch ((uint)Marshal.ReadInt32(obj.Address + 128)) + { + case 2007182: + case 2007183: + case 2007184: + case 2007185: + case 2007186: + case 2009504: + result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); + break; + + case 2007542: + case 2007543: + result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true }); + break; + + case 2007357: + result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true }); + break; + } + } + + while (NextUpdateObjects.TryDequeue(out nint address)) + { + var obj = _objectTable.FirstOrDefault(x => x.Address == address); + if (obj != null && obj.Position.Length() > 0.1) + result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); + } + + return result; + } + } +} diff --git a/Pal.Client/DependencyInjection/RepoVerification.cs b/Pal.Client/DependencyInjection/RepoVerification.cs new file mode 100644 index 0000000..db46b64 --- /dev/null +++ b/Pal.Client/DependencyInjection/RepoVerification.cs @@ -0,0 +1,25 @@ +using System; +using Dalamud.Game.Gui; +using Dalamud.Logging; +using Dalamud.Plugin; +using Pal.Client.Extensions; +using Pal.Client.Properties; + +namespace Pal.Client.DependencyInjection +{ + public class RepoVerification + { + public RepoVerification(DalamudPluginInterface pluginInterface, ChatGui chatGui) + { + PluginLog.Information($"Install source: {pluginInterface.SourceRepository}"); + if (!pluginInterface.IsDev + && !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/") + && !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/")) + { + chatGui.PalError(string.Format(Localization.Error_WrongRepository, + "https://github.com/carvelli/Dalamud-Plugins")); + throw new InvalidOperationException(); + } + } + } +} diff --git a/Pal.Client/DependencyInjection/StatisticsService.cs b/Pal.Client/DependencyInjection/StatisticsService.cs new file mode 100644 index 0000000..23f2ba9 --- /dev/null +++ b/Pal.Client/DependencyInjection/StatisticsService.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading.Tasks; +using Dalamud.Game.Gui; +using Grpc.Core; +using Pal.Client.Configuration; +using Pal.Client.Extensions; +using Pal.Client.Net; +using Pal.Client.Properties; +using Pal.Client.Windows; + +namespace Pal.Client.DependencyInjection +{ + internal sealed class StatisticsService + { + private readonly IPalacePalConfiguration _configuration; + private readonly RemoteApi _remoteApi; + private readonly StatisticsWindow _statisticsWindow; + private readonly ChatGui _chatGui; + + public StatisticsService(IPalacePalConfiguration configuration, RemoteApi remoteApi, + StatisticsWindow statisticsWindow, ChatGui chatGui) + { + _configuration = configuration; + _remoteApi = remoteApi; + _statisticsWindow = statisticsWindow; + _chatGui = chatGui; + } + + public void ShowGlobalStatistics() + { + Task.Run(async () => await FetchFloorStatistics()); + } + + private async Task FetchFloorStatistics() + { + if (!_configuration.HasRoleOnCurrentServer("statistics:view")) + { + _chatGui.PalError(Localization.Command_pal_stats_CurrentFloor); + return; + } + + try + { + var (success, floorStatistics) = await _remoteApi.FetchStatistics(); + if (success) + { + _statisticsWindow.SetFloorData(floorStatistics); + _statisticsWindow.IsOpen = true; + } + else + { + _chatGui.PalError(Localization.Command_pal_stats_UnableToFetchStatistics); + } + } + catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) + { + _chatGui.Print(Localization.Command_pal_stats_CurrentFloor); + } + catch (Exception e) + { + _chatGui.PalError(e.ToString()); + } + } + } +} diff --git a/Pal.Client/DependencyInjection/TerritoryState.cs b/Pal.Client/DependencyInjection/TerritoryState.cs new file mode 100644 index 0000000..15e21d4 --- /dev/null +++ b/Pal.Client/DependencyInjection/TerritoryState.cs @@ -0,0 +1,38 @@ +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Pal.Client.Scheduled; +using Pal.Common; + +namespace Pal.Client.DependencyInjection +{ + public sealed class TerritoryState + { + private readonly ClientState _clientState; + private readonly Condition _condition; + + public TerritoryState(ClientState clientState, Condition condition) + { + _clientState = clientState; + _condition = condition; + } + + public ushort LastTerritory { get; set; } + public SyncState TerritorySyncState { get; set; } + public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; + public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; + + public bool IsInDeepDungeon() => + _clientState.IsLoggedIn + && _condition[ConditionFlag.InDeepDungeon] + && typeof(ETerritoryType).IsEnumDefined(_clientState.TerritoryType); + + } + + public enum PomanderState + { + Inactive, + Active, + FoundOnCurrentFloor, + PomanderOfSafetyUsed, + } +} diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs index dca3228..4250075 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -5,11 +5,17 @@ using Dalamud.Memory; using Dalamud.Utility.Signatures; using System; using System.Text; +using Dalamud.Game.ClientState.Objects; +using Pal.Client.DependencyInjection; namespace Pal.Client { - internal unsafe class Hooks + internal unsafe class Hooks : IDisposable { + private readonly ObjectTable _objectTable; + private readonly TerritoryState _territoryState; + private readonly FrameworkService _frameworkService; + #pragma warning disable CS0649 private delegate nint ActorVfxCreateDelegate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7); @@ -17,8 +23,12 @@ namespace Pal.Client private Hook ActorVfxCreateHook { get; init; } = null!; #pragma warning restore CS0649 - public Hooks() + public Hooks(ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) { + _objectTable = objectTable; + _territoryState = territoryState; + _frameworkService = frameworkService; + SignatureHelper.Initialise(this); ActorVfxCreateHook.Enable(); } @@ -55,10 +65,10 @@ namespace Pal.Client { try { - if (Service.Plugin.IsInDeepDungeon()) + if (_territoryState.IsInDeepDungeon()) { var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256); - var obj = Service.ObjectTable.CreateObjectReference(a2); + var obj = _objectTable.CreateObjectReference(a2); /* if (Service.Configuration.BetaKey == "VFX") @@ -69,7 +79,7 @@ namespace Pal.Client { if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") { - Service.Plugin.NextUpdateObjects.Enqueue(obj.Address); + _frameworkService.NextUpdateObjects.Enqueue(obj.Address); } } } @@ -83,7 +93,7 @@ namespace Pal.Client public void Dispose() { - ActorVfxCreateHook?.Dispose(); + ActorVfxCreateHook.Dispose(); } } } diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index 96dc32e..747709d 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -19,7 +19,7 @@ namespace Pal.Client.Net { private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) { - if (Service.Configuration.Mode != EMode.Online) + if (_configuration.Mode != EMode.Online) { PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); return (false, Localization.ConnectionError_NotOnline); @@ -47,7 +47,7 @@ namespace Pal.Client.Net cancellationToken.ThrowIfCancellationRequested(); var accountClient = new AccountService.AccountServiceClient(_channel); - IAccountConfiguration? configuredAccount = Service.Configuration.FindAccount(RemoteUrl); + IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); if (configuredAccount == null) { PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account"); @@ -57,17 +57,17 @@ namespace Pal.Client.Net if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) throw new InvalidOperationException("invalid account id returned"); - configuredAccount = Service.Configuration.CreateAccount(RemoteUrl, accountId); + configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); PluginLog.Information($"TryConnect: Account created with id {accountId.ToPartialId()}"); - Service.ConfigurationManager.Save(Service.Configuration); + _configurationManager.Save(_configuration); } else { PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}"); if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) { - Service.Chat.PalError(Localization.ConnectionError_OldVersion); + _chatGui.PalError(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error)); @@ -102,7 +102,7 @@ namespace Pal.Client.Net } if (save) - Service.ConfigurationManager.Save(Service.Configuration); + _configurationManager.Save(_configuration); } else { @@ -110,8 +110,8 @@ namespace Pal.Client.Net _loginInfo = new LoginInfo(null); if (loginReply.Error == LoginError.InvalidAccountId) { - Service.Configuration.RemoveAccount(RemoteUrl); - Service.ConfigurationManager.Save(Service.Configuration); + _configuration.RemoveAccount(RemoteUrl); + _configurationManager.Save(_configuration); if (retry) { PluginLog.Information("TryConnect: Attempting connection retry without account id"); @@ -122,7 +122,7 @@ namespace Pal.Client.Net } if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) { - Service.Chat.PalError(Localization.ConnectionError_OldVersion); + _chatGui.PalError(Localization.ConnectionError_OldVersion); _warnedAboutUpgrade = true; } return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error)); @@ -161,7 +161,7 @@ namespace Pal.Client.Net return Localization.ConnectionSuccessful; } - internal class LoginInfo + internal sealed class LoginInfo { public LoginInfo(string? authToken) { diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index cee5337..259b1ea 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/Pal.Client/Net/RemoteApi.Utils.cs b/Pal.Client/Net/RemoteApi.Utils.cs index 0ff7af8..045adb8 100644 --- a/Pal.Client/Net/RemoteApi.Utils.cs +++ b/Pal.Client/Net/RemoteApi.Utils.cs @@ -53,14 +53,5 @@ namespace Pal.Client.Net return null; #endif } - - public bool HasRoleOnCurrentServer(string role) - { - if (Service.Configuration.Mode != Configuration.EMode.Online) - return false; - - var account = Service.Configuration.FindAccount(RemoteUrl); - return account == null || account.CachedRoles.Contains(role); - } } } diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 1f7d6d9..9315941 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -2,28 +2,40 @@ using Grpc.Net.Client; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; -using Pal.Client.Extensions; +using Dalamud.Game.Gui; using Pal.Client.Configuration; namespace Pal.Client.Net { - internal partial class RemoteApi : IDisposable + internal sealed partial class RemoteApi : IDisposable { #if DEBUG public const string RemoteUrl = "http://localhost:5145"; #else public const string RemoteUrl = "https://pal.liza.sh"; #endif - private readonly string _userAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; + private readonly string _userAgent = + $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; - private readonly ILoggerFactory _grpcToPluginLogLoggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new GrpcLoggerProvider()).AddFilter("Grpc", LogLevel.Trace)); + private readonly ILoggerFactory _grpcToPluginLogLoggerFactory = LoggerFactory.Create(builder => + builder.AddProvider(new GrpcLoggerProvider()).AddFilter("Grpc", LogLevel.Trace)); + + private readonly ChatGui _chatGui; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; private GrpcChannel? _channel; private LoginInfo _loginInfo = new(null); private bool _warnedAboutUpgrade; + public RemoteApi(ChatGui chatGui, ConfigurationManager configurationManager, + IPalacePalConfiguration configuration) + { + _chatGui = chatGui; + _configurationManager = configurationManager; + _configuration = configuration; + } + public void Dispose() { PluginLog.Debug("Disposing gRPC channel"); diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 60f5098..8d1449b 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -1,709 +1,87 @@ -using Dalamud.Game; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Plugin; -using Grpc.Core; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Client.Windows; -using Pal.Common; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Numerics; using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading.Tasks; +using Dalamud.Logging; using Pal.Client.Extensions; using Pal.Client.Properties; using ECommons; -using ECommons.Schedulers; +using Microsoft.Extensions.DependencyInjection; using Pal.Client.Configuration; -using Pal.Client.Net; namespace Pal.Client { - public class Plugin : IDisposable + internal sealed class Plugin : IDisposable { - internal const uint ColorInvisible = 0; - private readonly IDalamudPlugin _dalamudPlugin; + private readonly IServiceProvider _serviceProvider; + private readonly DalamudPluginInterface _pluginInterface; + private readonly IPalacePalConfiguration _configuration; + private readonly RenderAdapter _renderAdapter; - private LocalizedChatMessages _localizedChatMessages = new(); - - internal ConcurrentDictionary FloorMarkers { get; } = new(); - internal ConcurrentBag EphemeralMarkers { get; set; } = new(); - internal ushort LastTerritory { get; set; } - internal SyncState TerritorySyncState { get; set; } - internal PomanderState PomanderOfSight { get; private set; } = PomanderState.Inactive; - internal PomanderState PomanderOfIntuition { get; private set; } = PomanderState.Inactive; - internal string? DebugMessage { get; set; } - internal Queue EarlyEventQueue { get; } = new(); - internal Queue LateEventQueue { get; } = new(); - internal ConcurrentQueue NextUpdateObjects { get; } = new(); - internal IRenderer Renderer { get; private set; } = null!; - - public Plugin(DalamudPluginInterface pluginInterface, ChatGui chat, IDalamudPlugin dalamudPlugin) + public Plugin( + IServiceProvider serviceProvider, + DalamudPluginInterface pluginInterface, + IPalacePalConfiguration configuration, + RenderAdapter renderAdapter) { - _dalamudPlugin = dalamudPlugin; + PluginLog.Information("Initializing Palace Pal"); + + _serviceProvider = serviceProvider; + _pluginInterface = pluginInterface; + _configuration = configuration; + _renderAdapter = renderAdapter; + + // initialize legacy services + pluginInterface.Create(); + Service.Configuration = configuration; LanguageChanged(pluginInterface.UiLanguage); - PluginLog.Information($"Install source: {pluginInterface.SourceRepository}"); - -#if RELEASE - // You're welcome to remove this code in your fork, as long as: - // - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and - // - you host your own server instance - if (!pluginInterface.IsDev - && !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/") - && !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/")) - { - chat.PalError(string.Format(Localization.Error_WrongRepository, "https://github.com/carvelli/Dalamud-Plugins")); - throw new InvalidOperationException(); - } -#endif - - pluginInterface.Create(); - Service.Plugin = this; - - Service.ConfigurationManager = new(pluginInterface); - Service.ConfigurationManager.Migrate(); - Service.Configuration = Service.ConfigurationManager.Load(); - - ResetRenderer(); - - Service.Hooks = new Hooks(); - - var agreementWindow = pluginInterface.Create(); - if (agreementWindow is not null) - { - agreementWindow.IsOpen = Service.Configuration.FirstUse; - Service.WindowSystem.AddWindow(agreementWindow); - } - - var configWindow = pluginInterface.Create(); - if (configWindow is not null) - { - Service.WindowSystem.AddWindow(configWindow); - } - - var statisticsWindow = pluginInterface.Create(); - if (statisticsWindow is not null) - { - Service.WindowSystem.AddWindow(statisticsWindow); - } - pluginInterface.UiBuilder.Draw += Draw; pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; pluginInterface.LanguageChanged += LanguageChanged; - Service.Framework.Update += OnFrameworkUpdate; - Service.Chat.ChatMessage += OnChatMessage; - Service.CommandManager.AddHandler("/pal", new CommandInfo(OnCommand) - { - HelpMessage = Localization.Command_pal_HelpText - }); - - ReloadLanguageStrings(); } private void OpenConfigUi() { - Window? configWindow; - if (Service.Configuration.FirstUse) - configWindow = Service.WindowSystem.GetWindow(); + Window configWindow; + if (_configuration.FirstUse) + configWindow = _serviceProvider.GetRequiredService(); else - configWindow = Service.WindowSystem.GetWindow(); + configWindow = _serviceProvider.GetRequiredService(); - if (configWindow != null) - configWindow.IsOpen = true; - } - - private void OnCommand(string command, string arguments) - { - if (Service.Configuration.FirstUse) - { - Service.Chat.PalError(Localization.Error_FirstTimeSetupRequired); - return; - } - - try - { - arguments = arguments.Trim(); - switch (arguments) - { - case "stats": - Task.Run(async () => await FetchFloorStatistics()); - break; - - case "test-connection": - case "tc": - var configWindow = Service.WindowSystem.GetWindow(); - if (configWindow == null) - return; - - configWindow.IsOpen = true; - var _ = new TickScheduler(() => configWindow.TestConnection()); - break; - -#if DEBUG - case "update-saves": - LocalState.UpdateAll(); - Service.Chat.Print(Localization.Command_pal_updatesaves); - break; -#endif - - case "": - case "config": - Service.WindowSystem.GetWindow()?.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: - Service.Chat.PalError(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, command)); - break; - } - } - catch (Exception e) - { - Service.Chat.PalError(e.ToString()); - } + configWindow.IsOpen = true; } #region IDisposable Support - protected virtual void Dispose(bool disposing) - { - if (!disposing) return; - - Service.CommandManager.RemoveHandler("/pal"); - Service.PluginInterface.UiBuilder.Draw -= Draw; - Service.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - Service.PluginInterface.LanguageChanged -= LanguageChanged; - Service.Framework.Update -= OnFrameworkUpdate; - Service.Chat.ChatMessage -= OnChatMessage; - - Service.WindowSystem.GetWindow()?.Dispose(); - Service.WindowSystem.RemoveAllWindows(); - - Service.RemoteApi.Dispose(); - Service.Hooks.Dispose(); - - if (Renderer is IDisposable disposable) - disposable.Dispose(); - } - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; } #endregion - private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, ref bool isHandled) + private void LanguageChanged(string languageCode) { - if (Service.Configuration.FirstUse) - return; - - if (type != (XivChatType)2105) - return; - - string message = seMessage.ToString(); - if (_localizedChatMessages.FloorChanged.IsMatch(message)) - { - PomanderOfSight = PomanderState.Inactive; - - if (PomanderOfIntuition == PomanderState.FoundOnCurrentFloor) - PomanderOfIntuition = PomanderState.Inactive; - } - else if (message.EndsWith(_localizedChatMessages.MapRevealed)) - { - PomanderOfSight = PomanderState.Active; - } - else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved)) - { - PomanderOfSight = PomanderState.PomanderOfSafetyUsed; - } - else if (message.EndsWith(_localizedChatMessages.HoardNotOnCurrentFloor) || message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor)) - { - // There is no functional difference between these - if you don't open the marked coffer, - // going to higher floors will keep the pomander active. - PomanderOfIntuition = PomanderState.Active; - } - else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened)) - { - PomanderOfIntuition = PomanderState.FoundOnCurrentFloor; - } - } - - private void LanguageChanged(string langcode) - { - Localization.Culture = new CultureInfo(langcode); - Service.WindowSystem.Windows.OfType().Each(w => w.LanguageChanged()); - } - - private void OnFrameworkUpdate(Framework framework) - { - if (Service.Configuration.FirstUse) - return; - - try - { - bool recreateLayout = false; - bool saveMarkers = false; - - while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - queued.Run(this, ref recreateLayout, ref saveMarkers); - - if (LastTerritory != Service.ClientState.TerritoryType) - { - LastTerritory = Service.ClientState.TerritoryType; - TerritorySyncState = SyncState.NotAttempted; - NextUpdateObjects.Clear(); - - if (IsInDeepDungeon()) - GetFloorMarkers(LastTerritory); - EphemeralMarkers.Clear(); - PomanderOfSight = PomanderState.Inactive; - PomanderOfIntuition = PomanderState.Inactive; - recreateLayout = true; - DebugMessage = null; - } - - if (!IsInDeepDungeon()) - return; - - if (Service.Configuration.Mode == Configuration.EMode.Online && TerritorySyncState == SyncState.NotAttempted) - { - TerritorySyncState = SyncState.Started; - Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory)); - } - - while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - queued.Run(this, ref recreateLayout, ref saveMarkers); - - var currentFloor = GetFloorMarkers(LastTerritory); - - IList visibleMarkers = GetRelevantGameObjects(); - HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout); - HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - - internal LocalState GetFloorMarkers(ushort territoryType) - { - return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt)); - } - - #region Rendering markers - private void HandlePersistentMarkers(LocalState currentFloor, IList visibleMarkers, bool saveMarkers, bool recreateLayout) - { - var config = Service.Configuration; - var currentFloorMarkers = currentFloor.Markers; - - bool updateSeenMarkers = false; - var partialAccountId = Service.Configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - foreach (var visibleMarker in visibleMarkers) - { - Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); - if (knownMarker != null) - { - if (!knownMarker.Seen) - { - knownMarker.Seen = true; - saveMarkers = true; - } - - // This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, - // markers discovered afterwards are automatically marked seen. - if (partialAccountId != null && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && !knownMarker.RemoteSeenOn.Contains(partialAccountId)) - updateSeenMarkers = true; - - continue; - } - - currentFloorMarkers.Add(visibleMarker); - recreateLayout = true; - saveMarkers = true; - } - - if (!recreateLayout && currentFloorMarkers.Count > 0 && (config.DeepDungeons.Traps.OnlyVisibleAfterPomander || config.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) - { - - try - { - foreach (var marker in currentFloorMarkers) - { - uint desiredColor = DetermineColor(marker, visibleMarkers); - if (marker.RenderElement == null || !marker.RenderElement.IsValid) - { - recreateLayout = true; - break; - } - - if (marker.RenderElement.Color != desiredColor) - marker.RenderElement.Color = desiredColor; - } - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - recreateLayout = true; - } - } - - if (updateSeenMarkers && partialAccountId != null) - { - var markersToUpdate = currentFloorMarkers.Where(x => x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && !x.RemoteSeenOn.Contains(partialAccountId)).ToList(); - foreach (var marker in markersToUpdate) - marker.RemoteSeenRequested = true; - Task.Run(async () => await SyncSeenMarkersForTerritory(LastTerritory, markersToUpdate)); - } - - if (saveMarkers) - { - currentFloor.Save(); - - if (TerritorySyncState == SyncState.Complete) - { - var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); - if (markersToUpload.Count > 0) - { - foreach (var marker in markersToUpload) - marker.UploadRequested = true; - Task.Run(async () => await UploadMarkersForTerritory(LastTerritory, markersToUpload)); - } - } - } - - if (recreateLayout) - { - Renderer.ResetLayer(ELayer.TrapHoard); - - List elements = new(); - foreach (var marker in currentFloorMarkers) - { - if (marker.Seen || config.Mode == EMode.Online || marker is { WasImported: true, Imports.Count: > 0 }) - { - if (marker.Type == Marker.EType.Trap) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.Traps); - } - else if (marker.Type == Marker.EType.Hoard) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.HoardCoffers); - } - } - } - - if (elements.Count == 0) - return; - - Renderer.SetLayer(ELayer.TrapHoard, elements); - } - } - - private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) - { - recreateLayout |= EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker)); - recreateLayout |= visibleMarkers.Any(visibleMarker => EphemeralMarkers.All(x => x != visibleMarker)); - - if (recreateLayout) - { - Renderer.ResetLayer(ELayer.RegularCoffers); - EphemeralMarkers.Clear(); - - var config = Service.Configuration; - - List elements = new(); - foreach (var marker in visibleMarkers) - { - EphemeralMarkers.Add(marker); - - if (marker.Type == Marker.EType.SilverCoffer && config.DeepDungeons.SilverCoffers.Show) - { - CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.DeepDungeons.SilverCoffers); - } - } - - if (elements.Count == 0) - return; - - Renderer.SetLayer(ELayer.RegularCoffers, elements); - } - } - - private uint DetermineColor(Marker marker, IList visibleMarkers) - { - switch (marker.Type) - { - case Marker.EType.Trap when PomanderOfSight == PomanderState.Inactive || !Service.Configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): - return Service.Configuration.DeepDungeons.Traps.Color; - case Marker.EType.Hoard when PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || visibleMarkers.Any(x => x == marker): - return Service.Configuration.DeepDungeons.HoardCoffers.Color; - case Marker.EType.SilverCoffer: - return Service.Configuration.DeepDungeons.SilverCoffers.Color; - case Marker.EType.Trap: - case Marker.EType.Hoard: - return ColorInvisible; - default: - return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); - } - } - - private void CreateRenderElement(Marker marker, List elements, uint color, MarkerConfiguration config) - { - if (!config.Show) - return; - - var element = Renderer.CreateElement(marker.Type, marker.Position, color, config.Fill); - marker.RenderElement = element; - elements.Add(element); - } - #endregion - - #region Up-/Download - private async Task DownloadMarkersForTerritory(ushort territoryId) - { - try - { - var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.Download, - TerritoryType = territoryId, - Success = success, - Markers = downloadedMarkers - }); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - - private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) - { - try - { - var (success, uploadedMarkers) = await Service.RemoteApi.UploadMarker(territoryId, markersToUpload); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.Upload, - TerritoryType = territoryId, - Success = success, - Markers = uploadedMarkers - }); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - - private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) - { - try - { - var success = await Service.RemoteApi.MarkAsSeen(territoryId, markersToUpdate); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.MarkSeen, - TerritoryType = territoryId, - Success = success, - Markers = markersToUpdate, - }); - } - catch (Exception e) - { - DebugMessage = $"{DateTime.Now}\n{e}"; - } - } - #endregion - - #region Command Handling - private async Task FetchFloorStatistics() - { - if (!Service.RemoteApi.HasRoleOnCurrentServer("statistics:view")) - { - Service.Chat.PalError(Localization.Command_pal_stats_CurrentFloor); - return; - } - - try - { - var (success, floorStatistics) = await Service.RemoteApi.FetchStatistics(); - if (success) - { - var statisticsWindow = Service.WindowSystem.GetWindow()!; - statisticsWindow.SetFloorData(floorStatistics); - statisticsWindow.IsOpen = true; - } - else - { - Service.Chat.PalError(Localization.Command_pal_stats_UnableToFetchStatistics); - } - } - catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) - { - Service.Chat.Print(Localization.Command_pal_stats_CurrentFloor); - } - catch (Exception e) - { - Service.Chat.PalError(e.ToString()); - } - } - - private void DebugNearest(Predicate predicate) - { - if (!IsInDeepDungeon()) - return; - - var state = GetFloorMarkers(Service.ClientState.TerritoryType); - var playerPosition = Service.ClientState.LocalPlayer?.Position; - if (playerPosition == null) - return; - Service.Chat.Print($"[Palace Pal] {playerPosition}"); - - var nearbyMarkers = state.Markers - .Where(m => predicate(m)) - .Where(m => m.RenderElement != null && m.RenderElement.Color != ColorInvisible) - .Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue }) - .OrderBy(m => m.distance) - .Take(5) - .ToList(); - foreach (var nearbyMarker in nearbyMarkers) - Service.Chat.Print($"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); - } - #endregion - - private IList GetRelevantGameObjects() - { - List result = new(); - for (int i = 246; i < Service.ObjectTable.Length; i++) - { - GameObject? obj = Service.ObjectTable[i]; - if (obj == null) - continue; - - switch ((uint)Marshal.ReadInt32(obj.Address + 128)) - { - case 2007182: - case 2007183: - case 2007184: - case 2007185: - case 2007186: - case 2009504: - result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); - break; - - case 2007542: - case 2007543: - result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true }); - break; - - case 2007357: - result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true }); - break; - } - } - - while (NextUpdateObjects.TryDequeue(out nint address)) - { - var obj = Service.ObjectTable.FirstOrDefault(x => x.Address == address); - if (obj != null && obj.Position.Length() > 0.1) - result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true }); - } - - return result; - } - - internal bool IsInDeepDungeon() => - Service.ClientState.IsLoggedIn - && Service.Condition[ConditionFlag.InDeepDungeon] - && typeof(ETerritoryType).IsEnumDefined(Service.ClientState.TerritoryType); - - private void ReloadLanguageStrings() - { - _localizedChatMessages = new LocalizedChatMessages - { - MapRevealed = GetLocalizedString(7256), - AllTrapsRemoved = GetLocalizedString(7255), - HoardOnCurrentFloor = GetLocalizedString(7272), - HoardNotOnCurrentFloor = GetLocalizedString(7273), - HoardCofferOpened = GetLocalizedString(7274), - FloorChanged = new Regex("^" + GetLocalizedString(7270).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") + "$"), - }; - } - - internal void ResetRenderer() - { - if (Renderer is SplatoonRenderer && Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) - return; - else if (Renderer is SimpleRenderer && Service.Configuration.Renderer.SelectedRenderer == ERenderer.Simple) - return; - - if (Renderer is IDisposable disposable) - disposable.Dispose(); - - if (Service.Configuration.Renderer.SelectedRenderer == ERenderer.Splatoon) - Renderer = new SplatoonRenderer(Service.PluginInterface, _dalamudPlugin); - else - Renderer = new SimpleRenderer(); + Localization.Culture = new CultureInfo(languageCode); + _serviceProvider.GetRequiredService().Windows.OfType().Each(w => w.LanguageChanged()); } private void Draw() { - if (Renderer is SimpleRenderer sr) + if (_renderAdapter.Implementation is SimpleRenderer sr) sr.DrawLayers(); - Service.WindowSystem.Draw(); - } - - private string GetLocalizedString(uint id) - { - return Service.DataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; - } - - public enum PomanderState - { - Inactive, - Active, - FoundOnCurrentFloor, - PomanderOfSafetyUsed, - } - - private class LocalizedChatMessages - { - public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!"; - public string AllTrapsRemoved { get; init; } = "???"; // "All the traps on this floor have disappeared!"; - public string HoardOnCurrentFloor { get; init; } = "???"; // "You sense the Accursed Hoard calling you..."; - public string HoardNotOnCurrentFloor { get; init; } = "???"; // "You do not sense the call of the Accursed Hoard on this floor..."; - public string HoardCofferOpened { get; init; } = "???"; // "You discover a piece of the Accursed Hoard!"; - public Regex FloorChanged { get; init; } = new(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$"); + _serviceProvider.GetRequiredService().Draw(); } } } diff --git a/Pal.Client/Rendering/MarkerConfig.cs b/Pal.Client/Rendering/MarkerConfig.cs index 58d3642..2ef9dde 100644 --- a/Pal.Client/Rendering/MarkerConfig.cs +++ b/Pal.Client/Rendering/MarkerConfig.cs @@ -2,7 +2,7 @@ namespace Pal.Client.Rendering { - internal class MarkerConfig + internal sealed class MarkerConfig { private static readonly MarkerConfig EmptyConfig = new(); private static readonly Dictionary MarkerConfigs = new() @@ -12,8 +12,8 @@ namespace Pal.Client.Rendering { Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, }; - public float OffsetY { get; set; } - public float Radius { get; set; } = 0.25f; + public float OffsetY { get; private init; } + public float Radius { get; private init; } = 0.25f; public static MarkerConfig ForType(Marker.EType type) => MarkerConfigs.GetValueOrDefault(type, EmptyConfig); } diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs new file mode 100644 index 0000000..da8a28f --- /dev/null +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Numerics; +using Pal.Client.Configuration; + +namespace Pal.Client.Rendering +{ + internal sealed class RenderAdapter : IRenderer + { + private readonly SimpleRenderer _simpleRenderer; + private readonly SplatoonRenderer _splatoonRenderer; + private readonly IPalacePalConfiguration _configuration; + + public RenderAdapter(SimpleRenderer simpleRenderer, SplatoonRenderer splatoonRenderer, IPalacePalConfiguration configuration) + { + _simpleRenderer = simpleRenderer; + _splatoonRenderer = splatoonRenderer; + _configuration = configuration; + } + + public IRenderer Implementation => _configuration.Renderer.SelectedRenderer == ERenderer.Splatoon + ? _splatoonRenderer + : _simpleRenderer; + + public void SetLayer(ELayer layer, IReadOnlyList elements) + => Implementation.SetLayer(layer, elements); + + public void ResetLayer(ELayer layer) + => Implementation.ResetLayer(layer); + + public IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false) + => Implementation.CreateElement(type, pos, color, fill); + } +} diff --git a/Pal.Client/Rendering/RenderData.cs b/Pal.Client/Rendering/RenderData.cs new file mode 100644 index 0000000..2c4b802 --- /dev/null +++ b/Pal.Client/Rendering/RenderData.cs @@ -0,0 +1,7 @@ +namespace Pal.Client.Rendering +{ + internal static class RenderData + { + public static readonly uint ColorInvisible = 0; + } +} diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index 58ed62c..675426a 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -1,15 +1,14 @@ -using Dalamud.Game.Gui; -using Dalamud.Interface; -using Dalamud.Plugin; -using ECommons.ExcelServices.TerritoryEnumeration; +using Dalamud.Interface; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Xml.Linq; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client.Rendering { @@ -20,15 +19,30 @@ namespace Pal.Client.Rendering /// remade into PalacePal (which is the third or fourth iteration on the same idea /// I made, just with a clear vision). /// - internal class SimpleRenderer : IRenderer, IDisposable + internal sealed class SimpleRenderer : IRenderer, IDisposable { + private const int SegmentCount = 20; + + private readonly ClientState _clientState; + private readonly GameGui _gameGui; + private readonly IPalacePalConfiguration _configuration; + private readonly TerritoryState _territoryState; private readonly ConcurrentDictionary _layers = new(); + public SimpleRenderer(ClientState clientState, GameGui gameGui, IPalacePalConfiguration configuration, + TerritoryState territoryState) + { + _clientState = clientState; + _gameGui = gameGui; + _configuration = configuration; + _territoryState = territoryState; + } + public void SetLayer(ELayer layer, IReadOnlyList elements) { _layers[layer] = new SimpleLayer { - TerritoryType = Service.ClientState.TerritoryType, + TerritoryType = _clientState.TerritoryType, Elements = elements.Cast().ToList() }; } @@ -61,38 +75,88 @@ namespace Pal.Client.Rendering ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero, ImGuiCond.None, Vector2.Zero); ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); - if (ImGui.Begin("###PalacePalSimpleRender", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.AlwaysUseWindowPadding)) + if (ImGui.Begin("###PalacePalSimpleRender", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.AlwaysUseWindowPadding)) { - ushort territoryType = Service.ClientState.TerritoryType; + ushort territoryType = _clientState.TerritoryType; foreach (var layer in _layers.Values.Where(l => l.TerritoryType == territoryType)) - layer.Draw(); + { + foreach (var e in layer.Elements) + Draw(e); + } - foreach (var key in _layers.Where(l => l.Value.TerritoryType != territoryType).Select(l => l.Key).ToList()) + foreach (var key in _layers.Where(l => l.Value.TerritoryType != territoryType).Select(l => l.Key) + .ToList()) ResetLayer(key); ImGui.End(); } + ImGui.PopStyleVar(); } + private void Draw(SimpleElement e) + { + if (e.Color == RenderData.ColorInvisible) + return; + + switch (e.Type) + { + case Marker.EType.Hoard: + // ignore distance if this is a found hoard coffer + if (_territoryState.PomanderOfIntuition == PomanderState.Active && + _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) + break; + + goto case Marker.EType.Trap; + + case Marker.EType.Trap: + var playerPos = _clientState.LocalPlayer?.Position; + if (playerPos == null) + return; + + if ((playerPos.Value - e.Position).Length() > 65) + return; + break; + } + + bool onScreen = false; + for (int index = 0; index < 2 * SegmentCount; ++index) + { + onScreen |= _gameGui.WorldToScreen(new Vector3( + e.Position.X + e.Radius * (float)Math.Sin(Math.PI / SegmentCount * index), + e.Position.Y, + e.Position.Z + e.Radius * (float)Math.Cos(Math.PI / SegmentCount * index)), + out Vector2 vector2); + + ImGui.GetWindowDrawList().PathLineTo(vector2); + } + + if (onScreen) + { + if (e.Fill) + ImGui.GetWindowDrawList().PathFillConvex(e.Color); + else + ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2); + } + else + ImGui.GetWindowDrawList().PathClear(); + } + public void Dispose() { foreach (var l in _layers.Values) l.Dispose(); } - public class SimpleLayer : IDisposable + public sealed class SimpleLayer : IDisposable { public required ushort TerritoryType { get; init; } public required IReadOnlyList Elements { get; init; } - public void Draw() - { - foreach (var element in Elements) - element.Draw(); - } - public void Dispose() { foreach (var e in Elements) @@ -100,63 +164,14 @@ namespace Pal.Client.Rendering } } - public class SimpleElement : IRenderElement + public sealed class SimpleElement : IRenderElement { - private const int SegmentCount = 20; - public bool IsValid { get; set; } = true; public required Marker.EType Type { get; init; } public required Vector3 Position { get; init; } public required uint Color { get; set; } public required float Radius { get; init; } public required bool Fill { get; init; } - - public void Draw() - { - if (Color == Plugin.ColorInvisible) - return; - - switch (Type) - { - case Marker.EType.Hoard: - // ignore distance if this is a found hoard coffer - if (Service.Plugin.PomanderOfIntuition == Plugin.PomanderState.Active && Service.Configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) - break; - - goto case Marker.EType.Trap; - - case Marker.EType.Trap: - var playerPos = Service.ClientState.LocalPlayer?.Position; - if (playerPos == null) - return; - - if ((playerPos.Value - Position).Length() > 65) - return; - break; - } - - bool onScreen = false; - for (int index = 0; index < 2 * SegmentCount; ++index) - { - onScreen |= Service.GameGui.WorldToScreen(new Vector3( - Position.X + Radius * (float)Math.Sin(Math.PI / SegmentCount * index), - Position.Y, - Position.Z + Radius * (float)Math.Cos(Math.PI / SegmentCount * index)), - out Vector2 vector2); - - ImGui.GetWindowDrawList().PathLineTo(vector2); - } - - if (onScreen) - { - if (Fill) - ImGui.GetWindowDrawList().PathFillConvex(Color); - else - ImGui.GetWindowDrawList().PathStroke(Color, ImDrawFlags.Closed, 2); - } - else - ImGui.GetWindowDrawList().PathClear(); - } } } } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 2c07578..2fc5c02 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -11,21 +11,33 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Reflection; -using System.Text; -using System.Threading.Tasks; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using Pal.Client.DependencyInjection; namespace Pal.Client.Rendering { - internal class SplatoonRenderer : IRenderer, IDrawDebugItems, IDisposable + internal sealed class SplatoonRenderer : IRenderer, IDrawDebugItems, IDisposable { private const long OnTerritoryChange = -2; - private bool IsDisposed { get; set; } - public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin plugin) + private readonly DebugState _debugState; + private readonly ClientState _clientState; + private readonly ChatGui _chatGui; + + public SplatoonRenderer(DalamudPluginInterface pluginInterface, IDalamudPlugin dalamudPlugin, DebugState debugState, + ClientState clientState, ChatGui chatGui) { - ECommonsMain.Init(pluginInterface, plugin, ECommons.Module.SplatoonAPI); + _debugState = debugState; + _clientState = clientState; + _chatGui = chatGui; + + PluginLog.Information("Initializing splatoon..."); + ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); } + private bool IsDisposed { get; set; } + public void SetLayer(ELayer layer, IReadOnlyList elements) { // we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout @@ -33,12 +45,14 @@ namespace Pal.Client.Rendering { try { - Splatoon.AddDynamicElements(ToLayerName(layer), elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange }); + Splatoon.AddDynamicElements(ToLayerName(layer), + elements.Cast().Select(x => x.Delegate).ToArray(), + new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange }); } catch (Exception e) { PluginLog.Error(e, $"Could not create splatoon layer {layer} with {elements.Count} elements"); - Service.Plugin.DebugMessage = $"{DateTime.Now}\n{e}"; + _debugState.SetFromException(e); } }); } @@ -82,7 +96,7 @@ namespace Pal.Client.Rendering { try { - Vector3? pos = Service.ClientState.LocalPlayer?.Position; + Vector3? pos = _clientState.LocalPlayer?.Position; if (pos != null) { var elements = new List @@ -91,9 +105,11 @@ namespace Pal.Client.Rendering CreateElement(Marker.EType.Hoard, pos.Value, ImGui.ColorConvertFloat4ToU32(hoardColor)), }; - if (!Splatoon.AddDynamicElements("PalacePal.Test", elements.Cast().Select(x => x.Delegate).ToArray(), new[] { Environment.TickCount64 + 10000 })) + if (!Splatoon.AddDynamicElements("PalacePal.Test", + elements.Cast().Select(x => x.Delegate).ToArray(), + new[] { Environment.TickCount64 + 10000 })) { - Service.Chat.PrintError("Could not draw markers :("); + _chatGui.PrintError("Could not draw markers :("); } } } @@ -102,23 +118,31 @@ namespace Pal.Client.Rendering try { var pluginManager = DalamudReflector.GetPluginManager(); - IList installedPlugins = pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ?? new List(); + IList installedPlugins = + pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ?? + new List(); foreach (var t in installedPlugins) { - AssemblyName? assemblyName = (AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t); + AssemblyName? assemblyName = + (AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t); string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon") { - Service.Chat.PrintError($"[Palace Pal] Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); - Service.Chat.Print("[Palace Pal] You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); + _chatGui.PrintError( + $"[Palace Pal] Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); + _chatGui.Print( + "[Palace Pal] You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); return; } } } - catch (Exception) { } + catch (Exception) + { + // not relevant + } - Service.Chat.PrintError("Could not draw markers, is Splatoon installed and enabled?"); + _chatGui.PrintError("Could not draw markers, is Splatoon installed and enabled?"); } } @@ -132,7 +156,7 @@ namespace Pal.Client.Rendering ECommonsMain.Dispose(); } - public class SplatoonElement : IRenderElement + private sealed class SplatoonElement : IRenderElement { private readonly SplatoonRenderer _renderer; @@ -145,6 +169,7 @@ namespace Pal.Client.Rendering public Element Delegate { get; } public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid(); + public uint Color { get => Delegate.color; diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index bf02b9d..d04446f 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Pal.Client.Scheduled +namespace Pal.Client.Scheduled { internal interface IQueueOnFrameworkThread { - void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers); } } diff --git a/Pal.Client/Scheduled/QueueHandler.cs b/Pal.Client/Scheduled/QueueHandler.cs new file mode 100644 index 0000000..8ec67ef --- /dev/null +++ b/Pal.Client/Scheduled/QueueHandler.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.Gui; +using Dalamud.Logging; +using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; +using Pal.Client.Extensions; +using Pal.Client.Net; +using Pal.Client.Properties; +using Pal.Common; + +namespace Pal.Client.Scheduled +{ + // TODO The idea was to split this from the queue objects, should be in individual classes tho + internal sealed class QueueHandler + { + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private readonly FloorService _floorService; + private readonly TerritoryState _territoryState; + private readonly DebugState _debugState; + private readonly ChatGui _chatGui; + + public QueueHandler( + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + FloorService floorService, + TerritoryState territoryState, + DebugState debugState, + ChatGui chatGui) + { + _configurationManager = configurationManager; + _configuration = configuration; + _floorService = floorService; + _territoryState = territoryState; + _debugState = debugState; + _chatGui = chatGui; + } + + public void Handle(IQueueOnFrameworkThread queued, ref bool recreateLayout, ref bool saveMarkers) + { + if (queued is QueuedConfigUpdate) + { + ConfigUpdate(ref recreateLayout, ref saveMarkers); + } + else if (queued is QueuedSyncResponse queuedSyncResponse) + { + SyncResponse(queuedSyncResponse); + recreateLayout = true; + saveMarkers = true; + } + else if (queued is QueuedImport queuedImport) + { + Import(queuedImport); + recreateLayout = true; + saveMarkers = true; + } + else if (queued is QueuedUndoImport queuedUndoImport) + { + UndoImport(queuedUndoImport); + recreateLayout = true; + saveMarkers = true; + } + else + throw new InvalidOperationException(); + } + + private void ConfigUpdate(ref bool recreateLayout, ref bool saveMarkers) + { + if (_configuration.Mode == EMode.Offline) + { + LocalState.UpdateAll(); + _floorService.FloorMarkers.Clear(); + _floorService.EphemeralMarkers.Clear(); + _territoryState.LastTerritory = 0; + + recreateLayout = true; + saveMarkers = true; + } + } + + private void SyncResponse(QueuedSyncResponse queued) + { + try + { + var remoteMarkers = queued.Markers; + var currentFloor = _floorService.GetFloorMarkers(queued.TerritoryType); + if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) + { + switch (queued.Type) + { + case SyncType.Download: + case SyncType.Upload: + foreach (var remoteMarker in remoteMarkers) + { + // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + { + localMarker.NetworkId = remoteMarker.NetworkId; + continue; + } + + if (queued.Type == SyncType.Download) + currentFloor.Markers.Add(remoteMarker); + } + + break; + + case SyncType.MarkSeen: + var partialAccountId = + _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + if (partialAccountId == null) + break; + foreach (var remoteMarker in remoteMarkers) + { + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + localMarker.RemoteSeenOn.Add(partialAccountId); + } + + break; + } + } + + // don't modify state for outdated floors + if (_territoryState.LastTerritory != queued.TerritoryType) + return; + + if (queued.Type == SyncType.Download) + { + if (queued.Success) + _territoryState.TerritorySyncState = SyncState.Complete; + else + _territoryState.TerritorySyncState = SyncState.Failed; + } + } + catch (Exception e) + { + _debugState.SetFromException(e); + if (queued.Type == SyncType.Download) + _territoryState.TerritorySyncState = SyncState.Failed; + } + } + + private void Import(QueuedImport queued) + { + try + { + if (!queued.Validate(_chatGui)) + return; + + var oldExportIds = string.IsNullOrEmpty(queued.Export.ServerUrl) + ? _configuration.ImportHistory.Where(x => x.RemoteUrl == queued.Export.ServerUrl).Select(x => x.Id) + .Where(x => x != Guid.Empty).ToList() + : new List(); + + foreach (var remoteFloor in queued.Export.Floors) + { + ushort territoryType = (ushort)remoteFloor.TerritoryType; + var localState = _floorService.GetFloorMarkers(territoryType); + + localState.UndoImport(oldExportIds); + queued.ImportFloor(remoteFloor, localState); + + localState.Save(); + } + + _configuration.ImportHistory.RemoveAll(hist => + oldExportIds.Contains(hist.Id) || hist.Id == queued.ExportId); + _configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry + { + Id = queued.ExportId, + RemoteUrl = queued.Export.ServerUrl, + ExportedAt = queued.Export.CreatedAt.ToDateTime(), + ImportedAt = DateTime.UtcNow, + }); + _configurationManager.Save(_configuration); + + _chatGui.Print(string.Format(Localization.ImportCompleteStatistics, queued.ImportedTraps, + queued.ImportedHoardCoffers)); + } + catch (Exception e) + { + PluginLog.Error(e, "Import failed"); + _chatGui.PalError(string.Format(Localization.Error_ImportFailed, e)); + } + } + + private void UndoImport(QueuedUndoImport queued) + { + foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) + { + var localState = _floorService.GetFloorMarkers((ushort)territoryType); + localState.UndoImport(new List { queued.ExportId }); + localState.Save(); + } + + _configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId); + } + } +} diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index d816ec1..0536362 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -1,21 +1,6 @@ namespace Pal.Client.Scheduled { - internal class QueuedConfigUpdate : IQueueOnFrameworkThread + internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread { - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) - { - if (Service.Configuration.Mode == Configuration.EMode.Offline) - { - LocalState.UpdateAll(); - plugin.FloorMarkers.Clear(); - plugin.EphemeralMarkers.Clear(); - plugin.LastTerritory = 0; - - recreateLayout = true; - saveMarkers = true; - } - - plugin.ResetRenderer(); - } } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 45623d8..4a74c79 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -1,98 +1,54 @@ using Account; -using Dalamud.Logging; using Pal.Common; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; -using Pal.Client.Extensions; +using Dalamud.Game.Gui; using Pal.Client.Properties; -using Pal.Client.Configuration; namespace Pal.Client.Scheduled { - internal class QueuedImport : IQueueOnFrameworkThread + internal sealed class QueuedImport : IQueueOnFrameworkThread { - private readonly ExportRoot _export; - private Guid _exportId; - private int _importedTraps; - private int _importedHoardCoffers; + public ExportRoot Export { get; } + public Guid ExportId { get; private set; } + public int ImportedTraps { get; private set; } + public int ImportedHoardCoffers { get; private set; } public QueuedImport(string sourcePath) { using var input = File.OpenRead(sourcePath); - _export = ExportRoot.Parser.ParseFrom(input); + Export = ExportRoot.Parser.ParseFrom(input); } - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) + public bool Validate(ChatGui chatGui) { - try + if (Export.ExportVersion != ExportConfig.ExportVersion) { - if (!Validate()) - return; - - var config = Service.Configuration; - var oldExportIds = string.IsNullOrEmpty(_export.ServerUrl) ? config.ImportHistory.Where(x => x.RemoteUrl == _export.ServerUrl).Select(x => x.Id).Where(x => x != Guid.Empty).ToList() : new List(); - - foreach (var remoteFloor in _export.Floors) - { - ushort territoryType = (ushort)remoteFloor.TerritoryType; - var localState = plugin.GetFloorMarkers(territoryType); - - localState.UndoImport(oldExportIds); - ImportFloor(remoteFloor, localState); - - localState.Save(); - } - - config.ImportHistory.RemoveAll(hist => oldExportIds.Contains(hist.Id) || hist.Id == _exportId); - config.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry - { - Id = _exportId, - RemoteUrl = _export.ServerUrl, - ExportedAt = _export.CreatedAt.ToDateTime(), - ImportedAt = DateTime.UtcNow, - }); - Service.ConfigurationManager.Save(config); - - recreateLayout = true; - saveMarkers = true; - - Service.Chat.Print(string.Format(Localization.ImportCompleteStatistics, _importedTraps, _importedHoardCoffers)); - } - catch (Exception e) - { - PluginLog.Error(e, "Import failed"); - Service.Chat.PalError(string.Format(Localization.Error_ImportFailed, e)); - } - } - - private bool Validate() - { - if (_export.ExportVersion != ExportConfig.ExportVersion) - { - Service.Chat.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); + chatGui.PrintError(Localization.Error_ImportFailed_IncompatibleVersion); return false; } - if (!Guid.TryParse(_export.ExportId, out _exportId) || _exportId == Guid.Empty) + if (!Guid.TryParse(Export.ExportId, out Guid exportId) || ExportId == Guid.Empty) { - Service.Chat.PrintError(Localization.Error_ImportFailed_InvalidFile); + chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); return false; } - if (string.IsNullOrEmpty(_export.ServerUrl)) + ExportId = exportId; + + if (string.IsNullOrEmpty(Export.ServerUrl)) { // If we allow for backups as import/export, this should be removed - Service.Chat.PrintError(Localization.Error_ImportFailed_InvalidFile); + chatGui.PrintError(Localization.Error_ImportFailed_InvalidFile); return false; } return true; } - private void ImportFloor(ExportFloor remoteFloor, LocalState localState) + public void ImportFloor(ExportFloor remoteFloor, LocalState localState) { var remoteMarkers = remoteFloor.Objects.Select(m => new Marker((Marker.EType)m.Type, new Vector3(m.X, m.Y, m.Z)) { WasImported = true }); foreach (var remoteMarker in remoteMarkers) @@ -104,12 +60,12 @@ namespace Pal.Client.Scheduled localMarker = remoteMarker; if (localMarker.Type == Marker.EType.Trap) - _importedTraps++; + ImportedTraps++; else if (localMarker.Type == Marker.EType.Hoard) - _importedHoardCoffers++; + ImportedHoardCoffers++; } - remoteMarker.Imports.Add(_exportId); + remoteMarker.Imports.Add(ExportId); } } } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index 44a88a6..db94177 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -1,84 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Pal.Client.Extensions; -using Pal.Client.Net; -using static Pal.Client.Plugin; +using System.Collections.Generic; namespace Pal.Client.Scheduled { - internal class QueuedSyncResponse : IQueueOnFrameworkThread + internal sealed class QueuedSyncResponse : IQueueOnFrameworkThread { public required SyncType Type { get; init; } public required ushort TerritoryType { get; init; } public required bool Success { get; init; } public required List Markers { get; init; } - - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) - { - recreateLayout = true; - saveMarkers = true; - - try - { - var remoteMarkers = Markers; - var currentFloor = plugin.GetFloorMarkers(TerritoryType); - if (Service.Configuration.Mode == Configuration.EMode.Online && Success && remoteMarkers.Count > 0) - { - switch (Type) - { - case SyncType.Download: - case SyncType.Upload: - foreach (var remoteMarker in remoteMarkers) - { - // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - { - localMarker.NetworkId = remoteMarker.NetworkId; - continue; - } - - if (Type == SyncType.Download) - currentFloor.Markers.Add(remoteMarker); - } - break; - - case SyncType.MarkSeen: - var partialAccountId = Service.Configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - if (partialAccountId == null) - break; - foreach (var remoteMarker in remoteMarkers) - { - Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); - if (localMarker != null) - localMarker.RemoteSeenOn.Add(partialAccountId); - } - break; - } - } - - // don't modify state for outdated floors - if (plugin.LastTerritory != TerritoryType) - return; - - if (Type == SyncType.Download) - { - if (Success) - plugin.TerritorySyncState = SyncState.Complete; - else - plugin.TerritorySyncState = SyncState.Failed; - } - } - catch (Exception e) - { - plugin.DebugMessage = $"{DateTime.Now}\n{e}"; - if (Type == SyncType.Download) - plugin.TerritorySyncState = SyncState.Failed; - } - } } public enum SyncState diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index 1c0163e..961532a 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -1,35 +1,14 @@ -using ECommons.Configuration; -using Pal.Common; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; namespace Pal.Client.Scheduled { - internal class QueuedUndoImport : IQueueOnFrameworkThread + internal sealed class QueuedUndoImport : IQueueOnFrameworkThread { - private readonly Guid _exportId; - public QueuedUndoImport(Guid exportId) { - _exportId = exportId; + ExportId = exportId; } - public void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers) - { - recreateLayout = true; - saveMarkers = true; - - foreach (ETerritoryType territoryType in typeof(ETerritoryType).GetEnumValues()) - { - var localState = plugin.GetFloorMarkers((ushort)territoryType); - localState.UndoImport(new List { _exportId }); - localState.Save(); - } - - Service.Configuration.ImportHistory.RemoveAll(hist => hist.Id == _exportId); - } + public Guid ExportId { get; } } } diff --git a/Pal.Client/Service.cs b/Pal.Client/Service.cs index 4b54075..b5d0dc5 100644 --- a/Pal.Client/Service.cs +++ b/Pal.Client/Service.cs @@ -1,35 +1,15 @@ -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 System; using Dalamud.IoC; using Dalamud.Plugin; using Pal.Client.Configuration; -using Pal.Client.Net; namespace Pal.Client { + [Obsolete] public class Service { [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService] public static ClientState ClientState { get; set; } = null!; - [PluginService] public static ChatGui Chat { get; private set; } = null!; - [PluginService] public static ObjectTable ObjectTable { get; private set; } = null!; - [PluginService] public static Framework Framework { get; set; } = null!; - [PluginService] public static Condition Condition { get; set; } = null!; - [PluginService] public static CommandManager CommandManager { get; set; } = null!; - [PluginService] public static DataManager DataManager { get; set; } = null!; - [PluginService] public static GameGui GameGui { get; set; } = null!; - internal static Plugin Plugin { get; set; } = null!; - internal static WindowSystem WindowSystem { get; } = new(typeof(Service).AssemblyQualifiedName); - internal static RemoteApi RemoteApi { get; } = new(); - internal static ConfigurationManager ConfigurationManager { get; set; } = null!; internal static IPalacePalConfiguration Configuration { get; set; } = null!; - internal static Hooks Hooks { get; set; } = null!; } } diff --git a/Pal.Client/Windows/AgreementWindow.cs b/Pal.Client/Windows/AgreementWindow.cs index 41fb25c..316ee49 100644 --- a/Pal.Client/Windows/AgreementWindow.cs +++ b/Pal.Client/Windows/AgreementWindow.cs @@ -1,19 +1,32 @@ -using Dalamud.Interface.Colors; +using System; +using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using ECommons; using ImGuiNET; using System.Numerics; +using Pal.Client.Configuration; using Pal.Client.Properties; namespace Pal.Client.Windows { - internal class AgreementWindow : Window, ILanguageChanged + internal sealed class AgreementWindow : Window, IDisposable, ILanguageChanged { private const string WindowId = "###PalPalaceAgreement"; + private readonly WindowSystem _windowSystem; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; private int _choice; - public AgreementWindow() : base(WindowId) + public AgreementWindow( + WindowSystem windowSystem, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration) + : base(WindowId) { + _windowSystem = windowSystem; + _configurationManager = configurationManager; + _configuration = configuration; + LanguageChanged(); Flags = ImGuiWindowFlags.NoCollapse; @@ -27,8 +40,14 @@ namespace Pal.Client.Windows MinimumSize = new Vector2(500, 500), MaximumSize = new Vector2(2000, 2000), }; + + IsOpen = configuration.FirstUse; + _windowSystem.AddWindow(this); } + public void Dispose() + => _windowSystem.RemoveWindow(this); + public void LanguageChanged() => WindowName = $"{Localization.Palace_Pal}{WindowId}"; @@ -39,8 +58,6 @@ namespace Pal.Client.Windows public override void Draw() { - var config = Service.Configuration; - ImGui.TextWrapped(Localization.Explanation_1); ImGui.TextWrapped(Localization.Explanation_2); @@ -49,8 +66,8 @@ namespace Pal.Client.Windows ImGui.TextWrapped(Localization.Explanation_3); ImGui.TextWrapped(Localization.Explanation_4); - ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice, (int)Configuration.EMode.Online); - ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice, (int)Configuration.EMode.Offline); + ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice, (int)EMode.Online); + ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice, (int)EMode.Offline); ImGui.Separator(); @@ -67,12 +84,13 @@ namespace Pal.Client.Windows ImGui.BeginDisabled(_choice == -1); if (ImGui.Button(Localization.Agreement_UsingThisOnMyOwnRisk)) { - config.Mode = (Configuration.EMode)_choice; - config.FirstUse = false; - Service.ConfigurationManager.Save(config); + _configuration.Mode = (EMode)_choice; + _configuration.FirstUse = false; + _configurationManager.Save(_configuration); IsOpen = false; } + ImGui.EndDisabled(); ImGui.Separator(); diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 535f67e..e9fb03e 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -18,14 +18,28 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Dalamud.Game.Gui; using Pal.Client.Properties; using Pal.Client.Configuration; +using Pal.Client.DependencyInjection; namespace Pal.Client.Windows { - internal class ConfigWindow : Window, ILanguageChanged, IDisposable + internal sealed class ConfigWindow : Window, ILanguageChanged, IDisposable { private const string WindowId = "###PalPalaceConfig"; + + private readonly WindowSystem _windowSystem; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private readonly RenderAdapter _renderAdapter; + private readonly TerritoryState _territoryState; + private readonly FrameworkService _frameworkService; + private readonly FloorService _floorService; + private readonly DebugState _debugState; + private readonly ChatGui _chatGui; + private readonly RemoteApi _remoteApi; + private int _mode; private int _renderer; private ConfigurableMarker _trapConfig = new(); @@ -43,8 +57,30 @@ namespace Pal.Client.Windows private CancellationTokenSource? _testConnectionCts; - public ConfigWindow() : base(WindowId) + public ConfigWindow( + WindowSystem windowSystem, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + RenderAdapter renderAdapter, + TerritoryState territoryState, + FrameworkService frameworkService, + FloorService floorService, + DebugState debugState, + ChatGui chatGui, + RemoteApi remoteApi) + : base(WindowId) { + _windowSystem = windowSystem; + _configurationManager = configurationManager; + _configuration = configuration; + _renderAdapter = renderAdapter; + _territoryState = territoryState; + _frameworkService = frameworkService; + _floorService = floorService; + _debugState = debugState; + _chatGui = chatGui; + _remoteApi = remoteApi; + LanguageChanged(); Size = new Vector2(500, 400); @@ -52,8 +88,18 @@ namespace Pal.Client.Windows Position = new Vector2(300, 300); PositionCondition = ImGuiCond.FirstUseEver; - _importDialog = new FileDialogManager { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; - _exportDialog = new FileDialogManager { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + _importDialog = new FileDialogManager + { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + _exportDialog = new FileDialogManager + { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + + _windowSystem.AddWindow(this); + } + + public void Dispose() + { + _windowSystem.RemoveWindow(this); + _testConnectionCts?.Cancel(); } public void LanguageChanged() @@ -62,19 +108,13 @@ namespace Pal.Client.Windows WindowName = $"{Localization.Palace_Pal} v{version}{WindowId}"; } - public void Dispose() - { - _testConnectionCts?.Cancel(); - } - public override void OnOpen() { - var config = Service.Configuration; - _mode = (int)config.Mode; - _renderer = (int)config.Renderer.SelectedRenderer; - _trapConfig = new ConfigurableMarker(config.DeepDungeons.Traps); - _hoardConfig = new ConfigurableMarker(config.DeepDungeons.HoardCoffers); - _silverConfig = new ConfigurableMarker(config.DeepDungeons.SilverCoffers); + _mode = (int)_configuration.Mode; + _renderer = (int)_configuration.Renderer.SelectedRenderer; + _trapConfig = new ConfigurableMarker(_configuration.DeepDungeons.Traps); + _hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers); + _silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers); _connectionText = null; } @@ -106,14 +146,13 @@ namespace Pal.Client.Windows if (save || saveAndClose) { - var config = Service.Configuration; - config.Mode = (EMode)_mode; - config.Renderer.SelectedRenderer = (ERenderer)_renderer; - config.DeepDungeons.Traps = _trapConfig.Build(); - config.DeepDungeons.HoardCoffers = _hoardConfig.Build(); - config.DeepDungeons.SilverCoffers = _silverConfig.Build(); + _configuration.Mode = (EMode)_mode; + _configuration.Renderer.SelectedRenderer = (ERenderer)_renderer; + _configuration.DeepDungeons.Traps = _trapConfig.Build(); + _configuration.DeepDungeons.HoardCoffers = _hoardConfig.Build(); + _configuration.DeepDungeons.SilverCoffers = _silverConfig.Build(); - Service.ConfigurationManager.Save(config); + _configurationManager.Save(_configuration); if (saveAndClose) IsOpen = false; @@ -141,8 +180,10 @@ namespace Pal.Client.Windows ImGui.Indent(); ImGui.BeginDisabled(!_hoardConfig.Show); ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_HoardCoffers_Color, ref _hoardConfig.Color, ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_HoardCoffers_HideImpossible, ref _hoardConfig.OnlyVisibleAfterPomander); + ImGui.ColorEdit4(Localization.Config_HoardCoffers_Color, ref _hoardConfig.Color, + ImGuiColorEditFlags.NoInputs); + ImGui.Checkbox(Localization.Config_HoardCoffers_HideImpossible, + ref _hoardConfig.OnlyVisibleAfterPomander); ImGui.SameLine(); ImGuiComponents.HelpMarker(Localization.Config_HoardCoffers_HideImpossible_ToolTip); ImGui.EndDisabled(); @@ -155,7 +196,8 @@ namespace Pal.Client.Windows ImGui.Indent(); ImGui.BeginDisabled(!_silverConfig.Show); ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, ImGuiColorEditFlags.NoInputs); + ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, + ImGuiColorEditFlags.NoInputs); ImGui.Checkbox(Localization.Config_SilverCoffer_Filled, ref _silverConfig.Fill); ImGui.EndDisabled(); ImGui.Unindent(); @@ -172,7 +214,8 @@ namespace Pal.Client.Windows private void DrawCommunityTab(ref bool saveAndClose) { - if (BeginTabItemEx($"{Localization.ConfigTab_Community}###TabCommunity", _switchToCommunityTab ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) + if (BeginTabItemEx($"{Localization.ConfigTab_Community}###TabCommunity", + _switchToCommunityTab ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) { _switchToCommunityTab = false; @@ -180,12 +223,13 @@ namespace Pal.Client.Windows ImGui.TextWrapped(Localization.Explanation_4); ImGui.RadioButton(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _mode, (int)EMode.Online); - ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, (int)EMode.Offline); + ImGui.RadioButton(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, + (int)EMode.Offline); saveAndClose = ImGui.Button(Localization.SaveAndClose); ImGui.Separator(); - ImGui.BeginDisabled(Service.Configuration.Mode != EMode.Online); + ImGui.BeginDisabled(_configuration.Mode != EMode.Online); if (ImGui.Button(Localization.Config_TestConnection)) TestConnection(); @@ -205,7 +249,8 @@ namespace Pal.Client.Windows ImGui.TextWrapped(Localization.Config_ImportExplanation2); ImGui.TextWrapped(Localization.Config_ImportExplanation3); ImGui.Separator(); - ImGui.TextWrapped(string.Format(Localization.Config_ImportDownloadLocation, "https://github.com/carvelli/PalacePal/releases/")); + ImGui.TextWrapped(string.Format(Localization.Config_ImportDownloadLocation, + "https://github.com/carvelli/PalacePal/releases/")); if (ImGui.Button(Localization.Config_Import_VisitGitHub)) GenericHelpers.ShellStart("https://github.com/carvelli/PalacePal/releases/latest"); ImGui.Separator(); @@ -215,14 +260,16 @@ namespace Pal.Client.Windows ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) { - _importDialog.OpenFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", (success, paths) => - { - if (success && paths.Count == 1) + _importDialog.OpenFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", + (success, paths) => { - _openImportPath = paths.First(); - } - }, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false); - _openImportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls + if (success && paths.Count == 1) + { + _openImportPath = paths.First(); + } + }, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false); + _openImportDialogStartPath = + null; // only use this once, FileDialogManager will save path between calls } ImGui.BeginDisabled(string.IsNullOrEmpty(_openImportPath) || !File.Exists(_openImportPath)); @@ -230,11 +277,13 @@ namespace Pal.Client.Windows DoImport(_openImportPath); ImGui.EndDisabled(); - var importHistory = Service.Configuration.ImportHistory.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefault(); + var importHistory = _configuration.ImportHistory.OrderByDescending(x => x.ImportedAt) + .ThenBy(x => x.Id).FirstOrDefault(); if (importHistory != null) { ImGui.Separator(); - ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt)); + ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, + importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt)); ImGui.TextWrapped(Localization.Config_UndoImportExplanation2); if (ImGui.Button(Localization.Config_UndoImport)) UndoImport(importHistory.Id); @@ -246,7 +295,8 @@ namespace Pal.Client.Windows private void DrawExportTab() { - if (Service.RemoteApi.HasRoleOnCurrentServer("export:run") && ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) + if (_configuration.HasRoleOnCurrentServer("export:run") && + ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) { string todaysFileName = $"export-{DateTime.Today:yyyy-MM-dd}.pal"; if (string.IsNullOrEmpty(_saveExportPath) && !string.IsNullOrEmpty(_saveExportDialogStartPath)) @@ -259,14 +309,16 @@ namespace Pal.Client.Windows ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) { - _importDialog.SaveFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", todaysFileName, "pal", (success, path) => - { - if (success && !string.IsNullOrEmpty(path)) + _importDialog.SaveFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", + todaysFileName, "pal", (success, path) => { - _saveExportPath = path; - } - }, startPath: _saveExportDialogStartPath, isModal: false); - _saveExportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls + if (success && !string.IsNullOrEmpty(path)) + { + _saveExportPath = path; + } + }, startPath: _saveExportDialogStartPath, isModal: false); + _saveExportDialogStartPath = + null; // only use this once, FileDialogManager will save path between calls } ImGui.BeginDisabled(string.IsNullOrEmpty(_saveExportPath) || File.Exists(_saveExportPath)); @@ -283,8 +335,11 @@ namespace Pal.Client.Windows if (ImGui.BeginTabItem($"{Localization.ConfigTab_Renderer}###TabRenderer")) { ImGui.Text(Localization.Config_SelectRenderBackend); - ImGui.RadioButton($"{Localization.Config_Renderer_Splatoon} ({Localization.Config_Renderer_Splatoon_Hint})", ref _renderer, (int)ERenderer.Splatoon); - ImGui.RadioButton($"{Localization.Config_Renderer_Simple} ({Localization.Config_Renderer_Simple_Hint})", ref _renderer, (int)ERenderer.Simple); + ImGui.RadioButton( + $"{Localization.Config_Renderer_Splatoon} ({Localization.Config_Renderer_Splatoon_Hint})", + ref _renderer, (int)ERenderer.Splatoon); + ImGui.RadioButton($"{Localization.Config_Renderer_Simple} ({Localization.Config_Renderer_Simple_Hint})", + ref _renderer, (int)ERenderer.Simple); ImGui.Separator(); @@ -294,9 +349,9 @@ namespace Pal.Client.Windows ImGui.Separator(); ImGui.Text(Localization.Config_Splatoon_Test); - ImGui.BeginDisabled(!(Service.Plugin.Renderer is IDrawDebugItems)); + ImGui.BeginDisabled(!(_renderAdapter.Implementation is IDrawDebugItems)); if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) - (Service.Plugin.Renderer as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, _hoardConfig.Color); + (_renderAdapter.Implementation as IDrawDebugItems)?.DrawDebugItems(_trapConfig.Color, _hoardConfig.Color); ImGui.EndDisabled(); ImGui.EndTabItem(); @@ -307,39 +362,43 @@ namespace Pal.Client.Windows { if (ImGui.BeginTabItem($"{Localization.ConfigTab_Debug}###TabDebug")) { - var plugin = Service.Plugin; - if (plugin.IsInDeepDungeon()) + if (_territoryState.IsInDeepDungeon()) { - ImGui.Text($"You are in a deep dungeon, territory type {plugin.LastTerritory}."); - ImGui.Text($"Sync State = {plugin.TerritorySyncState}"); - ImGui.Text($"{plugin.DebugMessage}"); + ImGui.Text($"You are in a deep dungeon, territory type {_territoryState.LastTerritory}."); + ImGui.Text($"Sync State = {_territoryState.TerritorySyncState}"); + ImGui.Text($"{_debugState.DebugMessage}"); ImGui.Indent(); - if (plugin.FloorMarkers.TryGetValue(plugin.LastTerritory, out var currentFloor)) + if (_floorService.FloorMarkers.TryGetValue(_territoryState.LastTerritory, out var currentFloor)) { if (_trapConfig.Show) { int traps = currentFloor.Markers.Count(x => x.Type == Marker.EType.Trap); ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); } + if (_hoardConfig.Show) { int hoardCoffers = currentFloor.Markers.Count(x => x.Type == Marker.EType.Hoard); ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}"); } + if (_silverConfig.Show) { - int silverCoffers = plugin.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); - ImGui.Text($"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); + int silverCoffers = _floorService.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); + ImGui.Text( + $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); } - ImGui.Text($"Pomander of Sight: {plugin.PomanderOfSight}"); - ImGui.Text($"Pomander of Intuition: {plugin.PomanderOfIntuition}"); + ImGui.Text($"Pomander of Sight: {_territoryState.PomanderOfSight}"); + ImGui.Text($"Pomander of Intuition: {_territoryState.PomanderOfIntuition}"); } else ImGui.Text("Could not query current trap/coffer count."); + ImGui.Unindent(); - ImGui.TextWrapped("Traps and coffers may not be discovered even after using a pomander if they're far away (around 1,5-2 rooms)."); + ImGui.TextWrapped( + "Traps and coffers may not be discovered even after using a pomander if they're far away (around 1,5-2 rooms)."); } else ImGui.Text(Localization.Config_Debug_NotInADeepDungeon); @@ -378,7 +437,7 @@ namespace Pal.Client.Windows try { - _connectionText = await Service.RemoteApi.VerifyConnection(cts.Token); + _connectionText = await _remoteApi.VerifyConnection(cts.Token); } catch (Exception e) { @@ -388,19 +447,20 @@ namespace Pal.Client.Windows _connectionText = e.ToString(); } else - PluginLog.Warning(e, "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); + PluginLog.Warning(e, + "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); } }); } private void DoImport(string sourcePath) { - Service.Plugin.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath)); + _frameworkService.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath)); } private void UndoImport(Guid importId) { - Service.Plugin.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); + _frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); } private void DoExport(string destinationPath) @@ -409,28 +469,28 @@ namespace Pal.Client.Windows { try { - (bool success, ExportRoot export) = await Service.RemoteApi.DoExport(); + (bool success, ExportRoot export) = await _remoteApi.DoExport(); if (success) { await using var output = File.Create(destinationPath); export.WriteTo(output); - Service.Chat.Print($"Export saved as {destinationPath}."); + _chatGui.Print($"Export saved as {destinationPath}."); } else { - Service.Chat.PrintError("Export failed due to server error."); + _chatGui.PrintError("Export failed due to server error."); } } catch (Exception e) { PluginLog.Error(e, "Export failed"); - Service.Chat.PrintError($"Export failed: {e}"); + _chatGui.PrintError($"Export failed: {e}"); } }); } - private class ConfigurableMarker + private sealed class ConfigurableMarker { public bool Show; public Vector4 Color; diff --git a/Pal.Client/Windows/StatisticsWindow.cs b/Pal.Client/Windows/StatisticsWindow.cs index 03eabdc..05f953a 100644 --- a/Pal.Client/Windows/StatisticsWindow.cs +++ b/Pal.Client/Windows/StatisticsWindow.cs @@ -13,13 +13,17 @@ using System.Reflection; namespace Pal.Client.Windows { - internal class StatisticsWindow : Window, ILanguageChanged + internal class StatisticsWindow : Window, IDisposable, ILanguageChanged { private const string WindowId = "###PalacePalStats"; + private readonly WindowSystem _windowSystem; private readonly SortedDictionary _territoryStatistics = new(); - public StatisticsWindow() : base(WindowId) + public StatisticsWindow(WindowSystem windowSystem) + : base(WindowId) { + _windowSystem = windowSystem; + LanguageChanged(); Size = new Vector2(500, 500); @@ -30,8 +34,13 @@ namespace Pal.Client.Windows { _territoryStatistics[territory] = new TerritoryStatistics(territory.ToString()); } + + _windowSystem.AddWindow(this); } + public void Dispose() + => _windowSystem.RemoveWindow(this); + public void LanguageChanged() => WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}"; @@ -39,8 +48,10 @@ namespace Pal.Client.Windows { if (ImGui.BeginTabBar("Tabs")) { - DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10, ETerritoryType.Palace_191_200); - DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10, ETerritoryType.HeavenOnHigh_91_100); + DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10, + ETerritoryType.Palace_191_200); + DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10, + ETerritoryType.HeavenOnHigh_91_100); } } @@ -48,7 +59,8 @@ namespace Pal.Client.Windows { if (ImGui.BeginTabItem($"{name}###{id}")) { - if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) + if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4, + ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) { ImGui.TableSetupColumn(Localization.Statistics_TerritoryId); ImGui.TableSetupColumn(Localization.Statistics_InstanceName); @@ -56,7 +68,9 @@ namespace Pal.Client.Windows ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers); ImGui.TableHeadersRow(); - foreach (var (territoryType, stats) in _territoryStatistics.Where(x => x.Key >= minTerritory && x.Key <= maxTerritory).OrderBy(x => x.Key.GetOrder() ?? (int)x.Key)) + foreach (var (territoryType, stats) in _territoryStatistics + .Where(x => x.Key >= minTerritory && x.Key <= maxTerritory) + .OrderBy(x => x.Key.GetOrder() ?? (int)x.Key)) { ImGui.TableNextRow(); if (ImGui.TableNextColumn()) @@ -71,8 +85,10 @@ namespace Pal.Client.Windows if (ImGui.TableNextColumn()) ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-"); } + ImGui.EndTable(); } + ImGui.EndTabItem(); } } @@ -87,7 +103,8 @@ namespace Pal.Client.Windows foreach (var floor in floorStatistics) { - if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, out TerritoryStatistics? territoryStatistics)) + if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, + out TerritoryStatistics? territoryStatistics)) { territoryStatistics.TrapCount = floor.TrapCount; territoryStatistics.HoardCofferCount = floor.HoardCount; @@ -97,7 +114,7 @@ namespace Pal.Client.Windows private class TerritoryStatistics { - public string TerritoryName { get; set; } + public string TerritoryName { get; } public uint? TrapCount { get; set; } public uint? HoardCofferCount { get; set; }