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 ;
2023-02-05 00:18:21 +00:00
using Pal.Client.Net ;
2022-12-22 00:01:09 +00:00
using Pal.Client.Scheduled ;
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-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-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-12-22 00:01:09 +00:00
internal ushort LastTerritory { get ; set ; }
2022-10-23 02:38:58 +00:00
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-12-22 00:01:09 +00:00
internal Queue < IQueueOnFrameworkThread > EarlyEventQueue { get ; } = new ( ) ;
internal Queue < IQueueOnFrameworkThread > LateEventQueue { get ; } = new ( ) ;
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 ;
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
2023-02-02 16:16:03 +00:00
case "near" :
DebugNearest ( m = > true ) ;
break ;
case "tnear" :
DebugNearest ( m = > m . Type = = Marker . EType . Trap ) ;
break ;
case "hnear" :
DebugNearest ( m = > m . Type = = Marker . EType . Hoard ) ;
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 ;
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
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
bool saveMarkers = false ;
2022-12-22 00:01:09 +00:00
while ( EarlyEventQueue . TryDequeue ( out IQueueOnFrameworkThread ? queued ) )
queued ? . Run ( this , ref recreateLayout , ref saveMarkers ) ;
2022-10-23 02:38:58 +00:00
if ( LastTerritory ! = Service . ClientState . TerritoryType )
{
LastTerritory = Service . ClientState . TerritoryType ;
TerritorySyncState = SyncState . NotAttempted ;
2022-11-30 10:37:34 +00:00
if ( IsInDeepDungeon ( ) )
2022-12-22 00:01:09 +00:00
GetFloorMarkers ( 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-12-22 00:01:09 +00:00
while ( LateEventQueue . TryDequeue ( out IQueueOnFrameworkThread ? queued ) )
queued ? . Run ( this , ref recreateLayout , ref saveMarkers ) ;
2022-10-23 02:38:58 +00:00
2022-12-22 00:01:09 +00:00
var currentFloor = GetFloorMarkers ( 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-12-22 00:01:09 +00:00
internal LocalState GetFloorMarkers ( ushort territoryType )
{
return FloorMarkers . GetOrAdd ( territoryType , tt = > LocalState . Load ( tt ) ? ? new LocalState ( tt ) ) ;
}
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 )
{
2022-12-22 00:01:09 +00:00
if ( marker . Seen | | config . Mode = = Configuration . EMode . Online | | ( marker . WasImported & & marker . Imports . Count > 0 ) )
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
}
}
2023-02-02 16:16:03 +00:00
#region Up - / Download
2022-10-23 02:38:58 +00:00
private async Task DownloadMarkersForTerritory ( ushort territoryId )
{
try
{
var ( success , downloadedMarkers ) = await Service . RemoteApi . DownloadRemoteMarkers ( territoryId ) ;
2022-12-22 00:01:09 +00:00
LateEventQueue . Enqueue ( new QueuedSyncResponse
2022-10-31 16:34:47 +00:00
{
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 ) ;
2022-12-22 00:01:09 +00:00
LateEventQueue . Enqueue ( new QueuedSyncResponse
2022-10-31 16:34:47 +00:00
{
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 ) ;
2022-12-22 00:01:09 +00:00
LateEventQueue . Enqueue ( new QueuedSyncResponse
2022-10-31 16:34:47 +00:00
{
Type = SyncType . MarkSeen ,
TerritoryType = territoryId ,
Success = success ,
Markers = markersToUpdate ,
} ) ;
}
catch ( Exception e )
{
DebugMessage = $"{DateTime.Now}\n{e}" ;
}
}
2023-02-02 16:16:03 +00:00
#endregion
2022-10-31 16:34:47 +00:00
2023-02-02 16:16:03 +00:00
#region Command Handling
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}" ) ;
}
}
2023-02-02 16:16:03 +00:00
private void DebugNearest ( Predicate < Marker > predicate )
{
if ( ! IsInDeepDungeon ( ) )
return ;
var state = GetFloorMarkers ( Service . ClientState . TerritoryType ) ;
var playerPosition = Service . ClientState . LocalPlayer ? . Position ;
if ( playerPosition = = null )
return ;
Service . Chat . Print ( $"[Pal] {playerPosition}" ) ;
var nearbyMarkers = state . Markers
. Where ( m = > predicate ( m ) )
. Where ( m = > m . SplatoonElement ! = null & & m . SplatoonElement . color ! = COLOR_INVISIBLE )
. Select ( m = > new { m = m , distance = ( playerPosition - m . Position ) ? . Length ( ) ? ? float . MaxValue } )
. OrderBy ( m = > m . distance )
. Take ( 5 )
. ToList ( ) ;
foreach ( var nearbyMarker in nearbyMarkers )
Service . Chat . Print ( $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToString()?.Substring(0, 8)} - {nearbyMarker.m.Position}" ) ;
}
#endregion
2022-10-23 02:38:58 +00:00
private IList < Marker > GetRelevantGameObjects ( )
{
List < Marker > result = new ( ) ;
2023-02-05 00:18:21 +00:00
for ( int i = 246 ; i < Service . ObjectTable . Length ; i + + )
2022-10-23 02:38:58 +00:00
{
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 } ) ;
2023-02-05 00:18:21 +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 } ) ;
2023-02-05 00:18:21 +00:00
break ;
2022-10-25 21:31:35 +00:00
case 2007357 :
result . Add ( new Marker ( Marker . EType . SilverCoffer , obj . Position ) { Seen = true } ) ;
2023-02-05 00:18:21 +00:00
break ;
2022-10-23 02:38:58 +00:00
}
}
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
}
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
}
}