From 2cf6ded1cd09c6d5c1b744c59b70ad1eef39bb72 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 13 Oct 2023 11:38:52 +0200 Subject: [PATCH] Update to API 9 without the Kami touch --- .gitignore | 3 +- .gitmodules | 3 - CurrencyAlert/CurrencyAlert.csproj | 3 +- ...cyAlert.json => CurrencyAlertClassic.json} | 2 +- CurrencyAlert/DataModels/CurrencyInfo.cs | 5 +- CurrencyAlert/Service.cs | 7 +- CurrencyAlert/System.cs/CurrencyTracker.cs | 2 +- .../Components/GeneralSettingsSelectable.cs | 3 +- .../Components/TrackedCurrencySelectable.cs | 3 +- CurrencyAlert/Windows/ConfigurationWindow.cs | 1 + CurrencyAlert/packages.lock.json | 6 +- KamiLib | 1 - KamiLib/.gitignore | 4 + KamiLib/.idea/.idea.KamiLib/.idea/.gitignore | 13 + .../.idea/.idea.KamiLib/.idea/encodings.xml | 4 + .../.idea/.idea.KamiLib/.idea/indexLayout.xml | 8 + KamiLib/.idea/.idea.KamiLib/.idea/vcs.xml | 6 + KamiLib/Atk/AtkValueHelper.cs | 55 ++ KamiLib/Atk/NodeHelper.cs | 106 ++++ KamiLib/Blacklist/BlacklistDraw.cs | 208 ++++++++ KamiLib/Blacklist/SearchResult.cs | 12 + KamiLib/Caching/IconCache.cs | 69 +++ KamiLib/Caching/LuminaCache.cs | 61 +++ KamiLib/ChatCommands/Chat.cs | 55 ++ KamiLib/ChatCommands/ChatPayloadManager.cs | 47 ++ KamiLib/ChatCommands/Command.cs | 28 + KamiLib/ChatCommands/CommandData.cs | 39 ++ KamiLib/ChatCommands/CommandManager.cs | 66 +++ KamiLib/ChatCommands/HelpCommands.cs | 92 ++++ KamiLib/ChatCommands/OpenWindowCommand.cs | 62 +++ KamiLib/ChatCommands/SubCommand.cs | 42 ++ KamiLib/Configuration/Migrate.cs | 66 +++ KamiLib/Configuration/Setting.cs | 45 ++ KamiLib/Drawing/Colors.cs | 31 ++ KamiLib/Drawing/DrawList.cs | 491 ++++++++++++++++++ KamiLib/Drawing/InfoBox.cs | 144 +++++ KamiLib/Drawing/InfoBoxList.cs | 35 ++ KamiLib/Drawing/InfoBoxTable.cs | 115 ++++ KamiLib/Drawing/InfoBoxTableRow.cs | 24 + KamiLib/Drawing/TabBar.cs | 50 ++ KamiLib/Extensions/PartyListExtensions.cs | 38 ++ KamiLib/Extensions/PartyMemberExtensions.cs | 18 + .../Extensions/PlayerCharacterExtensions.cs | 62 +++ KamiLib/Extensions/TerritoryTypeExtensions.cs | 30 ++ KamiLib/GameState/Condition.cs | 79 +++ KamiLib/GameState/DutyState.cs | 124 +++++ KamiLib/Hooking/Delegates.cs | 26 + KamiLib/Hooking/Safety.cs | 32 ++ KamiLib/Interfaces/IDrawable.cs | 6 + KamiLib/Interfaces/IInfoBoxTableRow.cs | 18 + KamiLib/Interfaces/IPluginCommand.cs | 40 ++ KamiLib/Interfaces/ISelectable.cs | 8 + KamiLib/Interfaces/ISubCommand.cs | 14 + KamiLib/Interfaces/ITabItem.cs | 8 + KamiLib/KamiCommon.cs | 51 ++ KamiLib/KamiLib.csproj | 72 +++ KamiLib/KamiLib.sln | 16 + KamiLib/LICENSE | 14 + KamiLib/Localization/LocalizationManager.cs | 41 ++ KamiLib/Localization/Strings.Designer.cs | 331 ++++++++++++ KamiLib/Localization/Strings.de.resx | 109 ++++ KamiLib/Localization/Strings.es.resx | 109 ++++ KamiLib/Localization/Strings.fr.resx | 109 ++++ KamiLib/Localization/Strings.ja.resx | 109 ++++ KamiLib/Localization/Strings.resx | 111 ++++ KamiLib/Misc/DutyLists.cs | 76 +++ KamiLib/Misc/Time.cs | 116 +++++ KamiLib/Service.cs | 22 + KamiLib/Teleporter/TeleportManager.cs | 151 ++++++ KamiLib/UserInterface/GameUserInterface.cs | 61 +++ KamiLib/Windows/DrawFlags.cs | 19 + KamiLib/Windows/SelectionList.cs | 67 +++ KamiLib/Windows/SelectionWindow.cs | 87 ++++ KamiLib/Windows/WindowManager.cs | 62 +++ 74 files changed, 4235 insertions(+), 18 deletions(-) delete mode 100644 .gitmodules rename CurrencyAlert/{CurrencyAlert.json => CurrencyAlertClassic.json} (92%) delete mode 160000 KamiLib create mode 100644 KamiLib/.gitignore create mode 100644 KamiLib/.idea/.idea.KamiLib/.idea/.gitignore create mode 100644 KamiLib/.idea/.idea.KamiLib/.idea/encodings.xml create mode 100644 KamiLib/.idea/.idea.KamiLib/.idea/indexLayout.xml create mode 100644 KamiLib/.idea/.idea.KamiLib/.idea/vcs.xml create mode 100644 KamiLib/Atk/AtkValueHelper.cs create mode 100644 KamiLib/Atk/NodeHelper.cs create mode 100644 KamiLib/Blacklist/BlacklistDraw.cs create mode 100644 KamiLib/Blacklist/SearchResult.cs create mode 100644 KamiLib/Caching/IconCache.cs create mode 100644 KamiLib/Caching/LuminaCache.cs create mode 100644 KamiLib/ChatCommands/Chat.cs create mode 100644 KamiLib/ChatCommands/ChatPayloadManager.cs create mode 100644 KamiLib/ChatCommands/Command.cs create mode 100644 KamiLib/ChatCommands/CommandData.cs create mode 100644 KamiLib/ChatCommands/CommandManager.cs create mode 100644 KamiLib/ChatCommands/HelpCommands.cs create mode 100644 KamiLib/ChatCommands/OpenWindowCommand.cs create mode 100644 KamiLib/ChatCommands/SubCommand.cs create mode 100644 KamiLib/Configuration/Migrate.cs create mode 100644 KamiLib/Configuration/Setting.cs create mode 100644 KamiLib/Drawing/Colors.cs create mode 100644 KamiLib/Drawing/DrawList.cs create mode 100644 KamiLib/Drawing/InfoBox.cs create mode 100644 KamiLib/Drawing/InfoBoxList.cs create mode 100644 KamiLib/Drawing/InfoBoxTable.cs create mode 100644 KamiLib/Drawing/InfoBoxTableRow.cs create mode 100644 KamiLib/Drawing/TabBar.cs create mode 100644 KamiLib/Extensions/PartyListExtensions.cs create mode 100644 KamiLib/Extensions/PartyMemberExtensions.cs create mode 100644 KamiLib/Extensions/PlayerCharacterExtensions.cs create mode 100644 KamiLib/Extensions/TerritoryTypeExtensions.cs create mode 100644 KamiLib/GameState/Condition.cs create mode 100644 KamiLib/GameState/DutyState.cs create mode 100644 KamiLib/Hooking/Delegates.cs create mode 100644 KamiLib/Hooking/Safety.cs create mode 100644 KamiLib/Interfaces/IDrawable.cs create mode 100644 KamiLib/Interfaces/IInfoBoxTableRow.cs create mode 100644 KamiLib/Interfaces/IPluginCommand.cs create mode 100644 KamiLib/Interfaces/ISelectable.cs create mode 100644 KamiLib/Interfaces/ISubCommand.cs create mode 100644 KamiLib/Interfaces/ITabItem.cs create mode 100644 KamiLib/KamiCommon.cs create mode 100644 KamiLib/KamiLib.csproj create mode 100644 KamiLib/KamiLib.sln create mode 100644 KamiLib/LICENSE create mode 100644 KamiLib/Localization/LocalizationManager.cs create mode 100644 KamiLib/Localization/Strings.Designer.cs create mode 100644 KamiLib/Localization/Strings.de.resx create mode 100644 KamiLib/Localization/Strings.es.resx create mode 100644 KamiLib/Localization/Strings.fr.resx create mode 100644 KamiLib/Localization/Strings.ja.resx create mode 100644 KamiLib/Localization/Strings.resx create mode 100644 KamiLib/Misc/DutyLists.cs create mode 100644 KamiLib/Misc/Time.cs create mode 100644 KamiLib/Service.cs create mode 100644 KamiLib/Teleporter/TeleportManager.cs create mode 100644 KamiLib/UserInterface/GameUserInterface.cs create mode 100644 KamiLib/Windows/DrawFlags.cs create mode 100644 KamiLib/Windows/SelectionList.cs create mode 100644 KamiLib/Windows/SelectionWindow.cs create mode 100644 KamiLib/Windows/WindowManager.cs diff --git a/.gitignore b/.gitignore index 7990fe7..67891a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs/ obj/ bin/ -*.user \ No newline at end of file +*.user +/.idea diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f907bfb..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "KamiLib"] - path = KamiLib - url = https://github.com/MidoriKami/KamiLib diff --git a/CurrencyAlert/CurrencyAlert.csproj b/CurrencyAlert/CurrencyAlert.csproj index a51db62..1bba633 100644 --- a/CurrencyAlert/CurrencyAlert.csproj +++ b/CurrencyAlert/CurrencyAlert.csproj @@ -1,6 +1,7 @@  + CurrencyAlertClassic 0.5.0.1 Currency Alert https://github.com/Lharz/xiv-currency-alert @@ -26,7 +27,7 @@ - + $(DalamudLibPath)FFXIVClientStructs.dll false diff --git a/CurrencyAlert/CurrencyAlert.json b/CurrencyAlert/CurrencyAlertClassic.json similarity index 92% rename from CurrencyAlert/CurrencyAlert.json rename to CurrencyAlert/CurrencyAlertClassic.json index 3fb3f68..ec60417 100644 --- a/CurrencyAlert/CurrencyAlert.json +++ b/CurrencyAlert/CurrencyAlertClassic.json @@ -1,6 +1,6 @@ { "Author": "Lharz", - "Name": "CurrencyAlert", + "Name": "CurrencyAlert Classic", "Punchline": "Display alerts upon reaching configurable currencies thresholds (such as Poetics or PVP marks).", "Description": "This plugin lets you easily manage your various currencies to prevent from wasting precious resources.", "Tags": [ diff --git a/CurrencyAlert/DataModels/CurrencyInfo.cs b/CurrencyAlert/DataModels/CurrencyInfo.cs index ab474bc..de380aa 100644 --- a/CurrencyAlert/DataModels/CurrencyInfo.cs +++ b/CurrencyAlert/DataModels/CurrencyInfo.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Dalamud.Interface.Internal; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using ImGuiScene; @@ -13,7 +14,7 @@ public class CurrencyInfo : IDisposable public uint ItemID { get; } public string ItemName { get; } = string.Empty; public uint IconID { get; } - public TextureWrap? IconTexture { get; } + public IDalamudTextureWrap? IconTexture { get; } public CurrencyInfo(CurrencyName currency) { @@ -81,4 +82,4 @@ public class CurrencyInfo : IDisposable .First() .Item.Row; } -} \ No newline at end of file +} diff --git a/CurrencyAlert/Service.cs b/CurrencyAlert/Service.cs index f0b953f..5ce976f 100644 --- a/CurrencyAlert/Service.cs +++ b/CurrencyAlert/Service.cs @@ -3,15 +3,16 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.IoC; using Dalamud.Plugin; +using Dalamud.Plugin.Services; namespace CurrencyAlert; public class Service { [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService] public static Framework Framework { get; private set; } = null!; - [PluginService] public static ClientState ClientState { get; private set; } = null!; + [PluginService] public static IFramework Framework { get; private set; } = null!; + [PluginService] public static IClientState ClientState { get; private set; } = null!; public static Configuration Configuration { get; set; } = null!; public static CurrencyTracker CurrencyTracker { get; set; } = null!; -} \ No newline at end of file +} diff --git a/CurrencyAlert/System.cs/CurrencyTracker.cs b/CurrencyAlert/System.cs/CurrencyTracker.cs index 5afb866..84319e7 100644 --- a/CurrencyAlert/System.cs/CurrencyTracker.cs +++ b/CurrencyAlert/System.cs/CurrencyTracker.cs @@ -43,7 +43,7 @@ public class CurrencyTracker : IDisposable } } - private void OnZoneChange(object? sender, ushort e) + private void OnZoneChange(ushort e) { if (!Service.Configuration.ChatNotification) return; if (Condition.IsBoundByDuty()) return; diff --git a/CurrencyAlert/Windows/Components/GeneralSettingsSelectable.cs b/CurrencyAlert/Windows/Components/GeneralSettingsSelectable.cs index 81826e0..057c783 100644 --- a/CurrencyAlert/Windows/Components/GeneralSettingsSelectable.cs +++ b/CurrencyAlert/Windows/Components/GeneralSettingsSelectable.cs @@ -1,6 +1,7 @@ using CurrencyAlert.DataModels; using CurrencyAlert.Localization; using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using KamiLib.Drawing; using KamiLib.Interfaces; @@ -45,4 +46,4 @@ public class GeneralSettingsSelectable : ISelectable, IDrawable .AddConfigColor(Strings.TextColor, Strings.Default, DisplaySettings.TextColor, Colors.White) .Draw(); } -} \ No newline at end of file +} diff --git a/CurrencyAlert/Windows/Components/TrackedCurrencySelectable.cs b/CurrencyAlert/Windows/Components/TrackedCurrencySelectable.cs index 632efb4..d1036c0 100644 --- a/CurrencyAlert/Windows/Components/TrackedCurrencySelectable.cs +++ b/CurrencyAlert/Windows/Components/TrackedCurrencySelectable.cs @@ -2,6 +2,7 @@ using CurrencyAlert.DataModels; using CurrencyAlert.Localization; using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using KamiLib.Drawing; using KamiLib.Interfaces; @@ -43,4 +44,4 @@ public class TrackedCurrencySelectable : ISelectable, IDrawable .AddInputInt(Strings.Threshold, currency.Threshold, 0, 100000, 0, 0, innerWidth / 2.0f) .Draw(); } -} \ No newline at end of file +} diff --git a/CurrencyAlert/Windows/ConfigurationWindow.cs b/CurrencyAlert/Windows/ConfigurationWindow.cs index 36333b5..678360c 100644 --- a/CurrencyAlert/Windows/ConfigurationWindow.cs +++ b/CurrencyAlert/Windows/ConfigurationWindow.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Reflection; using CurrencyAlert.Windows.Components; using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using KamiLib.Drawing; using KamiLib.Interfaces; diff --git a/CurrencyAlert/packages.lock.json b/CurrencyAlert/packages.lock.json index 3a81152..7753b54 100644 --- a/CurrencyAlert/packages.lock.json +++ b/CurrencyAlert/packages.lock.json @@ -4,9 +4,9 @@ "net7.0-windows7.0": { "DalamudPackager": { "type": "Direct", - "requested": "[2.1.10, )", - "resolved": "2.1.10", - "contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw==" + "requested": "[2.1.12, )", + "resolved": "2.1.12", + "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" }, "kamilib": { "type": "Project" diff --git a/KamiLib b/KamiLib deleted file mode 160000 index f50ff89..0000000 --- a/KamiLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f50ff8903e0adeb2dfe1d64103f54da13e055f89 diff --git a/KamiLib/.gitignore b/KamiLib/.gitignore new file mode 100644 index 0000000..f5173de --- /dev/null +++ b/KamiLib/.gitignore @@ -0,0 +1,4 @@ +/obj/ +/bin/x64/Debug/KamiLib.deps.json +/bin/x64/Debug/KamiLib.dll +/bin/x64/Debug/KamiLib.pdb diff --git a/KamiLib/.idea/.idea.KamiLib/.idea/.gitignore b/KamiLib/.idea/.idea.KamiLib/.idea/.gitignore new file mode 100644 index 0000000..98fc7c4 --- /dev/null +++ b/KamiLib/.idea/.idea.KamiLib/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/modules.xml +/.idea.KamiLib.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/KamiLib/.idea/.idea.KamiLib/.idea/encodings.xml b/KamiLib/.idea/.idea.KamiLib/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/KamiLib/.idea/.idea.KamiLib/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/KamiLib/.idea/.idea.KamiLib/.idea/indexLayout.xml b/KamiLib/.idea/.idea.KamiLib/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/KamiLib/.idea/.idea.KamiLib/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/KamiLib/.idea/.idea.KamiLib/.idea/vcs.xml b/KamiLib/.idea/.idea.KamiLib/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/KamiLib/.idea/.idea.KamiLib/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/KamiLib/Atk/AtkValueHelper.cs b/KamiLib/Atk/AtkValueHelper.cs new file mode 100644 index 0000000..747e4b2 --- /dev/null +++ b/KamiLib/Atk/AtkValueHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace KamiLib.Atk; + +public static class AtkValueHelper +{ + public static unsafe void PrintAtkValue(AtkValue value, int index) + { + switch (value.Type) + { + case ValueType.Int: + PluginLog.Debug($"[{index:D3}] [{"int", 7}]: {value.Int}"); + break; + case ValueType.Bool: + PluginLog.Debug($"[{index:D3}] [{"bool", 7}]: {(value.Byte != 0 ? "true" : "false")}"); + break; + case ValueType.UInt: + PluginLog.Debug($"[{index:D3}] [{"uint", 7}]: {value.UInt}"); + break; + case ValueType.Float: + PluginLog.Debug($"[{index:D3}] [{"float", 7}]: {value.Float}"); + break; + case ValueType.String: + PluginLog.Debug($"[{index:D3}] [{"string", 7}]: {Marshal.PtrToStringUTF8(new nint(value.String))}"); + break; + case ValueType.String8: + PluginLog.Debug($"[{index:D3}] [{"string8", 7}]: {Marshal.PtrToStringUTF8(new nint(value.String))}"); + break; + case ValueType.Vector: + PluginLog.Debug($"[{index:D3}] [{"vector", 7}]: No Representation Implemented"); + break; + case ValueType.AllocatedString: + PluginLog.Debug($"[{index:D3}] [{"aString", 7}]: {Marshal.PtrToStringUTF8(new nint(value.String))}"); + break; + case ValueType.AllocatedVector: + PluginLog.Debug($"[{index:D3}] [{"aVector", 7}]: No Representation Implemented"); + break; + default: + PluginLog.Debug($"[{index:D3}] [{"unknown", 7}]: [{value.Type}]: {BitConverter.ToString(BitConverter.GetBytes((long)value.String)).Replace("-", " ")}"); + break; + } + } +} + +public static class AtkValueExtensions +{ + public static unsafe string GetString(this AtkValue value) + { + return Marshal.PtrToStringUTF8(new nint(value.String)) ?? "Unable to Allocate String"; + } +} \ No newline at end of file diff --git a/KamiLib/Atk/NodeHelper.cs b/KamiLib/Atk/NodeHelper.cs new file mode 100644 index 0000000..ba45cbc --- /dev/null +++ b/KamiLib/Atk/NodeHelper.cs @@ -0,0 +1,106 @@ +using System.Linq; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiLib.ChatCommands; + +namespace KamiLib.Atk; + +public unsafe class BaseNode +{ + private readonly AtkUnitBase* node; + + public bool NodeValid => node != null; + + public BaseNode(string addon) + { + node = (AtkUnitBase*) Service.GameGui.GetAddonByName(addon, 1); + } + + public BaseNode Print() + { + Chat.Print("AtkUnitBase", $"{new nint(node):X8}"); + return this; + } + + public AtkResNode* GetRootNode() => node == null ? null : node->RootNode; + + public T* GetNode(uint id) where T : unmanaged + { + if (node == null) return null; + + var targetNode = (T*) node->GetNodeById(id); + + return targetNode; + } + + public ComponentNode GetComponentNode(uint id) + { + if (node == null) return new ComponentNode(null); + + var targetNode = (AtkComponentNode*) node->GetNodeById(id); + + return new ComponentNode(targetNode); + } + + public ComponentNode GetNestedNode(params uint[] idList) + { + uint index = 0; + + ComponentNode startingNode; + + do + { + startingNode = GetComponentNode(idList[index]); + + } while (index++ < idList.Length); + + return startingNode; + } +} + +public unsafe class ComponentNode +{ + private readonly AtkComponentNode* node; + private readonly AtkComponentBase* componentBase; + + public ComponentNode(AtkComponentNode* node) + { + this.node = node; + + componentBase = node == null ? null : node->Component; + } + + public ComponentNode Print() + { + Chat.Print("AtkComponentNode", $"{new nint(node):X8}"); + return this; + } + + public ComponentNode GetComponentNode(uint id) + { + if (componentBase == null) return new ComponentNode(null); + + var targetNode = Node.GetNodeByID(componentBase->UldManager, id); + + return new ComponentNode(targetNode); + } + + public T* GetNode(uint id) where T : unmanaged => componentBase == null ? null : Node.GetNodeByID(componentBase->UldManager, id); + + public AtkComponentNode* GetPointer() => node; +} + +public static unsafe class Node +{ + public static T* GetNodeByID(AtkUldManager uldManager, uint nodeId) where T : unmanaged + { + foreach (var index in Enumerable.Range(0, uldManager.NodeListCount)) + { + var currentNode = uldManager.NodeList[index]; + if (currentNode->NodeID != nodeId) continue; + + return (T*) currentNode; + } + + return null; + } +} \ No newline at end of file diff --git a/KamiLib/Blacklist/BlacklistDraw.cs b/KamiLib/Blacklist/BlacklistDraw.cs new file mode 100644 index 0000000..955338c --- /dev/null +++ b/KamiLib/Blacklist/BlacklistDraw.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Logging; +using Dalamud.Utility; +using ImGuiNET; +using KamiLib.Caching; +using KamiLib.Configuration; +using KamiLib.Drawing; +using KamiLib.Extensions; +using KamiLib.Localization; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.Blacklist; + +public static class BlacklistDraw +{ + private static readonly List EntriesToRemove = new(); + private static readonly List EntriesToAdd = new(); + private static string _searchString = string.Empty; + private static List? _searchResults = new(); + + public static void DrawBlacklist(Setting> blacklistedAreas) + { + InfoBox.Instance + .AddTitle(Strings.Blacklist_CurrentlyBlacklisted, out var innerWidth, 1.0f) + .AddDummy(5.0f) + .AddAction(() => BlacklistedAreasList(blacklistedAreas)) + .AddDisabledButton(EntriesToRemove.Count == 0 ? Strings.Blacklist_ClearBlacklist : Strings.Blacklist_RemoveSelectedAreas.Format(EntriesToRemove.Count), () => + { + if (EntriesToRemove.Count == 0) + { + blacklistedAreas.Value.Clear(); + KamiCommon.SaveConfiguration(); + } + else + { + blacklistedAreas.Value.RemoveAll(entry => EntriesToRemove.Contains(entry)); + EntriesToRemove.Clear(); + KamiCommon.SaveConfiguration(); + } + }, !ImGui.GetIO().KeyShift, Strings.DisabledButton_HoldShift, innerWidth) + .Draw(); + } + + public static void DrawAddRemoveHere(Setting> blacklistedZones) + { + InfoBox.Instance + .AddTitle(Strings.Blacklist_AddRemoveZone, 1.0f) + .AddAction(() => LuminaCache.Instance.GetRow(Service.ClientState.TerritoryType)?.DrawLabel()) + .BeginTable() + .BeginRow() + .AddDisabledButton(Strings.Common_Add, () => + { + Add(blacklistedZones, Service.ClientState.TerritoryType); + }, blacklistedZones.Value.Contains(Service.ClientState.TerritoryType), buttonSize: InfoBox.Instance.InnerWidth / 2.0f - 5.0f * ImGuiHelpers.GlobalScale) + .AddDisabledButton(Strings.Common_Remove, () => + { + Remove(blacklistedZones, Service.ClientState.TerritoryType); + }, !blacklistedZones.Value.Contains(Service.ClientState.TerritoryType), buttonSize: InfoBox.Instance.InnerWidth / 2.0f - 5.0f * ImGuiHelpers.GlobalScale) + .EndRow() + .EndTable() + .Draw(); + } + + public static void DrawTerritorySearch(Setting> blacklistedZones) + { + InfoBox.Instance + .AddTitle(Strings.Blacklist_ZoneSearch, out var innerWidth, 1.0f) + .AddAction(() => + { + ImGui.PushItemWidth(InfoBox.Instance.InnerWidth); + if (ImGui.InputTextWithHint("###TerritorySearch", Strings.Blacklist_Search, ref _searchString, 60, ImGuiInputTextFlags.AutoSelectAll)) + { + _searchResults = Search(_searchString, 5); + PluginLog.Debug("Updating TerritorySearch Results"); + } + }) + .AddAction(() => DisplayResults(_searchResults)) + .AddDisabledButton(Strings.Blacklist_AddSelectedAreas.Format(EntriesToAdd.Count), () => + { + blacklistedZones.Value.AddRange(EntriesToAdd); + EntriesToAdd.Clear(); + KamiCommon.SaveConfiguration(); + + }, !EntriesToAdd.Any(), Strings.Blacklist_SelectZones, innerWidth) + .Draw(); + } + + public static void PrimeSearch() + { + _searchResults = Search("", 5); + } + + private static List Search(string searchTerms, int numResults) + { + return Service.DataManager.GetExcelSheet()! + .Where(territory => territory.PlaceName.Row is not 0) + .Where(territory => territory.PlaceName.Value is not null) + .GroupBy(territory => territory.PlaceName.Value!.Name.ToDalamudString().TextValue) + .Select(territory => territory.First()) + .Where(territory => territory.PlaceName.Value!.Name.ToDalamudString().TextValue.ToLower().Contains(searchTerms.ToLower())) + .Select(territory => new SearchResult { + TerritoryID = territory.RowId + }) + .OrderBy(searchResult => searchResult.TerritoryName) + .Take(numResults) + .ToList(); + } + + private static void DisplayResults(List? results) + { + if (results is null) return; + + if (ImGui.BeginChild("###SearchResultsChild", new Vector2(InfoBox.Instance.InnerWidth, 21.0f * 5 * ImGuiHelpers.GlobalScale ))) + { + foreach (var result in results) + { + if (ImGui.Selectable($"###SearchResult{result.TerritoryID}", EntriesToAdd.Contains(result.TerritoryID))) + { + if (!EntriesToAdd.Contains(result.TerritoryID)) + { + EntriesToAdd.Add(result.TerritoryID); + } + else + { + EntriesToAdd.Remove(result.TerritoryID); + } + } + + ImGui.SameLine(); + LuminaCache.Instance.GetRow(result.TerritoryID)?.DrawLabel(); + } + } + ImGui.EndChild(); + } + + private static void BlacklistedAreasList(Setting> blacklistedAreas) + { + var itemCount = Math.Min(blacklistedAreas.Value.Count, 10); + var listHeight = itemCount * ImGuiHelpers.GlobalScale * 21.0f; + var minHeight = 21.0f * ImGuiHelpers.GlobalScale; + + var size = new Vector2(InfoBox.Instance.InnerWidth, MathF.Max(listHeight, minHeight)); + + if(ImGui.BeginChild("###BlacklistFrame", size, false)) + { + if (!blacklistedAreas.Value.Any()) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() with { X = ImGui.GetContentRegionAvail().X / 2 - ImGui.CalcTextSize(Strings.Blacklist_Empty).X / 2.0f}); + ImGui.TextColored(Colors.Orange, Strings.Blacklist_Empty); + } + else + { + DrawBlacklistedAreas(blacklistedAreas); + } + } + ImGui.EndChild(); + } + + private static void DrawBlacklistedAreas(Setting> blacklistedAreas) + { + var territories = blacklistedAreas.Value + .Select(area => LuminaCache.Instance.GetRow(area)) + .OfType() + .OrderBy(territory => territory.GetPlaceNameString()); + + foreach (var territory in territories) + { + ImGui.PushItemWidth(InfoBox.Instance.InnerWidth); + if (ImGui.Selectable($"###{territory}", EntriesToRemove.Contains(territory.RowId))) + { + if (!EntriesToRemove.Contains(territory.RowId)) + { + EntriesToRemove.Add(territory.RowId); + } + else + { + EntriesToRemove.Remove(territory.RowId); + } + } + + ImGui.SameLine(); + territory.DrawLabel(); + } + } + + private static void Add(Setting> zones, uint id) + { + if (!zones.Value.Contains(id)) + { + zones.Value.Add(id); + KamiCommon.SaveConfiguration(); + } + } + + private static void Remove(Setting> zones, uint id) + { + if (zones.Value.Contains(id)) + { + zones.Value.Remove(id); + KamiCommon.SaveConfiguration(); + } + } +} diff --git a/KamiLib/Blacklist/SearchResult.cs b/KamiLib/Blacklist/SearchResult.cs new file mode 100644 index 0000000..6061065 --- /dev/null +++ b/KamiLib/Blacklist/SearchResult.cs @@ -0,0 +1,12 @@ +using Dalamud.Utility; +using KamiLib.Caching; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.Blacklist; + +public class SearchResult +{ + public uint TerritoryID { get; init; } + private uint PlaceNameRow => LuminaCache.Instance.GetRow(TerritoryID)?.PlaceName.Row ?? 0; + public string TerritoryName => LuminaCache.Instance.GetRow(PlaceNameRow)?.Name.ToDalamudString().TextValue ?? "Unknown PlaceName Row"; +} \ No newline at end of file diff --git a/KamiLib/Caching/IconCache.cs b/KamiLib/Caching/IconCache.cs new file mode 100644 index 0000000..c4ead37 --- /dev/null +++ b/KamiLib/Caching/IconCache.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Dalamud.Interface.Internal; +using Dalamud.Logging; +using Dalamud.Utility; +using ImGuiScene; + +namespace KamiLib.Caching; + +public class IconCache : IDisposable +{ + private readonly Dictionary iconTextures = new(); + + private const string IconFilePath = "ui/icon/{0:D3}000/{1:D6}_hr1.tex"; + + private static IconCache? _instance; + public static IconCache Instance => _instance ??= new IconCache(); + + public static void Cleanup() + { + _instance?.Dispose(); + } + + public void Dispose() + { + foreach (var texture in iconTextures.Values) + { + texture?.Dispose(); + } + + iconTextures.Clear(); + } + + private void LoadIconTexture(uint iconId) + { + Task.Run(() => + { + try + { + var path = IconFilePath.Format(iconId / 1000, iconId); + var tex = Service.TextureProvider.GetTextureFromGame(path); + + if (tex is not null && tex.ImGuiHandle != nint.Zero) + { + iconTextures[iconId] = tex; + } + else + { + tex?.Dispose(); + } + } + catch (Exception ex) + { + PluginLog.LogError($"Failed loading texture for icon {iconId} - {ex.Message}"); + } + }); + } + + public IDalamudTextureWrap? GetIcon(uint iconId) + { + if (iconTextures.TryGetValue(iconId, out var value)) return value; + + iconTextures.Add(iconId, null); + LoadIconTexture(iconId); + + return iconTextures[iconId]; + } +} diff --git a/KamiLib/Caching/LuminaCache.cs b/KamiLib/Caching/LuminaCache.cs new file mode 100644 index 0000000..7b2f18b --- /dev/null +++ b/KamiLib/Caching/LuminaCache.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Dalamud; +using Lumina.Excel; + +namespace KamiLib.Caching; + +public class LuminaCache : IEnumerable where T : ExcelRow +{ + private readonly Func searchAction; + + private static LuminaCache? _instance; + public static LuminaCache Instance => _instance ??= new LuminaCache(); + + private LuminaCache(Func? action = null) + { + searchAction = action ?? (row => Service.DataManager.GetExcelSheet()!.GetRow(row)); + } + + private readonly Dictionary cache = new(); + private readonly Dictionary, T> subRowCache = new (); + + public ExcelSheet OfLanguage(ClientLanguage language) + { + return Service.DataManager.GetExcelSheet(language)!; + } + + public T? GetRow(uint id) + { + if (cache.TryGetValue(id, out var value)) + { + return value; + } + else + { + if (searchAction(id) is not { } result) return null; + + return cache[id] = result; + } + } + + public T? GetRow(uint row, uint subRow) + { + var targetRow = new Tuple(row, subRow); + + if (subRowCache.TryGetValue(targetRow, out var value)) + { + return value; + } + else + { + if (Service.DataManager.GetExcelSheet()!.GetRow(row, subRow) is not { } result) return null; + + return subRowCache[targetRow] = result; + } + } + + public IEnumerator GetEnumerator() => Service.DataManager.GetExcelSheet()!.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/Chat.cs b/KamiLib/ChatCommands/Chat.cs new file mode 100644 index 0000000..7844a78 --- /dev/null +++ b/KamiLib/ChatCommands/Chat.cs @@ -0,0 +1,55 @@ +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using KamiLib.Localization; + +namespace KamiLib.ChatCommands; + +public static class Chat +{ + public static void Print(string tag, string message) => Service.Chat.Print(GetBaseString(tag, message).BuiltString); + + public static void PrintHelp(string command, string? helpText = null) + { + var message = GetBaseString(Strings.Command_Label, command); + + if (helpText is not null) + { + message.AddUiForeground("- " + helpText, 32); + } + + Service.Chat.Print(message.BuiltString); + } + + public static void Print(string tag, string message, DalamudLinkPayload? payload) + { + if (payload is null) + { + Print(tag, message); + return; + } + + Service.Chat.Print(GetBaseString(tag, message, payload).BuiltString); + } + + public static void PrintError(string message) => Service.Chat.PrintError(GetBaseString(Strings.Common_Error, message).BuiltString); + + private static SeStringBuilder GetBaseString(string tag, string message, DalamudLinkPayload? payload = null) + { + if (payload is null) + { + return new SeStringBuilder() + .AddUiForeground($"[{KamiCommon.PluginName}] ", 45) + .AddUiForeground($"[{tag}] ", 62) + .AddText(message); + } + else + { + return new SeStringBuilder() + .AddUiForeground($"[{KamiCommon.PluginName}] ", 45) + .AddUiForeground($"[{tag}] ", 62) + .Add(payload) + .AddUiForeground(message, 35) + .Add(RawPayload.LinkTerminator); + } + } +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/ChatPayloadManager.cs b/KamiLib/ChatCommands/ChatPayloadManager.cs new file mode 100644 index 0000000..5286bf0 --- /dev/null +++ b/KamiLib/ChatCommands/ChatPayloadManager.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; + +namespace KamiLib.ChatCommands; + +internal record ChatLinkPayload(uint CommandID, uint Type, DalamudLinkPayload Payload); + +public class ChatPayloadManager : IDisposable +{ + private static ChatPayloadManager? _instance; + public static ChatPayloadManager Instance => _instance ??= new ChatPayloadManager(); + + private List ChatLinkPayloads { get; } = new(); + + public static void Cleanup() + { + _instance?.Dispose(); + } + + public void Dispose() + { + foreach (var payload in ChatLinkPayloads) + { + Service.PluginInterface.RemoveChatLinkHandler( payload.Type + 1000 ); + } + } + + public DalamudLinkPayload AddChatLink(Enum type, Action payloadAction) => AddChatLink(Convert.ToUInt32(type), payloadAction); + + private DalamudLinkPayload AddChatLink(uint type, Action payloadAction) + { + // If the payload is already registered + var payload = ChatLinkPayloads.FirstOrDefault(linkPayload => linkPayload.CommandID == type + 1000)?.Payload; + if (payload != null) return payload; + + // else + Service.PluginInterface.RemoveChatLinkHandler(type + 1000); + payload = Service.PluginInterface.AddChatLinkHandler(type + 1000, payloadAction); + + ChatLinkPayloads.Add(new ChatLinkPayload(type + 1000, type, payload)); + + return payload; + } +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/Command.cs b/KamiLib/ChatCommands/Command.cs new file mode 100644 index 0000000..d73a81a --- /dev/null +++ b/KamiLib/ChatCommands/Command.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using KamiLib.Interfaces; +using KamiLib.Localization; + +namespace KamiLib.ChatCommands; + +public static class Command +{ + public static CommandData GetCommandData(string baseCommand, string arguments) => new(baseCommand, arguments); + + public static void ProcessCommand(CommandData data, IEnumerable commands) + { + var matchingCommands = commands.Where(command => command.CommandArgument == data.Command).ToList(); + + if (matchingCommands.Any()) + { + if (!matchingCommands.Any(command => command.Execute(data))) + { + Chat.PrintError(string.Format(Strings.Command_DoesntExistExtended, data.BaseCommand, data.Command, data.SubCommand)); + } + } + else + { + Chat.PrintError(string.Format(Strings.Command_DoesntExist, data.BaseCommand, data.Command)); + } + } +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/CommandData.cs b/KamiLib/ChatCommands/CommandData.cs new file mode 100644 index 0000000..ff591e7 --- /dev/null +++ b/KamiLib/ChatCommands/CommandData.cs @@ -0,0 +1,39 @@ +namespace KamiLib.ChatCommands; + +public class CommandData +{ + public string? BaseCommand; + public string? Command; + public string? SubCommand; + public string?[]? Arguments; + + public CommandData(string rootCommand, string arguments) + { + BaseCommand = rootCommand; + + if (arguments != string.Empty) + { + var splits = arguments.Split(' '); + + if (splits.Length >= 1) + { + Command = splits[0]; + } + + if (splits.Length >= 2) + { + SubCommand = splits[1]; + } + + if (splits.Length >= 3) + { + Arguments = splits[2..]; + } + } + } + + public override string ToString() => $"{BaseCommand ?? "Empty Base Command"}, " + + $"{Command ?? "Empty Command"}, " + + $"{SubCommand ?? "Empty SubCommand"}, " + + $"{(Arguments is null ? "Empty Args" : string.Join(", ", Arguments))}"; +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/CommandManager.cs b/KamiLib/ChatCommands/CommandManager.cs new file mode 100644 index 0000000..3f12aea --- /dev/null +++ b/KamiLib/ChatCommands/CommandManager.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Dalamud.Game.Command; +using Dalamud.Logging; +using KamiLib.Interfaces; +using KamiLib.Localization; + +namespace KamiLib.ChatCommands; + +public class CommandManager : IDisposable +{ + private static string SettingsCommand => $"/{KamiCommon.PluginName.ToLower()}"; + private static string HelpCommand => $"/{KamiCommon.PluginName.ToLower()} help"; + + public readonly List Commands = new(); + + private readonly List additionalCommands = new(); + + public CommandManager() + { + Commands.Add(new HelpCommands()); + + Service.Commands.AddHandler(SettingsCommand, new CommandInfo(OnCommand) + { + HelpMessage = Strings.Command_OpenConfigWindow + }); + + Service.Commands.AddHandler(HelpCommand, new CommandInfo(OnCommand) + { + HelpMessage = Strings.Command_DisplayHelpText + }); + } + + public void Dispose() + { + Service.Commands.RemoveHandler(SettingsCommand); + Service.Commands.RemoveHandler(HelpCommand); + + foreach (var additionalCommand in additionalCommands) + { + Service.Commands.RemoveHandler(additionalCommand); + } + } + + public void AddHandler(string additionalCommand, string description) + { + additionalCommands.Add(additionalCommand); + Service.Commands.AddHandler(additionalCommand, new CommandInfo(OnCommand) + { + HelpMessage = description + }); + } + + public void AddCommand(IPluginCommand command) + { + Commands.Add(command); + } + + public void OnCommand(string command, string arguments) + { + var commandData = Command.GetCommandData(command.ToLower(), arguments.ToLower()); + + PluginLog.Debug($"[{KamiCommon.PluginName}] Received Command: {commandData}"); + Command.ProcessCommand(commandData, Commands); + } +} diff --git a/KamiLib/ChatCommands/HelpCommands.cs b/KamiLib/ChatCommands/HelpCommands.cs new file mode 100644 index 0000000..6ed3e3f --- /dev/null +++ b/KamiLib/ChatCommands/HelpCommands.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using KamiLib.Interfaces; +using KamiLib.Localization; + +namespace KamiLib.ChatCommands; + +public class HelpCommands: IPluginCommand +{ + public string CommandArgument => "help"; + + public IEnumerable SubCommands { get; } = new List + { + new SubCommand + { + CommandKeyword = null, + CommandAction = () => + { + foreach (var command in KamiCommon.CommandManager.Commands) + { + PrintSubCommands(command); + } + }, + GetHelpText = () => Strings.Command_ShowThisMessage + } + }; + + private static void PrintSubCommands(IPluginCommand command) + { + foreach (var subCommand in command.SubCommands.GroupBy(subCommand => subCommand.GetCommand())) + { + var selectedSubCommand = subCommand.First(); + + if (!selectedSubCommand.Hidden) + { + PrintHelpText(command, selectedSubCommand); + + if (selectedSubCommand.GetAliases() is { } aliases) + { + foreach (var alias in aliases) + { + PrintAliasHelpText(command, alias, selectedSubCommand); + } + } + } + } + } + + private static void PrintHelpText(IPluginCommand mainCommand, ISubCommand subCommand) + { + var commandString = $"/{KamiCommon.PluginName.ToLower()} "; + + if (mainCommand.CommandArgument is not null) + { + commandString += mainCommand.CommandArgument + " "; + } + + if (subCommand.GetCommand() is not null) + { + commandString += subCommand.GetCommand() + " "; + } + + if (subCommand.HasParameterAction) + { + commandString += $"[{Strings.Common_Value}] "; + } + + Chat.PrintHelp(commandString, subCommand.GetHelpText()); + } + + private static void PrintAliasHelpText(IPluginCommand mainCommand, string? alias, ISubCommand subCommand) + { + var commandString = $"/{KamiCommon.PluginName.ToLower()} "; + + if (mainCommand.CommandArgument is not null) + { + commandString += mainCommand.CommandArgument + " "; + } + + if (alias is not null) + { + commandString += alias + " "; + } + + if (subCommand.HasParameterAction) + { + commandString += $"[{Strings.Common_Value}] "; + } + + Chat.PrintHelp(commandString, subCommand.GetHelpText()); + } +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/OpenWindowCommand.cs b/KamiLib/ChatCommands/OpenWindowCommand.cs new file mode 100644 index 0000000..a0fcfb6 --- /dev/null +++ b/KamiLib/ChatCommands/OpenWindowCommand.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Globalization; +using Dalamud.Interface.Windowing; +using KamiLib.Interfaces; +using KamiLib.Localization; + +namespace KamiLib.ChatCommands; + +public class OpenWindowCommand : IPluginCommand where T : Window +{ + public string? CommandArgument { get; } + + public OpenWindowCommand( string? commandArgument = null, bool silent = false, string? windowName = null) + { + CommandArgument = commandArgument?.ToLower(); + + if (commandArgument is not null) + { + windowName ??= CultureInfo.CurrentCulture.TextInfo.ToTitleCase(commandArgument); + } + + SubCommands = new List + { + new SubCommand + { + CommandKeyword = null, + CommandAction = () => Chat.PrintError(string.Format(Strings.Command_PvPError, windowName)), + CanExecute = () => Service.ClientState.IsPvP, + GetHelpText = () => string.Format(Strings.Command_OpenWindow, windowName), + Hidden = silent, + }, + new SubCommand + { + CommandKeyword = null, + CommandAction = () => + { + if ( KamiCommon.WindowManager.GetWindowOfType() is {} mainWindow ) + { + if (!silent) + { + Chat.Print(Strings.Command_Label, + !mainWindow.IsOpen ? + string.Format(Strings.Command_OpeningWindow, windowName) : + string.Format(Strings.Command_ClosingWindow, windowName)); + } + + mainWindow.IsOpen = !mainWindow.IsOpen; + } + else + { + Chat.PrintError($"Something went wrong trying to open {windowName} Window"); + } + }, + CanExecute = () => !Service.ClientState.IsPvP, + GetHelpText = () => string.Format(Strings.Command_OpenWindow, windowName), + Hidden = silent, + }, + }; + } + + public IEnumerable SubCommands { get; } +} \ No newline at end of file diff --git a/KamiLib/ChatCommands/SubCommand.cs b/KamiLib/ChatCommands/SubCommand.cs new file mode 100644 index 0000000..7314f1a --- /dev/null +++ b/KamiLib/ChatCommands/SubCommand.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using KamiLib.Interfaces; + +namespace KamiLib.ChatCommands; + +public class SubCommand : ISubCommand +{ + public string? CommandKeyword { get; init; } + public List? Aliases { get; init; } + public Action? CommandAction { get; init; } + public Action? ParameterAction { get; init; } + public Func? CanExecute { get; init; } + public Func? GetHelpText { get; init; } + public bool Hidden { get; init; } + public bool HasParameterAction => ParameterAction is not null; + + public string? GetCommand() => CommandKeyword; + public IEnumerable? GetAliases() => Aliases; + + string? ISubCommand.GetHelpText() => GetHelpText?.Invoke(); + + public bool Execute(CommandData commandData) + { + if (CanExecute?.Invoke() is null or true) + { + if (CommandAction is not null) + { + CommandAction.Invoke(); + return true; + } + + if (ParameterAction is not null) + { + ParameterAction.Invoke(commandData.Arguments); + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/KamiLib/Configuration/Migrate.cs b/KamiLib/Configuration/Migrate.cs new file mode 100644 index 0000000..db51b8f --- /dev/null +++ b/KamiLib/Configuration/Migrate.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Newtonsoft.Json.Linq; + +namespace KamiLib.Configuration; + +public static class Migrate +{ + private static JObject? _parsedJson; + + public static void LoadFile(FileInfo configFilePath) + { + var reader = new StreamReader(configFilePath.FullName); + var fileText = reader.ReadToEnd(); + reader.Dispose(); + + _parsedJson = JObject.Parse(fileText); + } + + public static int GetFileVersion() + { + return _parsedJson?.GetValue("Version")?.Value() ?? 0; + } + + public static Setting GetSettingValue(string key) where T : struct + { + return new Setting(_parsedJson!.SelectToken(key)!.Value()); + } + + public static Setting GetSettingEnum(string key) where T : struct + { + var readValue = _parsedJson!.SelectToken(key)!.Value(); + + return new Setting((T) Enum.ToObject(typeof(T), readValue)); + } + + public static T GetValue(string key) + { + return _parsedJson!.SelectToken(key)!.Value()!; + } + + public static JArray GetArray(string key) + { + return (JArray) _parsedJson!.SelectToken(key)!; + } + + public static List GetArray(string key) + { + var array = GetArray(key); + + return array.ToObject>()!; + } + + public static Setting GetVector4(string key) + { + return new Setting(new Vector4 + { + X = GetValue($"{key}.X"), + Y = GetValue($"{key}.Y"), + Z = GetValue($"{key}.Z"), + W = GetValue($"{key}.W"), + }); + } +} \ No newline at end of file diff --git a/KamiLib/Configuration/Setting.cs b/KamiLib/Configuration/Setting.cs new file mode 100644 index 0000000..20badf6 --- /dev/null +++ b/KamiLib/Configuration/Setting.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace KamiLib.Configuration; + +public sealed record Setting(T Value) : IEquatable where T : notnull +{ + public T Value = Value; + + public override string ToString() => Value.ToString() ?? "Null"; + + public bool Equals(T? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return EqualityComparer.Default.Equals(Value, other); + } + + public static bool operator ==(Setting leftSide, T rightSide) + { + return leftSide.Equals(rightSide); + } + + public static bool operator !=(Setting leftSide, T rightSide) + { + return !leftSide.Equals(rightSide); + } + + public static implicit operator bool(Setting obj) + { + if (obj.Value is bool value) + { + return value; + } + + throw new Exception("Invalid implicit conversion to bool"); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return EqualityComparer.Default.GetHashCode(Value); + } +} \ No newline at end of file diff --git a/KamiLib/Drawing/Colors.cs b/KamiLib/Drawing/Colors.cs new file mode 100644 index 0000000..b18ffbc --- /dev/null +++ b/KamiLib/Drawing/Colors.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using ImGuiNET; + +namespace KamiLib.Drawing; + +public static class Colors +{ + public static Vector4 Purple = new(176 / 255.0f, 38 / 255.0f, 236 / 255.0f, 1.0f); + public static Vector4 Blue = new(37 / 255.0f, 168 / 255.0f, 1.0f, 1.0f); + public static Vector4 ForestGreen = new(0.133f, 0.545f, 0.1333f, 1.0f); + public static Vector4 White = new(1.0f, 1.0f, 1.0f,1.0f); + public static Vector4 Red = new(1.0f, 0.0f, 0.0f, 1.0f); + public static Vector4 Green = new(0.0f, 1.0f, 0.0f, 1.0f); + public static Vector4 Black = new(0.0f, 0.0f, 0.0f, 1.0f); + public static Vector4 HealerGreen = new(33 / 255f, 193 / 255f, 0, 1.0f); + public static Vector4 DPSRed = new(210/255f, 42/255f, 43/255f, 1.0f); + public static Vector4 SoftRed = new(0.8f, 0.2f, 0.2f, 1.0f); + public static Vector4 Grey = new(0.6f, 0.6f, 0.6f, 1.0f); + public static Vector4 LightGrey = new(220/250.0f, 220/250.0f, 220/250f, 1.0f); + public static Vector4 Orange = new(1.0f, 165.0f / 255.0f, 0.0f, 1.0f); + public static Vector4 SoftGreen = new(0.2f, 0.8f, 0.2f, 1.0f); + public static Vector4 FatePink = new(0.58f, 0.388f, 0.827f, 0.33f); + public static Vector4 FateDarkPink = new(0.58f, 0.388f, 0.827f, 1.0f); + public static Vector4 MapTextBrown = new(0.655f, 0.396f, 0.149f, 1.0f); + public static Vector4 BabyBlue = new(0.537f, 0.812f, 0.941f, 1.0f); +} + +public static class ColorExtensions +{ + public static uint ToU32(this Vector4 color) => ImGui.GetColorU32(color); +} \ No newline at end of file diff --git a/KamiLib/Drawing/DrawList.cs b/KamiLib/Drawing/DrawList.cs new file mode 100644 index 0000000..b26b5eb --- /dev/null +++ b/KamiLib/Drawing/DrawList.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; +using ImGuiNET; +using KamiLib.Caching; +using KamiLib.Configuration; +using Action = System.Action; + +namespace KamiLib.Drawing; + +public abstract class DrawList +{ + protected T DrawListOwner { get; init; } = default!; + protected List DrawActions { get; } = new(); + + protected void DrawListContents() + { + foreach (var action in DrawActions) + { + action(); + } + } + + public T AddDummy(float value) + { + DrawActions.Add(() => ImGuiHelpers.ScaledDummy(value)); + + return DrawListOwner; + } + + public T AddIndent(int tab) + { + DrawActions.Add(() => ImGui.Indent(15.0f * tab)); + + return DrawListOwner; + } + + public T AddIcon(uint iconID, Vector2 size, Vector4 color) + { + var icon = IconCache.Instance.GetIcon(iconID); + + if (icon != null) + { + DrawActions.Add(() => + { + ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, color); + }); + } + + return DrawListOwner; + } + + public T AddIcon(uint iconID, Vector2 size, float transparency) + { + var icon = IconCache.Instance.GetIcon(iconID); + + if (icon != null) + { + DrawActions.Add(() => + { + ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, Vector4.One with {W = transparency}); + }); + } + + return DrawListOwner; + } + + public T AddString(string message, Vector4? color = null) + { + if (color == null) + { + DrawActions.Add(() => ImGui.Text(message)); + } + else + { + DrawActions.Add(() => ImGui.TextColored(color.Value, message)); + } + + return DrawListOwner; + } + + public T AddStringCentered(string message, float? availableArea = null, Vector4? color = null) + { + if (color == null) + { + DrawActions.Add(() => + { + var area = availableArea / 2.0f ?? ImGui.GetContentRegionAvail().X / 2.0f; + + ImGui.SetCursorPos(ImGui.GetCursorPos() with {X = ImGui.GetCursorPos().X + area - ImGui.CalcTextSize(message).X / 2.0f}); + ImGui.Text(message); + }); + } + else + { + DrawActions.Add(() => + { + var area = availableArea / 2.0f ?? ImGui.GetContentRegionAvail().X / 2.0f; + + ImGui.SetCursorPos(ImGui.GetCursorPos() with {X = ImGui.GetCursorPos().X + area - ImGui.CalcTextSize(message).X / 2.0f}); + ImGui.TextColored(color.Value, message); + }); + } + + return DrawListOwner; + } + + public T Indent(int indent) + { + DrawActions.Add( () => ImGui.Indent(indent) ); + + return DrawListOwner; + } + + public T UnIndent(int indent) + { + DrawActions.Add( () => ImGui.Unindent(indent) ); + + return DrawListOwner; + } + + public T AddConfigCheckbox(string label, Setting setting, string? helpText = null, string? additionalID = null) + { + DrawActions.Add(() => + { + if (additionalID != null) ImGui.PushID(additionalID); + + var cursorPosition = ImGui.GetCursorPos(); + + if (ImGui.Checkbox($"##{label}", ref setting.Value)) + { + KamiCommon.SaveConfiguration(); + } + + var spacing = ImGui.GetStyle().ItemSpacing; + cursorPosition += spacing; + ImGui.SetCursorPos(cursorPosition with { X = cursorPosition.X + 27.0f * ImGuiHelpers.GlobalScale }); + + ImGui.TextUnformatted(label); + + if (helpText != null) + { + ImGuiComponents.HelpMarker(helpText); + } + + if (additionalID != null) ImGui.PopID(); + }); + + return DrawListOwner; + } + + public T AddConfigCombo(IEnumerable values, Setting setting, Func localizeFunction, string label = "", float width = 0.0f) where TU : struct + { + DrawActions.Add(() => + { + if (width != 0.0f) + { + ImGui.SetNextItemWidth(width); + } + else + { + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + } + + if (ImGui.BeginCombo(label, localizeFunction(setting.Value))) + { + foreach (var value in values) + { + if (ImGui.Selectable(localizeFunction(value), setting.Value.Equals(value))) + { + setting.Value = value; + KamiCommon.SaveConfiguration(); + } + } + + ImGui.EndCombo(); + } + }); + + return DrawListOwner; + } + + public T AddConfigColor(string label, Setting setting) + { + DrawActions.Add(() => + { + if (ImGui.ColorEdit4(label, ref setting.Value, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + { + KamiCommon.SaveConfiguration(); + } + }); + + return DrawListOwner; + } + + public T AddConfigColor(string label, string defaultLabel, Setting setting, Vector4 defaultValue) + { + DrawActions.Add(() => + { + ImGui.PushID(label); + + if (ImGui.ColorEdit4($"##{label}", ref setting.Value, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + { + KamiCommon.SaveConfiguration(); + } + + ImGui.SameLine(); + ImGui.BeginDisabled(setting == defaultValue); + if (ImGui.Button(defaultLabel)) + { + setting.Value = defaultValue; + KamiCommon.SaveConfiguration(); + } + ImGui.EndDisabled(); + + ImGui.SameLine(); + ImGui.TextUnformatted(label); + + ImGui.PopID(); + }); + + return DrawListOwner; + } + + public T AddDragFloat(string label, Setting setting, float minValue, float maxValue, float width = 0.0f, int precision = 2) + { + DrawActions.Add(() => + { + if (width != 0.0f) + { + ImGui.SetNextItemWidth(width); + } + else + { + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + } + + ImGui.DragFloat(label, ref setting.Value, 0.01f * maxValue, minValue, maxValue, $"%.{precision}f"); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + KamiCommon.SaveConfiguration(); + } + }); + + return DrawListOwner; + } + + public T AddAction(Action action) + { + DrawActions.Add(action); + + return DrawListOwner; + } + + public T AddSliderInt(string label, Setting setting, int minValue, int maxValue, float width = 200.0f) + { + DrawActions.Add(() => + { + if (width != 0.0f) + { + ImGui.SetNextItemWidth(width); + } + else + { + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + } + + ImGui.SliderInt(label, ref setting.Value, minValue, maxValue); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + KamiCommon.SaveConfiguration(); + } + }); + + return DrawListOwner; + } + + public T AddConfigRadio(string label, Setting setting, TU buttonValue, string? helpText = null ) where TU : struct + { + DrawActions.Add(() => + { + var value = Convert.ToInt32(setting.Value); + + if (ImGui.RadioButton(label, ref value, Convert.ToInt32(buttonValue))) + { + setting.Value = (TU)Enum.ToObject(typeof(TU), value); + KamiCommon.SaveConfiguration(); + } + + if (helpText != null) + { + ImGuiComponents.HelpMarker(helpText); + } + }); + + return DrawListOwner; + } + + public T AddConfigString(Setting settingsCustomName, float width = 0.0f) + { + DrawActions.Add(() => + { + if (width != 0.0f) + { + ImGui.SetNextItemWidth(width); + } + + ImGui.InputText("", ref settingsCustomName.Value, 24); + + if (ImGui.IsItemDeactivatedAfterEdit()) + { + KamiCommon.SaveConfiguration(); + } + }); + + return DrawListOwner; + } + + public T AddConfigVector2(Setting setting, float width = 200.0f) + { + DrawActions.Add(() => + { + if (width != 0.0f) + { + ImGui.SetNextItemWidth(width); + } + else + { + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + } + + ImGui.InputFloat2("", ref setting.Value); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + KamiCommon.SaveConfiguration(); + } + }); + + return DrawListOwner; + } + + public T AddInputInt(string label, Setting settingsPriority, int min, int max, int step = 1, int stepFast = 1, float width = 77.0f) + { + DrawActions.Add(() => + { + ImGui.SetNextItemWidth(width); + ImGui.InputInt(label, ref settingsPriority.Value, step, stepFast); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + settingsPriority.Value = Math.Clamp(settingsPriority.Value, min, max); + KamiCommon.SaveConfiguration(); + } + }); + + return DrawListOwner; + } + + public T AddHelpMarker(string helpText) + { + DrawActions.Add(() => + { + ImGuiComponents.HelpMarker(helpText); + }); + + return DrawListOwner; + } + + public T BeginDisabled(bool shouldDisable) + { + DrawActions.Add(() => + { + ImGui.BeginDisabled(shouldDisable); + }); + + return DrawListOwner; + } + + public T EndDisabled() + { + DrawActions.Add(ImGui.EndDisabled); + + return DrawListOwner; + } + + public T AddSeparator() + { + DrawActions.Add(() => + { + var startPosition = ImGui.GetCursorScreenPos(); + var stopPosition = startPosition with { X = startPosition.X + InfoBox.Instance.InnerWidth }; + var color = ImGui.GetColorU32(Colors.White); + + ImGui.GetWindowDrawList().AddLine(startPosition, stopPosition, color); + }); + + return DrawListOwner; + } + + public T SameLine(float width = 0) + { + if (width == 0) + { + DrawActions.Add(ImGui.SameLine); + } + else + { + DrawActions.Add(() => ImGui.SameLine(width)); + } + + return DrawListOwner; + } + + public T AddDisabledButton(string label, Action action, bool disable, string? hoverTooltip = null, float? buttonSize = null) + { + if (buttonSize is not null) + { + AddDisabledButtonWithSize(label, action, disable, buttonSize.Value, hoverTooltip); + } + else + { + AddDisabledButtonWithoutSize(label, action, disable, hoverTooltip); + } + + return DrawListOwner; + } + + + private void AddDisabledButtonWithSize(string label, Action action, bool disable, float buttonSize, string? hoverTooltip = null) + { + DrawActions.Add(() => + { + if(disable) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f); + if (ImGui.Button(label, new Vector2(buttonSize, 23.0f * ImGuiHelpers.GlobalScale)) && !disable) + { + action.Invoke(); + } + if(disable) ImGui.PopStyleVar(); + + if (hoverTooltip is not null && ImGui.IsItemHovered() && disable) + { + ImGui.SetTooltip(hoverTooltip); + } + }); + } + + private void AddDisabledButtonWithoutSize(string label, Action action, bool disable, string? hoverTooltip = null) + { + DrawActions.Add(() => + { + if(disable) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f); + if (ImGui.Button(label) && !disable) + { + action.Invoke(); + } + if(disable) ImGui.PopStyleVar(); + + if (hoverTooltip is not null && ImGui.IsItemHovered() && disable) + { + ImGui.SetTooltip(hoverTooltip); + } + }); + } + + public T AddButton(string label, Action action, Vector2? buttonSize = null) + { + if (buttonSize is not null) + { + DrawActions.Add(() => + { + if (ImGui.Button(label, buttonSize.Value)) + { + action.Invoke(); + } + }); + } + else + { + DrawActions.Add(() => + { + if (ImGui.Button(label)) + { + action.Invoke(); + } + }); + } + + return DrawListOwner; + } +} diff --git a/KamiLib/Drawing/InfoBox.cs b/KamiLib/Drawing/InfoBox.cs new file mode 100644 index 0000000..11eca3f --- /dev/null +++ b/KamiLib/Drawing/InfoBox.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using ImGuiNET; +using KamiLib.Interfaces; + +namespace KamiLib.Drawing; + +public class InfoBox : DrawList, IDrawable +{ + private static float CurveRadius => 13.0f * ImGuiHelpers.GlobalScale; + private static float BorderThickness => 2.0f; + private static ImDrawListPtr DrawList => ImGui.GetWindowDrawList(); + private static Vector2 RegionAvailable => ImGui.GetContentRegionAvail(); + private static Vector2 StartPosition { get; set; } + private static Vector2 Size { get; set; } + private static Vector4 BorderColor { get; } = Colors.White; + private static Vector4 TitleColor { get; } = Colors.White; + private static float TotalWidth { get; set; } + private string Label { get; set; } = "Label Not Set"; + private float WidthPercentage { get; set; } + + public float InnerWidth { get; private set; } + + private InfoBox() + { + DrawListOwner = this; + } + + public static readonly InfoBox Instance = new(); + + public void Draw() + { + ImGuiHelpers.ScaledDummy(5.0f); + + TotalWidth = RegionAvailable.X * WidthPercentage; + InnerWidth = TotalWidth - CurveRadius * 3.0f; + + var startX = ImGui.GetCursorPos().X + RegionAvailable.X * ( 0.5f - WidthPercentage / 2.0f ) + CurveRadius / 2.0f; + ImGui.SetCursorPos(ImGui.GetCursorPos() with { X = startX }); + StartPosition = ImGui.GetCursorScreenPos(); + Size = new Vector2(InnerWidth + CurveRadius * 2.0f, 0); + + DrawContents(); + + var calculatedHeight = ImGui.GetItemRectMax().Y - ImGui.GetItemRectMin().Y + CurveRadius * 2.0f; + Size = new Vector2(InnerWidth + CurveRadius * 2.0f, calculatedHeight); + + DrawCorners(); + DrawBorders(); + + ImGuiHelpers.ScaledDummy(10.0f); + } + + private void DrawContents() + { + var topLeftCurveCenter = new Vector2(StartPosition.X + CurveRadius, StartPosition.Y + CurveRadius); + + ImGui.SetCursorScreenPos(topLeftCurveCenter); + ImGui.PushTextWrapPos(ImGui.GetCursorPos().X + Size.X - CurveRadius * 2.0f); + + ImGui.BeginGroup(); + ImGui.PushID(Label); + + DrawListContents(); + + ImGui.PopID(); + ImGui.EndGroup(); + + ImGui.PopTextWrapPos(); + + DrawActions.Clear(); + } + + private void DrawCorners() + { + var topLeftCurveCenter = new Vector2(StartPosition.X + CurveRadius, StartPosition.Y + CurveRadius); + var topRightCurveCenter = new Vector2(StartPosition.X + Size.X - CurveRadius, StartPosition.Y + CurveRadius); + var bottomLeftCurveCenter = new Vector2(StartPosition.X + CurveRadius, StartPosition.Y + Size.Y - CurveRadius); + var bottomRightCurveCenter = new Vector2(StartPosition.X + Size.X - CurveRadius, StartPosition.Y + Size.Y - CurveRadius); + + DrawList.PathArcTo(topLeftCurveCenter, CurveRadius, MathF.PI, 1.55f * MathF.PI); + DrawList.PathStroke(BorderColor.ToU32(), ImDrawFlags.None, BorderThickness); + + DrawList.PathArcTo(topRightCurveCenter, CurveRadius, 2.0f * MathF.PI, 1.45f * MathF.PI); + DrawList.PathStroke(BorderColor.ToU32(), ImDrawFlags.None, BorderThickness); + + DrawList.PathArcTo(bottomLeftCurveCenter, CurveRadius, 0.5f * MathF.PI, 1.0f * MathF.PI); + DrawList.PathStroke(BorderColor.ToU32(), ImDrawFlags.None, BorderThickness); + + DrawList.PathArcTo(bottomRightCurveCenter, CurveRadius, 0.0f, 0.5f * MathF.PI); + DrawList.PathStroke(BorderColor.ToU32(), ImDrawFlags.None, BorderThickness); + } + + private void DrawBorders() + { + var color = BorderColor.ToU32(); + + DrawList.AddLine(new Vector2(StartPosition.X - 0.5f, StartPosition.Y + CurveRadius - 0.5f), new Vector2(StartPosition.X - 0.5f, StartPosition.Y + Size.Y - CurveRadius + 0.5f), color, BorderThickness); + DrawList.AddLine(new Vector2(StartPosition.X + Size.X - 0.5f, StartPosition.Y + CurveRadius - 0.5f), new Vector2(StartPosition.X + Size.X - 0.5f, StartPosition.Y + Size.Y - CurveRadius + 0.5f), color, BorderThickness); + DrawList.AddLine(new Vector2(StartPosition.X + CurveRadius - 0.5f, StartPosition.Y + Size.Y - 0.5f), new Vector2(StartPosition.X + Size.X - CurveRadius + 0.5f, StartPosition.Y + Size.Y - 0.5f), color, BorderThickness); + + var textSize = ImGui.CalcTextSize(Label); + var textStartPadding = 7.0f * ImGuiHelpers.GlobalScale; + var textEndPadding = 7.0f * ImGuiHelpers.GlobalScale; + var textVerticalOffset = textSize.Y / 2.0f; + + DrawList.AddText(new Vector2(StartPosition.X + CurveRadius + textStartPadding, StartPosition.Y - textVerticalOffset), TitleColor.ToU32(), Label); + DrawList.AddLine(new Vector2(StartPosition.X + CurveRadius + textStartPadding + textSize.X + textEndPadding, StartPosition.Y - 0.5f), new Vector2(StartPosition.X + Size.X - CurveRadius - 0.5f, StartPosition.Y - 0.5f), color, BorderThickness); + } + + public InfoBox AddTitle(string title, float percentFill = 0.95f) + { + Label = title; + WidthPercentage = percentFill; + + return DrawListOwner; + } + + public InfoBox AddTitle(string title, out float innerWidth, float percentFill = .95f) + { + Label = title; + WidthPercentage = percentFill; + + TotalWidth = RegionAvailable.X * WidthPercentage; + InnerWidth = TotalWidth - CurveRadius * 3.0f; + + innerWidth = InnerWidth; + return DrawListOwner; + } + + public InfoBoxTable BeginTable(float weight = 0.50f) => new InfoBoxTable(this, weight); + + public InfoBoxList BeginList() => new InfoBoxList(this); + + public InfoBox AddList(IEnumerable rows) + { + return BeginList() + .AddRows(rows) + .EndList(); + } +} diff --git a/KamiLib/Drawing/InfoBoxList.cs b/KamiLib/Drawing/InfoBoxList.cs new file mode 100644 index 0000000..d473aa7 --- /dev/null +++ b/KamiLib/Drawing/InfoBoxList.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using KamiLib.Interfaces; + +namespace KamiLib.Drawing; + +public class InfoBoxList : DrawList +{ + private readonly InfoBox owner; + + public InfoBoxList(InfoBox owner) + { + this.owner = owner; + DrawListOwner = this; + } + + public InfoBox EndList() + { + foreach (var row in DrawActions) + { + owner.AddAction(row); + } + + return owner; + } + + public InfoBoxList AddRows(IEnumerable rows) + { + foreach (var row in rows) + { + row.GetConfigurationRow(this); + } + + return this; + } +} \ No newline at end of file diff --git a/KamiLib/Drawing/InfoBoxTable.cs b/KamiLib/Drawing/InfoBoxTable.cs new file mode 100644 index 0000000..2112ae2 --- /dev/null +++ b/KamiLib/Drawing/InfoBoxTable.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using KamiLib.Interfaces; + +namespace KamiLib.Drawing; + +public class InfoBoxTable +{ + private readonly InfoBox owner; + private readonly float weight; + + private readonly List rows = new(); + private string emptyListString = string.Empty; + + public InfoBoxTable(InfoBox owner, float weight = 0.5f) + { + this.owner = owner; + this.weight = weight; + } + + public InfoBoxTableRow BeginRow() + { + return new InfoBoxTableRow(this); + } + + public InfoBoxTable AddRow(InfoBoxTableRow row) + { + rows.Add(row); + + return this; + } + + public InfoBox EndTable() + { + owner.AddAction(() => + { + if (rows.Count == 0) + { + if (emptyListString != string.Empty) + { + ImGui.TextColored(Colors.Orange, emptyListString); + } + } + else + { + if (ImGui.BeginTable($"", 2, ImGuiTableFlags.None, new Vector2(owner.InnerWidth, 0))) + { + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.None, 1f * (weight)); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.None, 1f * (1 - weight)); + + foreach (var row in rows) + { + ImGui.TableNextColumn(); + + ImGui.PushTextWrapPos(GetWrapPosition()); + row.FirstColumn?.Invoke(); + ImGui.PopTextWrapPos(); + + ImGui.TableNextColumn(); + ImGui.PushTextWrapPos(GetWrapPosition()); + row.SecondColumn?.Invoke(); + ImGui.PopTextWrapPos(); + } + + ImGui.EndTable(); + } + + } + }); + + return owner; + } + + private static float GetWrapPosition() + { + var region = ImGui.GetContentRegionAvail(); + + var cursor = ImGui.GetCursorPos(); + + var wrapPosition = cursor.X + region.X; + + return wrapPosition; + } + + public InfoBoxTable AddConfigurationRows(IEnumerable configurableRows, string? emptyEnumerableString = null) + { + if(emptyEnumerableString is not null) + { + emptyListString = emptyEnumerableString; + } + + foreach (var row in configurableRows) + { + row.GetConfigurationRow(this); + } + + return this; + } + + public InfoBoxTable AddDataRows(IEnumerable dataRows, string? emptyEnumerableString = null) + { + if(emptyEnumerableString is not null) + { + emptyListString = emptyEnumerableString; + } + + foreach (var row in dataRows) + { + row.GetDataRow(this); + } + + return this; + } +} \ No newline at end of file diff --git a/KamiLib/Drawing/InfoBoxTableRow.cs b/KamiLib/Drawing/InfoBoxTableRow.cs new file mode 100644 index 0000000..d12b71a --- /dev/null +++ b/KamiLib/Drawing/InfoBoxTableRow.cs @@ -0,0 +1,24 @@ +using System; + +namespace KamiLib.Drawing; + +public class InfoBoxTableRow : DrawList +{ + private readonly InfoBoxTable owner; + + public Action? FirstColumn => DrawActions.Count > 0 ? DrawActions[0] : null; + public Action? SecondColumn => DrawActions.Count > 1 ? DrawActions[1] : null; + + public InfoBoxTableRow(InfoBoxTable owner) + { + this.owner = owner; + DrawListOwner = this; + } + + public InfoBoxTable EndRow() + { + owner.AddRow(this); + + return owner; + } +} \ No newline at end of file diff --git a/KamiLib/Drawing/TabBar.cs b/KamiLib/Drawing/TabBar.cs new file mode 100644 index 0000000..516bdfc --- /dev/null +++ b/KamiLib/Drawing/TabBar.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using KamiLib.Interfaces; + +namespace KamiLib.Drawing; + +public class TabBar : IDrawable +{ + private readonly List tabs = new(); + private readonly string tabBarID; + private readonly Vector2 childSize; + + public TabBar(string id, Vector2? size = null) + { + tabBarID = id; + childSize = size ?? Vector2.Zero; + } + + public void AddTab(ITabItem tab) => tabs.Add(tab); + public void AddTab(IEnumerable multipleTabs) => tabs.AddRange(multipleTabs); + + public void Draw() + { + ImGui.PushID(tabBarID); + + if (ImGui.BeginTabBar($"###{KamiCommon.PluginName}TabBar", ImGuiTabBarFlags.NoTooltip)) + { + foreach (var tab in tabs) + { + if(tab.Enabled == false) continue; + + if (ImGui.BeginTabItem(tab.TabName)) + { + if (ImGui.BeginChild($"###{KamiCommon.PluginName}TabBarChild", childSize, false, ImGuiWindowFlags.NoScrollbar)) + { + ImGui.PushID(tab.TabName); + tab.Draw(); + ImGui.PopID(); + } + ImGui.EndChild(); + + ImGui.EndTabItem(); + } + } + } + + ImGui.PopID(); + } +} \ No newline at end of file diff --git a/KamiLib/Extensions/PartyListExtensions.cs b/KamiLib/Extensions/PartyListExtensions.cs new file mode 100644 index 0000000..4cb8f9b --- /dev/null +++ b/KamiLib/Extensions/PartyListExtensions.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Party; + +namespace KamiLib.Extensions; + +public static class PartyListExtensions +{ + public static IEnumerable Alive(this IEnumerable list) + { + return list.Where(member => member.GameObject != null && !member.GameObject.IsDead); + } + + public static IEnumerable WithRole(this IEnumerable list, uint roleID) + { + return list.Where(member => member.ClassJob.GameData?.Role == roleID); + } + + public static IEnumerable WithJob(this IEnumerable list, uint jobID) + { + return list.Where(member => member.ClassJob.Id == jobID); + } + + public static IEnumerable WithJob(this IEnumerable list, List jobList) + { + return list.Where(member => jobList.Contains(member.ClassJob.Id)); + } + + public static IEnumerable WithStatus(this IEnumerable list, uint statusID) + { + return list.Where(member => member.HasStatus(statusID)); + } + + public static IEnumerable WithStatus(this IEnumerable list, List statusList) + { + return list.Where(member => member.HasStatus(statusList)); + } +} \ No newline at end of file diff --git a/KamiLib/Extensions/PartyMemberExtensions.cs b/KamiLib/Extensions/PartyMemberExtensions.cs new file mode 100644 index 0000000..5500305 --- /dev/null +++ b/KamiLib/Extensions/PartyMemberExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Party; + +namespace KamiLib.Extensions; + +public static class PartyMemberExtensions +{ + public static bool HasStatus(this PartyMember character, uint statusId) + { + return character.Statuses.Any(status => status.StatusId == statusId); + } + + public static bool HasStatus(this PartyMember character, List statusList) + { + return character.Statuses.Any(status => statusList.Contains(status.StatusId)); + } +} \ No newline at end of file diff --git a/KamiLib/Extensions/PlayerCharacterExtensions.cs b/KamiLib/Extensions/PlayerCharacterExtensions.cs new file mode 100644 index 0000000..6ffa3a6 --- /dev/null +++ b/KamiLib/Extensions/PlayerCharacterExtensions.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; + +namespace KamiLib.Extensions; + +public static class PlayerCharacterExtensions +{ + public static bool HasStatus(this PlayerCharacter character, uint statusId) + { + return character.StatusList.Any(status => status.StatusId == statusId); + } + + public static bool HasStatus(this PlayerCharacter character, List statusList) + { + return character.StatusList.Any(status => statusList.Contains(status.StatusId)); + } + + public static bool HasOnlineStatus(this PlayerCharacter character, uint statusId) + { + return character.OnlineStatus.Id == statusId; + } + + public static int StatusCount(this PlayerCharacter character, List statusList) + { + return character.StatusList.Count(status => statusList.Contains(status.StatusId)); + } + + public static bool HasPet(this PlayerCharacter character) + { + var ownedObjects = Service.ObjectTable.Where(obj => obj.OwnerId == character.ObjectId); + + return ownedObjects.Any(obj => obj.ObjectKind == ObjectKind.BattleNpc && (obj as BattleNpc)?.SubKind == (byte) BattleNpcSubKind.Pet); + } + + public static IEnumerable Alive(this IEnumerable list) + { + return list.Where(member => member.CurrentHp > 0); + } + + public static IEnumerable WithJob(this IEnumerable list, uint jobID) + { + return list.Where(member => member.ClassJob.Id == jobID); + } + + public static IEnumerable WithJob(this IEnumerable list, List jobList) + { + return list.Where(member => jobList.Contains(member.ClassJob.Id)); + } + + public static IEnumerable WithStatus(this IEnumerable list, uint statusID) + { + return list.Where(member => member.HasStatus(statusID)); + } + + public static IEnumerable WithStatus(this IEnumerable list, List statusList) + { + return list.Where(member => member.HasStatus(statusList)); + } +} \ No newline at end of file diff --git a/KamiLib/Extensions/TerritoryTypeExtensions.cs b/KamiLib/Extensions/TerritoryTypeExtensions.cs new file mode 100644 index 0000000..70416b3 --- /dev/null +++ b/KamiLib/Extensions/TerritoryTypeExtensions.cs @@ -0,0 +1,30 @@ +using Dalamud.Interface.Utility; +using Dalamud.Utility; +using ImGuiNET; +using KamiLib.Caching; +using KamiLib.Drawing; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.Extensions; + +public static class TerritoryTypeExtensions +{ + public static void DrawLabel(this TerritoryType data) + { + var placeString = data.GetPlaceNameString(); + + var startPosition = ImGui.GetCursorPos(); + ImGui.TextColored(Colors.Grey, data.RowId.ToString()); + ImGui.SameLine(startPosition.X + 50.0f * ImGuiHelpers.GlobalScale); + ImGui.Text(placeString); + } + + public static string GetPlaceNameString(this TerritoryType data) + { + var placeNameRow = data.PlaceName.Row; + var placeName = LuminaCache.Instance.GetRow(placeNameRow); + var placeString = placeName?.Name.ToDalamudString().TextValue ?? "Unknown PlaceName"; + + return placeString; + } +} diff --git a/KamiLib/GameState/Condition.cs b/KamiLib/GameState/Condition.cs new file mode 100644 index 0000000..050027a --- /dev/null +++ b/KamiLib/GameState/Condition.cs @@ -0,0 +1,79 @@ +using Dalamud.Game.ClientState.Conditions; +using FFXIVClientStructs.FFXIV.Client.Game; +using KamiLib.Caching; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.GameState; + +public static class Condition +{ + public static bool IsBoundByDuty() + { + if(IsInIslandSanctuary()) return false; + + return Service.Condition[ConditionFlag.BoundByDuty] || + Service.Condition[ConditionFlag.BoundByDuty56] || + Service.Condition[ConditionFlag.BoundByDuty95]; + } + + public static bool IsInCombat() => Service.Condition[ConditionFlag.InCombat]; + public static bool IsInCutsceneOrQuestEvent() => IsInCutscene() || IsInQuestEvent(); + public static bool IsDutyRecorderPlayback() => Service.Condition[ConditionFlag.DutyRecorderPlayback]; + public static bool IsIslandDoingSomethingMode() => Service.GameGui.GetAddonByName("MJIPadGuide", 1) != nint.Zero; + + public static bool IsInCutscene() + { + return Service.Condition[ConditionFlag.OccupiedInCutSceneEvent] || + Service.Condition[ConditionFlag.WatchingCutscene] || + Service.Condition[ConditionFlag.WatchingCutscene78]; + } + + public static bool IsInQuestEvent() + { + if (IsInIslandSanctuary() && IsIslandDoingSomethingMode()) return false; + + return Service.Condition[ConditionFlag.OccupiedInQuestEvent]; + } + + public static bool IsBetweenAreas() + { + return Service.Condition[ConditionFlag.BetweenAreas] || + Service.Condition[ConditionFlag.BetweenAreas51]; + } + + public static bool IsInIslandSanctuary() + { + var territoryInfo = LuminaCache.Instance.GetRow(Service.ClientState.TerritoryType); + if (territoryInfo is null) return false; + + // Island Sanctuary + return territoryInfo.TerritoryIntendedUse == 49; + } + + public static bool IsCrafting() + { + return Service.Condition[ConditionFlag.Crafting] || + Service.Condition[ConditionFlag.Crafting40]; + } + + public static bool IsCrossWorld() + { + return Service.Condition[ConditionFlag.ParticipatingInCrossWorldPartyOrAlliance]; + } + + public static bool IsInSanctuary() + { + return GameMain.IsInSanctuary(); + } + + public static bool CheckFlag(ConditionFlag flag) + { + return Service.Condition[flag]; + } + + public static bool IsGathering() + { + return Service.Condition[ConditionFlag.Gathering] || + Service.Condition[ConditionFlag.Gathering42]; + } +} \ No newline at end of file diff --git a/KamiLib/GameState/DutyState.cs b/KamiLib/GameState/DutyState.cs new file mode 100644 index 0000000..95f1aa1 --- /dev/null +++ b/KamiLib/GameState/DutyState.cs @@ -0,0 +1,124 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using KamiLib.Hooking; + +namespace KamiLib.GameState; + +public unsafe class DutyState : IDisposable +{ + private delegate byte DutyEventDelegate(void* a1, void* a2, ushort* a3); + [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08", DetourName = nameof(DutyEventFunction))] + private readonly Hook? dutyEventHook = null; + + public bool IsDutyStarted { get; private set; } + + public delegate void DutyStateEvent(uint duty); + + public event DutyStateEvent? DutyStarted; + public event DutyStateEvent? DutyWiped; + public event DutyStateEvent? DutyRecommenced; + public event DutyStateEvent? DutyCompleted; + + private bool completedThisTerritory; + + private static DutyState? _instance; + public static DutyState Instance => _instance ??= new DutyState(); + + private DutyState() + { + Service.GameInteropProvider.InitializeFromAttributes(this); + + dutyEventHook?.Enable(); + + if (Condition.IsBoundByDuty()) + { + IsDutyStarted = true; + } + + Service.Framework.Update += FrameworkUpdate; + Service.ClientState.TerritoryChanged += TerritoryChanged; + } + + public static void Cleanup() + { + _instance?.Dispose(); + } + + public void Dispose() + { + dutyEventHook?.Dispose(); + + Service.Framework.Update -= FrameworkUpdate; + Service.ClientState.TerritoryChanged -= TerritoryChanged; + } + + private void FrameworkUpdate(IFramework framework) + { + if (!IsDutyStarted && !completedThisTerritory) + { + if (Condition.IsBoundByDuty() && Condition.IsInCombat()) + { + IsDutyStarted = true; + } + } + else if (!Condition.IsBoundByDuty()) + { + IsDutyStarted = false; + } + } + + private void TerritoryChanged(ushort e) + { + if (IsDutyStarted) + { + IsDutyStarted = false; + } + + completedThisTerritory = false; + } + + private byte DutyEventFunction(void* a1, void* a2, ushort* a3) + { + Safety.ExecuteSafe(() => + { + var category = *(a3); + var type = *(uint*)(a3 + 4); + + // DirectorUpdate Category + if (category == 0x6D) + { + switch (type) + { + // Duty Commenced + case 0x40000001: + IsDutyStarted = true; + DutyStarted?.Invoke(Service.ClientState.TerritoryType); + break; + + // Party Wipe + case 0x40000005: + IsDutyStarted = false; + DutyWiped?.Invoke(Service.ClientState.TerritoryType); + break; + + // Duty Recommence + case 0x40000006: + IsDutyStarted = true; + DutyRecommenced?.Invoke(Service.ClientState.TerritoryType); + break; + + // Duty Completed + case 0x40000003: + IsDutyStarted = false; + completedThisTerritory = true; + DutyCompleted?.Invoke(Service.ClientState.TerritoryType); + break; + } + } + }); + + return dutyEventHook!.Original(a1, a2, a3); + } +} diff --git a/KamiLib/Hooking/Delegates.cs b/KamiLib/Hooking/Delegates.cs new file mode 100644 index 0000000..5980ed8 --- /dev/null +++ b/KamiLib/Hooking/Delegates.cs @@ -0,0 +1,26 @@ +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace KamiLib.Hooking; + +public static class Delegates +{ + public static unsafe class Addon + { + public delegate nint OnSetup(AtkUnitBase* addon, int valueCount, AtkValue* values); + public delegate void Draw(AtkUnitBase* addon); + public delegate byte OnRefresh(AtkUnitBase* addon, int valueCount, AtkValue* values); + public delegate void Finalize(AtkUnitBase* addon); + public delegate byte Update(AtkUnitBase* addon); + } + + public static unsafe class Agent + { + public delegate void Show(AgentInterface* agent); + public delegate nint ReceiveEvent(AgentInterface* agent, nint rawData, AtkValue* args, uint argCount, ulong sender); + } + + public static unsafe class Other + { + public delegate void* GoldSaucerUpdate(void* a1, byte* a2, uint a3, ushort a4, void* a5, int* data, byte eventID); + } +} diff --git a/KamiLib/Hooking/Safety.cs b/KamiLib/Hooking/Safety.cs new file mode 100644 index 0000000..4ab6405 --- /dev/null +++ b/KamiLib/Hooking/Safety.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using Dalamud.Logging; + +namespace KamiLib.Hooking; + +public static class Safety +{ + public static void ExecuteSafe(Action action, string? message = null) + { + try + { + action(); + } + catch (Exception exception) + { + var trace = new StackTrace().GetFrame(1); + var callingAssembly = Assembly.GetCallingAssembly().GetName().Name; + + if (trace is not null) + { + var callingClass = trace.GetMethod()?.DeclaringType; + var callingName = trace.GetMethod()?.Name; + + PluginLog.Error($"Exception Source: {callingAssembly} :: {callingClass} :: {callingName}"); + } + + PluginLog.Error(exception, message ?? "Caught Exception Safely"); + } + } +} \ No newline at end of file diff --git a/KamiLib/Interfaces/IDrawable.cs b/KamiLib/Interfaces/IDrawable.cs new file mode 100644 index 0000000..554027d --- /dev/null +++ b/KamiLib/Interfaces/IDrawable.cs @@ -0,0 +1,6 @@ +namespace KamiLib.Interfaces; + +public interface IDrawable +{ + void Draw(); +} \ No newline at end of file diff --git a/KamiLib/Interfaces/IInfoBoxTableRow.cs b/KamiLib/Interfaces/IInfoBoxTableRow.cs new file mode 100644 index 0000000..c3a52eb --- /dev/null +++ b/KamiLib/Interfaces/IInfoBoxTableRow.cs @@ -0,0 +1,18 @@ +using KamiLib.Drawing; + +namespace KamiLib.Interfaces; + +public interface IInfoBoxTableConfigurationRow +{ + void GetConfigurationRow(InfoBoxTable owner); +} + +public interface IInfoBoxTableDataRow +{ + void GetDataRow(InfoBoxTable owner); +} + +public interface IInfoBoxListConfigurationRow +{ + void GetConfigurationRow(InfoBoxList owner); +} \ No newline at end of file diff --git a/KamiLib/Interfaces/IPluginCommand.cs b/KamiLib/Interfaces/IPluginCommand.cs new file mode 100644 index 0000000..f2a8ce1 --- /dev/null +++ b/KamiLib/Interfaces/IPluginCommand.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using KamiLib.ChatCommands; + +namespace KamiLib.Interfaces; + +public interface IPluginCommand +{ + string? CommandArgument { get; } + + IEnumerable SubCommands { get; } + + public bool Execute(CommandData data) + { + var matchingSubCommands = SubCommands + .Where(subCommand => MatchingSubCommand(subCommand, data.SubCommand)) + .ToList(); + + if (matchingSubCommands.Any()) + { + foreach (var subCommand in matchingSubCommands) + { + subCommand.Execute(data); + } + + return true; + } + + return false; + } + + private static bool MatchingSubCommand(ISubCommand subCommand, string? targetCommand) + { + if (subCommand.GetCommand() == targetCommand) return true; + if (subCommand.GetAliases()?.Contains(targetCommand) ?? false) return true; + + return false; + } + +} \ No newline at end of file diff --git a/KamiLib/Interfaces/ISelectable.cs b/KamiLib/Interfaces/ISelectable.cs new file mode 100644 index 0000000..e1c2a8b --- /dev/null +++ b/KamiLib/Interfaces/ISelectable.cs @@ -0,0 +1,8 @@ +namespace KamiLib.Interfaces; + +public interface ISelectable +{ + IDrawable Contents { get; } + void DrawLabel(); + string ID { get; } +} \ No newline at end of file diff --git a/KamiLib/Interfaces/ISubCommand.cs b/KamiLib/Interfaces/ISubCommand.cs new file mode 100644 index 0000000..da6cb85 --- /dev/null +++ b/KamiLib/Interfaces/ISubCommand.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using KamiLib.ChatCommands; + +namespace KamiLib.Interfaces; + +public interface ISubCommand +{ + string? GetCommand(); + IEnumerable? GetAliases(); + bool Execute(CommandData commandData); + string? GetHelpText(); + bool Hidden { get; } + bool HasParameterAction { get; } +} \ No newline at end of file diff --git a/KamiLib/Interfaces/ITabItem.cs b/KamiLib/Interfaces/ITabItem.cs new file mode 100644 index 0000000..77d8970 --- /dev/null +++ b/KamiLib/Interfaces/ITabItem.cs @@ -0,0 +1,8 @@ +namespace KamiLib.Interfaces; + +public interface ITabItem +{ + string TabName { get; } + bool Enabled { get; } + void Draw(); +} \ No newline at end of file diff --git a/KamiLib/KamiCommon.cs b/KamiLib/KamiCommon.cs new file mode 100644 index 0000000..4b11aad --- /dev/null +++ b/KamiLib/KamiCommon.cs @@ -0,0 +1,51 @@ +using System; +using Dalamud.Plugin; +using KamiLib.Blacklist; +using KamiLib.Caching; +using KamiLib.ChatCommands; +using KamiLib.GameState; +using KamiLib.Localization; +using KamiLib.Teleporter; +using KamiLib.UserInterface; +using KamiLib.Windows; + +namespace KamiLib; + +public static class KamiCommon +{ + public static string PluginName { get; private set; } = string.Empty; + public static CommandManager CommandManager { get; private set; } = null!; + public static WindowManager WindowManager { get; private set; } = null!; + + private static Action _saveConfigFunction = null!; + + public static void Initialize(DalamudPluginInterface pluginInterface, string pluginName, Action saveConfig) + { + pluginInterface.Create(); + + PluginName = pluginName; + _saveConfigFunction = saveConfig; + + LocalizationManager.Instance.Initialize(); + + BlacklistDraw.PrimeSearch(); + + CommandManager = new CommandManager(); + WindowManager = new WindowManager(); + + } + + public static void Dispose() + { + CommandManager.Dispose(); + WindowManager.Dispose(); + IconCache.Cleanup(); + GameUserInterface.Cleanup(); + DutyState.Cleanup(); + ChatPayloadManager.Cleanup(); + TeleportManager.Cleanup(); + LocalizationManager.Cleanup(); + } + + public static void SaveConfiguration() => _saveConfigFunction(); +} \ No newline at end of file diff --git a/KamiLib/KamiLib.csproj b/KamiLib/KamiLib.csproj new file mode 100644 index 0000000..fae504c --- /dev/null +++ b/KamiLib/KamiLib.csproj @@ -0,0 +1,72 @@ + + + + net7.0-windows + x64 + enable + preview + true + false + false + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + + + + + + + + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)CheapLoc.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + + + + ResXFileCodeGenerator + Strings.Designer.cs + + + + + + True + True + Strings.resx + + + + diff --git a/KamiLib/KamiLib.sln b/KamiLib/KamiLib.sln new file mode 100644 index 0000000..9b90cf7 --- /dev/null +++ b/KamiLib/KamiLib.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiLib", "KamiLib.csproj", "{52A9E8E2-ACC7-4696-8684-5C4984D0350C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {52A9E8E2-ACC7-4696-8684-5C4984D0350C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52A9E8E2-ACC7-4696-8684-5C4984D0350C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52A9E8E2-ACC7-4696-8684-5C4984D0350C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52A9E8E2-ACC7-4696-8684-5C4984D0350C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/KamiLib/LICENSE b/KamiLib/LICENSE new file mode 100644 index 0000000..6d1a70d --- /dev/null +++ b/KamiLib/LICENSE @@ -0,0 +1,14 @@ + Copyright (C) 2022 MidoriKami + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . \ No newline at end of file diff --git a/KamiLib/Localization/LocalizationManager.cs b/KamiLib/Localization/LocalizationManager.cs new file mode 100644 index 0000000..c5b6457 --- /dev/null +++ b/KamiLib/Localization/LocalizationManager.cs @@ -0,0 +1,41 @@ +using System; +using System.Globalization; +using Dalamud.Logging; + +namespace KamiLib.Localization; + +internal class LocalizationManager : IDisposable +{ + private static LocalizationManager? _instance; + public static LocalizationManager Instance => _instance ??= new LocalizationManager(); + + public void Initialize() + { + Strings.Culture = new CultureInfo(Service.PluginInterface.UiLanguage); + + Service.PluginInterface.LanguageChanged += OnLanguageChange; + } + + public static void Cleanup() + { + _instance?.Dispose(); + } + + public void Dispose() + { + Service.PluginInterface.LanguageChanged -= OnLanguageChange; + } + + private void OnLanguageChange(string languageCode) + { + try + { + PluginLog.Information($"Loading Localization for {languageCode}"); + Strings.Culture = new CultureInfo(languageCode); + } + catch (Exception ex) + { + PluginLog.Error(ex, "Unable to Load Localization"); + } + } +} \ No newline at end of file diff --git a/KamiLib/Localization/Strings.Designer.cs b/KamiLib/Localization/Strings.Designer.cs new file mode 100644 index 0000000..5225651 --- /dev/null +++ b/KamiLib/Localization/Strings.Designer.cs @@ -0,0 +1,331 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace KamiLib.Localization { + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KamiLib.Localization.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Add or Remove Current Zone. + /// + internal static string Blacklist_AddRemoveZone { + get { + return ResourceManager.GetString("Blacklist_AddRemoveZone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add {0} Selected Areas. + /// + internal static string Blacklist_AddSelectedAreas { + get { + return ResourceManager.GetString("Blacklist_AddSelectedAreas", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clear Blacklist. + /// + internal static string Blacklist_ClearBlacklist { + get { + return ResourceManager.GetString("Blacklist_ClearBlacklist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Currently Blacklisted Areas. + /// + internal static string Blacklist_CurrentlyBlacklisted { + get { + return ResourceManager.GetString("Blacklist_CurrentlyBlacklisted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Blacklist is Empty. + /// + internal static string Blacklist_Empty { + get { + return ResourceManager.GetString("Blacklist_Empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove {0} Selected Areas. + /// + internal static string Blacklist_RemoveSelectedAreas { + get { + return ResourceManager.GetString("Blacklist_RemoveSelectedAreas", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search . . . . + /// + internal static string Blacklist_Search { + get { + return ResourceManager.GetString("Blacklist_Search", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select zones to add to blacklist. + /// + internal static string Blacklist_SelectZones { + get { + return ResourceManager.GetString("Blacklist_SelectZones", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Zone Search. + /// + internal static string Blacklist_ZoneSearch { + get { + return ResourceManager.GetString("Blacklist_ZoneSearch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closing {0} Window. + /// + internal static string Command_ClosingWindow { + get { + return ResourceManager.GetString("Command_ClosingWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to display a list of all available sub-commands. + /// + internal static string Command_DisplayHelpText { + get { + return ResourceManager.GetString("Command_DisplayHelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command '{0} {1}' does not exist.. + /// + internal static string Command_DoesntExist { + get { + return ResourceManager.GetString("Command_DoesntExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The command '{0} {1} {2}' does not exist.. + /// + internal static string Command_DoesntExistExtended { + get { + return ResourceManager.GetString("Command_DoesntExistExtended", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command. + /// + internal static string Command_Label { + get { + return ResourceManager.GetString("Command_Label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Configuration Window. + /// + internal static string Command_OpenConfigWindow { + get { + return ResourceManager.GetString("Command_OpenConfigWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening {0} Window. + /// + internal static string Command_OpeningWindow { + get { + return ResourceManager.GetString("Command_OpeningWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open {0} Window. + /// + internal static string Command_OpenWindow { + get { + return ResourceManager.GetString("Command_OpenWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} Window cannot be opened while in a PvP area. + /// + internal static string Command_PvPError { + get { + return ResourceManager.GetString("Command_PvPError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show this message. + /// + internal static string Command_ShowThisMessage { + get { + return ResourceManager.GetString("Command_ShowThisMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add. + /// + internal static string Common_Add { + get { + return ResourceManager.GetString("Common_Add", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error. + /// + internal static string Common_Error { + get { + return ResourceManager.GetString("Common_Error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove. + /// + internal static string Common_Remove { + get { + return ResourceManager.GetString("Common_Remove", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to value. + /// + internal static string Common_Value { + get { + return ResourceManager.GetString("Common_Value", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hold 'Shift' to enable button. + /// + internal static string DisabledButton_HoldShift { + get { + return ResourceManager.GetString("DisabledButton_HoldShift", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select an item in the left pane. + /// + internal static string Selection_NoItemSelected { + get { + return ResourceManager.GetString("Selection_NoItemSelected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot teleport in this situation. + /// + internal static string Teleport_BadSituation { + get { + return ResourceManager.GetString("Teleport_BadSituation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to To use the teleport function, you must install the "Teleporter" plugin. + /// + internal static string Teleport_InstallTeleporter { + get { + return ResourceManager.GetString("Teleport_InstallTeleporter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Teleport. + /// + internal static string Teleport_Label { + get { + return ResourceManager.GetString("Teleport_Label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Destination Aetheryte is not unlocked, teleport cancelled. + /// + internal static string Teleport_NotUnlocked { + get { + return ResourceManager.GetString("Teleport_NotUnlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Teleporting to '{0}'. + /// + internal static string Teleport_TeleportingTo { + get { + return ResourceManager.GetString("Teleport_TeleportingTo", resourceCulture); + } + } + } +} diff --git a/KamiLib/Localization/Strings.de.resx b/KamiLib/Localization/Strings.de.resx new file mode 100644 index 0000000..6eab534 --- /dev/null +++ b/KamiLib/Localization/Strings.de.resx @@ -0,0 +1,109 @@ + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bereiche aktuell auf der Schwarzen Liste + + + Schwarze Liste leeren + + + Löschen der {0} ausgewählten Zonen + + + Halten Sie 'Umschalt' gedrückt, um den Knopf zu aktivieren + + + Aktuelle Zone hinzufügen oder entfernen + + + Hinzufügen + + + Entfernen + + + Zonen Suche + + + Hinzufügen der {0} ausgewählten Zonen + + + Wählen sie Zonen aus um sie der Schwarzen Liste hinzuzufügen + + + Die Schwarze Liste ist leer + + + Der Befehl "{0} {1}" existiert nicht. + + + Öffne Konfigurationsfenster + + + Zeige eine Liste aller vorhandenen Sub-Befehle an + + + Befehl + + + Die Suche . . . + + + Diese Nachricht anzeigen + + + Wert + + + Das {0}-Fenster kann in einem PvP-Bereich nicht geöffnet werden + + + Öffne {0} Fenster + + + Öffne {0} Fenster + + + Schließe {0} Fenster + + + Der Befehl "{0} {1} {2}" existiert nicht. + + + Fehler + + + Ziel-Aetheryte ist nicht freigeschaltet, Teleport abgebrochen + + + In dieser Situation kann nicht teleportiert werden + + + Teleportiere zu {0} + + + Teleport + + + Um die Teleport-Funktion nutzen zu können, müssen Sie das "Teleporter"-Plugin installieren + + + Wählen sie einen Gegenstand im linken Bereich aus + + \ No newline at end of file diff --git a/KamiLib/Localization/Strings.es.resx b/KamiLib/Localization/Strings.es.resx new file mode 100644 index 0000000..826d0d7 --- /dev/null +++ b/KamiLib/Localization/Strings.es.resx @@ -0,0 +1,109 @@ + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Zonas en la lista negra + + + Limpiar Lista Negra + + + Eliminar las {0} zonas seleccionadas + + + Mantén «mayús» para habilitar botón + + + Añadir o Eliminar zona actual + + + Añadir + + + Eliminar + + + Buscar zonas + + + Añadir las {0} zonas seleccionadas + + + Selecciona qué zonas añadir a la lista negra + + + La lista negra está vacía + + + El comando «{0} {1}» no existe. + + + Abrir ventana de configuración + + + muestra una lista de todos los subcomandos disponibles + + + Comando + + + Buscar . . . + + + Mostrar este mensaje + + + valor + + + La ventana {0} no puede abrirse en las zonas PvP + + + Abrir ventana {0} + + + Abriendo ventana {0} + + + Cerrando ventana {0} + + + El comando «{0} {1} {2}» no existe. + + + Error + + + La eterita de destino no está desbloqueada. Teletransporte cancelado + + + Imposible teletransportarse en esta situación + + + Teletransportándose a {0} + + + Teletransporte + + + Para usar la función de teletransporte, debes instalar el plugin «Teleporter» + + + Selecciona un objeto en el panel izquierdo + + \ No newline at end of file diff --git a/KamiLib/Localization/Strings.fr.resx b/KamiLib/Localization/Strings.fr.resx new file mode 100644 index 0000000..628d2cc --- /dev/null +++ b/KamiLib/Localization/Strings.fr.resx @@ -0,0 +1,109 @@ + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Zones actuellement dans la liste noire + + + Vider la liste noire + + + Enlever les {0} zones sélectionnées + + + Tenir 'Maj' pour activer le bouton + + + Ajouter ou enlever la zone actuelle + + + Ajouter + + + Enlever + + + Recherche de zone + + + Ajouter les {0} zones sélectionnées + + + Sélectionner les zones à ajouter à la liste noire + + + La liste noire est vide + + + La commande '{0} {1}' n'existe pas. + + + Ouvrir la fenêtre de configuration + + + afficher une liste de toutes les sous-commandes disponibles + + + Commande + + + Rechercher . . . + + + Afficher ce message + + + valeur + + + La fenêtre {0} ne peut pas être ouverte lorsque vous êtes dans une zone JcJ + + + Ouvrir la fenêtre {0} + + + Ouverture de la fenêtre {0} + + + Fermeture de la fenêtre {0} + + + La commande '{0} {1} {2}' n'existe pas. + + + Erreur + + + L'éthérite de destination n'est pas harmonisée, la téléportation a été annulée + + + La téléportation ne peut pas être effectuée dans cette situation + + + Téléportation vers '{0}' + + + Téléportation + + + Pour utiliser la fonction de téléportation, vous devez installer le plugin "Teleporter" + + + Sélectionnez un élément dans le panneau à gauche + + \ No newline at end of file diff --git a/KamiLib/Localization/Strings.ja.resx b/KamiLib/Localization/Strings.ja.resx new file mode 100644 index 0000000..39e1877 --- /dev/null +++ b/KamiLib/Localization/Strings.ja.resx @@ -0,0 +1,109 @@ + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ブラックリストに登録されているエリア + + + ブラックリストをクリア + + + {0} 個の選択された領域を削除 + + + Shiftキーを押しながらボタンを有効にします + + + 現在のゾーンを追加または削除 + + + 追加 + + + 削除 + + + ゾーン検索 + + + {0} 個の選択されたエリアを削除 + + + ブラックリストに追加するゾーンを選択 + + + ブラックリストは空です。 + + + コマンド '{0} {1}' は存在しません。 + + + 設定ウィンドウを開く + + + 利用可能なコマンドの一覧を表示します。 + + + コマンド + + + 検索 ... + + + このメッセージを表示 + + + + + + PvPエリアでは {0} ウィンドウを開くことができません + + + {0} のウィンドウを開く + + + {0} のウィンドウを開いています... + + + {0} のウィンドウを閉じる + + + コマンド '{0} {1} {2}' は存在しません。 + + + エラー + + + 宛先のエーテライトが未解放のため、テレポートはキャンセルされました。 + + + この状況ではテレポートできません + + + '{0} ' にテレポート中 + + + テレポート + + + テレポート機能を使用するには、"Teleporter" プラグインをインストールする必要があります + + + 左から設定する項目を選択してください + + \ No newline at end of file diff --git a/KamiLib/Localization/Strings.resx b/KamiLib/Localization/Strings.resx new file mode 100644 index 0000000..fceeb2f --- /dev/null +++ b/KamiLib/Localization/Strings.resx @@ -0,0 +1,111 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Currently Blacklisted Areas + + + Clear Blacklist + + + Remove {0} Selected Areas + + + Hold 'Shift' to enable button + + + Add or Remove Current Zone + + + Add + + + Remove + + + Zone Search + + + Add {0} Selected Areas + + + Select zones to add to blacklist + + + Blacklist is Empty + + + The command '{0} {1}' does not exist. + + + Open Configuration Window + + + display a list of all available sub-commands + + + Command + + + Search . . . + + + Show this message + + + value + + + The {0} Window cannot be opened while in a PvP area + + + Open {0} Window + + + Opening {0} Window + + + Closing {0} Window + + + The command '{0} {1} {2}' does not exist. + + + Error + + + Destination Aetheryte is not unlocked, teleport cancelled + + + Cannot teleport in this situation + + + Teleporting to '{0}' + + + Teleport + + + To use the teleport function, you must install the "Teleporter" plugin + + + Select an item in the left pane + + \ No newline at end of file diff --git a/KamiLib/Misc/DutyLists.cs b/KamiLib/Misc/DutyLists.cs new file mode 100644 index 0000000..156c5f5 --- /dev/null +++ b/KamiLib/Misc/DutyLists.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud; +using KamiLib.Caching; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.Misc; + +public enum DutyType +{ + Savage, + Ultimate, + ExtremeUnreal, + Criterion, + Alliance, + None, +} + +public class DutyLists +{ + private List Savage { get; } + private List Ultimate { get; } + private List ExtremeUnreal { get; } + private List Criterion { get; } + private List Alliance { get; } + + private static DutyLists? _instance; + public static DutyLists Instance => _instance ??= new DutyLists(); + + private DutyLists() + { + // ContentType.Row 5 == Raids + Savage = LuminaCache.Instance.OfLanguage(ClientLanguage.English) + .Where(t => t.ContentType.Row == 5) + .Where(t => t.Name.RawString.Contains("Savage")) + .Select(r => r.TerritoryType.Row) + .ToList(); + + // ContentType.Row 28 == Ultimate Raids + Ultimate = LuminaCache.Instance + .Where(t => t.ContentType.Row == 28) + .Select(t => t.TerritoryType.Row) + .ToList(); + + // ContentType.Row 4 == Trials + ExtremeUnreal = LuminaCache.Instance.OfLanguage(ClientLanguage.English) + .Where(t => t.ContentType.Row == 4) + .Where(t => t.Name.RawString.Contains("Extreme") || t.Name.RawString.Contains("Unreal") || t.Name.RawString.Contains("The Minstrel")) + .Select(t => t.TerritoryType.Row) + .ToList(); + + Criterion = LuminaCache.Instance + .Where(row => row.ContentType.Row is 30) + .Select(row => row.TerritoryType.Row) + .ToList(); + + Alliance = LuminaCache.Instance + .Where(r => r.TerritoryIntendedUse is 8) + .Select(r => r.RowId) + .ToList(); + } + + private DutyType GetDutyType(uint dutyId) + { + if (Savage.Contains(dutyId)) return DutyType.Savage; + if (Ultimate.Contains(dutyId)) return DutyType.Ultimate; + if (ExtremeUnreal.Contains(dutyId)) return DutyType.ExtremeUnreal; + if (Criterion.Contains(dutyId)) return DutyType.Criterion; + if (Alliance.Contains(dutyId)) return DutyType.Alliance; + + return DutyType.None; + } + + public bool IsType(uint dutyId, DutyType type) => GetDutyType(dutyId) == type; + public bool IsType(uint dutyId, IEnumerable types) => types.Any(type => IsType(dutyId, type)); +} \ No newline at end of file diff --git a/KamiLib/Misc/Time.cs b/KamiLib/Misc/Time.cs new file mode 100644 index 0000000..ec12a68 --- /dev/null +++ b/KamiLib/Misc/Time.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using KamiLib.Caching; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.Misc; + +public static class Time +{ + public static DateTime NextDailyReset() + { + var now = DateTime.UtcNow; + + if( now.Hour < 15 ) + { + return now.Date.AddHours(15); + } + else + { + return now.AddDays(1).Date.AddHours(15); + } + } + + public static DateTime NextWeeklyReset() + { + return NextDayOfWeek(DayOfWeek.Tuesday, 8); + } + + public static DateTime NextFashionReportReset() + { + return NextWeeklyReset().AddDays(-4); + } + + public static DateTime NextGrandCompanyReset() + { + var now = DateTime.UtcNow; + var targetHour = 20; + + if( now.Hour < targetHour ) + { + return now.Date.AddHours(targetHour); + } + else + { + return now.AddDays(1).Date.AddHours(targetHour); + } + } + + public static DateTime NextLeveAllowanceReset() + { + var now = DateTime.UtcNow; + + if( now.Hour < 12 ) + { + return now.Date.AddHours(12); + } + else + { + return now.Date.AddDays(1); + } + } + + public static DateTime NextDayOfWeek(DayOfWeek weekday, int hour) + { + var today = DateTime.UtcNow; + + if(today.Hour < hour && today.DayOfWeek == weekday) + { + return today.Date.AddHours(hour); + } + else + { + var nextReset = today.AddDays(1); + + while (nextReset.DayOfWeek != weekday) + { + nextReset = nextReset.AddDays(1); + } + + return nextReset.Date.AddHours(hour); + } + } + + public static DateTime NextJumboCactpotReset() + { + var region = LookupDatacenterRegion(Service.ClientState.LocalPlayer?.HomeWorld.GameData?.DataCenter.Row); + + return region switch + { + // Japan + 1 => NextDayOfWeek(DayOfWeek.Saturday, 12), + + // North America + 2 => NextDayOfWeek(DayOfWeek.Sunday, 2), + + // Europe + 3 => NextDayOfWeek(DayOfWeek.Saturday, 19), + + // Australia + 4 => NextDayOfWeek(DayOfWeek.Saturday, 9), + + // Unknown Region + _ => DateTime.MinValue + }; + } + + private static byte LookupDatacenterRegion(uint? playerDatacenterID) + { + if (playerDatacenterID == null) return 0; + + return LuminaCache.Instance + .Where(world => world.RowId == playerDatacenterID.Value) + .Select(dc => dc.Region) + .FirstOrDefault(); + } +} \ No newline at end of file diff --git a/KamiLib/Service.cs b/KamiLib/Service.cs new file mode 100644 index 0000000..21403d4 --- /dev/null +++ b/KamiLib/Service.cs @@ -0,0 +1,22 @@ +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; + +namespace KamiLib; + +internal class Service +{ + [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService] public static ICommandManager Commands { get; private set; } = null!; + [PluginService] public static IClientState ClientState { get; private set; } = null!; + [PluginService] public static IChatGui Chat { get; private set; } = null!; + [PluginService] public static IGameGui GameGui { get; private set; } = null!; + [PluginService] public static ICondition Condition { get; private set; } = null!; + [PluginService] public static IDataManager DataManager { get; private set; } = null!; + [PluginService] public static IObjectTable ObjectTable { get; private set; } = null!; + [PluginService] public static IFramework Framework { get; private set; } = null!; + [PluginService] public static IToastGui Toast { get; private set;} = null!; + [PluginService] public static IAetheryteList AetheryteList { get; private set;} = null!; + [PluginService] public static IGameInteropProvider GameInteropProvider { get; private set;} = null!; + [PluginService] public static ITextureProvider TextureProvider { get; private set;} = null!; +} diff --git a/KamiLib/Teleporter/TeleportManager.cs b/KamiLib/Teleporter/TeleportManager.cs new file mode 100644 index 0000000..bf983a1 --- /dev/null +++ b/KamiLib/Teleporter/TeleportManager.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Aetherytes; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Logging; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; +using KamiLib.Caching; +using KamiLib.ChatCommands; +using KamiLib.Localization; +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; + +namespace KamiLib.Teleporter; + +public class TeleportInfo +{ + public uint CommandID { get; } + public Enum Target { get; } + public Aetheryte Aetherite { get; } + + public TeleportInfo(uint commandID, Enum target, uint aetheriteID) + { + CommandID = commandID; + Target = target; + Aetherite = GetAetheryte(aetheriteID); + } + + private static Aetheryte GetAetheryte(uint id) => LuminaCache.Instance.GetRow(id)!; +} + +public record TeleportLinkPayloads(Enum Location, DalamudLinkPayload Payload); + +public class TeleportManager : IDisposable +{ + private static TeleportManager? _instance; + public static TeleportManager Instance => _instance ??= new TeleportManager(); + + private readonly ICallGateSubscriber teleportIpc; + private readonly ICallGateSubscriber showChatMessageIpc; + + private readonly List teleportInfoList = new(); + + private List ChatLinkPayloads { get; } = new(); + + private TeleportManager() + { + teleportIpc = Service.PluginInterface.GetIpcSubscriber(Strings.Teleport_Label); + showChatMessageIpc = Service.PluginInterface.GetIpcSubscriber("Teleport.ChatMessage"); + } + + public static void Cleanup() + { + _instance?.Dispose(); + } + + public void Dispose() + { + foreach (var payload in teleportInfoList) + { + Service.PluginInterface.RemoveChatLinkHandler(payload.CommandID); + } + } + + public void AddTeleports(IEnumerable teleports) + { + teleportInfoList.AddRange(teleports); + + foreach (var teleport in teleportInfoList) + { + Service.PluginInterface.RemoveChatLinkHandler(teleport.CommandID); + + var linkPayload = Service.PluginInterface.AddChatLinkHandler(teleport.CommandID, TeleportAction); + + ChatLinkPayloads.Add(new TeleportLinkPayloads(teleport.Target, linkPayload)); + } + } + + private void TeleportAction(uint command, SeString message) + { + var teleportInfo = teleportInfoList.First(teleport => teleport.CommandID == command); + + if (AetheryteUnlocked(teleportInfo.Aetherite, out var targetAetheriteEntry)) + { + Teleport(targetAetheriteEntry!); + } + else + { + PluginLog.Error("User attempted to teleport to an aetheryte that is not unlocked"); + UserError(Strings.Teleport_NotUnlocked); + } + } + + public DalamudLinkPayload GetPayload(Enum targetLocation) + { + return ChatLinkPayloads.First(payload => Equals(payload.Location, targetLocation)).Payload; + } + + private void Teleport(AetheryteEntry aetheryte) + { + try + { + var didTeleport = teleportIpc.InvokeFunc(aetheryte.AetheryteId, aetheryte.SubIndex); + var showMessage = showChatMessageIpc.InvokeFunc(); + + if (!didTeleport) + { + UserError(Strings.Teleport_BadSituation); + } + else if (showMessage) + { + Chat.Print(Strings.Teleport_Label, string.Format(Strings.Teleport_TeleportingTo, GetAetheryteName(aetheryte))); + } + } + catch (IpcNotReadyError) + { + PluginLog.Error("Teleport IPC not found"); + UserError(Strings.Teleport_InstallTeleporter); + } + } + + private void UserError(string error) + { + Service.Chat.PrintError(error); + Service.Toast.ShowError(error); + } + + private string GetAetheryteName(AetheryteEntry aetheryte) + { + var gameData = aetheryte.AetheryteData.GameData; + var placeName = gameData?.PlaceName.Value; + + return placeName == null ? "[Name Lookup Failed]" : placeName.Name; + } + + private bool AetheryteUnlocked(ExcelRow aetheryte, out AetheryteEntry? entry) + { + if (Service.AetheryteList.Any(entry => entry.AetheryteId == aetheryte.RowId)) + { + entry = Service.AetheryteList.First(entry => entry.AetheryteId == aetheryte.RowId); + return true; + } + else + { + entry = null; + return false; + } + } +} diff --git a/KamiLib/UserInterface/GameUserInterface.cs b/KamiLib/UserInterface/GameUserInterface.cs new file mode 100644 index 0000000..59e5ad5 --- /dev/null +++ b/KamiLib/UserInterface/GameUserInterface.cs @@ -0,0 +1,61 @@ +using System; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; +// ReSharper disable ConditionIsAlwaysTrueOrFalse + +namespace KamiLib.UserInterface; + +public unsafe class GameUserInterface : IDisposable +{ + public event EventHandler? UiHidden; + public event EventHandler? UiShown; + + private static GameUserInterface? _instance; + public static GameUserInterface Instance => _instance ??= new GameUserInterface(); + + public bool IsVisible => !lastState; + private bool lastState; + + private GameUserInterface() + { + Service.Framework.Update += FrameworkUpdate; + } + + public static void Cleanup() + { + _instance?.Dispose(); + } + + public void Dispose() + { + Service.Framework.Update -= FrameworkUpdate; + } + + private void FrameworkUpdate(IFramework framework) + { + var partyList = (AtkUnitBase*) Service.GameGui.GetAddonByName("_PartyList", 1); + var todoList = (AtkUnitBase*) Service.GameGui.GetAddonByName("_ToDoList", 1); + var enemyList = (AtkUnitBase*) Service.GameGui.GetAddonByName("_EnemyList", 1); + + var partyListVisible = partyList != null && partyList->IsVisible; + var todoListVisible = todoList != null && todoList->IsVisible; + var enemyListVisible = enemyList != null && enemyList->IsVisible; + + var shouldHideUi = !partyListVisible && !todoListVisible && !enemyListVisible; + + if (lastState != shouldHideUi) + { + if (shouldHideUi) + { + UiHidden?.Invoke(this, EventArgs.Empty); + } + else + { + UiShown?.Invoke(this, EventArgs.Empty); + } + } + + lastState = shouldHideUi; + } +} diff --git a/KamiLib/Windows/DrawFlags.cs b/KamiLib/Windows/DrawFlags.cs new file mode 100644 index 0000000..15e95c5 --- /dev/null +++ b/KamiLib/Windows/DrawFlags.cs @@ -0,0 +1,19 @@ +using ImGuiNET; + +namespace KamiLib.Windows; + +public static class DrawFlags +{ + public const ImGuiWindowFlags AutoResize = ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoScrollbar | + ImGuiWindowFlags.NoCollapse | + ImGuiWindowFlags.AlwaysAutoResize; + + public const ImGuiWindowFlags ManualSize = ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoCollapse; + + public const ImGuiWindowFlags LockPosition = ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize; +} \ No newline at end of file diff --git a/KamiLib/Windows/SelectionList.cs b/KamiLib/Windows/SelectionList.cs new file mode 100644 index 0000000..03b6971 --- /dev/null +++ b/KamiLib/Windows/SelectionList.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using KamiLib.Interfaces; +using KamiLib.Localization; + +namespace KamiLib.Windows; + +public class SelectionList +{ + private ISelectable? Selected { get; set; } + + public void Draw(IEnumerable selectables) + { + var frameBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.FrameBg]; + + ImGui.PushStyleColor(ImGuiCol.FrameBg, frameBgColor with { W = 0.05f }); + ImGui.PushStyleVar(ImGuiStyleVar.ScrollbarSize, 0.0f); + var listBoxValid = ImGui.BeginListBox("", new Vector2(-1, -1)); + ImGui.PopStyleVar(); + ImGui.PopStyleColor(); + + if (listBoxValid) + { + foreach (var item in selectables) + { + var headerHoveredColor = ImGui.GetStyle().Colors[(int)ImGuiCol.HeaderHovered]; + var textSelectedColor = ImGui.GetStyle().Colors[(int)ImGuiCol.Header]; + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, headerHoveredColor with { W = 0.1f }); + ImGui.PushStyleColor(ImGuiCol.Header, textSelectedColor with { W = 0.1f }); + + if (ImGui.Selectable($"##SelectableID{item.ID}", Selected?.ID == item.ID)) + { + Selected = Selected == item ? null : item; + } + + ImGui.PopStyleColor(); + ImGui.PopStyleColor(); + + ImGui.SameLine(3.0f); + + item.DrawLabel(); + + ImGui.Spacing(); + } + + ImGui.EndListBox(); + } + } + + public void DrawSelected() + { + if (Selected is null) + { + var available = ImGui.GetContentRegionAvail() / 2.0f; + var textSize = ImGui.CalcTextSize(Strings.Selection_NoItemSelected) / 2.0f; + var center = new Vector2(available.X - textSize.X, available.Y - textSize.Y); + + ImGui.SetCursorPos(center); + ImGui.TextWrapped(Strings.Selection_NoItemSelected); + } + else + { + Selected.Contents.Draw(); + } + } +} \ No newline at end of file diff --git a/KamiLib/Windows/SelectionWindow.cs b/KamiLib/Windows/SelectionWindow.cs new file mode 100644 index 0000000..5bda4b7 --- /dev/null +++ b/KamiLib/Windows/SelectionWindow.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using KamiLib.Drawing; +using KamiLib.Interfaces; + +namespace KamiLib.Windows; + +public abstract class SelectionWindow : Window, IDrawable +{ + private readonly float horizontalWeight; + private readonly float verticalHeight; + private const bool ShowBorders = false; + + private readonly SelectionList selectionList = new(); + + protected abstract IEnumerable GetSelectables(); + protected bool ShowScrollBar = true; + + protected SelectionWindow(string windowName, float xPercent, float height) : base(windowName) + { + horizontalWeight = xPercent; + verticalHeight = height; + } + + public override void Draw() + { + var region = ImGui.GetContentRegionAvail(); + var itemSpacing = ImGui.GetStyle().ItemSpacing; + + var leftSideWidth = region.X * horizontalWeight - itemSpacing.X / 2.0f; + var topLeftSideHeight = region.Y - verticalHeight * ImGuiHelpers.GlobalScale - itemSpacing.Y / 2.0f; + + if(ImGui.BeginChild($"###{KamiCommon.PluginName}LeftSide", new Vector2( leftSideWidth, topLeftSideHeight), ShowBorders, ImGuiWindowFlags.NoDecoration)) + { + selectionList.Draw(GetSelectables()); + } + ImGui.EndChild(); + + var bottomLeftChildPosition = ImGui.GetCursorPos(); + + ImGui.SameLine(); + DrawVerticalLine(); + + var rightSideWidth = region.X * (1.0f - horizontalWeight) - itemSpacing.X / 2.0f; + + if(ImGui.BeginChild($"###{KamiCommon.PluginName}RightSide", new Vector2(rightSideWidth, 0), ShowBorders, (ShowScrollBar ? ImGuiWindowFlags.AlwaysVerticalScrollbar : ImGuiWindowFlags.None) | ImGuiWindowFlags.NoDecoration)) + { + selectionList.DrawSelected(); + } + ImGui.EndChild(); + + ImGui.SetCursorPos(bottomLeftChildPosition); + + if(ImGui.BeginChild($"###{KamiCommon.PluginName}BottomLeftSide", new Vector2(leftSideWidth, verticalHeight * ImGuiHelpers.GlobalScale), ShowBorders, ImGuiWindowFlags.NoDecoration)) + { + DrawExtras(); + } + ImGui.EndChild(); + + DrawSpecial(); + } + + protected virtual void DrawExtras() + { + + } + + protected virtual void DrawSpecial() + { + + } + + private static void DrawVerticalLine() + { + var contentArea = ImGui.GetContentRegionAvail(); + var itemSpacing = ImGui.GetStyle().ItemSpacing; + var cursor = new Vector2(ImGui.GetCursorScreenPos().X - itemSpacing.X / 2.0f, ImGui.GetCursorScreenPos().Y ); + var drawList = ImGui.GetWindowDrawList(); + var color = ImGui.GetColorU32(Colors.White); + + drawList.AddLine(cursor, cursor with {Y = cursor.Y + contentArea.Y}, color, 1.0f); + } +} diff --git a/KamiLib/Windows/WindowManager.cs b/KamiLib/Windows/WindowManager.cs new file mode 100644 index 0000000..2001e55 --- /dev/null +++ b/KamiLib/Windows/WindowManager.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Interface.Windowing; +using KamiLib.ChatCommands; + +namespace KamiLib.Windows; + +public class WindowManager : IDisposable +{ + private readonly WindowSystem windowSystem; + + private readonly List windows = new(); + + public WindowManager() + { + windowSystem = new WindowSystem(KamiCommon.PluginName); + + windows.ForEach(window => windowSystem.AddWindow(window)); + + Service.PluginInterface.UiBuilder.Draw += DrawUI; + Service.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; + } + + public void Dispose() + { + Service.PluginInterface.UiBuilder.Draw -= DrawUI; + Service.PluginInterface.UiBuilder.OpenConfigUi -= DrawConfigUI; + + windowSystem.RemoveAllWindows(); + } + + public void AddWindow(Window newWindow) + { + if (windowSystem.Windows.All(w => w.WindowName != newWindow.WindowName)) + { + windows.Add(newWindow); + windowSystem.AddWindow(newWindow); + } + } + + public void AddConfigurationWindow(T configWindow) where T : Window + { + windows.Add(configWindow); + windowSystem.AddWindow(configWindow); + + KamiCommon.CommandManager.AddCommand(new OpenWindowCommand(null, false, "Configuration")); + KamiCommon.CommandManager.AddCommand(new OpenWindowCommand("silent", true, "Configuration")); + } + + public void RemoveWindow(Window window) + { + windows.Remove(window); + windowSystem.RemoveWindow(window); + } + + public IReadOnlyCollection GetWindows() => windows; + + public T? GetWindowOfType() => windows.OfType().FirstOrDefault(); + private void DrawUI() => windowSystem.Draw(); + private void DrawConfigUI() => KamiCommon.CommandManager.OnCommand($"{KamiCommon.PluginName}", "silent"); +} \ No newline at end of file