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