commit aae717955deb49155bdc2524937fb74073dd31bc Author: Liza Carvelli Date: Sun Oct 23 04:38:58 2022 +0200 Initial Commit Change release db path cx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ce6fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,340 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..212ec30 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/ECommons"] + path = vendor/ECommons + url = https://github.com/NightmareXIV/ECommons diff --git a/Pal.Client/AgreementWindow.cs b/Pal.Client/AgreementWindow.cs new file mode 100644 index 0000000..769a191 --- /dev/null +++ b/Pal.Client/AgreementWindow.cs @@ -0,0 +1,69 @@ +using Dalamud.Interface.Colors; +using Dalamud.Interface.Windowing; +using ECommons; +using ImGuiNET; +using System.Numerics; + +namespace Pal.Client +{ + internal class AgreementWindow : Window + { + private int _choice; + + public AgreementWindow() : base("Pal Palace###PalPalaceAgreement") + { + Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse; + Size = new Vector2(500, 500); + SizeCondition = ImGuiCond.Always; + PositionCondition = ImGuiCond.Always; + Position = new Vector2(310, 310); + } + + public override void OnOpen() + { + _choice = -1; + } + + public override void Draw() + { + var config = Service.Configuration; + + ImGui.TextWrapped("Pal Palace will show you via Splatoon overlays where potential trap & hoard coffer locations are."); + ImGui.TextWrapped("To do this, using a pomander to reveal trap or treasure chest locations will save the position of what you see."); + + ImGui.Spacing(); + + 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."); + ImGui.TextColored(ImGuiColors.DalamudRed, "Other players in your party can always see where you're standing/walking."); + ImGui.TextColored(ImGuiColors.DalamudRed, "As such, please avoid mentioning it in-game and do not share videos/screenshots."); + + ImGui.Separator(); + + if (_choice == -1) + ImGui.TextDisabled("Please chose one of the options above."); + ImGui.BeginDisabled(_choice == -1); + if (ImGui.Button("I understand I'm using this plugin on my own risk.")) + { + config.Mode = (Configuration.EMode)_choice; + config.FirstUse = false; + config.Save(); + + IsOpen = false; + } + ImGui.EndDisabled(); + + ImGui.Separator(); + + if (ImGui.Button("View plugin & server source code")) + GenericHelpers.ShellStart("https://github.com/LizaCarvbelli/PalPalace"); + } + } +} diff --git a/Pal.Client/ConfigWindow.cs b/Pal.Client/ConfigWindow.cs new file mode 100644 index 0000000..19f9b9a --- /dev/null +++ b/Pal.Client/ConfigWindow.cs @@ -0,0 +1,192 @@ +using Dalamud.Interface.Windowing; +using ECommons.Automation; +using ECommons.DalamudServices; +using ECommons.SplatoonAPI; +using ImGuiNET; +using ImGuizmoNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Pal.Client +{ + internal class ConfigWindow : Window + { + private int _mode; + private bool _showTraps; + private Vector4 _trapColor; + private bool _showHoard; + private Vector4 _hoardColor; + private string _connectionText; + + public ConfigWindow() : base("Pal Palace - Configuration###PalPalaceConfig") + { + Size = new Vector2(500, 400); + SizeCondition = ImGuiCond.FirstUseEver; + Position = new Vector2(300, 300); + PositionCondition = ImGuiCond.FirstUseEver; + } + + public override void OnOpen() + { + var config = Service.Configuration; + _mode = (int)config.Mode; + _showTraps = config.ShowTraps; + _trapColor = config.TrapColor; + _showHoard = config.ShowHoard; + _hoardColor = config.HoardColor; + _connectionText = null; + } + + public override void Draw() + { + bool save = false; + bool saveAndClose = false; + if (ImGui.BeginTabBar("PalTabs")) + { + if (ImGui.BeginTabItem("PotD/HoH")) + { + ImGui.Checkbox("Show traps", ref _showTraps); + ImGui.Indent(); + ImGui.BeginDisabled(!_showTraps); + ImGui.Spacing(); + ImGui.ColorEdit4("Trap color", ref _trapColor, ImGuiColorEditFlags.NoInputs); + ImGui.EndDisabled(); + ImGui.Unindent(); + + ImGui.Separator(); + + ImGui.Checkbox("Show hoard coffers", ref _showHoard); + ImGui.Indent(); + ImGui.BeginDisabled(!_showHoard); + ImGui.Spacing(); + ImGui.ColorEdit4("Hoard Coffer color", ref _hoardColor, ImGuiColorEditFlags.NoInputs); + ImGui.EndDisabled(); + ImGui.Unindent(); + + ImGui.Separator(); + + save = ImGui.Button("Save"); + ImGui.SameLine(); + saveAndClose = ImGui.Button("Save & Close"); + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Community")) + { + 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 _mode, (int)Configuration.EMode.Online); + ImGui.RadioButton("Never upload discoveries, show only traps and coffers I found myself", ref _mode, (int)Configuration.EMode.Offline); + saveAndClose = ImGui.Button("Save & Close"); + + ImGui.Separator(); + + ImGui.BeginDisabled(Service.Configuration.Mode != Configuration.EMode.Online); + if (ImGui.Button("Test Connection")) + { + Task.Run(async () => + { + _connectionText = "Testing..."; + try + { + _connectionText = await Service.RemoteApi.VerifyConnection(); + } + catch (Exception e) + { + _connectionText = e.ToString(); + } + }); + } + + if (_connectionText != null) + ImGui.Text(_connectionText); + + ImGui.EndDisabled(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Debug")) + { + var plugin = Service.Plugin; + if (plugin.IsInPotdOrHoh()) + { + ImGui.Text($"You are in a deep dungeon, territory type {plugin.LastTerritory}."); + ImGui.Text($"Sync State = {plugin.TerritorySyncState}"); + ImGui.Text($"{plugin.DebugMessage}"); + + ImGui.Indent(); + if (plugin.FloorMarkers.TryGetValue(plugin.LastTerritory, out var currentFloorMarkers)) + { + if (_showTraps) + ImGui.Text($"{currentFloorMarkers.Count(x => x != null && x.Type == Palace.ObjectType.Trap)} known traps"); + if (_showHoard) + ImGui.Text($"{currentFloorMarkers.Count(x => x != null && x.Type == Palace.ObjectType.Hoard)} known hoard coffers"); + + foreach (var m in currentFloorMarkers) + { + var dup = currentFloorMarkers.FirstOrDefault(x => !ReferenceEquals(x, m) && x.GetHashCode() == m.GetHashCode()); + if (dup != null) + ImGui.Text($"{m.Type} {m.Position} // {dup.Type} {dup.Position}"); + + } + } + else + ImGui.Text("Could not query current trap/coffer count."); + ImGui.Unindent(); + } + else + ImGui.Text("You are NOT in a deep dungeon."); + + ImGui.Separator(); + + if (ImGui.Button("Draw trap & coffer circles around self")) + { + try + { + var pos = Service.ClientState.LocalPlayer.Position; + var elements = new List + { + Plugin.CreateSplatoonElement(Palace.ObjectType.Trap, pos, _trapColor), + Plugin.CreateSplatoonElement(Palace.ObjectType.Hoard, pos, _hoardColor), + }; + + if (!Splatoon.AddDynamicElements("PalacePal.Test", elements.ToArray(), new long[] { Environment.TickCount64 + 10000 })) + { + Service.Chat.PrintError("Could not draw markers :("); + } + } + catch (Exception) + { + Service.Chat.PrintError("Could not draw markers, is Splatoon installed and enabled?"); + } + } + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + + if (save || saveAndClose) + { + var config = Service.Configuration; + config.Mode = (Configuration.EMode)_mode; + config.ShowTraps = _showTraps; + config.TrapColor = _trapColor; + config.ShowHoard = _showHoard; + config.HoardColor = _hoardColor; + config.Save(); + + if (saveAndClose) + IsOpen = false; + } + } + } +} diff --git a/Pal.Client/Configuration.cs b/Pal.Client/Configuration.cs new file mode 100644 index 0000000..11499c4 --- /dev/null +++ b/Pal.Client/Configuration.cs @@ -0,0 +1,47 @@ +using Dalamud.Configuration; +using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Client.Graphics; +using System.Numerics; + +namespace Pal.Client +{ + public class Configuration : IPluginConfiguration + { + public int Version { get; set; } + + #region Saved configuration values + public bool FirstUse { get; set; } = true; + public EMode Mode { get; set; } = EMode.Offline; + public string AccountId { get; set; } + + public bool ShowTraps { get; set; } = true; + public Vector4 TrapColor { get; set; } = new Vector4(1, 0, 0, 0.4f); + public bool ShowHoard { get; set; } = true; + public Vector4 HoardColor { get; set; } = new Vector4(0, 1, 1, 0.4f); + #endregion + + public delegate void OnSaved(); + public event OnSaved Saved; + + public void Save() + { + Version = 1; + Service.PluginInterface.SavePluginConfig(this); + Saved?.Invoke(); + } + + public enum EMode + { + /// + /// Fetches trap locations from remote server. + /// + Online = 1, + + /// + /// Only shows traps found by yourself uisng a pomander of sight. + /// + Offline = 2, + } + } +} diff --git a/Pal.Client/DalamudPackager.targets b/Pal.Client/DalamudPackager.targets new file mode 100644 index 0000000..05ca48f --- /dev/null +++ b/Pal.Client/DalamudPackager.targets @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Pal.Client/DownloadGithubActions.sh b/Pal.Client/DownloadGithubActions.sh new file mode 100644 index 0000000..5aa23ce --- /dev/null +++ b/Pal.Client/DownloadGithubActions.sh @@ -0,0 +1,5 @@ +#!/bin/sh +curl -O https://raw.githubusercontent.com/karashiiro/DalamudPluginProjectTemplate/master/.github/workflows/dotnet.yml +mkdir .github +mkdir .github/workflows +mv dotnet.yml .github/workflows \ No newline at end of file diff --git a/Pal.Client/Marker.cs b/Pal.Client/Marker.cs new file mode 100644 index 0000000..e13787c --- /dev/null +++ b/Pal.Client/Marker.cs @@ -0,0 +1,37 @@ +using ECommons.SplatoonAPI; +using Palace; +using System; +using System.Numerics; +using System.Text.Json.Serialization; + +namespace Pal.Client +{ + internal class Marker + { + public ObjectType Type { get; set; } = ObjectType.Unknown; + public Vector3 Position { get; set; } + public bool Seen { get; set; } = false; + + [JsonIgnore] + public bool RemoteSeen { get; set; } = false; + + [JsonIgnore] + public Element SplatoonElement { get; set; } + + public Marker(ObjectType type, Vector3 position) + { + Type = type; + Position = position; + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, (int)Position.X, (int)Position.Y, (int)Position.Z); + } + + public override bool Equals(object obj) + { + return obj is Marker otherMarker && Type == otherMarker.Type && Position == otherMarker.Position; + } + } +} diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj new file mode 100644 index 0000000..b032e0c --- /dev/null +++ b/Pal.Client/Pal.Client.csproj @@ -0,0 +1,67 @@ + + + + net6.0-windows + 9.0 + 1.0.0.0 + + + + false + x64 + Palace Pal + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll + false + + + + diff --git a/Pal.Client/Palace Pal.json b/Pal.Client/Palace Pal.json new file mode 100644 index 0000000..5068ed2 --- /dev/null +++ b/Pal.Client/Palace Pal.json @@ -0,0 +1,8 @@ +{ + "Name": "Palace Pal", + "Author": "Liza Carvelli", + "Punchline": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High.", + "Description": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High. Requires Splatoon to be installed.", + "RepoUrl": "https://github.com/carvelli/PalacePal", + "Tags": [ "potd", "palace", "hoh", "splatoon" ] +} \ No newline at end of file diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs new file mode 100644 index 0000000..516b9bc --- /dev/null +++ b/Pal.Client/Plugin.cs @@ -0,0 +1,360 @@ +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using ECommons; +using ECommons.Schedulers; +using ECommons.SplatoonAPI; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Pal.Client +{ + public class Plugin : IDalamudPlugin + { + private const long ON_TERRITORY_CHANGE = -2; + + private readonly ConcurrentQueue<(ushort territoryId, bool success, IList markers)> _remoteDownloads = new(); + private bool _configUpdated = false; + + internal ConcurrentDictionary> FloorMarkers { get; } = new(); + internal ushort LastTerritory { get; private set; } + public SyncState TerritorySyncState { get; set; } + public string DebugMessage { get; set; } + + public string Name => "Palace Pal"; + + public Plugin(DalamudPluginInterface pluginInterface) + { + + ECommons.ECommons.Init(pluginInterface, this, Module.SplatoonAPI); + + pluginInterface.Create(); + Service.Plugin = this; + Service.Configuration = (Configuration)pluginInterface.GetPluginConfig() ?? pluginInterface.Create(); + + 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) + { + configWindow.IsOpen = true; + Service.WindowSystem.AddWindow(configWindow); + } + + pluginInterface.UiBuilder.Draw += Service.WindowSystem.Draw; + pluginInterface.UiBuilder.OpenConfigUi += OnOpenConfigUi; + Service.Framework.Update += OnFrameworkUpdate; + Service.Configuration.Saved += OnConfigSaved; + } + + public void OnOpenConfigUi() + { + Window configWindow; + if (Service.Configuration.FirstUse) + configWindow = Service.WindowSystem.GetWindow(); + else + configWindow = Service.WindowSystem.GetWindow(); + + if (configWindow != null) + configWindow.IsOpen = true; + } + + #region IDisposable Support + protected virtual void Dispose(bool disposing) + { + if (!disposing) return; + + Service.PluginInterface.UiBuilder.Draw -= Service.WindowSystem.Draw; + Service.PluginInterface.UiBuilder.OpenConfigUi -= OnOpenConfigUi; + Service.Framework.Update -= OnFrameworkUpdate; + Service.Configuration.Saved -= OnConfigSaved; + + Service.WindowSystem.RemoveAllWindows(); + + Service.RemoteApi.Dispose(); + ECommons.ECommons.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + + private void OnConfigSaved() + { + _configUpdated = true; + } + + private void OnFrameworkUpdate(Framework framework) + { + try + { + if (_configUpdated) + { + if (Service.Configuration.Mode == Configuration.EMode.Offline) + { + foreach (var path in Directory.GetFiles(Service.PluginInterface.GetPluginConfigDirectory())) + { + if (path.EndsWith(".json")) + { + var markers = JsonSerializer.Deserialize>(File.ReadAllText(path), new JsonSerializerOptions { IncludeFields = true }).Where(x => x.Seen).ToList(); + File.WriteAllText(path, JsonSerializer.Serialize(markers, new JsonSerializerOptions { IncludeFields = true })); + } + } + + FloorMarkers.Clear(); + LastTerritory = 0; + } + _configUpdated = false; + } + + bool recreateLayout = false; + bool saveMarkers = false; + if (LastTerritory != Service.ClientState.TerritoryType) + { + LastTerritory = Service.ClientState.TerritoryType; + TerritorySyncState = SyncState.NotAttempted; + + if (IsInPotdOrHoh()) + FloorMarkers[LastTerritory] = new ConcurrentBag(LoadSavedMarkers()); + recreateLayout = true; + } + + if (!IsInPotdOrHoh()) + return; + + if (Service.Configuration.Mode == Configuration.EMode.Online && TerritorySyncState == SyncState.NotAttempted) + { + TerritorySyncState = SyncState.Started; + Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory)); + } + + if (_remoteDownloads.Count > 0) + { + HandleRemoteDownloads(); + recreateLayout = true; + saveMarkers = true; + } + + if (!FloorMarkers.TryGetValue(LastTerritory, out var currentFloorMarkers)) + FloorMarkers[LastTerritory] = currentFloorMarkers = new ConcurrentBag(); + + IList visibleMarkers = GetRelevantGameObjects(); + foreach (var visibleMarker in visibleMarkers) + { + Marker knownMarker = currentFloorMarkers.SingleOrDefault(x => x != null && x.GetHashCode() == visibleMarker.GetHashCode()); + if (knownMarker != null) + { + if (!knownMarker.Seen) + { + knownMarker.Seen = true; + saveMarkers = true; + } + continue; + } + + currentFloorMarkers.Add(visibleMarker); + recreateLayout = true; + saveMarkers = true; + } + + if (saveMarkers) + { + SaveMarkers(); + + if (TerritorySyncState == SyncState.Complete) + { + var markersToUpload = currentFloorMarkers.Where(x => !x.RemoteSeen).ToList(); + Task.Run(async () => await Service.RemoteApi.UploadMarker(LastTerritory, markersToUpload)); + } + } + + if (recreateLayout) + { + Splatoon.RemoveDynamicElements("PalacePal.Markers"); + + + var config = Service.Configuration; + + List elements = new List(); + foreach (var marker in currentFloorMarkers) + { + if (marker.Seen || config.Mode == Configuration.EMode.Online) + { + if (marker.Type == Palace.ObjectType.Trap && config.ShowTraps) + { + var element = CreateSplatoonElement(marker.Type, marker.Position, config.TrapColor); + marker.SplatoonElement = element; + elements.Add(element); + } + else if (marker.Type == Palace.ObjectType.Hoard && config.ShowHoard) + { + var element = CreateSplatoonElement(marker.Type, marker.Position, config.HoardColor); + marker.SplatoonElement = element; + elements.Add(element); + } + } + } + + // we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout + new TickScheduler(delegate + { + try + { + Splatoon.AddDynamicElements("PalacePal.Markers", elements.ToArray(), new long[] { Environment.TickCount64 + 60 * 60 * 1000, ON_TERRITORY_CHANGE }); + } + catch (Exception e) + { + DebugMessage = $"{DateTime.Now}\n{e}"; + } + }); + } + } + catch (Exception e) + { + DebugMessage = $"{DateTime.Now}\n{e}"; + } + } + + public string GetSaveForCurrentTerritory() => Path.Join(Service.PluginInterface.GetPluginConfigDirectory(), $"{LastTerritory}.json"); + + private List LoadSavedMarkers() + { + string path = GetSaveForCurrentTerritory(); + if (File.Exists(path)) + return JsonSerializer.Deserialize>(File.ReadAllText(path), new JsonSerializerOptions { IncludeFields = true }).Where(x => x.Seen || Service.Configuration.Mode == Configuration.EMode.Online).ToList(); + else + return new List(); + } + + private void SaveMarkers() + { + string path = GetSaveForCurrentTerritory(); + File.WriteAllText(path, JsonSerializer.Serialize(FloorMarkers[LastTerritory], new JsonSerializerOptions { IncludeFields = true })); + } + + private async Task DownloadMarkersForTerritory(ushort territoryId) + { + try + { + var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId); + _remoteDownloads.Enqueue((territoryId, success, downloadedMarkers)); + } + catch (Exception e) + { + DebugMessage = $"{DateTime.Now}\n{e}"; + } + } + + private void HandleRemoteDownloads() + { + while (_remoteDownloads.TryDequeue(out var download)) + { + var (territoryId, success, downloadedMarkers) = download; + if (Service.Configuration.Mode == Configuration.EMode.Online && success && FloorMarkers.TryGetValue(territoryId, out var currentFloorMarkers) && downloadedMarkers.Count > 0) + { + foreach (var downloadedMarker in downloadedMarkers) + { + Marker seenMarker = currentFloorMarkers.SingleOrDefault(x => x.GetHashCode() == downloadedMarker.GetHashCode()); + if (seenMarker != null) + { + seenMarker.RemoteSeen = true; + continue; + } + + downloadedMarkers.Add(seenMarker); + } + } + + // don't modify state for outdated floors + if (LastTerritory != territoryId) + continue; + + if (success) + TerritorySyncState = SyncState.Complete; + else + TerritorySyncState = SyncState.Failed; + } + } + + 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(Palace.ObjectType.Trap, obj.Position) { Seen = true }); + break; + + case 2007542: + case 2007543: + result.Add(new Marker(Palace.ObjectType.Hoard, obj.Position) { Seen = true }); + break; + } + } + + return result; + } + + internal bool IsInPotdOrHoh() => Service.ClientState.IsLoggedIn && Service.Condition[ConditionFlag.InDeepDungeon]; + + internal static Element CreateSplatoonElement(Palace.ObjectType type, Vector3 pos, Vector4 color) + { + return new Element(ElementType.CircleAtFixedCoordinates) + { + refX = pos.X, + refY = pos.Z, // z and y are swapped + refZ = pos.Y, + offX = 0, + offY = 0, + offZ = type == Palace.ObjectType.Trap ? 0 : -0.03f, + Filled = false, + radius = 1.7f, + FillStep = 1, + color = ImGui.ColorConvertFloat4ToU32(color), + thicc = 2, + }; + } + + public enum SyncState + { + NotAttempted, + Started, + Complete, + Failed, + } + } +} diff --git a/Pal.Client/RemoteApi.cs b/Pal.Client/RemoteApi.cs new file mode 100644 index 0000000..bb0ea31 --- /dev/null +++ b/Pal.Client/RemoteApi.cs @@ -0,0 +1,128 @@ +using Account; +using Grpc.Core; +using Grpc.Net.Client; +using Palace; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +namespace Pal.Client +{ + internal class RemoteApi : IDisposable + { +#if DEBUG + private const string remoteUrl = "http://localhost:5145"; +#else + private const string remoteUrl = "https://pal.μ.tv:47701"; +#endif + private GrpcChannel _channel; + private string _authToken; + private DateTime _tokenExpiresAt; + + private async Task Connect(CancellationToken cancellationToken) + { + if (Service.Configuration.Mode != Configuration.EMode.Online) + return false; + + if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) + { + Dispose(); + + _channel = GrpcChannel.ForAddress(remoteUrl, new GrpcChannelOptions + { + HttpHandler = new SocketsHttpHandler + { + ConnectTimeout = TimeSpan.FromSeconds(5), + } + }); + await _channel.ConnectAsync(cancellationToken); + } + + var accountClient = new AccountService.AccountServiceClient(_channel); + string accountId = Service.Configuration.AccountId; + if (string.IsNullOrEmpty(accountId)) + { + var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + if (createAccountReply.Success) + { + Service.Configuration.AccountId = accountId = createAccountReply.AccountId; + Service.Configuration.Save(); + } + } + + if (string.IsNullOrEmpty(accountId)) + return false; + + if (string.IsNullOrEmpty(_authToken) || _tokenExpiresAt < DateTime.Now) + { + var loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = accountId }, deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + if (loginReply.Success) + { + _authToken = loginReply.AuthToken; + _tokenExpiresAt = loginReply.ExpiresAt.ToDateTime().ToLocalTime(); + } + } + + return !string.IsNullOrEmpty(_authToken); + } + + public async Task VerifyConnection(CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return "Could not connect to server"; + + var accountClient = new AccountService.AccountServiceClient(_channel); + await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + return "Connection successful"; + } + + public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return (false, new()); + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); + return (downloadReply.Success, downloadReply.Objects.Select(o => new Marker(o.Type, new Vector3(o.X, o.Y, o.Z)) { RemoteSeen = true }).ToList()); + } + + public async Task UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + { + if (markers.Count == 0) + return true; + + if (!await Connect(cancellationToken)) + return false; + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var uploadRequest = new UploadFloorsRequest + { + TerritoryType = territoryType, + }; + uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject + { + Type = m.Type, + X = m.Position.X, + Y = m.Position.Y, + Z = m.Position.Z + })); + var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); + return uploadReply.Success; + } + + private Metadata AuthorizedHeaders() => new Metadata + { + { "Authorization", $"Bearer {_authToken}" }, + }; + + public void Dispose() + { + _channel?.Dispose(); + _channel = null; + } + } +} diff --git a/Pal.Client/Service.cs b/Pal.Client/Service.cs new file mode 100644 index 0000000..29741f5 --- /dev/null +++ b/Pal.Client/Service.cs @@ -0,0 +1,26 @@ +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; +using Dalamud.Interface.Windowing; +using Dalamud.IoC; +using Dalamud.Plugin; + +namespace Pal.Client +{ + 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!; + + public static Plugin Plugin { get; set; } = null!; + public static WindowSystem WindowSystem { get; set; } = new(typeof(Service).AssemblyQualifiedName); + internal static RemoteApi RemoteApi { get; set; } = new RemoteApi(); + public static Configuration Configuration { get; set; } = null!; + } +} diff --git a/Pal.Client/WindowSystemExtensions.cs b/Pal.Client/WindowSystemExtensions.cs new file mode 100644 index 0000000..65c41dd --- /dev/null +++ b/Pal.Client/WindowSystemExtensions.cs @@ -0,0 +1,14 @@ +using Dalamud.Interface.Windowing; +using System.Linq; + +namespace Pal.Client +{ + internal static class WindowSystemExtensions + { + public static T GetWindow(this WindowSystem windowSystem) + where T : Window + { + return windowSystem.Windows.Select(w => w as T).FirstOrDefault(w => w != null); + } + } +} diff --git a/Pal.Common/Pal.Common.csproj b/Pal.Common/Pal.Common.csproj new file mode 100644 index 0000000..0d2f45b --- /dev/null +++ b/Pal.Common/Pal.Common.csproj @@ -0,0 +1,8 @@ + + + + net6.0 + enable + enable + + diff --git a/Pal.Common/Protos/account.proto b/Pal.Common/Protos/account.proto new file mode 100644 index 0000000..059eddf --- /dev/null +++ b/Pal.Common/Protos/account.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package account; + +import "google/protobuf/timestamp.proto"; + +service AccountService { + // Accounts are a way to distinguish different players. + // + // Their primary purpose is tracking who has seen a trap/coffer appear to ensure reliability, + // as well as allowing some basic protection against garabage data. + // + // We never store any character data/xiv account data in an account. + rpc CreateAccount(CreateAccountRequest) returns (CreateAccountReply); + + rpc Login(LoginRequest) returns (LoginReply); + + // Ensures that the auth token we use is valid in calls. + rpc Verify(VerifyRequest) returns (VerifyReply); +} + +message CreateAccountRequest { +} + +message CreateAccountReply { + bool success = 1; + string accountId = 2; +} + +message LoginRequest { + string accountId = 1; +} + +message LoginReply { + bool success = 1; + string authToken = 2; + google.protobuf.Timestamp expiresAt = 3; +} + +message VerifyRequest { +} + +message VerifyReply { +} \ No newline at end of file diff --git a/Pal.Common/Protos/palace.proto b/Pal.Common/Protos/palace.proto new file mode 100644 index 0000000..985dbc0 --- /dev/null +++ b/Pal.Common/Protos/palace.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package palace; + +service PalaceService { + rpc DownloadFloors(DownloadFloorsRequest) returns (DownloadFloorsReply); + rpc UploadFloors(UploadFloorsRequest) returns (UploadFloorsReply); +} + +message DownloadFloorsRequest { + uint32 territoryType = 1; +} + +message DownloadFloorsReply { + bool success = 1; + repeated PalaceObject objects = 2; +} + +message UploadFloorsRequest { + uint32 territoryType = 1; + repeated PalaceObject objects = 2; +} + +message UploadFloorsReply { + bool success = 1; +} + +message PalaceObject { + ObjectType type = 1; + float x = 2; + float y = 3; + float z = 4; +} + +enum ObjectType { + OBJECT_TYPE_UNKNOWN = 0; + OBJECT_TYPE_TRAP = 1; + OBJECT_TYPE_HOARD = 2; +} diff --git a/Pal.sln b/Pal.sln new file mode 100644 index 0000000..8ff8f7e --- /dev/null +++ b/Pal.sln @@ -0,0 +1,66 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32929.385 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Server", "Pal.Server\Pal.Server.csproj", "{AB3E2849-DB06-46F6-8457-9AC1096B4125}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Client", "Pal.Client\Pal.Client.csproj", "{7F1985B3-D376-4A91-BC9B-46C2A860F9EF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Common", "Pal.Common\Pal.Common.csproj", "{106389CB-23D6-4784-BD78-A6C5C990CF6E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{753F1752-AB41-4908-8359-C5809A79E5E7}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ECommons", "vendor\ECommons\ECommons\ECommons.csproj", "{D0B37096-5BC3-41B0-8D81-203CBA3932B0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Debug|x64.Build.0 = Debug|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Release|Any CPU.Build.0 = Release|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Release|x64.ActiveCfg = Release|Any CPU + {AB3E2849-DB06-46F6-8457-9AC1096B4125}.Release|x64.Build.0 = Release|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Debug|x64.Build.0 = Debug|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Release|Any CPU.Build.0 = Release|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Release|x64.ActiveCfg = Release|Any CPU + {7F1985B3-D376-4A91-BC9B-46C2A860F9EF}.Release|x64.Build.0 = Release|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Debug|x64.ActiveCfg = Debug|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Debug|x64.Build.0 = Debug|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Release|Any CPU.Build.0 = Release|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Release|x64.ActiveCfg = Release|Any CPU + {106389CB-23D6-4784-BD78-A6C5C990CF6E}.Release|x64.Build.0 = Release|Any CPU + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Debug|Any CPU.ActiveCfg = Debug|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Debug|Any CPU.Build.0 = Debug|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Debug|x64.ActiveCfg = Debug|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Debug|x64.Build.0 = Debug|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|Any CPU.ActiveCfg = Release|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|Any CPU.Build.0 = Release|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|x64.ActiveCfg = Release|x64 + {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EC5A100E-8143-4C3E-BE54-C62E507771E2} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae7d624 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Palace Pal + +Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High. + +Requires Splatoon to be installed. diff --git a/vendor/ECommons b/vendor/ECommons new file mode 160000 index 0000000..e568318 --- /dev/null +++ b/vendor/ECommons @@ -0,0 +1 @@ +Subproject commit e568318fb59dd1170909862735f87ac479021b1b