diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index fd39574..70f3f93 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -3,7 +3,7 @@ net6.0-windows 9.0 - 1.4.0.0 + 1.5.0.0 @@ -29,6 +29,7 @@ + diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index b924bfd..ffdc715 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -7,7 +7,9 @@ using Dalamud.Plugin; using ECommons; using ECommons.Schedulers; using ECommons.SplatoonAPI; +using Grpc.Core; using ImGuiNET; +using Pal.Client.Windows; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -63,6 +65,12 @@ namespace Pal.Client Service.WindowSystem.AddWindow(configWindow); } + var statisticsWindow = pluginInterface.Create(); + if (statisticsWindow is not null) + { + Service.WindowSystem.AddWindow(statisticsWindow); + } + pluginInterface.UiBuilder.Draw += Service.WindowSystem.Draw; pluginInterface.UiBuilder.OpenConfigUi += OnOpenConfigUi; Service.Framework.Update += OnFrameworkUpdate; @@ -93,7 +101,16 @@ namespace Pal.Client return; } - Service.WindowSystem.GetWindow()?.Toggle(); + switch (arguments) + { + case "stats": + Task.Run(async () => await FetchFloorStatistics()); + break; + + default: + Service.WindowSystem.GetWindow()?.Toggle(); + break; + } } #region IDisposable Support @@ -150,7 +167,7 @@ namespace Pal.Client _configUpdated = false; recreateLayout = true; } - + bool saveMarkers = false; if (LastTerritory != Service.ClientState.TerritoryType) { @@ -282,8 +299,8 @@ namespace Pal.Client var config = Service.Configuration; List elements = new List(); - foreach (var marker in visibleMarkers) - { + foreach (var marker in visibleMarkers) + { EphemeralMarkers.Add(marker); if (marker.Type == Marker.EType.SilverCoffer && config.ShowSilverCoffers) @@ -341,6 +358,38 @@ namespace Pal.Client } } + private async Task FetchFloorStatistics() + { + if (Service.Configuration.Mode != Configuration.EMode.Online) + { + Service.Chat.Print($"[Palace Pal] You can view statistics for the floor you're currently on by opening the 'Debug' tab in the configuration window."); + 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.PrintError("[Palace Pal] Unable to fetch statistics."); + } + } + catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) + { + Service.Chat.Print($"[Palace Pal] You can view statistics for the floor you're currently on by opening the 'Debug' tab in the configuration window."); + } + catch (Exception e) + { + Service.Chat.PrintError($"[Palace Pal] {e}"); + } + } + private void HandleRemoteDownloads() { while (_remoteDownloads.TryDequeue(out var download)) diff --git a/Pal.Client/RemoteApi.cs b/Pal.Client/RemoteApi.cs index 371c3d6..d093cdb 100644 --- a/Pal.Client/RemoteApi.cs +++ b/Pal.Client/RemoteApi.cs @@ -20,8 +20,7 @@ namespace Pal.Client private const string remoteUrl = "https://pal.μ.tv"; #endif private GrpcChannel _channel; - private string _authToken; - private DateTime _tokenExpiresAt; + private LoginReply _lastLoginReply; private async Task Connect(CancellationToken cancellationToken, bool retry = true) { @@ -67,18 +66,12 @@ namespace Pal.Client if (string.IsNullOrEmpty(accountId)) return false; - if (string.IsNullOrEmpty(_authToken) || _tokenExpiresAt < DateTime.Now) + if (_lastLoginReply == null || string.IsNullOrEmpty(_lastLoginReply.AuthToken) || _lastLoginReply.ExpiresAt.ToDateTime().ToLocalTime() < DateTime.Now) { - var loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = accountId }, deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); - if (loginReply.Success) + _lastLoginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = accountId }, deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + if (!_lastLoginReply.Success) { - _authToken = loginReply.AuthToken; - _tokenExpiresAt = loginReply.ExpiresAt.ToDateTime().ToLocalTime(); - } - else - { - _authToken = null; - if (loginReply.Error == LoginError.InvalidAccountId) + if (_lastLoginReply.Error == LoginError.InvalidAccountId) { accountId = null; #if DEBUG @@ -95,7 +88,7 @@ namespace Pal.Client } } - return !string.IsNullOrEmpty(_authToken); + return !string.IsNullOrEmpty(_lastLoginReply?.AuthToken); } public async Task VerifyConnection(CancellationToken cancellationToken = default) @@ -142,9 +135,19 @@ namespace Pal.Client return uploadReply.Success; } + public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return new(false, new List()); + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), cancellationToken: cancellationToken); + return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList()); + } + private Metadata AuthorizedHeaders() => new Metadata { - { "Authorization", $"Bearer {_authToken}" }, + { "Authorization", $"Bearer {_lastLoginReply?.AuthToken}" }, }; public void Dispose() diff --git a/Pal.Client/AgreementWindow.cs b/Pal.Client/Windows/AgreementWindow.cs similarity index 98% rename from Pal.Client/AgreementWindow.cs rename to Pal.Client/Windows/AgreementWindow.cs index 42c9070..3847e14 100644 --- a/Pal.Client/AgreementWindow.cs +++ b/Pal.Client/Windows/AgreementWindow.cs @@ -4,7 +4,7 @@ using ECommons; using ImGuiNET; using System.Numerics; -namespace Pal.Client +namespace Pal.Client.Windows { internal class AgreementWindow : Window { @@ -35,10 +35,10 @@ namespace Pal.Client ImGui.TextWrapped("Ideally, we want to discover every potential trap and chest location in the game, but doing this alone is very tedious. Floor 51-60 has over 100 trap locations and over 50 coffer locations, the last of which took over 50 runs to find - and we don't know if that map is complete. Higher floors naturally see fewer runs, making solo attempts to map the place much harder."); ImGui.TextWrapped("You can decide whether you want to share traps and chests you find with the community, which likewise also will let you see chests and coffers found by other players. This can be changed at any time. No data regarding your FFXIV character or account is ever sent to our server."); - + ImGui.RadioButton("Upload my discoveries, show traps & coffers other players have discovered", ref _choice, (int)Configuration.EMode.Online); ImGui.RadioButton("Never upload discoveries, show only traps and coffers I found myself", ref _choice, (int)Configuration.EMode.Offline); - + ImGui.Separator(); ImGui.TextColored(ImGuiColors.DalamudRed, "While this is not an automation feature, you're still very likely to break the ToS."); diff --git a/Pal.Client/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs similarity index 99% rename from Pal.Client/ConfigWindow.cs rename to Pal.Client/Windows/ConfigWindow.cs index a3ed21e..92c8969 100644 --- a/Pal.Client/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Numerics; using System.Threading.Tasks; -namespace Pal.Client +namespace Pal.Client.Windows { internal class ConfigWindow : Window { @@ -139,7 +139,7 @@ namespace Pal.Client ImGui.Indent(); if (plugin.FloorMarkers.TryGetValue(plugin.LastTerritory, out var currentFloorMarkers)) { - if (_showTraps) + if (_showTraps) { int traps = currentFloorMarkers.Count(x => x != null && x.Type == Marker.EType.Trap); ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); diff --git a/Pal.Client/Windows/StatisticsWindow.cs b/Pal.Client/Windows/StatisticsWindow.cs new file mode 100644 index 0000000..690efa5 --- /dev/null +++ b/Pal.Client/Windows/StatisticsWindow.cs @@ -0,0 +1,84 @@ +using Dalamud.Interface.Windowing; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using Pal.Common; +using Palace; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace Pal.Client.Windows +{ + internal class StatisticsWindow : Window + { + private SortedDictionary _territoryStatistics = new(); + + public StatisticsWindow() : base("Palace Pal - Statistics###PalacePalStats") + { + Size = new Vector2(500, 500); + SizeCondition = ImGuiCond.FirstUseEver; + + foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues()) + { + _territoryStatistics[territory] = new TerritoryStatistics { TerritoryName = territory.ToString() }; + } + } + + public override void Draw() + { + if (ImGui.CollapsingHeader("Discovered Traps & Coffers per Instance", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (ImGui.BeginTable("TrapHoardStatistics", 3, ImGuiTableFlags.Borders)) + { + ImGui.TableSetupColumn("Instance", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Traps", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Hoard", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableHeadersRow(); + + foreach (var (territoryType, stats) in _territoryStatistics) + { + ImGui.TableNextRow(); + if (ImGui.TableNextColumn()) + ImGui.Text(stats.TerritoryName); + + if (ImGui.TableNextColumn()) + ImGui.Text(stats.TrapCount?.ToString() ?? "-"); + + if (ImGui.TableNextColumn()) + ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-"); + } + ImGui.EndTable(); + } + } + } + + internal void SetFloorData(IEnumerable floorStatistics) + { + foreach (var territoryStatistics in _territoryStatistics.Values) + { + territoryStatistics.TrapCount = null; + territoryStatistics.HoardCofferCount = null; + } + + foreach (var floor in floorStatistics) + { + if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, out TerritoryStatistics territoryStatistics)) + { + territoryStatistics.TrapCount = floor.TrapCount; + territoryStatistics.HoardCofferCount = floor.HoardCount; + } + } + } + + private class TerritoryStatistics + { + public string TerritoryName { get; set; } + public uint? TrapCount { get; set; } + public uint? HoardCofferCount { get; set; } + } + } +} diff --git a/Pal.Common/ETerritoryType.cs b/Pal.Common/ETerritoryType.cs new file mode 100644 index 0000000..674a4cc --- /dev/null +++ b/Pal.Common/ETerritoryType.cs @@ -0,0 +1,37 @@ +namespace Pal.Common +{ + public enum ETerritoryType : ushort + { + Palace_1_10 = 561, + Palace_11_20, + Palace_21_30, + Palace_31_40, + Palace_41_50, + Palace_51_60 = 593, + Palace_61_70, + Palace_71_80, + Palace_81_90, + Palace_91_100, + Palace_101_110, + Palace_111_120, + Palace_121_130, + Palace_131_140, + Palace_141_150, + Palace_151_160, + Palace_161_170, + Palace_171_180, + Palace_181_190, + Palace_191_200, + + HeavenOnHigh_1_10 = 770, + HeavenOnHigh_11_20, + HeavenOnHigh_21_30, + HeavenOnHigh_31_40, + HeavenOnHigh_41_50, + HeavenOnHigh_51_60, + HeavenOnHigh_61_70 = 782, + HeavenOnHigh_71_80, + HeavenOnHigh_81_90, + HeavenOnHigh_91_100 + } +} diff --git a/Pal.Common/Protos/palace.proto b/Pal.Common/Protos/palace.proto index 985dbc0..a5a5797 100644 --- a/Pal.Common/Protos/palace.proto +++ b/Pal.Common/Protos/palace.proto @@ -5,6 +5,7 @@ package palace; service PalaceService { rpc DownloadFloors(DownloadFloorsRequest) returns (DownloadFloorsReply); rpc UploadFloors(UploadFloorsRequest) returns (UploadFloorsReply); + rpc FetchStatistics(StatisticsRequest) returns (StatisticsReply); } message DownloadFloorsRequest { @@ -25,6 +26,20 @@ message UploadFloorsReply { bool success = 1; } +message StatisticsRequest { +} + +message StatisticsReply { + bool success = 1; + repeated FloorStatistics floorStatistics = 2; +} + +message FloorStatistics { + uint32 territoryType = 1; + uint32 trapCount = 2; + uint32 hoardCount = 3; +} + message PalaceObject { ObjectType type = 1; float x = 2;