diff --git a/Dalamud.DrunkenToad/Caching/CacheService.cs b/Dalamud.DrunkenToad/Caching/CacheService.cs
deleted file mode 100644
index fa65b02..0000000
--- a/Dalamud.DrunkenToad/Caching/CacheService.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-namespace Dalamud.DrunkenToad.Caching;
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Core;
-
-///
-/// An abstract base class for cache services that manage thread-safe caching operations.
-/// This class provides a framework for efficiently handling cache updates and reset operations
-/// while ensuring thread safety. It includes a queue () to store
-/// pending cache operations, allowing them to be executed once cache resetting is complete.
-///
-public abstract class CacheService : IDisposable
-{
- private readonly ReaderWriterLockSlim resetLock = new ();
- private readonly Queue pendingOperations = new ();
- private volatile bool isResettingCache;
-
- ///
- /// Event triggered when the cache is updated or changed.
- ///
- public event Action? CacheUpdated;
-
- ///
- /// Disposes of the resources used by the cache service.
- ///
- public void Dispose()
- {
- this.resetLock.Dispose();
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Invokes the CacheUpdated event. Derived classes should call this method
- /// to signal that the cache has been updated.
- ///
- protected void OnCacheUpdated() => this.CacheUpdated?.Invoke();
-
- ///
- /// Executes the specified operation immediately or enqueues it for later execution.
- ///
- /// The operation to execute or enqueue.
- protected void ExecuteOrEnqueue(Action operation)
- {
- if (this.isResettingCache)
- {
- this.resetLock.EnterReadLock();
- try
- {
- this.pendingOperations.Enqueue(operation);
- }
- finally
- {
- this.resetLock.ExitReadLock();
- }
- }
- else
- {
- operation();
- }
- }
-
- ///
- /// Reloads the cache and optionally executes a custom action before processing pending operations.
- ///
- /// An optional custom action to execute before processing pending operations.
- /// A task representing the asynchronous operation.
- protected async Task ExecuteReloadCacheAsync(Func customAction)
- {
- if (this.isResettingCache)
- {
- DalamudContext.PluginLog.Verbose("A cache reset is already in progress. Ignoring this request.");
- return;
- }
-
- await customAction.Invoke();
-
- while (this.pendingOperations.TryDequeue(out var operation))
- {
- operation();
- }
-
- this.isResettingCache = false;
- this.CacheUpdated?.Invoke();
- }
-
- ///
- /// Reloads the cache and optionally executes a custom action before processing pending operations.
- ///
- /// An optional custom action to execute before processing pending operations.
- protected void ExecuteReloadCache(Action customAction)
- {
- if (this.isResettingCache)
- {
- DalamudContext.PluginLog.Verbose("A cache reset is already in progress. Ignoring this request.");
- return;
- }
-
- customAction.Invoke();
-
- while (this.pendingOperations.TryDequeue(out var operation))
- {
- operation();
- }
-
- this.isResettingCache = false;
- this.CacheUpdated?.Invoke();
- }
-}
diff --git a/Dalamud.DrunkenToad/Caching/SortedCacheService.cs b/Dalamud.DrunkenToad/Caching/SortedCacheService.cs
deleted file mode 100644
index 99b328f..0000000
--- a/Dalamud.DrunkenToad/Caching/SortedCacheService.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Dalamud.DrunkenToad.Caching;
-
-using Collections;
-
-///
-/// An abstract base class for cache services that manage thread-safe caching operations with sorted collections.
-/// This class extends and provides a sorted cache collection for managing cached items.
-///
-/// The type of items to be cached.
-public abstract class SortedCacheService : CacheService where T : notnull
-{
- protected ThreadSafeSortedCollection cache = null!;
-}
diff --git a/Dalamud.DrunkenToad/Caching/UnsortedCacheService.cs b/Dalamud.DrunkenToad/Caching/UnsortedCacheService.cs
deleted file mode 100644
index 695c78e..0000000
--- a/Dalamud.DrunkenToad/Caching/UnsortedCacheService.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Dalamud.DrunkenToad.Caching;
-
-using Collections;
-
-///
-/// An abstract base class for cache services that manage thread-safe caching operations with unsorted collections.
-/// This class extends and provides an unsorted cache collection for managing cached items.
-///
-/// The type of items to be cached.
-public abstract class UnsortedCacheService : CacheService where T : notnull
-{
- protected ThreadSafeCollection cache = null!;
-}
diff --git a/Dalamud.DrunkenToad/Core/DalamudContext.cs b/Dalamud.DrunkenToad/Core/DalamudContext.cs
index cd0def6..9e05f3d 100644
--- a/Dalamud.DrunkenToad/Core/DalamudContext.cs
+++ b/Dalamud.DrunkenToad/Core/DalamudContext.cs
@@ -41,6 +41,11 @@ public class DalamudContext
///
public static PlayerEventDispatcher PlayerEventDispatcher { get; private set; } = null!;
+ ///
+ /// Gets service for providing events when social list data is available.
+ ///
+ public static SocialListHandler SocialListHandler { get; private set; } = null!;
+
///
/// Gets service that provides an extended location data API derived from territory change events.
///
@@ -229,6 +234,7 @@ public static bool Initialize(DalamudPluginInterface pluginInterface)
TargetManager = new TargetManagerEx(DalamudTargetManager);
PlayerLocationManager = new PlayerLocationManager(ClientStateHandler, DataManager);
PlayerEventDispatcher = new PlayerEventDispatcher(GameFramework, ObjectCollection);
+ SocialListHandler = new SocialListHandler();
LocManager = new PluginLocalization(pluginInterface);
WindowManager = new WindowManager(pluginInterface);
ToadGui.Initialize(LocManager, DataManager);
@@ -251,6 +257,7 @@ public static void Dispose()
WindowManager.Dispose();
LocManager.Dispose();
PlayerEventDispatcher.Dispose();
+ SocialListHandler.Dispose();
PlayerLocationManager.Dispose();
}
catch (Exception ex)
diff --git a/Dalamud.DrunkenToad/Core/Models/ToadDataCenter.cs b/Dalamud.DrunkenToad/Core/Models/ToadDataCenter.cs
new file mode 100644
index 0000000..67a4bdc
--- /dev/null
+++ b/Dalamud.DrunkenToad/Core/Models/ToadDataCenter.cs
@@ -0,0 +1,17 @@
+namespace Dalamud.DrunkenToad.Core.Models;
+
+///
+/// DataCenter data.
+///
+public class ToadDataCenter
+{
+ ///
+ /// Gets or sets data center id.
+ ///
+ public uint Id { get; set; }
+
+ ///
+ /// Gets data center name.
+ ///
+ public string Name { get; init; } = string.Empty;
+}
diff --git a/Dalamud.DrunkenToad/Core/Models/ToadLocalPlayer.cs b/Dalamud.DrunkenToad/Core/Models/ToadLocalPlayer.cs
index 9ff3515..450fc63 100644
--- a/Dalamud.DrunkenToad/Core/Models/ToadLocalPlayer.cs
+++ b/Dalamud.DrunkenToad/Core/Models/ToadLocalPlayer.cs
@@ -1,5 +1,7 @@
namespace Dalamud.DrunkenToad.Core.Models;
+using Utility;
+
///
/// Subset of key properties from local player for eventing.
///
@@ -25,14 +27,9 @@ public class ToadLocalPlayer
///
public byte[]? Customize;
- ///
- /// Player Company Tag.
- ///
- public string CompanyTag = string.Empty;
-
///
/// Validate if local player is valid.
///
/// Indicator if local player is valid.
- public bool IsValid() => this.ContentId != 0 && this.Name != string.Empty && this.HomeWorld != 0;
+ public bool IsValid() => this.ContentId != 0 && this.Name.IsValidCharacterName() && this.HomeWorld != 0;
}
diff --git a/Dalamud.DrunkenToad/Core/Models/ToadSocialListMember.cs b/Dalamud.DrunkenToad/Core/Models/ToadSocialListMember.cs
new file mode 100644
index 0000000..20e7d21
--- /dev/null
+++ b/Dalamud.DrunkenToad/Core/Models/ToadSocialListMember.cs
@@ -0,0 +1,46 @@
+namespace Dalamud.DrunkenToad.Core.Models;
+
+using Utility;
+
+public class ToadSocialListMember
+{
+ ///
+ /// The content ID of the local character.
+ ///
+ public ulong ContentId;
+
+ ///
+ /// Player Name.
+ ///
+ public string Name = string.Empty;
+
+ ///
+ /// Player HomeWorld ID.
+ ///
+ public ushort HomeWorld;
+
+ ///
+ /// Player is unable to retrieve (no name).
+ ///
+ public bool IsUnableToRetrieve;
+
+ ///
+ /// Validate if player is valid.
+ ///
+ /// Indicator if player is valid.
+ public bool IsValid()
+ {
+ if (this.ContentId == 0)
+ {
+ return false;
+ }
+
+ // If the player's name is null or empty, the player is valid only if they are marked as unable to retrieve.
+ if (string.IsNullOrEmpty(this.Name))
+ {
+ return this.IsUnableToRetrieve;
+ }
+
+ return DalamudContext.DataManager.IsValidWorld(this.HomeWorld) && this.Name.IsValidCharacterName();
+ }
+}
diff --git a/Dalamud.DrunkenToad/Core/Models/ToadWorld.cs b/Dalamud.DrunkenToad/Core/Models/ToadWorld.cs
index fae4b42..9a8c31f 100644
--- a/Dalamud.DrunkenToad/Core/Models/ToadWorld.cs
+++ b/Dalamud.DrunkenToad/Core/Models/ToadWorld.cs
@@ -14,4 +14,9 @@ public class ToadWorld
/// Gets world name.
///
public string Name { get; init; } = string.Empty;
+
+ ///
+ /// Gets world's data center.
+ ///
+ public uint DataCenterId { get; init; }
}
diff --git a/Dalamud.DrunkenToad/Core/Services/Custom/SocialListHandler.cs b/Dalamud.DrunkenToad/Core/Services/Custom/SocialListHandler.cs
new file mode 100644
index 0000000..5cf42c4
--- /dev/null
+++ b/Dalamud.DrunkenToad/Core/Services/Custom/SocialListHandler.cs
@@ -0,0 +1,321 @@
+namespace Dalamud.DrunkenToad.Core.Services;
+
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Runtime.CompilerServices;
+using Core;
+using Hooking;
+using Memory;
+using FFXIVClientStructs.FFXIV.Client.System.Framework;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Client.UI.Info;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using Models;
+
+///
+/// Provides events when data is received for social lists (e.g. Friends List).
+///
+public unsafe class SocialListHandler
+{
+ private const int VtblFuncIndex = 6;
+ private const int BlackListStringArray = 14;
+ private const int BlackListStartIndex = 200;
+
+ private Hook? infoProxyFriendListEndRequestHook;
+
+ private Hook? infoProxyFreeCompanyEndRequestHook;
+
+ private Hook? infoProxyLinkShellEndRequestHook;
+
+ private Hook? infoProxyCrossWorldLinkShellEndRequestHook;
+
+ private Hook? infoProxyBlackListEndRequestHook;
+
+ private bool isEnabled;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SocialListHandler()
+ {
+ DalamudContext.PluginLog.Verbose("Entering SocialListHandler.Start()");
+ this.SetupFriendList();
+ this.SetupFreeCompany();
+ this.SetupLinkShell();
+ this.SetupCrossWorldLinkShell();
+ this.SetupBlackList();
+ }
+
+ public delegate void FriendListReceivedDelegate(List members);
+
+ public delegate void FreeCompanyReceivedDelegate(byte pageCount, byte currentPage, List members);
+
+ public delegate void LinkShellReceivedDelegate(byte index, List members);
+
+ public delegate void CrossWorldLinkShellReceivedDelegate(byte index, List members);
+
+ public delegate void BlackListReceivedDelegate(List members);
+
+ private delegate void InfoProxyFriendListEndRequestDelegate(InfoProxyInterface* a1);
+
+ private delegate void InfoProxyFreeCompanyEndRequestDelegate(InfoProxyInterface* a1);
+
+ private delegate void InfoProxyLinkShellEndRequestDelegate(InfoProxyInterface* a1);
+
+ private delegate void InfoProxyCrossWorldLinkShellEndRequestDelegate(InfoProxyInterface* a1);
+
+ private delegate void InfoProxyBlackListEndRequestDelegate(InfoProxyInterface* a1);
+
+ public event FriendListReceivedDelegate? FriendListReceived;
+
+ public event FreeCompanyReceivedDelegate? FreeCompanyReceived;
+
+ public event LinkShellReceivedDelegate? LinkShellReceived;
+
+ public event CrossWorldLinkShellReceivedDelegate? CrossWorldLinkShellReceived;
+
+ public event BlackListReceivedDelegate? BlackListReceived;
+
+ public void Dispose()
+ {
+ DalamudContext.PluginLog.Verbose("Entering SocialListHandler.Dispose()");
+ this.infoProxyFriendListEndRequestHook?.Dispose();
+ this.infoProxyFreeCompanyEndRequestHook?.Dispose();
+ this.infoProxyLinkShellEndRequestHook?.Dispose();
+ this.infoProxyCrossWorldLinkShellEndRequestHook?.Dispose();
+ this.infoProxyBlackListEndRequestHook?.Dispose();
+ }
+
+ public void Start() => this.isEnabled = true;
+
+ private static List ExtractInfoProxyMembers(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose($"Entering ExtractInfoProxyMembers: EntryCount: {infoProxyInterface->EntryCount}");
+ var members = new List();
+ for (uint i = 0; i < infoProxyInterface->EntryCount; i++)
+ {
+ var entry = ((InfoProxyCommonList*)infoProxyInterface)->GetEntry(i);
+ var member = new ToadSocialListMember
+ {
+ ContentId = entry->ContentId,
+ Name = MemoryHelper.ReadSeStringNullTerminated((nint)entry->Name).ToString(),
+ HomeWorld = entry->HomeWorld,
+ };
+
+ if (string.IsNullOrEmpty(member.Name))
+ {
+ member.IsUnableToRetrieve = true;
+ }
+
+ if (!member.IsValid())
+ {
+ throw new DataException($"Invalid member: {member.Name} {member.ContentId} {member.HomeWorld}");
+ }
+
+ members.Add(member);
+ }
+
+ return members;
+ }
+
+ private static List ExtractInfoProxyBlackListMembers(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose($"Entering ExtractInfoProxyBlackListMembers: EntryCount: {infoProxyInterface->EntryCount}");
+ var members = new List();
+ for (uint i = 0; i < infoProxyInterface->EntryCount; i++)
+ {
+ var member = new ToadSocialListMember
+ {
+ ContentId = (ulong)((InfoProxyBlacklist*)infoProxyInterface)->ContentIdArray[i],
+ };
+
+ var data = (nint*)AtkStage.GetSingleton()->AtkArrayDataHolder->StringArrays[BlackListStringArray]->StringArray;
+ var worldName = MemoryHelper.ReadStringNullTerminated(data[200 + i]);
+ if (!string.IsNullOrEmpty(worldName))
+ {
+ member.Name = MemoryHelper.ReadStringNullTerminated(data[i]);
+ member.HomeWorld = (ushort)DalamudContext.DataManager.GetWorldIdByName(worldName);
+ }
+ else
+ {
+ member.Name = string.Empty;
+ member.HomeWorld = 0;
+ member.IsUnableToRetrieve = true;
+ }
+
+ if (!member.IsValid())
+ {
+ throw new DataException($"Invalid member: {member.Name} {member.ContentId} {member.HomeWorld}");
+ }
+
+ members.Add(member);
+ }
+
+ return members;
+ }
+
+ private void SetupFriendList()
+ {
+ var infoProxyFriendListEndRequestAddress = (nint)Framework.Instance()->GetUiModule()->GetInfoModule()->GetInfoProxyById(InfoProxyId.FriendList)->vtbl[VtblFuncIndex];
+ this.infoProxyFriendListEndRequestHook = DalamudContext.HookManager.HookFromAddress(infoProxyFriendListEndRequestAddress, this.InfoProxyFriendListEndRequestDetour);
+ this.infoProxyFriendListEndRequestHook.Enable();
+ }
+
+ private void SetupFreeCompany()
+ {
+ var infoProxyFreeCompanyEndRequestAddress = (nint)Framework.Instance()->GetUiModule()->GetInfoModule()->GetInfoProxyById(InfoProxyId.FreeCompanyMember)->vtbl[VtblFuncIndex];
+ this.infoProxyFreeCompanyEndRequestHook = DalamudContext.HookManager.HookFromAddress(infoProxyFreeCompanyEndRequestAddress, this.InfoProxyFreeCompanyEndRequestDetour);
+ this.infoProxyFreeCompanyEndRequestHook.Enable();
+ }
+
+ private void SetupLinkShell()
+ {
+ var infoProxyLinkShellEndRequestAddress = (nint)Framework.Instance()->GetUiModule()->GetInfoModule()->GetInfoProxyById(InfoProxyId.LinkShellMember)->vtbl[VtblFuncIndex];
+ this.infoProxyLinkShellEndRequestHook = DalamudContext.HookManager.HookFromAddress(infoProxyLinkShellEndRequestAddress, this.InfoProxyLinkShellEndRequestDetour);
+ this.infoProxyLinkShellEndRequestHook.Enable();
+ }
+
+ private void SetupCrossWorldLinkShell()
+ {
+ var infoProxyCrossWorldLinkShellEndRequestAddress = (nint)Framework.Instance()->GetUiModule()->GetInfoModule()->GetInfoProxyById(InfoProxyId.CrossWorldLinkShellMember)->vtbl[VtblFuncIndex];
+ this.infoProxyCrossWorldLinkShellEndRequestHook = DalamudContext.HookManager.HookFromAddress(infoProxyCrossWorldLinkShellEndRequestAddress, this.InfoProxyCrossWorldLinkShellEndRequestDetour);
+ this.infoProxyCrossWorldLinkShellEndRequestHook.Enable();
+ }
+
+ private void SetupBlackList()
+ {
+ var infoProxyBlackListEndRequestAddress = (nint)Framework.Instance()->GetUiModule()->GetInfoModule()->GetInfoProxyById(InfoProxyId.Blacklist)->vtbl[VtblFuncIndex];
+ this.infoProxyBlackListEndRequestHook = DalamudContext.HookManager.HookFromAddress(infoProxyBlackListEndRequestAddress, this.InfoProxyBlackListEndRequestDetour);
+ this.infoProxyBlackListEndRequestHook.Enable();
+ }
+
+ private void InfoProxyFriendListEndRequestDetour(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose("Entering InfoProxyFriendListEndRequestDetour");
+ this.infoProxyFriendListEndRequestHook?.Original(infoProxyInterface);
+ if (!this.isEnabled)
+ {
+ return;
+ }
+
+ try
+ {
+ this.FriendListReceived?.Invoke(ExtractInfoProxyMembers(infoProxyInterface));
+ }
+ catch (Exception ex)
+ {
+ DalamudContext.PluginLog.Error(ex, "Exception in InfoProxyFriendListEndRequestDetour");
+ }
+ }
+
+ private void InfoProxyFreeCompanyEndRequestDetour(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose("Entering InfoProxyFreeCompanyEndRequestDetour");
+ this.infoProxyFreeCompanyEndRequestHook?.Original(infoProxyInterface);
+ if (!this.isEnabled)
+ {
+ return;
+ }
+
+ try
+ {
+ var proxyFC = (InfoProxyFreeCompany*)Framework.Instance()->GetUiModule()->GetInfoModule()->GetInfoProxyById(InfoProxyId.FreeCompany);
+ if (proxyFC == null || proxyFC->TotalMembers == 0 || proxyFC->ActiveListItemNum != 1)
+ {
+ DalamudContext.PluginLog.Verbose("No FC members to process.");
+ return;
+ }
+
+ var maxPage = (proxyFC->TotalMembers / 200) + 1;
+ if (maxPage is < 1 or > 3)
+ {
+ DalamudContext.PluginLog.Warning($"Invalid FC page count: {maxPage}");
+ return;
+ }
+
+ var agentFC = (nint)Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.FreeCompany);
+ if (agentFC == nint.Zero)
+ {
+ DalamudContext.PluginLog.Warning("Failed to get FC agent.");
+ return;
+ }
+
+ var pageIndex = *(byte*)(agentFC + 0x5E);
+ var currentPage = pageIndex + 1;
+ if (currentPage > maxPage)
+ {
+ DalamudContext.PluginLog.Warning($"Invalid FC page: {currentPage}");
+ return;
+ }
+
+ var members = ExtractInfoProxyMembers(infoProxyInterface);
+ this.FreeCompanyReceived?.Invoke((byte)maxPage, (byte)currentPage, members);
+ }
+ catch (Exception ex)
+ {
+ DalamudContext.PluginLog.Error(ex, "Exception in InfoProxyFreeCompanyEndRequestDetour");
+ }
+ }
+
+ private void InfoProxyCrossWorldLinkShellEndRequestDetour(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose("Entering InfoProxyCrossWorldLinkShellEndRequestDetour");
+ this.infoProxyCrossWorldLinkShellEndRequestHook?.Original(infoProxyInterface);
+ if (!this.isEnabled)
+ {
+ return;
+ }
+
+ try
+ {
+ var agentCrossWorldLinkShell = (AgentCrossWorldLinkshell*)Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.CrossWorldLinkShell);
+ var index = agentCrossWorldLinkShell != null ? agentCrossWorldLinkShell->SelectedCWLSIndex : (byte)0;
+ this.CrossWorldLinkShellReceived?.Invoke(index, ExtractInfoProxyMembers(infoProxyInterface));
+ }
+ catch (Exception ex)
+ {
+ DalamudContext.PluginLog.Error(ex, "Exception in InfoProxyCrossWorldLinkShellEndRequestDetour");
+ }
+ }
+
+ private void InfoProxyLinkShellEndRequestDetour(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose("Entering InfoProxyLinkShellEndRequestDetour");
+ this.infoProxyLinkShellEndRequestHook?.Original(infoProxyInterface);
+ if (!this.isEnabled)
+ {
+ return;
+ }
+
+ try
+ {
+ var agentLinkShell = (AgentLinkshell*)Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Linkshell);
+ var index = agentLinkShell != null ? agentLinkShell->SelectedLSIndex : (byte)0;
+ this.LinkShellReceived?.Invoke(index, ExtractInfoProxyMembers(infoProxyInterface));
+ }
+ catch (Exception ex)
+ {
+ DalamudContext.PluginLog.Error(ex, "Exception in InfoProxyLinkShellEndRequestDetour");
+ }
+ }
+
+ private void InfoProxyBlackListEndRequestDetour(InfoProxyInterface* infoProxyInterface)
+ {
+ DalamudContext.PluginLog.Verbose("Entering InfoProxyBlackListEndRequestDetour");
+ this.infoProxyBlackListEndRequestHook?.Original(infoProxyInterface);
+ if (!this.isEnabled)
+ {
+ return;
+ }
+
+ try
+ {
+ this.BlackListReceived?.Invoke(ExtractInfoProxyBlackListMembers(infoProxyInterface));
+ }
+ catch (Exception ex)
+ {
+ DalamudContext.PluginLog.Error(ex, "Exception in InfoProxyBlackListEndRequestDetour");
+ }
+ }
+}
diff --git a/Dalamud.DrunkenToad/Core/Services/Ex/DataManagerEx.cs b/Dalamud.DrunkenToad/Core/Services/Ex/DataManagerEx.cs
index 223ad58..f8e8c0a 100644
--- a/Dalamud.DrunkenToad/Core/Services/Ex/DataManagerEx.cs
+++ b/Dalamud.DrunkenToad/Core/Services/Ex/DataManagerEx.cs
@@ -33,6 +33,7 @@ public DataManagerEx(IDataManager dataManager, DalamudPluginInterface pluginInte
this.pluginInterface = pluginInterface;
this.Excel = dataManager.Excel;
this.Worlds = this.LoadWorlds();
+ this.DataCenters = this.LoadDataCenters();
this.Locations = this.LoadLocations();
this.ClassJobs = this.LoadClassJobs();
this.Races = this.LoadRaces();
@@ -46,10 +47,15 @@ public DataManagerEx(IDataManager dataManager, DalamudPluginInterface pluginInte
public ExcelModule Excel { get; private set; }
///
- /// Gets all public worlds by world id.
+ /// Gets all public worlds.
///
public Dictionary Worlds { get; }
+ ///
+ /// Gets all data centers.
+ ///
+ public Dictionary DataCenters { get; }
+
///
/// Gets all locations (territory type and content data).
///
@@ -159,6 +165,13 @@ public uint GetWorldIdByName(string worldName)
return 0;
}
+ ///
+ /// Validates if the world id is a valid world.
+ ///
+ /// world id.
+ /// indicator whether world is valid.
+ public bool IsValidWorld(uint worldId) => this.Worlds.ContainsKey(worldId);
+
///
/// Get indicator whether world is a test data center.
///
@@ -183,7 +196,17 @@ private Dictionary LoadWorlds()
return luminaWorlds.ToDictionary(
luminaWorld => luminaWorld.RowId,
- luminaWorld => new ToadWorld { Id = luminaWorld.RowId, Name = this.pluginInterface.Sanitize(luminaWorld.Name) });
+ luminaWorld => new ToadWorld { Id = luminaWorld.RowId, Name = this.pluginInterface.Sanitize(luminaWorld.Name), DataCenterId = luminaWorld.DataCenter.Row });
+ }
+
+ private Dictionary LoadDataCenters()
+ {
+ var dataCenterSheet = this.dataManager.GetExcelSheet() !;
+ var luminaDataCenters = dataCenterSheet.Where(dataCenter => !string.IsNullOrEmpty(dataCenter.Name) && dataCenter.Region != 0 && dataCenter.Region != 7);
+
+ return luminaDataCenters.ToDictionary(
+ luminaDataCenter => luminaDataCenter.RowId,
+ luminaDataCenter => new ToadDataCenter { Id = luminaDataCenter.RowId, Name = this.pluginInterface.Sanitize(luminaDataCenter.Name) });
}
private Dictionary LoadLocations()
diff --git a/Dalamud.DrunkenToad/Core/ToadServiceInitializer.cs b/Dalamud.DrunkenToad/Core/ToadServiceInitializer.cs
index cc244b2..784e6e0 100644
--- a/Dalamud.DrunkenToad/Core/ToadServiceInitializer.cs
+++ b/Dalamud.DrunkenToad/Core/ToadServiceInitializer.cs
@@ -237,6 +237,7 @@ public void AddToadServices(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
}
diff --git a/Dalamud.DrunkenToad/Extensions/ClientStateHandlerExtensions.cs b/Dalamud.DrunkenToad/Extensions/ClientStateHandlerExtensions.cs
index 2c65a2c..e34186e 100644
--- a/Dalamud.DrunkenToad/Extensions/ClientStateHandlerExtensions.cs
+++ b/Dalamud.DrunkenToad/Extensions/ClientStateHandlerExtensions.cs
@@ -26,7 +26,6 @@ public static class ClientStateHandlerExtensions
HomeWorld = value.LocalPlayer.HomeWorld.Id,
ContentId = value.LocalContentId,
Customize = value.LocalPlayer.Customize,
- CompanyTag = value.LocalPlayer.CompanyTag.ToString(),
};
return localPlayer.IsValid() ? localPlayer : null;
diff --git a/Dalamud.DrunkenToad/Gui/ToadGui.cs b/Dalamud.DrunkenToad/Gui/ToadGui.cs
index f105353..da6967f 100644
--- a/Dalamud.DrunkenToad/Gui/ToadGui.cs
+++ b/Dalamud.DrunkenToad/Gui/ToadGui.cs
@@ -11,6 +11,7 @@ namespace Dalamud.DrunkenToad.Gui;
using ImGuiNET;
using Interface;
using Interface.Colors;
+using Interface.Components;
using Interface.Utility;
using Loc.ImGui;
using PluginLocalization = Loc.Localization;
@@ -77,12 +78,37 @@ public static bool Checkbox(string key, ref bool value, bool useLabel = true)
{
var result = ImGui.Checkbox($"###{key}_Checkbox", ref value);
- if (useLabel)
+ if (!useLabel)
{
- ImGui.SameLine();
- LocGui.Text(key);
+ return result;
+ }
+
+ ImGui.SameLine();
+ LocGui.Text(key);
+
+ return result;
+ }
+
+ ///
+ /// Checkbox with better label and loopable key.
+ ///
+ /// localization key.
+ /// suffix for key.
+ /// local value reference.
+ /// use localized label.
+ /// Indicator if changed.
+ public static bool Checkbox(string key, string suffix, ref bool value, bool useLabel = true)
+ {
+ var result = ImGui.Checkbox($"###{key}_{suffix}_Checkbox", ref value);
+
+ if (!useLabel)
+ {
+ return result;
}
+ ImGui.SameLine();
+ LocGui.Text(key);
+
return result;
}
@@ -157,6 +183,34 @@ public static bool Combo(string key, ref int value, IEnumerable options,
return isChanged;
}
+ ///
+ /// Styled and localized ComboBox with loopable key.
+ ///
+ /// primary key.
+ /// suffix for key.
+ /// current selected index value.
+ /// keys for options.
+ /// width (default to fill).
+ /// indicates if combo box value was changed.
+ public static bool Combo(string key, string suffix, ref int value, IEnumerable options, int comboWidth = 100)
+ {
+ var isChanged = false;
+ var localizedOptions = new List();
+ foreach (var option in options)
+ {
+ localizedOptions.Add(Localization.GetString(option));
+ }
+
+ ImGuiHelpers.ScaledDummy(1f);
+ ImGui.SetNextItemWidth(ImGuiUtil.CalcScaledComboWidth(comboWidth));
+ if (ImGui.Combo($"{Localization.GetString(key)}###{suffix}_Combo", ref value, localizedOptions.ToArray(), localizedOptions.Count))
+ {
+ isChanged = true;
+ }
+
+ return isChanged;
+ }
+
///
/// InputText with localization and label option.
///
@@ -396,6 +450,44 @@ public static void Confirm(T item, FontAwesomeIcon icon, string messageKey, r
}
}
+ ///
+ /// Localized Action Prompt for Delete/Restore.
+ ///
+ /// Item type for action to be performed upon (e.g. delete, restore).
+ /// current item being evaluated.
+ /// confirmation message key to display.
+ /// tuple with action state and instance of item under review.
+ public static void Confirm(T item, string messageKey, ref Tuple? request)
+ {
+ if (request == null)
+ {
+ return;
+ }
+
+ dynamic newItem = item!;
+ dynamic savedItem = request.Item2!;
+ if (newItem.Id != savedItem.Id)
+ {
+ return;
+ }
+
+ ImGui.SameLine();
+ LocGui.TextColored(messageKey, ImGuiColors.DalamudYellow);
+ ImGui.SameLine();
+ if (LocGui.SmallButton("Cancel"))
+ {
+ request = new Tuple(ActionRequest.None, request.Item2);
+ }
+
+ ImGui.SameLine();
+ if (LocGui.SmallButton("OK"))
+ {
+ request = new Tuple(ActionRequest.Confirmed, request.Item2);
+ }
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5.0f * ImGuiHelpers.GlobalScale));
+ }
+
///
/// Localized dummy tab to use while waiting for content to load.
///
@@ -427,6 +519,12 @@ public static void DummyTab(string key)
public static void TableSetupColumn(string label, ImGuiTableColumnFlags flags, float initWidthOrWeight) =>
ImGui.TableSetupColumn(label, flags, initWidthOrWeight * ImGuiHelpers.GlobalScale);
+ ///
+ /// Localized HelpMarker.
+ ///
+ /// primary key.
+ public static void HelpMarker(string key) => ImGuiComponents.HelpMarker(Localization.GetString(key));
+
///
/// Localized Colored BeginTabItem.
///