2022-10-23 02:38:58 +00:00
using Dalamud.Game ;
using Dalamud.Game.ClientState.Conditions ;
using Dalamud.Game.ClientState.Objects.Types ;
2022-10-25 17:28:11 +00:00
using Dalamud.Game.Command ;
2022-11-30 21:15:26 +00:00
using Dalamud.Game.Gui ;
2022-10-26 21:38:29 +00:00
using Dalamud.Game.Text ;
using Dalamud.Game.Text.SeStringHandling ;
2022-10-23 02:38:58 +00:00
using Dalamud.Interface.Windowing ;
2022-11-30 20:17:47 +00:00
using Dalamud.Logging ;
2022-10-23 02:38:58 +00:00
using Dalamud.Plugin ;
using ECommons ;
using ECommons.Schedulers ;
using ECommons.SplatoonAPI ;
2022-10-26 18:43:24 +00:00
using Grpc.Core ;
2022-10-23 02:38:58 +00:00
using ImGuiNET ;
2022-10-26 21:38:29 +00:00
using Lumina.Excel.GeneratedSheets ;
2022-12-11 14:22:41 +00:00
using Pal.Client.Net ;
2022-10-26 18:43:24 +00:00
using Pal.Client.Windows ;
2022-11-30 10:37:34 +00:00
using Pal.Common ;
2022-10-23 02:38:58 +00:00
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Linq ;
using System.Numerics ;
using System.Runtime.InteropServices ;
2022-10-26 21:38:29 +00:00
using System.Text.RegularExpressions ;
2022-10-23 02:38:58 +00:00
using System.Threading.Tasks ;
namespace Pal.Client
{
public class Plugin : IDalamudPlugin
{
private const long ON_TERRITORY_CHANGE = - 2 ;
2022-10-26 21:38:29 +00:00
private const uint COLOR_INVISIBLE = 0 ;
2022-10-28 18:31:33 +00:00
private const string SPLATOON_TRAP_HOARD = "PalacePal.TrapHoard" ;
private const string SPLATOON_REGULAR_COFFERS = "PalacePal.RegularCoffers" ;
2022-10-23 02:38:58 +00:00
2022-10-31 16:34:47 +00:00
private readonly ConcurrentQueue < Sync > _pendingSyncResponses = new ( ) ;
2022-10-25 21:31:35 +00:00
private readonly static Dictionary < Marker . EType , MarkerConfig > _markerConfig = new Dictionary < Marker . EType , MarkerConfig >
{
{ Marker . EType . Trap , new MarkerConfig { Radius = 1.7f } } ,
{ Marker . EType . Hoard , new MarkerConfig { Radius = 1.7f , OffsetY = - 0.03f } } ,
{ Marker . EType . SilverCoffer , new MarkerConfig { Radius = 1f } } ,
} ;
2022-10-23 02:38:58 +00:00
private bool _configUpdated = false ;
2022-10-26 21:38:29 +00:00
private LocalizedChatMessages _localizedChatMessages = new ( ) ;
2022-10-23 02:38:58 +00:00
2022-10-29 22:21:17 +00:00
internal ConcurrentDictionary < ushort , LocalState > FloorMarkers { get ; } = new ( ) ;
2022-10-25 21:31:35 +00:00
internal ConcurrentBag < Marker > EphemeralMarkers { get ; set ; } = new ( ) ;
2022-10-23 02:38:58 +00:00
internal ushort LastTerritory { get ; private set ; }
public SyncState TerritorySyncState { get ; set ; }
2022-10-26 21:38:29 +00:00
public PomanderState PomanderOfSight { get ; set ; } = PomanderState . Inactive ;
public PomanderState PomanderOfIntuition { get ; set ; } = PomanderState . Inactive ;
2022-10-30 10:02:49 +00:00
public string? DebugMessage { get ; set ; }
2022-10-23 02:38:58 +00:00
public string Name = > "Palace Pal" ;
2022-11-30 21:15:26 +00:00
public Plugin ( DalamudPluginInterface pluginInterface , ChatGui chat )
2022-10-23 02:38:58 +00:00
{
2022-11-30 21:15:26 +00:00
PluginLog . Information ( $"Install source: {pluginInterface.SourceRepository}" ) ;
#if RELEASE
// You're welcome to remove this code in your fork, as long as:
// - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and
// - you host your own server instance
2022-12-07 16:29:50 +00:00
if ( ! pluginInterface . IsDev
& & ! pluginInterface . SourceRepository . StartsWith ( "https://raw.githubusercontent.com/carvelli/" )
& & ! pluginInterface . SourceRepository . StartsWith ( "https://github.com/carvelli/" ) )
2022-11-30 21:15:26 +00:00
{
chat . PrintError ( "[Palace Pal] Please install this plugin from the official repository at https://github.com/carvelli/Dalamud-Plugins to continue using it." ) ;
throw new InvalidOperationException ( ) ;
}
#endif
2022-10-31 23:44:27 +00:00
ECommonsMain . Init ( pluginInterface , this , Module . SplatoonAPI ) ;
2022-10-23 02:38:58 +00:00
pluginInterface . Create < Service > ( ) ;
Service . Plugin = this ;
2022-10-30 10:02:49 +00:00
Service . Configuration = ( Configuration ? ) pluginInterface . GetPluginConfig ( ) ? ? pluginInterface . Create < Configuration > ( ) ! ;
2022-10-31 15:21:27 +00:00
Service . Configuration . Migrate ( ) ;
2022-10-23 02:38:58 +00:00
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 )
{
Service . WindowSystem . AddWindow ( configWindow ) ;
}
2022-10-26 18:43:24 +00:00
var statisticsWindow = pluginInterface . Create < StatisticsWindow > ( ) ;
if ( statisticsWindow is not null )
{
Service . WindowSystem . AddWindow ( statisticsWindow ) ;
}
2022-10-23 02:38:58 +00:00
pluginInterface . UiBuilder . Draw + = Service . WindowSystem . Draw ;
pluginInterface . UiBuilder . OpenConfigUi + = OnOpenConfigUi ;
Service . Framework . Update + = OnFrameworkUpdate ;
Service . Configuration . Saved + = OnConfigSaved ;
2022-10-26 21:38:29 +00:00
Service . Chat . ChatMessage + = OnChatMessage ;
2022-10-25 17:28:11 +00:00
Service . CommandManager . AddHandler ( "/pal" , new CommandInfo ( OnCommand )
{
HelpMessage = "Open the configuration/debug window"
} ) ;
2022-10-26 21:38:29 +00:00
ReloadLanguageStrings ( ) ;
2022-10-23 02:38:58 +00:00
}
public void OnOpenConfigUi ( )
{
2022-10-30 10:02:49 +00:00
Window ? configWindow ;
2022-10-23 02:38:58 +00:00
if ( Service . Configuration . FirstUse )
configWindow = Service . WindowSystem . GetWindow < AgreementWindow > ( ) ;
else
configWindow = Service . WindowSystem . GetWindow < ConfigWindow > ( ) ;
if ( configWindow ! = null )
configWindow . IsOpen = true ;
}
2022-10-25 17:28:11 +00:00
private void OnCommand ( string command , string arguments )
{
if ( Service . Configuration . FirstUse )
{
Service . Chat . PrintError ( "[Palace Pal] Please finish the first-time setup first." ) ;
return ;
}
2022-10-29 22:21:17 +00:00
try
2022-10-26 18:43:24 +00:00
{
2022-11-30 10:53:26 +00:00
arguments = arguments . Trim ( ) ;
2022-10-29 22:21:17 +00:00
switch ( arguments )
{
case "stats" :
Task . Run ( async ( ) = > await FetchFloorStatistics ( ) ) ;
break ;
2022-11-25 08:43:24 +00:00
case "test-connection" :
case "tc" :
var configWindow = Service . WindowSystem . GetWindow < ConfigWindow > ( ) ;
if ( configWindow = = null )
return ;
configWindow . IsOpen = true ;
configWindow . TestConnection ( ) ;
break ;
2022-10-29 22:21:17 +00:00
#if DEBUG
case "update-saves" :
LocalState . UpdateAll ( ) ;
Service . Chat . Print ( "Updated all locally cached marker files to latest version." ) ;
break ;
#endif
2022-11-30 10:53:26 +00:00
case "" :
2022-11-30 15:39:50 +00:00
case "config" :
2022-10-29 22:21:17 +00:00
Service . WindowSystem . GetWindow < ConfigWindow > ( ) ? . Toggle ( ) ;
break ;
2022-11-30 10:53:26 +00:00
default :
Service . Chat . PrintError ( $"[Palace Pal] Unknown sub-command '{arguments}' for '{command}'." ) ;
break ;
2022-10-29 22:21:17 +00:00
}
}
catch ( Exception e )
{
Service . Chat . PrintError ( $"[Palace Pal] {e}" ) ;
2022-10-26 18:43:24 +00:00
}
2022-10-25 17:28:11 +00:00
}
2022-10-23 02:38:58 +00:00
#region IDisposable Support
protected virtual void Dispose ( bool disposing )
{
if ( ! disposing ) return ;
2022-10-25 17:28:11 +00:00
Service . CommandManager . RemoveHandler ( "/pal" ) ;
2022-10-23 02:38:58 +00:00
Service . PluginInterface . UiBuilder . Draw - = Service . WindowSystem . Draw ;
Service . PluginInterface . UiBuilder . OpenConfigUi - = OnOpenConfigUi ;
Service . Framework . Update - = OnFrameworkUpdate ;
Service . Configuration . Saved - = OnConfigSaved ;
2022-10-26 21:38:29 +00:00
Service . Chat . ChatMessage - = OnChatMessage ;
2022-10-23 02:38:58 +00:00
Service . WindowSystem . RemoveAllWindows ( ) ;
Service . RemoteApi . Dispose ( ) ;
2022-10-28 18:31:33 +00:00
try
{
Splatoon . RemoveDynamicElements ( SPLATOON_TRAP_HOARD ) ;
Splatoon . RemoveDynamicElements ( SPLATOON_REGULAR_COFFERS ) ;
2022-12-01 16:56:26 +00:00
}
2022-10-28 18:31:33 +00:00
catch
{
// destroyed on territory change either way
}
2022-10-31 23:44:27 +00:00
ECommonsMain . Dispose ( ) ;
2022-10-23 02:38:58 +00:00
}
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
#endregion
private void OnConfigSaved ( )
{
_configUpdated = true ;
}
2022-10-26 21:38:29 +00:00
private void OnChatMessage ( XivChatType type , uint senderId , ref SeString sender , ref SeString seMessage , ref bool isHandled )
{
2022-11-01 18:20:43 +00:00
if ( Service . Configuration . FirstUse )
return ;
2022-10-26 21:38:29 +00:00
if ( type ! = ( XivChatType ) 2105 )
return ;
string message = seMessage . ToString ( ) ;
if ( _localizedChatMessages . FloorChanged . IsMatch ( message ) )
{
PomanderOfSight = PomanderState . Inactive ;
if ( PomanderOfIntuition = = PomanderState . FoundOnCurrentFloor )
PomanderOfIntuition = PomanderState . Inactive ;
}
else if ( message . EndsWith ( _localizedChatMessages . MapRevealed ) )
{
PomanderOfSight = PomanderState . Active ;
}
else if ( message . EndsWith ( _localizedChatMessages . AllTrapsRemoved ) )
{
PomanderOfSight = PomanderState . PomanderOfSafetyUsed ;
}
else if ( message . EndsWith ( _localizedChatMessages . HoardNotOnCurrentFloor ) | | message . EndsWith ( _localizedChatMessages . HoardOnCurrentFloor ) )
{
// There is no functional difference between these - if you don't open the marked coffer,
// going to higher floors will keep the pomander active.
PomanderOfIntuition = PomanderState . Active ;
}
else if ( message . EndsWith ( _localizedChatMessages . HoardCofferOpened ) )
{
PomanderOfIntuition = PomanderState . FoundOnCurrentFloor ;
}
else
return ;
}
2022-10-23 02:38:58 +00:00
private void OnFrameworkUpdate ( Framework framework )
{
2022-11-01 18:20:43 +00:00
if ( Service . Configuration . FirstUse )
return ;
2022-10-23 02:38:58 +00:00
try
{
2022-10-25 21:31:35 +00:00
bool recreateLayout = false ;
2022-10-23 02:38:58 +00:00
if ( _configUpdated )
{
if ( Service . Configuration . Mode = = Configuration . EMode . Offline )
{
2022-10-29 22:21:17 +00:00
LocalState . UpdateAll ( ) ;
2022-10-23 02:38:58 +00:00
FloorMarkers . Clear ( ) ;
2022-10-25 21:31:35 +00:00
EphemeralMarkers . Clear ( ) ;
2022-10-23 02:38:58 +00:00
LastTerritory = 0 ;
}
_configUpdated = false ;
2022-10-25 21:31:35 +00:00
recreateLayout = true ;
2022-10-23 02:38:58 +00:00
}
2022-10-26 18:43:24 +00:00
2022-10-23 02:38:58 +00:00
bool saveMarkers = false ;
if ( LastTerritory ! = Service . ClientState . TerritoryType )
{
LastTerritory = Service . ClientState . TerritoryType ;
TerritorySyncState = SyncState . NotAttempted ;
2022-11-30 10:37:34 +00:00
if ( IsInDeepDungeon ( ) )
2022-10-29 22:21:17 +00:00
FloorMarkers [ LastTerritory ] = LocalState . Load ( LastTerritory ) ? ? new LocalState ( LastTerritory ) ;
2022-10-25 21:31:35 +00:00
EphemeralMarkers . Clear ( ) ;
2022-10-26 21:38:29 +00:00
PomanderOfSight = PomanderState . Inactive ;
PomanderOfIntuition = PomanderState . Inactive ;
2022-10-23 02:38:58 +00:00
recreateLayout = true ;
2022-10-24 07:29:46 +00:00
DebugMessage = null ;
2022-10-23 02:38:58 +00:00
}
2022-11-30 10:37:34 +00:00
if ( ! IsInDeepDungeon ( ) )
2022-10-23 02:38:58 +00:00
return ;
if ( Service . Configuration . Mode = = Configuration . EMode . Online & & TerritorySyncState = = SyncState . NotAttempted )
{
TerritorySyncState = SyncState . Started ;
Task . Run ( async ( ) = > await DownloadMarkersForTerritory ( LastTerritory ) ) ;
}
2022-10-31 16:34:47 +00:00
if ( _pendingSyncResponses . Count > 0 )
2022-10-23 02:38:58 +00:00
{
2022-10-31 16:34:47 +00:00
HandleSyncResponses ( ) ;
2022-10-23 02:38:58 +00:00
recreateLayout = true ;
saveMarkers = true ;
}
2022-10-29 22:21:17 +00:00
if ( ! FloorMarkers . TryGetValue ( LastTerritory , out var currentFloor ) )
FloorMarkers [ LastTerritory ] = currentFloor = new LocalState ( LastTerritory ) ;
2022-10-23 02:38:58 +00:00
IList < Marker > visibleMarkers = GetRelevantGameObjects ( ) ;
2022-10-29 22:21:17 +00:00
HandlePersistentMarkers ( currentFloor , visibleMarkers . Where ( x = > x . IsPermanent ( ) ) . ToList ( ) , saveMarkers , recreateLayout ) ;
2022-10-25 21:31:35 +00:00
HandleEphemeralMarkers ( visibleMarkers . Where ( x = > ! x . IsPermanent ( ) ) . ToList ( ) , recreateLayout ) ;
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
}
2022-10-23 02:38:58 +00:00
2022-10-29 22:21:17 +00:00
private void HandlePersistentMarkers ( LocalState currentFloor , IList < Marker > visibleMarkers , bool saveMarkers , bool recreateLayout )
2022-10-25 21:31:35 +00:00
{
2022-10-26 21:38:29 +00:00
var config = Service . Configuration ;
2022-10-29 22:21:17 +00:00
var currentFloorMarkers = currentFloor . Markers ;
2022-10-23 02:38:58 +00:00
2022-10-31 16:34:47 +00:00
bool updateSeenMarkers = false ;
var accountId = Service . RemoteApi . AccountId ;
2022-10-25 21:31:35 +00:00
foreach ( var visibleMarker in visibleMarkers )
{
2022-10-30 10:02:49 +00:00
Marker ? knownMarker = currentFloorMarkers . SingleOrDefault ( x = > x = = visibleMarker ) ;
2022-10-25 21:31:35 +00:00
if ( knownMarker ! = null )
2022-10-23 02:38:58 +00:00
{
2022-10-25 21:31:35 +00:00
if ( ! knownMarker . Seen )
2022-10-23 02:38:58 +00:00
{
2022-10-25 21:31:35 +00:00
knownMarker . Seen = true ;
saveMarkers = true ;
2022-10-23 02:38:58 +00:00
}
2022-10-31 16:34:47 +00:00
// This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states,
// markers discovered afterwards are automatically marked seen.
if ( accountId ! = null & & knownMarker . NetworkId ! = null & & ! knownMarker . RemoteSeenRequested & & ! knownMarker . RemoteSeenOn . Contains ( accountId . Value ) )
updateSeenMarkers = true ;
2022-12-01 16:56:26 +00:00
2022-10-25 21:31:35 +00:00
continue ;
2022-10-23 02:38:58 +00:00
}
2022-10-25 21:31:35 +00:00
currentFloorMarkers . Add ( visibleMarker ) ;
recreateLayout = true ;
saveMarkers = true ;
}
2022-10-23 02:38:58 +00:00
2022-10-26 22:25:09 +00:00
if ( ! recreateLayout & & currentFloorMarkers . Count > 0 & & ( config . OnlyVisibleTrapsAfterPomander | | config . OnlyVisibleHoardAfterPomander ) )
2022-10-26 21:38:29 +00:00
{
2022-10-26 22:25:09 +00:00
try
{
foreach ( var marker in currentFloorMarkers )
2022-10-26 21:38:29 +00:00
{
2022-10-26 22:25:09 +00:00
uint desiredColor = DetermineColor ( marker , visibleMarkers ) ;
if ( marker . SplatoonElement = = null | | ! marker . SplatoonElement . IsValid ( ) )
2022-10-26 21:38:29 +00:00
{
2022-10-26 22:25:09 +00:00
recreateLayout = true ;
break ;
2022-10-26 21:38:29 +00:00
}
2022-10-26 22:25:09 +00:00
if ( marker . SplatoonElement . color ! = desiredColor )
marker . SplatoonElement . color = desiredColor ;
2022-10-26 21:38:29 +00:00
}
}
2022-10-26 22:25:09 +00:00
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
recreateLayout = true ;
}
2022-10-26 21:38:29 +00:00
}
2022-10-31 16:34:47 +00:00
if ( updateSeenMarkers & & accountId ! = null )
{
var markersToUpdate = currentFloorMarkers . Where ( x = > x . Seen & & x . NetworkId ! = null & & ! x . RemoteSeenRequested & & ! x . RemoteSeenOn . Contains ( accountId . Value ) ) . ToList ( ) ;
foreach ( var marker in markersToUpdate )
marker . RemoteSeenRequested = true ;
Task . Run ( async ( ) = > await SyncSeenMarkersForTerritory ( LastTerritory , markersToUpdate ) ) ;
}
2022-10-25 21:31:35 +00:00
if ( saveMarkers )
{
2022-10-29 22:21:17 +00:00
currentFloor . Save ( ) ;
2022-10-23 02:38:58 +00:00
2022-10-25 21:31:35 +00:00
if ( TerritorySyncState = = SyncState . Complete )
{
2022-10-31 16:34:47 +00:00
var markersToUpload = currentFloorMarkers . Where ( x = > x . IsPermanent ( ) & & x . NetworkId = = null & & ! x . UploadRequested ) . ToList ( ) ;
2022-10-31 18:27:54 +00:00
if ( markersToUpload . Count > 0 )
{
foreach ( var marker in markersToUpload )
marker . UploadRequested = true ;
Task . Run ( async ( ) = > await UploadMarkersForTerritory ( LastTerritory , markersToUpload ) ) ;
}
2022-10-25 21:31:35 +00:00
}
}
2022-10-23 02:38:58 +00:00
2022-10-25 21:31:35 +00:00
if ( recreateLayout )
{
2022-10-28 18:31:33 +00:00
Splatoon . RemoveDynamicElements ( SPLATOON_TRAP_HOARD ) ;
2022-10-23 02:38:58 +00:00
2022-10-25 21:31:35 +00:00
List < Element > elements = new List < Element > ( ) ;
foreach ( var marker in currentFloorMarkers )
{
if ( marker . Seen | | config . Mode = = Configuration . EMode . Online )
2022-10-23 02:38:58 +00:00
{
2022-10-25 21:31:35 +00:00
if ( marker . Type = = Marker . EType . Trap & & config . ShowTraps )
2022-10-23 02:38:58 +00:00
{
2022-10-26 21:38:29 +00:00
var element = CreateSplatoonElement ( marker . Type , marker . Position , DetermineColor ( marker , visibleMarkers ) ) ;
2022-10-25 21:31:35 +00:00
marker . SplatoonElement = element ;
elements . Add ( element ) ;
2022-10-23 02:38:58 +00:00
}
2022-10-25 21:31:35 +00:00
else if ( marker . Type = = Marker . EType . Hoard & & config . ShowHoard )
2022-10-23 02:38:58 +00:00
{
2022-10-26 21:38:29 +00:00
var element = CreateSplatoonElement ( marker . Type , marker . Position , DetermineColor ( marker , visibleMarkers ) ) ;
2022-10-25 21:31:35 +00:00
marker . SplatoonElement = element ;
elements . Add ( element ) ;
2022-10-23 02:38:58 +00:00
}
2022-10-25 21:31:35 +00:00
}
2022-10-23 02:38:58 +00:00
}
2022-10-25 21:31:35 +00:00
if ( elements . Count = = 0 )
return ;
// 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
{
2022-10-28 18:31:33 +00:00
Splatoon . AddDynamicElements ( SPLATOON_TRAP_HOARD , elements . ToArray ( ) , new long [ ] { Environment . TickCount64 + 60 * 60 * 1000 , ON_TERRITORY_CHANGE } ) ;
2022-10-25 21:31:35 +00:00
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
} ) ;
2022-10-23 02:38:58 +00:00
}
2022-10-25 21:31:35 +00:00
}
2022-10-26 21:38:29 +00:00
private uint DetermineColor ( Marker marker , IList < Marker > visibleMarkers )
{
if ( marker . Type = = Marker . EType . Trap )
{
if ( PomanderOfSight = = PomanderState . Inactive | | ! Service . Configuration . OnlyVisibleTrapsAfterPomander | | visibleMarkers . Any ( x = > x = = marker ) )
return ImGui . ColorConvertFloat4ToU32 ( Service . Configuration . TrapColor ) ;
else
return COLOR_INVISIBLE ;
}
else
{
if ( PomanderOfIntuition = = PomanderState . Inactive | | ! Service . Configuration . OnlyVisibleHoardAfterPomander | | visibleMarkers . Any ( x = > x = = marker ) )
return ImGui . ColorConvertFloat4ToU32 ( Service . Configuration . HoardColor ) ;
else
return COLOR_INVISIBLE ;
}
}
2022-10-25 21:31:35 +00:00
private void HandleEphemeralMarkers ( IList < Marker > visibleMarkers , bool recreateLayout )
{
recreateLayout | = EphemeralMarkers . Any ( existingMarker = > ! visibleMarkers . Any ( x = > x = = existingMarker ) ) ;
recreateLayout | = visibleMarkers . Any ( visibleMarker = > ! EphemeralMarkers . Any ( x = > x = = visibleMarker ) ) ;
if ( recreateLayout )
2022-10-23 02:38:58 +00:00
{
2022-10-28 18:31:33 +00:00
Splatoon . RemoveDynamicElements ( SPLATOON_REGULAR_COFFERS ) ;
2022-10-25 21:31:35 +00:00
EphemeralMarkers . Clear ( ) ;
var config = Service . Configuration ;
List < Element > elements = new List < Element > ( ) ;
2022-10-26 18:43:24 +00:00
foreach ( var marker in visibleMarkers )
{
2022-10-25 21:31:35 +00:00
EphemeralMarkers . Add ( marker ) ;
if ( marker . Type = = Marker . EType . SilverCoffer & & config . ShowSilverCoffers )
{
var element = CreateSplatoonElement ( marker . Type , marker . Position , config . SilverCofferColor , config . FillSilverCoffers ) ;
marker . SplatoonElement = element ;
elements . Add ( element ) ;
}
}
if ( elements . Count = = 0 )
return ;
new TickScheduler ( delegate
{
try
{
2022-10-28 18:31:33 +00:00
Splatoon . AddDynamicElements ( SPLATOON_REGULAR_COFFERS , elements . ToArray ( ) , new long [ ] { Environment . TickCount64 + 60 * 60 * 1000 , ON_TERRITORY_CHANGE } ) ;
2022-10-25 21:31:35 +00:00
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
} ) ;
2022-10-23 02:38:58 +00:00
}
}
private async Task DownloadMarkersForTerritory ( ushort territoryId )
{
try
{
var ( success , downloadedMarkers ) = await Service . RemoteApi . DownloadRemoteMarkers ( territoryId ) ;
2022-10-31 16:34:47 +00:00
_pendingSyncResponses . Enqueue ( new Sync
{
Type = SyncType . Download ,
TerritoryType = territoryId ,
Success = success ,
Markers = downloadedMarkers
} ) ;
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
}
private async Task UploadMarkersForTerritory ( ushort territoryId , List < Marker > markersToUpload )
{
try
{
var ( success , uploadedMarkers ) = await Service . RemoteApi . UploadMarker ( territoryId , markersToUpload ) ;
_pendingSyncResponses . Enqueue ( new Sync
{
Type = SyncType . Upload ,
TerritoryType = territoryId ,
Success = success ,
Markers = uploadedMarkers
} ) ;
2022-10-23 02:38:58 +00:00
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
}
2022-10-31 16:34:47 +00:00
private async Task SyncSeenMarkersForTerritory ( ushort territoryId , List < Marker > markersToUpdate )
{
try
{
var success = await Service . RemoteApi . MarkAsSeen ( territoryId , markersToUpdate ) ;
_pendingSyncResponses . Enqueue ( new Sync
{
Type = SyncType . MarkSeen ,
TerritoryType = territoryId ,
Success = success ,
Markers = markersToUpdate ,
} ) ;
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
}
2022-10-26 18:43:24 +00:00
private async Task FetchFloorStatistics ( )
{
2022-12-11 14:22:41 +00:00
if ( ! Service . RemoteApi . HasRoleOnCurrentServer ( "statistics:view" ) )
2022-10-26 18:43:24 +00:00
{
2022-12-11 14:22:41 +00:00
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." ) ;
2022-10-26 18:43:24 +00:00
return ;
}
try
{
var ( success , floorStatistics ) = await Service . RemoteApi . FetchStatistics ( ) ;
if ( success )
{
2022-10-30 10:02:49 +00:00
var statisticsWindow = Service . WindowSystem . GetWindow < StatisticsWindow > ( ) ! ;
2022-10-26 18:43:24 +00:00
statisticsWindow . SetFloorData ( floorStatistics ) ;
statisticsWindow . IsOpen = true ;
}
else
{
Service . Chat . PrintError ( "[Palace Pal] Unable to fetch statistics." ) ;
}
}
catch ( RpcException e ) when ( e . StatusCode = = StatusCode . PermissionDenied )
{
2022-12-11 14:22:41 +00:00
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." ) ;
2022-10-26 18:43:24 +00:00
}
catch ( Exception e )
{
Service . Chat . PrintError ( $"[Palace Pal] {e}" ) ;
}
}
2022-10-31 16:34:47 +00:00
private void HandleSyncResponses ( )
2022-10-23 02:38:58 +00:00
{
2022-10-31 16:34:47 +00:00
while ( _pendingSyncResponses . TryDequeue ( out Sync ? sync ) & & sync ! = null )
2022-10-23 02:38:58 +00:00
{
2022-10-31 16:34:47 +00:00
try
2022-10-23 02:38:58 +00:00
{
2022-10-31 16:34:47 +00:00
var territoryId = sync . TerritoryType ;
var remoteMarkers = sync . Markers ;
if ( Service . Configuration . Mode = = Configuration . EMode . Online & & sync . Success & & FloorMarkers . TryGetValue ( territoryId , out var currentFloor ) & & remoteMarkers . Count > 0 )
2022-10-23 02:38:58 +00:00
{
2022-10-31 16:34:47 +00:00
switch ( sync . Type )
2022-10-23 02:38:58 +00:00
{
2022-10-31 16:34:47 +00:00
case SyncType . Download :
case SyncType . Upload :
foreach ( var remoteMarker in remoteMarkers )
{
// Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved.
Marker ? localMarker = currentFloor . Markers . SingleOrDefault ( x = > x = = remoteMarker ) ;
if ( localMarker ! = null )
{
localMarker . NetworkId = remoteMarker . NetworkId ;
continue ;
}
if ( sync . Type = = SyncType . Download )
currentFloor . Markers . Add ( remoteMarker ) ;
}
break ;
case SyncType . MarkSeen :
var accountId = Service . RemoteApi . AccountId ;
if ( accountId = = null )
break ;
foreach ( var remoteMarker in remoteMarkers )
{
Marker ? localMarker = currentFloor . Markers . SingleOrDefault ( x = > x = = remoteMarker ) ;
if ( localMarker ! = null )
localMarker . RemoteSeenOn . Add ( accountId . Value ) ;
}
break ;
2022-10-23 02:38:58 +00:00
}
}
2022-10-31 16:34:47 +00:00
// don't modify state for outdated floors
if ( LastTerritory ! = territoryId )
continue ;
2022-10-23 02:38:58 +00:00
2022-10-31 16:34:47 +00:00
if ( sync . Type = = SyncType . Download )
{
if ( sync . Success )
TerritorySyncState = SyncState . Complete ;
else
TerritorySyncState = SyncState . Failed ;
}
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
if ( sync . Type = = SyncType . Download )
TerritorySyncState = SyncState . Failed ;
}
2022-10-23 02:38:58 +00:00
}
}
private IList < Marker > GetRelevantGameObjects ( )
{
List < Marker > result = new ( ) ;
for ( int i = 246 ; i < Service . ObjectTable . Length ; i + + )
{
2022-10-30 10:02:49 +00:00
GameObject ? obj = Service . ObjectTable [ i ] ;
2022-10-23 02:38:58 +00:00
if ( obj = = null )
continue ;
switch ( ( uint ) Marshal . ReadInt32 ( obj . Address + 128 ) )
{
case 2007182 :
case 2007183 :
case 2007184 :
case 2007185 :
case 2007186 :
case 2009504 :
2022-10-25 21:31:35 +00:00
result . Add ( new Marker ( Marker . EType . Trap , obj . Position ) { Seen = true } ) ;
2022-10-23 02:38:58 +00:00
break ;
2022-12-01 16:56:26 +00:00
2022-10-23 02:38:58 +00:00
case 2007542 :
case 2007543 :
2022-10-25 21:31:35 +00:00
result . Add ( new Marker ( Marker . EType . Hoard , obj . Position ) { Seen = true } ) ;
break ;
case 2007357 :
result . Add ( new Marker ( Marker . EType . SilverCoffer , obj . Position ) { Seen = true } ) ;
2022-10-23 02:38:58 +00:00
break ;
}
}
return result ;
}
2022-11-30 10:37:34 +00:00
internal bool IsInDeepDungeon ( ) = >
2022-12-01 16:56:26 +00:00
Service . ClientState . IsLoggedIn
2022-11-30 10:37:34 +00:00
& & Service . Condition [ ConditionFlag . InDeepDungeon ]
& & typeof ( ETerritoryType ) . IsEnumDefined ( Service . ClientState . TerritoryType ) ;
2022-10-23 02:38:58 +00:00
2022-12-01 16:56:26 +00:00
internal static Element CreateSplatoonElement ( Marker . EType type , Vector3 pos , Vector4 color , bool fill = false )
2022-10-26 21:38:29 +00:00
= > CreateSplatoonElement ( type , pos , ImGui . ColorConvertFloat4ToU32 ( color ) , fill ) ;
internal static Element CreateSplatoonElement ( Marker . EType type , Vector3 pos , uint color , bool fill = false )
2022-10-23 02:38:58 +00:00
{
return new Element ( ElementType . CircleAtFixedCoordinates )
{
refX = pos . X ,
refY = pos . Z , // z and y are swapped
refZ = pos . Y ,
offX = 0 ,
offY = 0 ,
2022-10-25 21:31:35 +00:00
offZ = _markerConfig [ type ] . OffsetY ,
Filled = fill ,
radius = _markerConfig [ type ] . Radius ,
2022-10-23 02:38:58 +00:00
FillStep = 1 ,
2022-10-26 21:38:29 +00:00
color = color ,
2022-10-23 02:38:58 +00:00
thicc = 2 ,
} ;
}
2022-10-26 21:38:29 +00:00
private void ReloadLanguageStrings ( )
{
_localizedChatMessages = new LocalizedChatMessages
{
MapRevealed = GetLocalizedString ( 7256 ) ,
AllTrapsRemoved = GetLocalizedString ( 7255 ) ,
HoardOnCurrentFloor = GetLocalizedString ( 7272 ) ,
HoardNotOnCurrentFloor = GetLocalizedString ( 7273 ) ,
HoardCofferOpened = GetLocalizedString ( 7274 ) ,
FloorChanged = new Regex ( "^" + GetLocalizedString ( 7270 ) . Replace ( "\u0002 \u0003\ufffd\u0002\u0003" , @"(\d+)" ) + "$" ) ,
} ;
}
private string GetLocalizedString ( uint id )
{
2022-10-30 10:02:49 +00:00
return Service . DataManager . GetExcelSheet < LogMessage > ( ) ? . GetRow ( id ) ? . Text ? . ToString ( ) ? ? "Unknown" ;
2022-10-26 21:38:29 +00:00
}
2022-10-31 16:34:47 +00:00
internal class Sync
{
public SyncType Type { get ; set ; }
public ushort TerritoryType { get ; set ; }
public bool Success { get ; set ; }
public List < Marker > Markers { get ; set ; } = new ( ) ;
}
2022-10-23 02:38:58 +00:00
public enum SyncState
{
NotAttempted ,
2022-10-31 16:34:47 +00:00
NotNeeded ,
2022-10-23 02:38:58 +00:00
Started ,
Complete ,
Failed ,
}
2022-10-25 21:31:35 +00:00
2022-10-31 16:34:47 +00:00
public enum SyncType
{
Upload ,
Download ,
MarkSeen ,
}
2022-10-26 21:38:29 +00:00
public enum PomanderState
{
Inactive ,
Active ,
FoundOnCurrentFloor ,
PomanderOfSafetyUsed ,
}
2022-10-25 21:31:35 +00:00
private class MarkerConfig
{
public float OffsetY { get ; set ; } = 0 ;
public float Radius { get ; set ; } = 0.25f ;
}
2022-10-26 21:38:29 +00:00
private class LocalizedChatMessages
{
public string MapRevealed { get ; set ; } = "???" ; //"The map for this floor has been revealed!";
public string AllTrapsRemoved { get ; set ; } = "???" ; // "All the traps on this floor have disappeared!";
public string HoardOnCurrentFloor { get ; set ; } = "???" ; // "You sense the Accursed Hoard calling you...";
public string HoardNotOnCurrentFloor { get ; set ; } = "???" ; // "You do not sense the call of the Accursed Hoard on this floor...";
public string HoardCofferOpened { get ; set ; } = "???" ; // "You discover a piece of the Accursed Hoard!";
public Regex FloorChanged { get ; set ; } = new Regex ( @"This isn't a game message, but will be replaced" ) ; // new Regex(@"^Floor (\d+)$");
}
2022-10-23 02:38:58 +00:00
}
}