Initial Commit

Change release db path

cx
rendering
Liza 2022-10-23 04:38:58 +02:00
commit aae717955d
20 changed files with 1477 additions and 0 deletions

340
.gitignore vendored Normal file
View File

@ -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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "vendor/ECommons"]
path = vendor/ECommons
url = https://github.com/NightmareXIV/ECommons

View File

@ -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");
}
}
}

192
Pal.Client/ConfigWindow.cs Normal file
View File

@ -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<Element>
{
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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// Fetches trap locations from remote server.
/// </summary>
Online = 1,
/// <summary>
/// Only shows traps found by yourself uisng a pomander of sight.
/// </summary>
Offline = 2,
}
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<DalamudPackager
ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="false"/>
</Target>
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<DalamudPackager
ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="true"/>
</Target>
</Project>

View File

@ -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

37
Pal.Client/Marker.cs Normal file
View File

@ -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;
}
}
}

View File

@ -0,0 +1,67 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>9.0</LangVersion>
<Version>1.0.0.0</Version>
</PropertyGroup>
<PropertyGroup>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>Palace Pal</AssemblyName>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.8" />
<PackageReference Include="Google.Protobuf" Version="3.21.8" />
<PackageReference Include="Grpc.Net.Client" Version="2.49.0" />
<PackageReference Include="Grpc.Tools" Version="2.50.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\Pal.Common\Protos\account.proto" Link="Protos\account.proto" GrpcServices="Client" Access="Internal" />
<Protobuf Include="..\Pal.Common\Protos\palace.proto" Link="Protos\palace.proto" GrpcServices="Client" Access="Internal" />
</ItemGroup>
<ItemGroup>
<!--You may need to adjust these paths yourself. These point to a Dalamud assembly in AppData.-->
<Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>

View File

@ -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" ]
}

360
Pal.Client/Plugin.cs Normal file
View File

@ -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<Marker> markers)> _remoteDownloads = new();
private bool _configUpdated = false;
internal ConcurrentDictionary<ushort, ConcurrentBag<Marker>> 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>();
Service.Plugin = this;
Service.Configuration = (Configuration)pluginInterface.GetPluginConfig() ?? pluginInterface.Create<Configuration>();
var agreementWindow = pluginInterface.Create<AgreementWindow>();
if (agreementWindow is not null)
{
agreementWindow.IsOpen = Service.Configuration.FirstUse;
Service.WindowSystem.AddWindow(agreementWindow);
}
var configWindow = pluginInterface.Create<ConfigWindow>();
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<AgreementWindow>();
else
configWindow = Service.WindowSystem.GetWindow<ConfigWindow>();
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<List<Marker>>(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<Marker>(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<Marker>();
IList<Marker> 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<Element> elements = new List<Element>();
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<Marker> LoadSavedMarkers()
{
string path = GetSaveForCurrentTerritory();
if (File.Exists(path))
return JsonSerializer.Deserialize<List<Marker>>(File.ReadAllText(path), new JsonSerializerOptions { IncludeFields = true }).Where(x => x.Seen || Service.Configuration.Mode == Configuration.EMode.Online).ToList();
else
return new List<Marker>();
}
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<Marker> GetRelevantGameObjects()
{
List<Marker> 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,
}
}
}

128
Pal.Client/RemoteApi.cs Normal file
View File

@ -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<bool> 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<string> 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<Marker>)> 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<bool> UploadMarker(ushort territoryType, IList<Marker> 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;
}
}
}

26
Pal.Client/Service.cs Normal file
View File

@ -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!;
}
}

View File

@ -0,0 +1,14 @@
using Dalamud.Interface.Windowing;
using System.Linq;
namespace Pal.Client
{
internal static class WindowSystemExtensions
{
public static T GetWindow<T>(this WindowSystem windowSystem)
where T : Window
{
return windowSystem.Windows.Select(w => w as T).FirstOrDefault(w => w != null);
}
}
}

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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 {
}

View File

@ -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;
}

66
Pal.sln Normal file
View File

@ -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

5
README.md Normal file
View File

@ -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.

1
vendor/ECommons vendored Submodule

@ -0,0 +1 @@
Subproject commit e568318fb59dd1170909862735f87ac479021b1b