├── custom ├── preview-changelog ├── AetherCompassPreview.json └── PreviewBuild.targets ├── res └── img │ ├── icon.png │ ├── _iconalt │ ├── icon0_0_256.png │ ├── icon0_0_512.png │ └── icon0_base_512.png │ └── icon_by_arkfrostlumas.png ├── src ├── Compasses │ ├── Configs │ │ ├── EurekanCompassConfig.cs │ │ ├── AetherCurrentCompassConfig.cs │ │ ├── DebugCompassConfig.cs │ │ ├── GatheringPointCompassConfig.cs │ │ ├── MobHuntCompassConfig.cs │ │ ├── QuestCompassConfig.cs │ │ └── IslandSanctuaryCompassConfig.cs │ ├── Objectives │ │ ├── IslandCachedCompassObjective.cs │ │ ├── MobHunCachedCompassObjective.cs │ │ ├── DebugCachedCompassObjective.cs │ │ └── CachedCompassObjective.cs │ ├── CompassConfig.cs │ ├── AetherCurrentCompass.cs │ ├── EurekanCompass.cs │ ├── DebugCompass.cs │ ├── MobHuntCompass.cs │ ├── CompassManager.cs │ ├── GatheringPointCompass.cs │ ├── IslandSanctuaryCompass.cs │ └── QuestCompass.cs ├── Common │ ├── Attributes │ │ └── CompassTypeAttribute.cs │ ├── MathUtil.cs │ ├── Direction.cs │ ├── DrawAction.cs │ ├── FixedMapLinkPayload.cs │ ├── ActionQueue.cs │ └── CompassUtil.cs ├── Game │ ├── Language.cs │ ├── SeFunctions │ │ ├── Sound.cs │ │ ├── ZoneMap.cs │ │ ├── Projection.cs │ │ └── Quests.cs │ ├── GameObjects.cs │ └── ZoneWatcher.cs ├── UI │ ├── Notifier.cs │ ├── GUI │ │ ├── CompassOverlay.cs │ │ ├── ImGuiEx.cs │ │ ├── CompassDetailsWindow.cs │ │ ├── IconManager.cs │ │ ├── UiHelper.cs │ │ └── ConfigUi.cs │ └── Chat.cs ├── PluginUtil.cs ├── PluginCommands.cs ├── Plugin.cs └── PluginConfig.cs ├── packages.lock.json ├── DalamudPackager.targets ├── .github └── FUNDING.yml ├── AetherCompass.json ├── AetherCompass.sln ├── .gitattributes ├── AetherCompass.csproj ├── .editorconfig ├── README.md └── .gitignore /custom/preview-changelog: -------------------------------------------------------------------------------- 1 | - Fix: Not detecting gathering objects in Island Sanctuary -------------------------------------------------------------------------------- /res/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/HEAD/res/img/icon.png -------------------------------------------------------------------------------- /res/img/_iconalt/icon0_0_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/HEAD/res/img/_iconalt/icon0_0_256.png -------------------------------------------------------------------------------- /res/img/_iconalt/icon0_0_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/HEAD/res/img/_iconalt/icon0_0_512.png -------------------------------------------------------------------------------- /res/img/icon_by_arkfrostlumas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/HEAD/res/img/icon_by_arkfrostlumas.png -------------------------------------------------------------------------------- /res/img/_iconalt/icon0_base_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/HEAD/res/img/_iconalt/icon0_base_512.png -------------------------------------------------------------------------------- /src/Compasses/Configs/EurekanCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | [Serializable] 4 | public class EurekanCompassConfig : CompassConfig 5 | { } 6 | } 7 | -------------------------------------------------------------------------------- /src/Compasses/Configs/AetherCurrentCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | [Serializable] 4 | public class AetherCurrentCompassConfig : CompassConfig 5 | { } 6 | } 7 | -------------------------------------------------------------------------------- /src/Compasses/Configs/DebugCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | public class DebugCompassConfig : CompassConfig 4 | { 5 | public DebugCompassConfig() 6 | { 7 | Enabled = false; 8 | ShowDetail = true; 9 | NotifySeId = 4; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[2.1.12, )", 8 | "resolved": "2.1.12", 9 | "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /DalamudPackager.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /src/Compasses/Configs/GatheringPointCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | [Serializable] 4 | public class GatheringPointCompassConfig : CompassConfig 5 | { 6 | public bool ShowExported = true; 7 | 8 | public override void Load(CompassConfig config) 9 | { 10 | base.Load(config); 11 | if (config is GatheringPointCompassConfig gpc) 12 | ShowExported = gpc.ShowExported; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Common/Attributes/CompassTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Common.Attributes 2 | { 3 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 4 | public sealed class CompassTypeAttribute : Attribute 5 | { 6 | public readonly CompassType Type; 7 | public CompassTypeAttribute(CompassType type) 8 | { 9 | Type = type; 10 | } 11 | } 12 | 13 | 14 | public enum CompassType : byte 15 | { 16 | Unknown = 0, 17 | Standard, 18 | Experimental, 19 | Debug, 20 | Invalid, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/MathUtil.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Common 2 | { 3 | public static class MathUtil 4 | { 5 | public const float PI2 = MathF.PI * 2; 6 | public const float PIOver2 = MathF.PI / 2; 7 | 8 | public static bool IsBetween(float x, float min, float max, bool inclusive = false) 9 | => inclusive ? (x >= min && x <= max) : (x > min && x < max); 10 | 11 | public static float Clamp(float x, float min, float max) 12 | => MathF.Min(max, MathF.Max(min, x)); 13 | 14 | public static float TruncateToOneDecimalPlace(float v) 15 | => MathF.Truncate(v * 10) / 10f; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Compasses/Configs/MobHuntCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | [Serializable] 4 | public class MobHuntCompassConfig : CompassConfig 5 | { 6 | public bool DetectS = true; 7 | public bool DetectA = true; 8 | public bool DetectB = true; 9 | public bool DetectSSMinion = true; 10 | 11 | public override void Load(CompassConfig config) 12 | { 13 | base.Load(config); 14 | if (config is not MobHuntCompassConfig mhc) return; 15 | DetectS = mhc.DetectS; 16 | DetectA = mhc.DetectA; 17 | DetectB = mhc.DetectB; 18 | DetectSSMinion = mhc.DetectSSMinion; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: yomishino 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/Common/Direction.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Common 2 | { 3 | [Flags] 4 | public enum Direction : byte 5 | { 6 | O = 0, 7 | Up = 1<<0, 8 | Right = 1<<1, 9 | Down = 1<<2, 10 | Left = 1<<3, 11 | 12 | UpperRight = Up | Right, 13 | UpperLeft = Up | Left, 14 | BottomRight = Down | Right, 15 | BottomLeft = Down | Left, 16 | 17 | NaN = byte.MaxValue 18 | } 19 | 20 | [Flags] 21 | public enum CompassDirection : byte 22 | { 23 | O = 0, 24 | North = 1<<0, 25 | East = 1<<1, 26 | South = 1<<2, 27 | West = 1<<3, 28 | 29 | NorthEast = North|East, 30 | NorthWest = North|West, 31 | SouthEast = South|East, 32 | SouthWest = South|West, 33 | 34 | NaN = byte.MaxValue 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Game/Language.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.Text.Sanitizer; 2 | using static Dalamud.ClientLanguage; 3 | 4 | namespace AetherCompass.Game 5 | { 6 | public static class Language 7 | { 8 | public static Dalamud.ClientLanguage ClientLanguage 9 | => Plugin.ClientState.ClientLanguage; 10 | 11 | public static int GetAdjustedTextMaxLength(int maxLengthEN) 12 | => ClientLanguage == Japanese ? maxLengthEN / 2 : maxLengthEN; 13 | 14 | private static readonly Sanitizer sanitizer = new(ClientLanguage); 15 | 16 | public static string SanitizeText(string rawString) 17 | => sanitizer.Sanitize(rawString, ClientLanguage); 18 | 19 | public static class Unit 20 | { 21 | public static string Yalm 22 | => ClientLanguage == Japanese ? "m" : "y"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Compasses/Objectives/IslandCachedCompassObjective.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 2 | using static FFXIVClientStructs.FFXIV.Client.UI.UI3DModule; 3 | 4 | namespace AetherCompass.Compasses.Objectives 5 | { 6 | public unsafe class IslandCachedCompassObjective : CachedCompassObjective 7 | { 8 | public IslandObjectType Type { get; private set; } 9 | 10 | public IslandCachedCompassObjective(GameObject* obj, IslandObjectType type) 11 | : base(obj) 12 | { 13 | Init(type); 14 | } 15 | 16 | public IslandCachedCompassObjective(ObjectInfo* info, IslandObjectType type) 17 | : base(info) 18 | { 19 | Init(type); 20 | } 21 | 22 | private void Init(IslandObjectType type) 23 | { 24 | Type = type; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Compasses/Configs/QuestCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | [Serializable] 4 | public class QuestCompassConfig : CompassConfig 5 | { 6 | public bool EnabledInSoloContents = true; 7 | //public bool DetectEnemy = true; 8 | public bool HideHidden = true; 9 | public bool ShowQuestName = true; 10 | public bool ShowAllRelated = true; 11 | public bool MarkerTextInOneLine = false; 12 | 13 | public override void Load(CompassConfig config) 14 | { 15 | base.Load(config); 16 | if (config is not QuestCompassConfig qc) return; 17 | EnabledInSoloContents = qc.EnabledInSoloContents; 18 | //DetectEnemy = qc.DetectEnemy; 19 | HideHidden = qc.HideHidden; 20 | ShowQuestName = qc.ShowQuestName; 21 | ShowAllRelated = qc.ShowAllRelated; 22 | MarkerTextInOneLine = qc.MarkerTextInOneLine; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Compasses/CompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses 2 | { 3 | [Serializable] 4 | public abstract class CompassConfig 5 | { 6 | public bool Enabled = false; 7 | public bool MarkScreen = true; 8 | public bool ShowDetail = true; 9 | public bool NotifyChat = false; 10 | public bool NotifySe = false; 11 | public int NotifySeId = 1; 12 | public bool NotifyToast = false; 13 | 14 | public virtual void Load(CompassConfig config) 15 | { 16 | Enabled = config.Enabled; 17 | MarkScreen = config.MarkScreen; 18 | ShowDetail = config.ShowDetail; 19 | NotifyChat = config.NotifyChat; 20 | NotifySe = config.NotifySe; 21 | NotifySeId = config.NotifySeId; 22 | NotifyToast = config.NotifyToast; 23 | } 24 | 25 | public virtual void CheckValueValidity() 26 | { 27 | if (NotifySeId < 1) NotifySeId = 1; 28 | if (NotifySeId > 16) NotifySeId = 16; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Common/DrawAction.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Common 2 | { 3 | public class DrawAction 4 | { 5 | private readonly Action action; 6 | public bool Important { get; } 7 | 8 | 9 | public DrawAction(Action action, bool important = false) 10 | { 11 | this.action = action ?? throw new ArgumentNullException(nameof(action)); 12 | Important = important; 13 | } 14 | 15 | public void Invoke() => action.Invoke(); 16 | 17 | public static implicit operator Action(DrawAction a) => a.action; 18 | 19 | public static DrawAction? Combine(bool important, params DrawAction?[] drawActions) 20 | { 21 | if (drawActions.Length == 0) return null; 22 | return Delegate.Combine(Array.ConvertAll(drawActions, drawAction => drawAction == null ? null : (Action)drawAction)) 23 | is not Action combined ? null : new(combined, important); 24 | } 25 | 26 | public static DrawAction? Combine(params DrawAction?[] drawActions) 27 | => Combine(false, drawActions); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Compasses/Objectives/MobHunCachedCompassObjective.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 2 | using ObjectInfo = FFXIVClientStructs.FFXIV.Client.UI.UI3DModule.ObjectInfo; 3 | 4 | namespace AetherCompass.Compasses.Objectives 5 | { 6 | public unsafe class MobHunCachedCompassObjective : CachedCompassObjective 7 | { 8 | public NMRank Rank { get; private set; } 9 | public bool IsSSMinion { get; private set; } 10 | 11 | public MobHunCachedCompassObjective(GameObject* obj, NMRank rank, bool isHostile) 12 | : base (obj) 13 | { 14 | Init(rank, isHostile); 15 | } 16 | 17 | public MobHunCachedCompassObjective(ObjectInfo* info, NMRank rank, bool isHostile) 18 | : base(info) 19 | { 20 | Init(rank, isHostile); 21 | } 22 | 23 | private void Init(NMRank rank, bool isHostile) 24 | { 25 | Rank = rank; 26 | IsSSMinion = rank == NMRank.B && isHostile; 27 | } 28 | 29 | public string GetExtendedRank() => 30 | IsSSMinion ? $"{Rank} - SS Minion" : $"{Rank}"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AetherCompass.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Aether Compass", 3 | "Author": "yomishino", 4 | "Punchline": "Compasses that detect and mark certain nearby objects/NPCs", 5 | "Description": "This plugin detects certain objects/NPCs such as Aether Currents nearby and shows where they are by various ways, e.g. pointing to their locations on screen and notifying you in Chat.\n\nCurrently detects:\n\t- Aether Currents\n\t- Eureka Elementals (by apetih)\n\t- Mob Hunt Elite Marks\n\t- Gathering Points\n\t- [Experimental] Island Sanctuary Gathering Objects and Animals\n\t- [Experimental] Quest-related NPCs/Objects \n\nNote: Because most objects/NPCs are not loaded when they are too faraway or when there are too many entities nearby (such as too many player characters), they will not be detected in this case.\n\nIcon by Lumas Arkfrost", 6 | "RepoUrl": "https://github.com/yomishino/FFXIVAetherCompass", 7 | "Tags": [ 8 | "map", 9 | "compass" 10 | ], 11 | "CategoryTags": [ 12 | "UI" 13 | ], 14 | "ImageUrls": "", 15 | "IconUrl": "https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/master/res/img/icon_by_arkfrostlumas.png", 16 | "DalamudApiLevel": 9 17 | } -------------------------------------------------------------------------------- /src/UI/Notifier.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Game.SeFunctions; 2 | using Dalamud.Game.Text.SeStringHandling; 3 | using System.Threading.Tasks; 4 | 5 | namespace AetherCompass.UI 6 | { 7 | public static class Notifier 8 | { 9 | 10 | private static DateTime lastSeNotifiedTime = DateTime.MinValue; 11 | 12 | 13 | public static async void TryNotifyByChat(SeString msg, bool playSe, int macroSeId = 0) 14 | { 15 | Chat.PrintChat(msg); 16 | await Task.Run(() => 17 | { 18 | if (playSe && CanNotifyBySe()) 19 | { 20 | Sound.PlaySoundEffect(macroSeId); 21 | lastSeNotifiedTime = DateTime.UtcNow; 22 | } 23 | }); 24 | } 25 | 26 | public static void TryNotifyByToast(string msg) 27 | { 28 | Plugin.ToastGui.ShowNormal(msg); 29 | } 30 | 31 | 32 | private static bool CanNotifyBySe() 33 | => (DateTime.UtcNow - lastSeNotifiedTime).TotalSeconds > 3; 34 | 35 | 36 | public static void ResetTimer() 37 | { 38 | lastSeNotifiedTime = DateTime.MinValue; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Compasses/Objectives/DebugCachedCompassObjective.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 3 | using ObjectInfo = FFXIVClientStructs.FFXIV.Client.UI.UI3DModule.ObjectInfo; 4 | 5 | namespace AetherCompass.Compasses.Objectives 6 | { 7 | public unsafe class DebugCachedCompassObjective : CachedCompassObjective 8 | { 9 | public ObjectKind ObjectKind { get; private set; } 10 | public float Distance2D { get; private set; } 11 | public float RotationFromPlayer { get; private set; } 12 | 13 | 14 | public DebugCachedCompassObjective(GameObject* obj) : base(obj) 15 | { 16 | Init(obj); 17 | } 18 | 19 | public DebugCachedCompassObjective(ObjectInfo* info) : base(info) 20 | { 21 | var obj = info != null ? info->GameObject : null; 22 | Init(obj); 23 | } 24 | 25 | private void Init(GameObject* obj) 26 | { 27 | if (obj != null) 28 | { 29 | ObjectKind = (ObjectKind)obj->ObjectKind; 30 | Distance2D = CompassUtil.Get2DDistanceFromPlayer(obj); 31 | RotationFromPlayer = CompassUtil.GetRotationFromPlayer(obj); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Game/SeFunctions/Sound.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace AetherCompass.Game.SeFunctions 4 | { 5 | internal static class Sound 6 | { 7 | private delegate long PlaySoundDelegate(int id, long unk1, long unk2); 8 | private static readonly PlaySoundDelegate? playSound; 9 | 10 | static Sound() 11 | { 12 | var addr = Plugin.SigScanner.ScanText("E8 ?? ?? ?? ?? 4D 39 BE"); 13 | playSound ??= Marshal.GetDelegateForFunctionPointer(addr); 14 | } 15 | 16 | public static void PlaySoundEffect(int macroSeId) 17 | => playSound?.Invoke(FromMacroSeId(macroSeId), 0, 0); 18 | 19 | private static int FromMacroSeId(int id) 20 | => id switch 21 | { 22 | 1 => 0x25, 23 | 2 => 0x26, 24 | 3 => 0x27, 25 | 4 => 0x28, 26 | 5 => 0x29, 27 | 6 => 0x2A, 28 | 7 => 0x2B, 29 | 8 => 0x2C, 30 | 9 => 0x2D, 31 | 10 => 0x2E, 32 | 11 => 0x2F, 33 | 12 => 0x30, 34 | 13 => 0x31, 35 | 14 => 0x32, 36 | 15 => 0x33, 37 | 16 => 0x34, 38 | _ => 0x0 39 | }; 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/UI/GUI/CompassOverlay.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using Dalamud.Interface; 3 | using Dalamud.Interface.Utility; 4 | using ImGuiNET; 5 | 6 | 7 | namespace AetherCompass.UI.GUI 8 | { 9 | public class CompassOverlay 10 | { 11 | private readonly ActionQueue drawActions = new(200); 12 | 13 | private static readonly ImGuiWindowFlags winFlags = 14 | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking 15 | | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoInputs 16 | | ImGuiWindowFlags.NoBackground 17 | | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing; 18 | 19 | public void Draw() 20 | { 21 | ImGuiHelpers.ForceNextWindowMainViewport(); 22 | ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos); 23 | ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); 24 | ImGui.Begin("AetherCompassOverlay", winFlags); 25 | drawActions.DoAll(); 26 | ImGui.End(); 27 | } 28 | 29 | public bool AddDrawAction(Action? a, bool important = false) 30 | => a != null && drawActions.QueueAction(a, important); 31 | 32 | public bool AddDrawAction(DrawAction? a) 33 | => a != null && drawActions.QueueAction(a); 34 | 35 | public void Clear() => drawActions.Clear(); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Game/SeFunctions/ZoneMap.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace AetherCompass.Game.SeFunctions 4 | { 5 | public static class ZoneMap 6 | { 7 | private static readonly IntPtr ZoneMapInfoPtr 8 | = Plugin.SigScanner.GetStaticAddressFromSig( 9 | "8B 2D ?? ?? ?? ?? 41 BF"); 10 | 11 | // Some territories' use maps that are not "00" named, such as "s1t1/01", 12 | // and many but not all of them are multi-layered maps such as housing maps. 13 | // For those, this returns the real map id. 14 | // But will return 0 for those "00" named maps. 15 | public static uint GetCurrentAltMapId() 16 | => ZoneMapInfoPtr == IntPtr.Zero 17 | ? 0 : (uint)Marshal.ReadInt32(ZoneMapInfoPtr, 0); 18 | 19 | // Same as above? 20 | public static uint GetCurrentAltMapId2() 21 | => ZoneMapInfoPtr == IntPtr.Zero 22 | ? 0 : (uint)Marshal.ReadInt32(ZoneMapInfoPtr, 4); 23 | 24 | // Returns 0 when not in any named area 25 | public static uint GetCurrentAreaPlaceNameId() 26 | => ZoneMapInfoPtr == IntPtr.Zero 27 | ? 0 : (uint)Marshal.ReadInt32(ZoneMapInfoPtr, 0x10); 28 | 29 | // Returns 0 when not in range of any named landmark 30 | public static uint GetCurrentLandmarkPlaceNameId() 31 | => ZoneMapInfoPtr == IntPtr.Zero ? 0 : (uint)Marshal.ReadInt32(ZoneMapInfoPtr, 0x14); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /custom/AetherCompassPreview.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Aether Compass [Preview]", 3 | "Author": "yomishino", 4 | "Punchline": "Compasses that detect and mark certain nearby objects/NPCs", 5 | "Description": "[Important!] Please disable AND uninstall any non-preview version if you'd like to install this version.\n\nThis plugin detects certain objects/NPCs such as Aether Currents nearby and shows where they are by various ways, e.g. pointing to their locations on screen and notifying you in Chat.\n\nCurrently detects:\n\t- Aether Currents\n\t- Eureka Elementals (by apetih)\n\t- Mob Hunt Elite Marks\n\t- Gathering Points\n\t- [Experimental] Island Sanctuary Gathering Objects and Animals\n\t- [Experimental] Quest-related NPCs/Objects\n\t- [Experimental] Eureka Elementals (by apetih) \n\nNote: Because most objects/NPCs are not loaded when they are too faraway or when there are too many entities nearby (such as too many player characters), they will not be detected in this case.\n\nIcon by Lumas Arkfrost", 6 | "RepoUrl": "https://github.com/yomishino/FFXIVAetherCompass", 7 | "Tags": [ 8 | "map", 9 | "compass" 10 | ], 11 | "CategoryTags": [ 12 | "UI" 13 | ], 14 | "ImageUrls": "", 15 | "IconUrl": "https://raw.githubusercontent.com/yomishino/FFXIVAetherCompass/master/res/img/icon_by_arkfrostlumas.png", 16 | "DalamudApiLevel": 9, 17 | "LoadRequiredState": 0, 18 | "LoadSync": false, 19 | "LoadPriority": 0, 20 | "InternalName": "AetherCompassPreview", 21 | "ApplicableVersion": "any", 22 | "": null 23 | } -------------------------------------------------------------------------------- /src/Compasses/Configs/IslandSanctuaryCompassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AetherCompass.Compasses.Configs 2 | { 3 | [Serializable] 4 | public class IslandSanctuaryCompassConfig : CompassConfig 5 | { 6 | public bool DetectGathering = true; 7 | public bool DetectAnimals = true; 8 | public bool ShowNameOnMarkerGathering = true; 9 | public bool ShowNameOnMarkerAnimals = true; 10 | public bool HideMarkerWhenNotInScreenGathering = false; 11 | public bool HideMarkerWhenNotInScreenAnimals = false; 12 | public bool UseAnimalSpecificIcons = false; 13 | public uint GatheringObjectsToShow = uint.MaxValue; 14 | public uint AnimalsToShow = uint.MaxValue; 15 | 16 | public override void Load(CompassConfig config) 17 | { 18 | base.Load(config); 19 | if (config is not IslandSanctuaryCompassConfig isc) return; 20 | DetectGathering = isc.DetectGathering; 21 | DetectAnimals = isc.DetectAnimals; 22 | ShowNameOnMarkerGathering = isc.ShowNameOnMarkerGathering; 23 | ShowNameOnMarkerAnimals = isc.ShowNameOnMarkerAnimals; 24 | HideMarkerWhenNotInScreenGathering = isc.HideMarkerWhenNotInScreenGathering; 25 | HideMarkerWhenNotInScreenAnimals = isc.HideMarkerWhenNotInScreenAnimals; 26 | UseAnimalSpecificIcons = isc.UseAnimalSpecificIcons; 27 | GatheringObjectsToShow = isc.GatheringObjectsToShow; 28 | AnimalsToShow = isc.AnimalsToShow; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AetherCompass.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32825.248 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AetherCompass", "AetherCompass.csproj", "{E999447C-5E05-4992-84C2-E906A22C070D}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | PreRelease|x64 = PreRelease|x64 12 | PreTest|x64 = PreTest|x64 13 | Release|x64 = Release|x64 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {E999447C-5E05-4992-84C2-E906A22C070D}.Debug|x64.ActiveCfg = Debug|x64 17 | {E999447C-5E05-4992-84C2-E906A22C070D}.Debug|x64.Build.0 = Debug|x64 18 | {E999447C-5E05-4992-84C2-E906A22C070D}.PreRelease|x64.ActiveCfg = PreRelease|x64 19 | {E999447C-5E05-4992-84C2-E906A22C070D}.PreRelease|x64.Build.0 = PreRelease|x64 20 | {E999447C-5E05-4992-84C2-E906A22C070D}.PreTest|x64.ActiveCfg = PreTest|x64 21 | {E999447C-5E05-4992-84C2-E906A22C070D}.PreTest|x64.Build.0 = PreTest|x64 22 | {E999447C-5E05-4992-84C2-E906A22C070D}.Release|x64.ActiveCfg = Release|x64 23 | {E999447C-5E05-4992-84C2-E906A22C070D}.Release|x64.Build.0 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2E11D229-ECAD-40B3-912D-D6EE7EB3FABF} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Game/GameObjects.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 2 | using FFXIVClientStructs.FFXIV.Client.UI; 3 | using static FFXIVClientStructs.FFXIV.Client.UI.UI3DModule; 4 | 5 | namespace AetherCompass.Game 6 | { 7 | internal unsafe static class GameObjects 8 | { 9 | private unsafe static readonly UI3DModule* UI3DModule 10 | = ((UIModule*)Plugin.GameGui.GetUIModule())->GetUI3DModule(); 11 | 12 | // Those that would be rendered on screen 13 | internal unsafe static ObjectInfo** SortedObjectInfoPointerArray 14 | => UI3DModule != null 15 | ? (ObjectInfo**)UI3DModule->SortedObjectInfoPointerArray : null; 16 | internal unsafe static int SortedObjectInfoCount 17 | => UI3DModule != null ? UI3DModule->SortedObjectInfoCount : 0; 18 | 19 | #if DEBUG 20 | private unsafe static readonly GameObjectManager* gameObjMgr 21 | = GameObjectManager.Instance(); 22 | internal unsafe static GameObject* ObjectListFiltered 23 | => (GameObject*)gameObjMgr->ObjectListFiltered; 24 | internal unsafe static int ObjectListFilteredCount 25 | => gameObjMgr->ObjectListFilteredCount; 26 | 27 | static GameObjects() 28 | { 29 | LogDebug($"UI3DModule @{(IntPtr)UI3DModule:X}"); 30 | LogDebug($"SortedObjectInfoPointerArray @{(IntPtr)UI3DModule->SortedObjectInfoPointerArray:X}"); 31 | LogDebug($"SortedObjectInfoCount = {SortedObjectInfoCount}"); 32 | } 33 | #endif 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/PluginUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace AetherCompass 4 | { 5 | internal class PluginUtil 6 | { 7 | public static void LogDebug(string msg) => Plugin.PluginLog.Debug(msg); 8 | 9 | public static void LogInfo(string msg) => Plugin.PluginLog.Information(msg); 10 | 11 | public static void LogWarning(string msg) => Plugin.PluginLog.Warning(msg); 12 | 13 | public static void LogError(string msg) => Plugin.PluginLog.Error(msg); 14 | 15 | public static void LogWarningExcelSheetNotLoaded(string sheetName) 16 | => Plugin.PluginLog.Warning($"Failed to load Excel Sheet: {sheetName}"); 17 | 18 | public static Version? GetPluginVersion() 19 | => Assembly.GetExecutingAssembly().GetName().Version; 20 | 21 | public static string GetPluginVersionAsString() 22 | => GetPluginVersion()?.ToString() ?? "0.0.0.0"; 23 | 24 | public static int ComparePluginVersion(string v1, string v2) 25 | { 26 | var v1split = v1.Split('.', StringSplitOptions.TrimEntries); 27 | var v2split = v2.Split('.', StringSplitOptions.TrimEntries); 28 | var len = v1split.Length <= v2split.Length 29 | ? v1split.Length : v2split.Length; 30 | for (int i = 0; i < len; i++) 31 | { 32 | var comp1 = i < v1split.Length && int.TryParse(v1split[i], out var c1) 33 | ? c1 : 0; 34 | var comp2 = i < v2split.Length && int.TryParse(v2split[i], out var c2) 35 | ? c2 : 0; 36 | if (comp1 == comp2) continue; 37 | return comp1.CompareTo(comp2); 38 | } 39 | return 0; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Common/FixedMapLinkPayload.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.Text.SeStringHandling.Payloads; 2 | using System.Numerics; 3 | 4 | namespace AetherCompass.Common 5 | { 6 | // To fix the issue with Dalamud's MapLinkPayload not taking into account map's OffsetX and OffsetY 7 | // Only a quick fix so that the payload records our calculated coords instead of Dalamud's 8 | // which will be used in showing in Chat etc. 9 | public class FixedMapLinkPayload : MapLinkPayload 10 | { 11 | private readonly uint terrId; 12 | private readonly uint mapId; 13 | private readonly float coordX; 14 | private readonly float coordY; 15 | 16 | public FixedMapLinkPayload(uint terrId, uint mapId, int rawX, int rawY, float coordX, float coordY) : base(terrId, mapId, rawX, rawY) 17 | { 18 | this.terrId = terrId; 19 | this.mapId = mapId; 20 | this.coordX = coordX; 21 | this.coordY = coordY; 22 | } 23 | 24 | public static FixedMapLinkPayload FromMapCoord(uint terrId, uint mapId, float xCoord, float yCoord) 25 | { 26 | var map = Plugin.DataManager.GetExcelSheet()?.GetRow(mapId); 27 | if (map == null) return new(terrId, mapId, 0, 0, 0, 0); 28 | return FromMapCoord(terrId, mapId, xCoord, yCoord, map.SizeFactor, map.OffsetX, map.OffsetY); 29 | } 30 | 31 | public static FixedMapLinkPayload FromMapCoord(uint terrId, uint mapId, float xCoord, float yCoord, ushort scale, short offsetX, short offsetY) 32 | { 33 | // because we don't care about Z-coord here 34 | var coord3 = new Vector3(xCoord, yCoord, 0); 35 | var pos = CompassUtil.GetWorldPosition(coord3, scale, offsetX, offsetY, 0); 36 | return new(terrId, mapId, (int)(pos.X * 1000), (int)(pos.Z * 1000), xCoord, yCoord); 37 | } 38 | 39 | public new string CoordinateString => $"( {coordX:0.0} , {coordY:0.0} )"; 40 | 41 | public override string ToString() 42 | { 43 | return $"{this.Type}(Fixed) - TerritoryTypeId: {terrId}, MapId: {mapId}, RawX: {RawX}, RawY: {RawY}, display: {PlaceName} {CoordinateString}"; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Common/ActionQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace AetherCompass.Common 4 | { 5 | public class ActionQueue 6 | { 7 | private readonly BlockingCollection importantActions; 8 | private readonly BlockingCollection normalActions; 9 | 10 | public int Threshold { get; } 11 | // count may be inaccurate 12 | public int Count => importantActions.Count + normalActions.Count; 13 | 14 | 15 | public ActionQueue(int threshold) 16 | { 17 | normalActions = new(threshold); 18 | importantActions = new(threshold); 19 | Threshold = threshold > 0 ? threshold 20 | : throw new ArgumentOutOfRangeException(nameof(threshold), "threshold should be positive"); 21 | } 22 | 23 | 24 | public bool QueueAction(Action? a, bool important = false) 25 | => a != null && QueueAction(new(a, important)); 26 | 27 | public bool QueueAction(DrawAction? a) 28 | { 29 | if (a == null) return false; 30 | if (a.Important) return EnqueueImportant(a); 31 | else return EnqueueNormal(a); 32 | } 33 | 34 | private bool EnqueueImportant(DrawAction a) 35 | { 36 | if (!importantActions.TryAdd(a)) return false; 37 | // Try make space by dequeueing normal 38 | // if important queue still has space but total bound reached 39 | // This is approximate tho 40 | if (Count >= Threshold && !TryDequeueNormal(out _)) return false; 41 | return importantActions.TryAdd(a); 42 | } 43 | 44 | private bool EnqueueNormal(DrawAction a) => normalActions.TryAdd(a); 45 | 46 | private bool TryDequeueImportant(out DrawAction? a) => importantActions.TryTake(out a); 47 | 48 | private bool TryDequeueNormal(out DrawAction? a) => normalActions.TryTake(out a); 49 | 50 | public void DoAll() 51 | { 52 | while (TryDequeueNormal(out var a)) 53 | a?.Invoke(); 54 | while (TryDequeueImportant(out var a)) 55 | a?.Invoke(); 56 | } 57 | 58 | public void Clear() 59 | { 60 | while (normalActions.TryTake(out var _)) ; 61 | while (importantActions.TryTake(out var _)) ; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Compasses/Objectives/CachedCompassObjective.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 3 | using System.Numerics; 4 | using static FFXIVClientStructs.FFXIV.Client.UI.UI3DModule; 5 | 6 | namespace AetherCompass.Compasses.Objectives 7 | { 8 | public unsafe class CachedCompassObjective 9 | { 10 | public readonly IntPtr GameObject; 11 | public readonly GameObjectID GameObjectId; 12 | public readonly string Name; 13 | public readonly uint NpcId; 14 | public readonly uint DataId; 15 | public readonly Vector3 Position; 16 | public readonly float Distance3D; 17 | public readonly float AltitudeDiff; 18 | public readonly CompassDirection CompassDirectionFromPlayer; 19 | public readonly float GameObjectHeight; 20 | public readonly Vector3 CurrentMapCoord; 21 | public readonly Vector3 NormalisedNameplatePos; 22 | 23 | 24 | public CachedCompassObjective(GameObject* obj) 25 | { 26 | GameObject = (IntPtr)obj; 27 | if (obj != null) 28 | { 29 | GameObjectId = obj->GetObjectID(); 30 | Name = CompassUtil.GetName(obj); 31 | NpcId = obj->GetNpcID(); 32 | DataId = obj->DataID; 33 | Position = obj->Position; 34 | Distance3D = CompassUtil.Get3DDistanceFromPlayer(Position); 35 | AltitudeDiff = CompassUtil.GetAltitudeDiffFromPlayer(Position); 36 | CompassDirectionFromPlayer = CompassUtil.GetDirectionFromPlayer(Position); 37 | GameObjectHeight = obj->GetHeight(); 38 | CurrentMapCoord = CompassUtil.GetMapCoordInCurrentMap(Position); 39 | } 40 | else 41 | Name = string.Empty; 42 | } 43 | 44 | public CachedCompassObjective(ObjectInfo* info) 45 | : this(info != null ? info->GameObject : null) 46 | { 47 | if (info != null) 48 | { 49 | NormalisedNameplatePos = info->NamePlatePos; 50 | } 51 | } 52 | 53 | public bool IsCacheFor(GameObject* obj) => IsCacheFor((IntPtr)obj); 54 | 55 | public bool IsCacheFor(IntPtr gameObjPtr) 56 | => GameObject == gameObjPtr; 57 | 58 | public bool IsEmpty() => GameObject == IntPtr.Zero; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/UI/Chat.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.Text; 2 | using Dalamud.Game.Text.SeStringHandling; 3 | using Dalamud.Game.Text.SeStringHandling.Payloads; 4 | using System.Numerics; 5 | 6 | namespace AetherCompass.UI 7 | { 8 | public static class Chat 9 | { 10 | 11 | public static void PrintChat(string msg) 12 | { 13 | Plugin.ChatGui.Print("[AetherCompass] " + msg); 14 | } 15 | 16 | public static void PrintChat(SeString msg) 17 | { 18 | msg.Payloads.Insert(0, new TextPayload("[AetherCompass] ")); 19 | Plugin.ChatGui.Print(new XivChatEntry() 20 | { 21 | Message = msg, 22 | }); 23 | } 24 | 25 | public static void PrintErrorChat(string msg) 26 | { 27 | Plugin.ChatGui.PrintError("[AetherCompass] " + msg); 28 | } 29 | 30 | public static SeString CreateMapLink(Common.FixedMapLinkPayload fixedMapPayload) 31 | { 32 | var nameString = $"{fixedMapPayload.PlaceName} {fixedMapPayload.CoordinateString}"; 33 | 34 | var payloads = new List(new Payload[] 35 | { 36 | fixedMapPayload, 37 | // arrow goes here 38 | new TextPayload(nameString), 39 | RawPayload.LinkTerminator, 40 | }); 41 | payloads.InsertRange(1, SeString.TextArrowPayloads); 42 | 43 | return new(payloads); 44 | } 45 | 46 | public static SeString CreateMapLink(uint terrId, uint mapId, float xCoord, float yCoord) 47 | { 48 | var maplink = SeString.CreateMapLink(terrId, mapId, xCoord, yCoord, .01f); 49 | return maplink; 50 | } 51 | 52 | public static SeString CreateMapLink(uint terrId, uint mapId, Vector3 coord, bool showZ = false) 53 | { 54 | var maplink = SeString.CreateMapLink(terrId, mapId, coord.X, coord.Y, .01f); 55 | if (showZ) 56 | maplink.Append(new TextPayload($" Z:{coord.Z: 0.0}")); 57 | return maplink; 58 | } 59 | 60 | public static SeString PrependText(this SeString s, string text) 61 | { 62 | s.Payloads.Insert(0, new TextPayload(text)); 63 | return s; 64 | } 65 | 66 | public static SeString AppendText(this SeString s, string text) 67 | => s.Append(new TextPayload(text)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PluginCommands.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.UI; 2 | using Dalamud.Game.Command; 3 | 4 | namespace AetherCompass 5 | { 6 | public static class PluginCommands 7 | { 8 | public const string MainCommand = "/aethercompass"; 9 | 10 | public static void AddCommands() 11 | { 12 | Plugin.CommandManager.AddHandler(MainCommand, new CommandInfo((cmd, args) => ProcessMainCommand(cmd, args)) 13 | { 14 | HelpMessage = "Toggle the plugin between enabled/disabled when no options provided\n" + 15 | $"{MainCommand} [on|off] → Enable/Disable the plugin\n" + 16 | $"{MainCommand} mark → Toggle enabled/disabled for marking detected objects on screen\n" + 17 | $"{MainCommand} detail → Toggle enabled/disabled for showing Object Detail Window\n" + 18 | $"{MainCommand} config → Open the Configuration window", 19 | ShowInHelp = true 20 | }); 21 | } 22 | 23 | public static void RemoveCommands() 24 | { 25 | Plugin.CommandManager.RemoveHandler(MainCommand); 26 | } 27 | 28 | private static void ProcessMainCommand(string command, string args) 29 | { 30 | if (string.IsNullOrWhiteSpace(args)) 31 | { 32 | Plugin.Enabled = !Plugin.Enabled; 33 | return; 34 | } 35 | var argList = args.Split(' ', StringSplitOptions.RemoveEmptyEntries); 36 | if (argList.Length == 0) 37 | { 38 | Plugin.Enabled = !Plugin.Enabled; 39 | return; 40 | } 41 | switch (argList[0]) 42 | { 43 | case "on": 44 | Plugin.Enabled = true; 45 | return; 46 | case "off": 47 | Plugin.Enabled = false; 48 | return; 49 | case "mark": 50 | Plugin.Config.ShowScreenMark = !Plugin.Config.ShowScreenMark; 51 | return; 52 | case "detail": 53 | Plugin.Config.ShowDetailWindow = !Plugin.Config.ShowDetailWindow; 54 | return; 55 | case "config": 56 | Plugin.OpenConfig(true); 57 | return; 58 | default: 59 | Chat.PrintErrorChat($"{command}: Unknown command args: {args}"); 60 | return; 61 | } 62 | 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Game/SeFunctions/Projection.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface; 2 | using Dalamud.Interface.Utility; 3 | using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; 4 | using System.Numerics; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace AetherCompass.Game.SeFunctions 8 | { 9 | internal unsafe static class Projection 10 | { 11 | private delegate IntPtr GetMatrixSingletonDelegate(); 12 | private static readonly GetMatrixSingletonDelegate getMatrixSingleton; 13 | private static readonly Device* device; 14 | 15 | static Projection() 16 | { 17 | IntPtr addr = Plugin.SigScanner.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??"); 18 | getMatrixSingleton ??= Marshal.GetDelegateForFunctionPointer(addr); 19 | device = Device.Instance(); 20 | } 21 | 22 | public static bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) => WorldToScreen(worldPos, out screenPos, out _); 23 | 24 | // Used to be a rewrite version of Dalamud's WorldToScreen to fix an off-screen position issue, 25 | // but since the Dalamud's version's been fixed in the same way, they are almost identical 26 | internal static unsafe bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out Vector3 pCoordsRaw) 27 | { 28 | if (getMatrixSingleton == null) 29 | throw new InvalidOperationException("getMatrixSingleton did not initiate correctly"); 30 | 31 | var matrixSingleton = getMatrixSingleton(); 32 | if (matrixSingleton == IntPtr.Zero) 33 | throw new InvalidOperationException("Cannot get matrixSingleton"); 34 | 35 | var windowPos = ImGuiHelpers.MainViewport.Pos; 36 | 37 | var viewProjectionMatrix = *(Matrix4x4*)(matrixSingleton + 0x1b4); 38 | float width = device->Width; 39 | float height = device->Height; 40 | 41 | var pCoords = Vector3.Transform(worldPos, viewProjectionMatrix); 42 | pCoordsRaw = pCoords; 43 | 44 | screenPos = new Vector2(pCoords.X / MathF.Abs(pCoords.Z), pCoords.Y / MathF.Abs(pCoords.Z)); 45 | 46 | screenPos.X = (0.5f * width * (screenPos.X + 1f)) + windowPos.X; 47 | screenPos.Y = (0.5f * height * (1f - screenPos.Y)) + windowPos.Y; 48 | 49 | return pCoords.Z > 0 50 | && screenPos.X > windowPos.X && screenPos.X < windowPos.X + width 51 | && screenPos.Y > windowPos.Y && screenPos.Y < windowPos.Y + height; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Game/ZoneWatcher.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Game.SeFunctions; 2 | using Excel = Lumina.Excel.GeneratedSheets; 3 | 4 | namespace AetherCompass.Game 5 | { 6 | internal static class ZoneWatcher 7 | { 8 | public static readonly Lumina.Excel.ExcelSheet? PlaceName 9 | = Plugin.DataManager.GetExcelSheet(); 10 | 11 | public static Excel.TerritoryType? CurrentTerritoryType { get; private set; } 12 | public static Excel.TerritoryTypeTransient? CurrentTerritoryTypeTransient { get; private set; } 13 | 14 | 15 | public static uint CurrentMapId 16 | { 17 | get 18 | { 19 | var altMapId = ZoneMap.GetCurrentAltMapId(); 20 | if (altMapId > 0) return altMapId; 21 | return CurrentTerritoryType?.Map.Row ?? 0; 22 | } 23 | } 24 | private static Excel.Map? cachedMap; 25 | public static Excel.Map? CurrentMap 26 | { 27 | get 28 | { 29 | if (cachedMap == null || CurrentMapId != cachedMap.RowId) cachedMap = GetMap(); 30 | return cachedMap; 31 | } 32 | } 33 | 34 | public static bool IsInCompassWorkZone { get; private set; } 35 | public static bool IsInDetailWindowHideZone { get; private set; } 36 | 37 | public static void OnZoneChange() 38 | { 39 | var terrId = Plugin.ClientState.TerritoryType; 40 | CurrentTerritoryType = terrId == 0 ? null 41 | : Plugin.DataManager.GetExcelSheet()?.GetRow(terrId); 42 | CurrentTerritoryTypeTransient = terrId == 0 ? null 43 | : Plugin.DataManager.GetExcelSheet()?.GetRow(terrId); 44 | 45 | cachedMap = GetMap(); 46 | 47 | CheckCompassWorkZone(); 48 | CheckDetailWindowHideZone(); 49 | } 50 | 51 | private static Excel.Map? GetMap() 52 | => Plugin.DataManager.GetExcelSheet()?.GetRow(CurrentMapId); 53 | 54 | 55 | // Work only in PvE zone, also excl LoVM / chocobo race etc. 56 | private static void CheckCompassWorkZone() 57 | { 58 | IsInCompassWorkZone = CurrentTerritoryType != null 59 | && !CurrentTerritoryType.IsPvpZone 60 | && CurrentTerritoryType.BattalionMode <= 1 // > 1 are pvp contents or LoVM 61 | && CurrentTerritoryType.TerritoryIntendedUse != 20 // chocobo race terr? 62 | ; 63 | } 64 | 65 | private static void CheckDetailWindowHideZone() 66 | { 67 | // Exclusive type: 0 not instanced, 1 is solo instance, 2 is nonsolo instance. 68 | // Not sure about 3, seems quite mixed up with solo battles, diadem and misc stuff like LoVM 69 | IsInDetailWindowHideZone = CurrentTerritoryType == null || CurrentTerritoryType.ExclusiveType > 0; 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Compasses/AetherCurrentCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Objectives; 4 | using AetherCompass.Game; 5 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 6 | using ImGuiNET; 7 | 8 | 9 | namespace AetherCompass.Compasses 10 | { 11 | [CompassType(CompassType.Standard)] 12 | public class AetherCurrentCompass : Compass 13 | { 14 | public override string CompassName => "Aether Current Compass"; 15 | public override string Description => "Detecting Aether Currents nearby."; 16 | 17 | private protected override CompassConfig CompassConfig => Plugin.Config.AetherCurrentConfig; 18 | 19 | private static System.Numerics.Vector4 infoTextColour = new(.8f, .95f, .75f, 1); 20 | private const float infoTextShadowLightness = .1f; 21 | 22 | private const uint aetherCurrentMarkerIconId = 60033; 23 | 24 | 25 | public override bool IsEnabledInCurrentTerritory() 26 | => ZoneWatcher.CurrentTerritoryType?.TerritoryIntendedUse == 1; // mostly normal wild field 27 | 28 | 29 | private protected override unsafe string GetClosestObjectiveDescription(CachedCompassObjective _) 30 | => "Aether Current"; 31 | 32 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 33 | { 34 | if (objective.IsEmpty()) return null; 35 | return new(() => 36 | { 37 | ImGui.Text($"{objective.Name}"); 38 | ImGui.BulletText($"{CompassUtil.MapCoordToFormattedString(objective.CurrentMapCoord)} (approx.)"); 39 | ImGui.BulletText($"{objective.CompassDirectionFromPlayer}, " + 40 | $"{CompassUtil.DistanceToDescriptiveString(objective.Distance3D, false)}"); 41 | ImGui.BulletText(CompassUtil.AltitudeDiffToDescriptiveString(objective.AltitudeDiff)); 42 | DrawFlagButton($"{(long)objective.GameObject}", objective.CurrentMapCoord); 43 | ImGui.Separator(); 44 | }); 45 | } 46 | 47 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 48 | { 49 | if (objective.IsEmpty()) return null; 50 | return GenerateDefaultScreenMarkerDrawAction(objective, 51 | aetherCurrentMarkerIconId, DefaultMarkerIconSize, .9f, 52 | $"{objective.Name}, {CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}", 53 | infoTextColour, infoTextShadowLightness, out _, important: objective.Distance3D < 60); 54 | } 55 | 56 | public override unsafe bool IsObjective(GameObject* o) 57 | { 58 | if (o == null) return false; 59 | if (o->ObjectKind != (byte)ObjectKind.EventObj) return false; 60 | return IsNameOfAetherCurrent(CompassUtil.GetName(o)); 61 | } 62 | 63 | private static bool IsNameOfAetherCurrent(string? name) 64 | { 65 | if (name == null) return false; 66 | name = name.ToLower(); 67 | return name == "aether current" 68 | || name == "風脈の泉" 69 | || name == "windätherquelle" 70 | || name == "vent éthéré" 71 | ; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /AetherCompass.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | yomishino 7 | 1.6.4.0 8 | An FFXIV Dalamud plugin providing a set of compasses that detect and mark certain nearby objects/NPCs. 9 | 10 | https://github.com/yomishino/FFXIVAetherCompass 11 | Debug;Release;PreRelease;PreTest 12 | 13 | 14 | 15 | net7.0 16 | x64 17 | enable 18 | latest 19 | true 20 | false 21 | false 22 | 23 | CA1416 24 | true 25 | 26 | 27 | 28 | $(AppData)\XIVLauncher\addon\Hooks\dev 29 | 30 | 31 | 32 | TRACE;TEST;PRE 33 | true 34 | x64 35 | 36 | 37 | 38 | PRE 39 | True 40 | x64 41 | 42 | 43 | 44 | 45 | 46 | $(DalamudLibPath)\Dalamud.dll 47 | false 48 | 49 | 50 | $(DalamudLibPath)\ImGui.NET.dll 51 | false 52 | 53 | 54 | $(DalamudLibPath)\ImGuiScene.dll 55 | false 56 | 57 | 58 | $(DalamudLibPath)\Lumina.dll 59 | false 60 | 61 | 62 | $(DalamudLibPath)\Lumina.Excel.dll 63 | false 64 | 65 | 66 | $(DalamudLibPath)\FFXIVClientStructs.dll 67 | false 68 | 69 | 70 | $(DalamudLibPath)\Newtonsoft.Json.dll 71 | false 72 | 73 | 74 | $(DalamudLibPath)\SharpDX.Mathematics.dll 75 | false 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /custom/PreviewBuild.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(OutputPath)$(AssemblyName) 6 | $(OutputPath)\temp 7 | $(OutputPath)\$(AssemblyName)Preview.json 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | custom\$(AssemblyName)Preview.json 36 | custom\preview-changelog 37 | "AssemblyVersion": "$(AssemblyVersion)", 38 | "IsTestingExclusive": "False", 39 | "IsTestingExclusive": "True", 40 | "TestingAssemblyVersion": "$(AssemblyVersion)", 41 | "Changelog": $(ChangelogVal) 42 | "": null 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 63 | 64 | 65 | 69 | 70 | 71 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/Game/SeFunctions/Quests.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace AetherCompass.Game.SeFunctions 5 | { 6 | internal unsafe static class Quests 7 | { 8 | private readonly static IntPtr questManagerPtr; 9 | private readonly static Quest* questListArray; 10 | 11 | public unsafe static Quest* GetQuestListArray() 12 | => questListArray; 13 | 14 | public const int QuestListArrayLength = 30; 15 | 16 | public unsafe static bool HasQuest(ushort questId) 17 | { 18 | if (questId == 0) return false; 19 | var array = GetQuestListArray(); 20 | if (array == null) return false; 21 | for (int i = 0; i < QuestListArrayLength; i ++) 22 | if (array[i].QuestID == questId) return true; 23 | return false; 24 | } 25 | 26 | public unsafe static bool TryGetQuest(ushort questId, out Quest quest) 27 | { 28 | quest = new(); 29 | if (questId == 0) return false; 30 | var array = GetQuestListArray(); 31 | if (array == null) return false; 32 | for (int i = 0; i < QuestListArrayLength; i++) 33 | { 34 | if (array[i].QuestID == questId) 35 | { 36 | quest = array[i]; 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | static Quests() 44 | { 45 | questManagerPtr = (IntPtr)QuestManager.Instance(); 46 | if (questManagerPtr != IntPtr.Zero) 47 | questListArray = (Quest*)(questManagerPtr + 0x10); 48 | } 49 | } 50 | 51 | 52 | [StructLayout(LayoutKind.Explicit, Size = 0x18)] 53 | public struct Quest 54 | { 55 | [FieldOffset(0x08)] public ushort QuestID; 56 | [FieldOffset(0x0A)] public byte QuestSeq; // Currently at which step of quest; 57 | // typically start from 1, as 0 is when quest is to be offered. 58 | // Different from ToDo, one Seq can have many ToDos 59 | [FieldOffset(0x0B)] public QuestFlags Flags; // 1 for Priority, 8 for Hidden 60 | [FieldOffset(0x0C)] public uint TodoFlags; // Flag the complete status of Todos of current Seq, 61 | // (seems basically each digit is a sort of counter but different quests are using it differently..) 62 | [FieldOffset(0x11)] public byte ObjectiveObjectsInteractedFlags; // Maybe. Each bit set for an objective interacted; 63 | // smaller indexed uses more significant bit 64 | [FieldOffset(0x12)] public byte StartClassJobID; // the classjob when player starts the quest 65 | 66 | public bool IsHidden => Flags.HasFlag(QuestFlags.Hidden); 67 | public bool IsPriority => Flags.HasFlag(QuestFlags.Priority); 68 | 69 | // objectiveIdxInThisSeq should be 0~7 70 | public bool IsObjectiveInteracted(int objectiveIdxInThisSeq) 71 | => objectiveIdxInThisSeq >= 0 && objectiveIdxInThisSeq <= 7 72 | && (ObjectiveObjectsInteractedFlags & (1 << (7 - objectiveIdxInThisSeq))) != 0; 73 | 74 | 75 | [Flags] 76 | public enum QuestFlags : byte 77 | { 78 | None, 79 | Priority, 80 | Hidden = 0x8 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Compasses/EurekanCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Objectives; 4 | using AetherCompass.Game; 5 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 6 | using ImGuiNET; 7 | 8 | 9 | namespace AetherCompass.Compasses 10 | { 11 | [CompassType(CompassType.Standard)] 12 | public class EurekanCompass : Compass 13 | { 14 | public override string CompassName => "Eureka Elemental Compass"; 15 | public override string Description => "Detecting nearby Eureka Elementals. (By apetih.)"; 16 | 17 | private protected override CompassConfig CompassConfig => Plugin.Config.EurekanConfig; 18 | 19 | private static System.Numerics.Vector4 infoTextColour = new(.8f, .95f, .75f, 1); 20 | private const float infoTextShadowLightness = .1f; 21 | 22 | private const uint elementalMarkerIconId = 15835; 23 | private static readonly System.Numerics.Vector2 24 | elementalMarkerIconSize = new(25, 25); 25 | 26 | public override bool IsEnabledInCurrentTerritory() 27 | => ZoneWatcher.CurrentTerritoryType?.TerritoryIntendedUse == 41; 28 | 29 | 30 | private protected override unsafe string GetClosestObjectiveDescription( 31 | CachedCompassObjective objective) => objective.Name; 32 | 33 | public override unsafe bool IsObjective(GameObject* o) 34 | => o != null && (o->ObjectKind == (byte)ObjectKind.BattleNpc) 35 | && IsEurekanElementalName(CompassUtil.GetName(o)); 36 | 37 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 38 | { 39 | if (objective.IsEmpty()) return null; 40 | return new(() => 41 | { 42 | ImGui.Text($"{objective.Name}"); 43 | ImGui.BulletText($"{CompassUtil.MapCoordToFormattedString(objective.CurrentMapCoord)} (approx.)"); 44 | ImGui.BulletText($"{objective.CompassDirectionFromPlayer}, " + 45 | $"{CompassUtil.DistanceToDescriptiveString(objective.Distance3D, false)}"); 46 | ImGui.BulletText(CompassUtil.AltitudeDiffToDescriptiveString(objective.AltitudeDiff)); 47 | DrawFlagButton($"{(long)objective.GameObject}", objective.CurrentMapCoord); 48 | ImGui.Separator(); 49 | }); 50 | } 51 | 52 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 53 | { 54 | if (objective.IsEmpty()) return null; 55 | return GenerateDefaultScreenMarkerDrawAction(objective, 56 | elementalMarkerIconId, new System.Numerics.Vector2(24,32), .9f, 57 | $"{objective.Name}, {CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}", 58 | infoTextColour, infoTextShadowLightness, out _, important: objective.Distance3D < 60); 59 | } 60 | 61 | private static bool IsEurekanElementalName(string? name) 62 | { 63 | if (name == null) return false; 64 | name = name.ToLower(); 65 | return name == "hydatos elemental" || name == "pyros elemental" || name == "pagos elemental" || name == "anemos elemental" 66 | || name == "ヒュダトス・エレメンタル" || name == "パゴス・エレメンタル" || name == "ピューロス・エレメンタル" || name == "アネモス・エレメンタル" 67 | || name == "élémentaire hydatos" || name == "élémentaire pyros" || name == "élémentaire pagos" || name == "élémentaire anemos" 68 | || name == "hydatos-elementar" || name == "pyros-elementar" || name == "pagos-elementar" || name == "anemos-elementar" 69 | ; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/UI/GUI/ImGuiEx.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface; 2 | using Dalamud.Interface.Components; 3 | using Dalamud.Interface.Utility; 4 | using ImGuiNET; 5 | using System.Numerics; 6 | 7 | namespace AetherCompass.UI.GUI 8 | { 9 | public static class ImGuiEx 10 | { 11 | public static void IconText(FontAwesomeIcon icon, bool nextSameLine = false) 12 | { 13 | ImGui.PushFont(UiBuilder.IconFont); 14 | ImGui.Text(FontAwesomeExtensions.ToIconString(icon)); 15 | ImGui.PopFont(); 16 | if (nextSameLine) ImGui.SameLine(); 17 | } 18 | 19 | public static void IconTextCompass(bool nextSameLine = false) 20 | => IconText(FontAwesomeIcon.Compass, nextSameLine); 21 | 22 | public static void IconTextMapMarker(bool nextSameLine = false) 23 | => IconText(FontAwesomeIcon.MapMarkerAlt, nextSameLine); 24 | 25 | public static void Separator(bool prependNewLine = false, bool appendNewLine = false) 26 | { 27 | if (prependNewLine) ImGui.NewLine(); 28 | ImGui.Separator(); 29 | if (appendNewLine) ImGui.NewLine(); 30 | } 31 | 32 | public static void Checkbox(string label, ref bool v, string? tooltip = null) 33 | { 34 | ImGui.Checkbox(label, ref v); 35 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 36 | } 37 | 38 | public static bool Button(string label, string? tooltip = null) 39 | { 40 | var r = ImGui.Button(label); 41 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 42 | return r; 43 | } 44 | 45 | public static bool IconButton(FontAwesomeIcon icon, int id, string? tooltip = null) 46 | { 47 | var r = ImGuiComponents.IconButton(id, icon); 48 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 49 | return r; 50 | } 51 | 52 | public static void InputInt(string label, int itemWidth, ref int v, string? tooltip = null) 53 | { 54 | ImGui.Text(label + ": "); 55 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 56 | ImGui.SameLine(); 57 | ImGui.SetNextItemWidth(itemWidth * ImGuiHelpers.GlobalScale); 58 | ImGui.InputInt("##" + label, ref v); 59 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 60 | } 61 | 62 | public static void DragInt(string label, string unit, int itemWidth, 63 | ref int v, int v_spd, int v_min, int v_max, string? tooltip = null) 64 | { 65 | ImGui.Text(label + ": "); 66 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 67 | ImGui.SameLine(); 68 | ImGui.SetNextItemWidth(itemWidth * ImGuiHelpers.GlobalScale); 69 | ImGui.DragInt($"{unit}##{label}", ref v, v_spd, v_min, v_max); 70 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 71 | } 72 | 73 | public static void DragFloat(string label, int itemWidth, ref float v, float v_spd, 74 | float v_min, float v_max, string v_fmt = "%.2f", string? tooltip = null) 75 | { 76 | ImGui.Text(label + ": "); 77 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 78 | ImGui.SameLine(); 79 | ImGui.SetNextItemWidth(itemWidth * ImGuiHelpers.GlobalScale); 80 | ImGui.DragFloat("##" + label, ref v, v_spd, v_min, v_max, v_fmt); 81 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 82 | } 83 | 84 | public static void DragFloat4(string label, ref Vector4 v, float v_spd, 85 | float v_min, float v_max, string v_fmt = "%.1f", string? tooltip = null) 86 | { 87 | ImGui.Text(label + ": "); 88 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 89 | ImGui.Indent(); 90 | ImGui.DragFloat4("##" + label, ref v, v_spd, v_min, v_max, v_fmt); 91 | if (tooltip != null && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); 92 | ImGui.Unindent(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | # IDE0003: Remove qualification 4 | dotnet_style_qualification_for_event = false:none 5 | 6 | # IDE0003: Remove qualification 7 | dotnet_style_qualification_for_field = false:none 8 | 9 | # IDE0003: Remove qualification 10 | dotnet_style_qualification_for_method = false:none 11 | 12 | # IDE0003: Remove qualification 13 | dotnet_style_qualification_for_property = false:none 14 | 15 | # IDE0060: Remove unused parameter 16 | dotnet_code_quality_unused_parameters = non_public:suggestion 17 | 18 | [*.{cs,vb}] 19 | #### Naming styles #### 20 | 21 | # Naming rules 22 | 23 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 24 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 25 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 26 | 27 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 28 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 29 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 30 | 31 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 32 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 33 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 34 | 35 | # Symbol specifications 36 | 37 | dotnet_naming_symbols.interface.applicable_kinds = interface 38 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 39 | dotnet_naming_symbols.interface.required_modifiers = 40 | 41 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 42 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 43 | dotnet_naming_symbols.types.required_modifiers = 44 | 45 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 46 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 47 | dotnet_naming_symbols.non_field_members.required_modifiers = 48 | 49 | # Naming styles 50 | 51 | dotnet_naming_style.begins_with_i.required_prefix = I 52 | dotnet_naming_style.begins_with_i.required_suffix = 53 | dotnet_naming_style.begins_with_i.word_separator = 54 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 55 | 56 | dotnet_naming_style.pascal_case.required_prefix = 57 | dotnet_naming_style.pascal_case.required_suffix = 58 | dotnet_naming_style.pascal_case.word_separator = 59 | dotnet_naming_style.pascal_case.capitalization = pascal_case 60 | 61 | dotnet_naming_style.pascal_case.required_prefix = 62 | dotnet_naming_style.pascal_case.required_suffix = 63 | dotnet_naming_style.pascal_case.word_separator = 64 | dotnet_naming_style.pascal_case.capitalization = pascal_case 65 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 66 | tab_width = 4 67 | indent_size = 4 68 | end_of_line = lf 69 | dotnet_style_coalesce_expression = true:suggestion 70 | dotnet_style_null_propagation = true:suggestion 71 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 72 | dotnet_style_prefer_auto_properties = true:silent 73 | dotnet_style_object_initializer = true:suggestion 74 | dotnet_style_collection_initializer = true:suggestion 75 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 76 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 77 | 78 | [*.cs] 79 | csharp_indent_labels = one_less_than_current 80 | csharp_using_directive_placement = outside_namespace:silent 81 | csharp_prefer_simple_using_statement = true:suggestion 82 | csharp_prefer_braces = true:silent 83 | csharp_style_namespace_declarations = block_scoped:silent 84 | csharp_style_prefer_method_group_conversion = true:silent 85 | csharp_style_prefer_top_level_statements = true:silent 86 | csharp_style_expression_bodied_methods = false:silent 87 | csharp_style_expression_bodied_constructors = false:silent 88 | csharp_style_expression_bodied_operators = false:silent 89 | csharp_style_expression_bodied_properties = true:silent 90 | csharp_style_expression_bodied_indexers = true:silent 91 | csharp_style_expression_bodied_accessors = true:silent 92 | csharp_style_expression_bodied_lambdas = true:silent 93 | csharp_style_expression_bodied_local_functions = false:silent 94 | insert_final_newline = true -------------------------------------------------------------------------------- /src/UI/GUI/CompassDetailsWindow.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Compasses; 3 | using AetherCompass.Game; 4 | using ImGuiNET; 5 | 6 | namespace AetherCompass.UI.GUI 7 | { 8 | public class CompassDetailsWindow 9 | { 10 | private readonly Dictionary drawActions = new(); 11 | 12 | 13 | public bool RegisterCompass(Compass c) 14 | => drawActions.TryAdd(c, new(80)); 15 | 16 | public bool UnregisterCompass(Compass c) 17 | => drawActions.Remove(c); 18 | 19 | public bool AddDrawAction(Compass c, Action? a, bool important) 20 | { 21 | if (a == null) return false; 22 | if (!drawActions.TryGetValue(c, out var queue)) 23 | return false; 24 | return queue.QueueAction(a, important); 25 | } 26 | 27 | public bool AddDrawAction(Compass c, DrawAction? a) 28 | { 29 | if (a == null) return false; 30 | if (!drawActions.TryGetValue(c, out var queue)) 31 | return false; 32 | return queue.QueueAction(a); 33 | } 34 | 35 | public void Draw() 36 | { 37 | var map = ZoneWatcher.CurrentMap; 38 | if (map == null) return; 39 | 40 | if (ImGui.Begin("AetherCompass: Detected Objects' Details")) 41 | { 42 | var regionName = CompassUtil.GetPlaceNameToString(map.PlaceNameRegion.Row); 43 | var placeName = CompassUtil.GetPlaceNameToString(map.PlaceName.Row); 44 | var subName = CompassUtil.GetPlaceNameToString(map.PlaceNameSub.Row); 45 | var mapName = regionName; 46 | if (!string.IsNullOrEmpty(mapName) && !string.IsNullOrEmpty(placeName)) 47 | mapName += " > " + placeName; 48 | else if (!string.IsNullOrEmpty(placeName)) 49 | mapName = placeName; 50 | if (!string.IsNullOrEmpty(mapName) && !string.IsNullOrEmpty(subName)) 51 | mapName += " > " + subName; 52 | 53 | if (ImGui.BeginTable("##Tbl_DetailWindowMapInfo", 2, 54 | ImGuiTableFlags.SizingStretchProp)) 55 | { 56 | ImGui.TableSetupColumn("##MapInfo", ImGuiTableColumnFlags.WidthStretch); 57 | ImGui.TableSetupColumn("##ConfigButton", ImGuiTableColumnFlags.WidthFixed); 58 | ImGui.TableNextColumn(); 59 | ImGuiEx.IconTextMapMarker(true); 60 | ImGui.TextWrapped($"{mapName}"); 61 | ImGui.TableNextColumn(); 62 | if (ImGuiEx.IconButton( 63 | Dalamud.Interface.FontAwesomeIcon.Cog, 0, "Open Config")) 64 | Plugin.OpenConfig(true); 65 | ImGui.EndTable(); 66 | } 67 | 68 | #if DEBUG 69 | ImGui.BulletText($"TerritoryType={ZoneWatcher.CurrentTerritoryType?.RowId ?? 0}"); 70 | ImGui.BulletText($"Map data: RowId={map.RowId}, SizeFactor={map.SizeFactor}, " + 71 | $"OffsetX={map.OffsetX}, OffsetY={map.OffsetY}, OffsetZ={CompassUtil.GetCurrentTerritoryZOffset()}"); 72 | ImGui.BulletText($"Main Viewport: pos={Dalamud.Interface.Utility.ImGuiHelpers.MainViewport.Pos}, " + 73 | $"size={Dalamud.Interface.Utility.ImGuiHelpers.MainViewport.Size}, dpi={Dalamud.Interface.Utility.ImGuiHelpers.MainViewport.DpiScale}"); 74 | #endif 75 | if (ImGui.BeginTabBar("CompassesTabBar", ImGuiTabBarFlags.Reorderable)) 76 | { 77 | foreach (Compass c in drawActions.Keys) 78 | { 79 | if (c.CompassEnabled && c.ShowDetail) 80 | { 81 | var name = c.GetType().Name; 82 | name = name.Substring(0, name.Length - "Compass".Length); 83 | if (ImGui.BeginTabItem(name)) 84 | { 85 | drawActions[c].DoAll(); 86 | ImGui.EndTabItem(); 87 | } 88 | } 89 | } 90 | ImGui.EndTabBar(); 91 | } 92 | } 93 | ImGui.End(); 94 | } 95 | 96 | public void Clear() 97 | { 98 | foreach (var q in drawActions.Values) 99 | q.Clear(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/UI/GUI/IconManager.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Internal; 2 | using System.Collections.Concurrent; 3 | using System.Numerics; 4 | using System.Threading.Tasks; 5 | 6 | using TextureWrap = ImGuiScene.TextureWrap; 7 | 8 | namespace AetherCompass.UI.GUI 9 | { 10 | public sealed class IconManager : IDisposable 11 | { 12 | private readonly ConcurrentIconMap iconMap = new(); 13 | 14 | public IDalamudTextureWrap? GetIcon(uint iconId) => iconMap[iconId]; 15 | 16 | public void DisposeIcons(HashSet iconIds) 17 | { 18 | foreach (var id in iconIds) 19 | iconMap.Remove(id); 20 | } 21 | 22 | public void DisposeAllIcons() => iconMap.Clear(); 23 | public void Dispose() => DisposeAllIcons(); 24 | 25 | 26 | #region Common Icons 27 | 28 | public const uint DefaultMarkerIconId = 25948; 29 | internal IDalamudTextureWrap? DefaultMarkerIcon => iconMap[DefaultMarkerIconId]; 30 | 31 | public const uint AltitudeHigherIconId = 60954; 32 | internal IDalamudTextureWrap? AltitudeHigherIcon => iconMap[AltitudeHigherIconId]; 33 | public const uint AltitudeLowerIconId = 60955; 34 | internal IDalamudTextureWrap? AltitudeLowerIcon => iconMap[AltitudeLowerIconId]; 35 | public static readonly Vector2 AltitudeIconSize = new(45, 45); 36 | 37 | // NaviMap thing with those quests/fate etc. direction markers are in 10001400 38 | // but use something else for easier work: 39 | // 60541 up, 60545 down; there are also two sets that are smaller 40 | public const uint DirectionScreenIndicatorIconId = 60541; 41 | internal IDalamudTextureWrap? DirectionScreenIndicatorIcon => iconMap[DirectionScreenIndicatorIconId]; 42 | public static readonly Vector2 DirectionScreenIndicatorIconSize = new(45, 45); 43 | public static readonly uint DirectionScreenIndicatorIconColour = ImGuiNET.ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 1)); 44 | 45 | 46 | public static readonly Vector2 MarkerIconSize = new(30, 30); 47 | 48 | public const uint ConfigDummyMarkerIconId = DefaultMarkerIconId; 49 | internal IDalamudTextureWrap? ConfigDummyMarkerIcon => iconMap[ConfigDummyMarkerIconId]; 50 | 51 | //private static void DisposeCommonIcons() 52 | //{ 53 | // iconMap.Remove(AltitudeHigherIconId); 54 | // iconMap.Remove(AltitudeLowerIconId); 55 | // iconMap.Remove(DirectionScreenIndicatorIconId); 56 | // iconMap.Remove(ConfigDummyMarkerIconId); 57 | //} 58 | 59 | #endregion 60 | } 61 | 62 | 63 | 64 | class ConcurrentIconMap : IDisposable 65 | { 66 | private readonly ConcurrentDictionary> map = new(); 67 | 68 | public IDalamudTextureWrap? this[uint iconId] 69 | { 70 | get 71 | { 72 | if (iconId == 0) return null; 73 | if (map.TryGetValue(iconId, out var tex)) 74 | return tex.Value; 75 | LoadIconAsync(iconId); 76 | return null; 77 | } 78 | } 79 | 80 | public bool Remove(uint id) 81 | { 82 | if (id == 0) return false; 83 | var removed = map.TryRemove(id, out var tex); 84 | tex?.Value?.Dispose(); 85 | return removed; 86 | } 87 | 88 | public void Clear() 89 | { 90 | foreach (var tex in map.Values) tex?.Value?.Dispose(); 91 | map.Clear(); 92 | } 93 | 94 | private async void LoadIconAsync(uint iconId) 95 | { 96 | var icon = await Task.Run(() => Plugin.TextureProvider.GetIcon(iconId)); 97 | if (icon == null) throw new IconLoadFailException(iconId); 98 | map.TryAdd(iconId, new(() => icon, 99 | System.Threading.LazyThreadSafetyMode.ExecutionAndPublication)); 100 | } 101 | 102 | private bool disposed; 103 | 104 | protected virtual void Dispose(bool disposing) 105 | { 106 | if (!disposed) 107 | { 108 | if (disposing) 109 | { 110 | Clear(); 111 | } 112 | } 113 | disposed = true; 114 | } 115 | 116 | public void Dispose() 117 | { 118 | Dispose(disposing: true); 119 | GC.SuppressFinalize(this); 120 | } 121 | } 122 | 123 | 124 | public class IconLoadFailException : Exception 125 | { 126 | public readonly uint IconId; 127 | 128 | public IconLoadFailException(uint iconId) : base($"Failed to load icon: {iconId}") 129 | => IconId = iconId; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Compasses/DebugCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Objectives; 4 | using AetherCompass.Game; 5 | using AetherCompass.UI.GUI; 6 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 7 | using FFXIVClientStructs.FFXIV.Client.UI; 8 | using ImGuiNET; 9 | 10 | 11 | namespace AetherCompass.Compasses 12 | { 13 | [CompassType(CompassType.Debug)] 14 | public class DebugCompass : Compass 15 | { 16 | public override string CompassName => "Debug Compass"; 17 | public override string Description => "For Debug"; 18 | 19 | private protected override CompassConfig CompassConfig => Plugin.Config.DebugConfig; 20 | 21 | private const uint markerIconId = IconManager.DefaultMarkerIconId; 22 | 23 | 24 | public override bool IsEnabledInCurrentTerritory() 25 | => ZoneWatcher.CurrentTerritoryType?.RowId != 0; 26 | 27 | public override unsafe bool IsObjective(GameObject* o) 28 | => o != null && ( 29 | #if DEBUG 30 | Plugin.Config.DebugTestAllGameObjects || 31 | #endif 32 | o->ObjectID == Plugin.ClientState.LocalPlayer?.ObjectId 33 | || o->ObjectKind == (byte)ObjectKind.EventObj 34 | //|| o->ObjectKind == (byte)ObjectKind.EventNpc 35 | || o->ObjectKind == (byte)ObjectKind.GatheringPoint 36 | || o->ObjectKind == (byte)ObjectKind.Aetheryte 37 | || o->ObjectKind == (byte)ObjectKind.AreaObject 38 | || o->ObjectKind == (byte)ObjectKind.CardStand 39 | || o->ObjectKind == (byte)ObjectKind.MjiObject 40 | || o->ObjectKind == (byte)ObjectKind.BattleNpc 41 | ); 42 | 43 | protected override unsafe CachedCompassObjective CreateCompassObjective(GameObject* obj) 44 | => new DebugCachedCompassObjective(obj); 45 | 46 | protected override unsafe CachedCompassObjective CreateCompassObjective(UI3DModule.ObjectInfo* info) 47 | => new DebugCachedCompassObjective(info); 48 | 49 | private protected override unsafe string GetClosestObjectiveDescription(CachedCompassObjective _) 50 | => "Debug Obj"; 51 | 52 | 53 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 54 | { 55 | if (objective.IsEmpty() || objective is not DebugCachedCompassObjective debugObjective) return null; 56 | return new(() => 57 | { 58 | ImGui.Text($"Object: {debugObjective.Name}"); 59 | ImGui.BulletText($"ObjectId: {debugObjective.GameObjectId.ObjectID}, type {debugObjective.GameObjectId.Type}"); 60 | ImGui.BulletText($"ObjectKind: {debugObjective.ObjectKind}"); 61 | ImGui.BulletText($"NpcId: {debugObjective.NpcId} DataId: {debugObjective.DataId}"); 62 | ImGui.BulletText($"2D-Distance: {debugObjective.Distance2D:0.0}"); 63 | ImGui.BulletText($"Height diff: {debugObjective.AltitudeDiff:0.0}"); 64 | ImGui.BulletText($"3D-Distance: {debugObjective.Distance3D:0.0}"); 65 | ImGui.BulletText($"Direction: {debugObjective.CompassDirectionFromPlayer}, {debugObjective.RotationFromPlayer:0.00}"); 66 | ImGui.BulletText($"Position: {debugObjective.Position}"); 67 | ImGui.BulletText($"MapCoord: {CompassUtil.MapCoordToFormattedString(debugObjective.CurrentMapCoord)}"); 68 | ImGui.BulletText($"Normalised Nameplate Pos: {objective.NormalisedNameplatePos}"); 69 | 70 | var o = (GameObject*)objective.GameObject.ToPointer(); 71 | var ischar = CompassUtil.IsCharacter(o); 72 | ImGui.BulletText($"Is character: {ischar}"); 73 | if (ischar) 74 | { 75 | ImGui.BulletText($"Level: {CompassUtil.GetCharacterLevel(o)}"); 76 | ImGui.BulletText($"Alive: {CompassUtil.IsCharacterAlive(o)}"); 77 | ImGui.BulletText($"IsHostile: {CompassUtil.IsHostileCharacter(o)}"); 78 | } 79 | 80 | DrawFlagButton(((long)debugObjective.GameObject).ToString(), debugObjective.CurrentMapCoord); 81 | 82 | ImGui.NewLine(); 83 | }); 84 | } 85 | 86 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 87 | { 88 | if (objective.IsEmpty()) return null; 89 | // These are already handled by the Draw...Default method, 90 | // here is just for debug record 91 | UiHelper.WorldToScreenPos(objective.Position, out var screenPos, out var pCoordsRaw); 92 | screenPos.Y -= ImGui.GetMainViewport().Size.Y / 50; // slightly raise it up from hitbox screen pos 93 | 94 | string info = $"name={objective.Name}, " + 95 | $"worldPos=<{objective.Position.X:0.00}, {objective.Position.Y:0.00}, {objective.Position.Z:0.00}, " + 96 | $"dist={objective.Distance3D:0.0}\n" + 97 | $"sPosUnfixed=<{screenPos.X:0.0}, {screenPos.Y:0.0}>, " + 98 | $"raw=<{pCoordsRaw.X:0.0}, {pCoordsRaw.Y:0.0}, {pCoordsRaw.Z:0.0}>\n" + 99 | $"npPos=<{objective.NormalisedNameplatePos.X:0.0}, {objective.NormalisedNameplatePos.Y:0.0}, {objective.NormalisedNameplatePos.Z:0.0}>"; 100 | return GenerateDefaultScreenMarkerDrawAction(objective, 101 | markerIconId, DefaultMarkerIconSize, .9f, info, new(1, 1, 1, 1), 0, out _); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aether Compass 2 | 3 | 17 | 18 | > 19 | > Have you ever run into something when looking at your compass ... oooops! 20 | 21 | _Aether Compass_ is an FFXIV Dalamud plugin that has a compass (a set of compasses, actually) 22 | that automatically detects certain objects/NPCs such as Aether Currents nearby and shows where they are. 23 | 24 | Detected objects/NPCs are displayed in various ways, 25 | notably through markers on your screen that point to the locations of these objects/NPCs. 26 | Optionally, it can notify you through Chat and/or Toast messages. 27 | 28 | Currently supports detecting: 29 | - Aether Currents 30 | - Mob Hunt Elite Marks (Notorious Monsters) 31 | - Gathering Points 32 | - Eureka Elementals (by apetih) 33 | - *\[Experimental\] Quest-related NPCs/Objects* 34 | - *\[Experimental\] Island Sanctuary Gathering Objects and Animals* 35 | 36 | **NOTE:** Because most objects/NPCs are not loaded 37 | when they are too faraway or when there are too many entities nearby (such as too many player characters), 38 | they will not be detected in this case. 39 | 40 | 41 | ## Installation 42 | 43 | [XIVLauncher](https://github.com/goatcorp/FFXIVQuickLauncher) is required to install and run the plugin. 44 | 45 | The plugin is currently only available in my custom repo. 46 | To access it, you can add my [Dalamud plugin repo](https://github.com/yomishino/MyDalamudPlugins) to Dalamud's Custom Plugin Repositories, 47 | and look for the plugin "Aether Compass [Preview]" in Plugin Installer's available plugins. 48 | 49 | If you cannot find a release version (i.e. not a testing version) of it, 50 | then the plugin is either only available as a testing plugin, or it is not updated at all. 51 | 52 | In the former case, you can find it by enabling the setting "Get plugin testing builds" in Dalamud. 53 | 54 | :warning: **Please be cautious in doing so, as testing plugins are expected to be unstable. 55 | There could be major bugs that may even crash your game client in some worse cases.** 56 | In addition, by enabling this setting you might also receive unstable test builds of other plugins. 57 | 58 | 59 | 131 | 132 | ## Special Thanks 133 | 134 | - [apetih](https://github.com/apetih) - For making the Eureka Elementals compass 135 | - [Lumas Arkfrost](https://github.com/ArkfrostLumas) - For the plugin icon 136 | - And thanks to all who have contributed to bug fixes 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/Compasses/MobHuntCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Objectives; 4 | using AetherCompass.Game; 5 | using AetherCompass.Compasses.Configs; 6 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 7 | using FFXIVClientStructs.FFXIV.Client.UI; 8 | using ImGuiNET; 9 | using Lumina.Excel; 10 | using System.Numerics; 11 | 12 | namespace AetherCompass.Compasses 13 | { 14 | [CompassType(CompassType.Standard)] 15 | public class MobHuntCompass : Compass 16 | { 17 | public override string CompassName => "Mob Hunt Compass"; 18 | public override string Description => "Detecting Elite Marks (Notorious Monsters) nearby."; 19 | 20 | private readonly Dictionary nmDataMap = new(); // BnpcDataId => NMData 21 | private static readonly Vector4 infoTextColour = new(1, .6f, .6f, 1); 22 | private static readonly float infoTextShadowLightness = .1f; 23 | 24 | private const uint rankSMarkerIconId = 61710; 25 | private const uint rankAMarkerIconId = 61709; 26 | private const uint rankBMarkerIconId = 61704; 27 | 28 | private protected override CompassConfig CompassConfig => Plugin.Config.MobHuntConfig; 29 | private MobHuntCompassConfig MobHuntConfig => (MobHuntCompassConfig)CompassConfig; 30 | 31 | 32 | public override bool IsEnabledInCurrentTerritory() 33 | => ZoneWatcher.CurrentTerritoryType?.TerritoryIntendedUse == 1; 34 | 35 | public override unsafe bool IsObjective(GameObject* o) 36 | => o != null && nmDataMap.TryGetValue(o->DataID, out var data) && data.IsValid 37 | && ((data.Rank == NMRank.S && MobHuntConfig.DetectS) 38 | || (data.Rank == NMRank.A && MobHuntConfig.DetectA) 39 | || (data.Rank == NMRank.B && !CompassUtil.IsHostileCharacter(o) && MobHuntConfig.DetectB) 40 | || (data.Rank == NMRank.B && CompassUtil.IsHostileCharacter(o) && MobHuntConfig.DetectSSMinion)) 41 | && CompassUtil.IsCharacterAlive(o); 42 | 43 | protected override unsafe CachedCompassObjective CreateCompassObjective(GameObject* obj) 44 | => obj != null && nmDataMap.TryGetValue(obj->DataID, out var data) && data.IsValid 45 | ? new MobHunCachedCompassObjective(obj, data.Rank, CompassUtil.IsHostileCharacter(obj)) 46 | : new MobHunCachedCompassObjective(obj, 0, false); 47 | 48 | protected override unsafe CachedCompassObjective CreateCompassObjective(UI3DModule.ObjectInfo* info) 49 | { 50 | var obj = info != null ? info->GameObject : null; 51 | if (obj == null) return new MobHunCachedCompassObjective(obj, 0, false); 52 | return nmDataMap.TryGetValue(obj->DataID, out var data) && data.IsValid 53 | ? new MobHunCachedCompassObjective(info, data.Rank, CompassUtil.IsHostileCharacter(obj)) 54 | : new MobHunCachedCompassObjective(info, 0, false); 55 | } 56 | 57 | private protected override unsafe string GetClosestObjectiveDescription(CachedCompassObjective objective) 58 | => objective.IsEmpty() || objective is not MobHunCachedCompassObjective mhObjective 59 | ? string.Empty : $"{mhObjective.Name} (Rank: {mhObjective.GetExtendedRank()})"; 60 | 61 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 62 | => objective.IsEmpty() || objective is not MobHunCachedCompassObjective mhObjective ? null : new(() => 63 | { 64 | ImGui.Text($"{mhObjective.Name}, Rank: {mhObjective.GetExtendedRank()}"); 65 | ImGui.BulletText($"{CompassUtil.MapCoordToFormattedString(mhObjective.CurrentMapCoord)} (approx.)"); 66 | ImGui.BulletText($"{mhObjective.CurrentMapCoord}, " + 67 | $"{CompassUtil.DistanceToDescriptiveString(mhObjective.Distance3D, false)}"); 68 | ImGui.BulletText(CompassUtil.AltitudeDiffToDescriptiveString(mhObjective.AltitudeDiff)); 69 | DrawFlagButton($"##{(long)mhObjective.GameObject}", mhObjective.CurrentMapCoord); 70 | ImGui.Separator(); 71 | }); 72 | 73 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 74 | { 75 | if (objective.IsEmpty() || objective is not MobHunCachedCompassObjective mhObjective) return null; 76 | string descr = $"{mhObjective.Name} (Rank: {mhObjective.GetExtendedRank()}), " + 77 | $"{CompassUtil.DistanceToDescriptiveString(mhObjective.Distance3D, true)}"; 78 | var iconId = mhObjective.Rank switch 79 | { 80 | NMRank.S => rankSMarkerIconId, 81 | NMRank.A => rankAMarkerIconId, 82 | NMRank.B => mhObjective.IsSSMinion 83 | ? rankSMarkerIconId : rankBMarkerIconId, 84 | _ => 0u 85 | }; 86 | return GenerateDefaultScreenMarkerDrawAction(objective, iconId, 87 | DefaultMarkerIconSize, .9f, descr, infoTextColour, infoTextShadowLightness, out _, 88 | important: mhObjective.Rank == NMRank.S || mhObjective.Rank == NMRank.A || mhObjective.IsSSMinion); 89 | } 90 | 91 | 92 | public override void DrawConfigUiExtra() 93 | { 94 | ImGui.BulletText("More options:"); 95 | ImGui.Indent(); 96 | ImGui.Checkbox("Detect S Ranks / SS Ranks", ref MobHuntConfig.DetectS); 97 | ImGui.Checkbox("Detect SS Minions", ref MobHuntConfig.DetectSSMinion); 98 | ImGui.Checkbox("Detect A Ranks", ref MobHuntConfig.DetectA); 99 | ImGui.Checkbox("Detect B Ranks", ref MobHuntConfig.DetectB); 100 | ImGui.Unindent(); 101 | } 102 | 103 | 104 | private static ExcelSheet? NMSheet 105 | => Plugin.DataManager.GetExcelSheet(); 106 | 107 | private void InitNMDataMap() 108 | { 109 | if (NMSheet == null) 110 | { 111 | LogWarningExcelSheetNotLoaded(typeof(Sheets.NotoriousMonster).Name); 112 | return; 113 | } 114 | foreach (var row in NMSheet) 115 | { 116 | if (row.BNpcBase.Row != 0) 117 | nmDataMap.TryAdd(row.BNpcBase.Row, new(row.RowId)); 118 | } 119 | } 120 | 121 | public MobHuntCompass() : base() 122 | { 123 | InitNMDataMap(); 124 | } 125 | 126 | 127 | 128 | class NMData 129 | { 130 | public readonly uint BNpcDataId; 131 | public readonly uint NMSheetRowId; 132 | public readonly NMRank Rank; 133 | public readonly bool IsValid; 134 | 135 | public NMData(uint nmSheetRowId) 136 | { 137 | NMSheetRowId = nmSheetRowId; 138 | if (NMSheet == null) return; 139 | var row = NMSheet.GetRow(nmSheetRowId); 140 | if (row == null) return; 141 | BNpcDataId = row.BNpcBase.Row; 142 | Rank = (NMRank)row.Rank; 143 | IsValid = true; 144 | } 145 | } 146 | } 147 | 148 | public enum NMRank : byte 149 | { 150 | B = 1, 151 | A = 2, 152 | S = 3, 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /src/Compasses/CompassManager.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Game; 4 | using AetherCompass.UI; 5 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 6 | using ImGuiNET; 7 | using ObjectInfo = FFXIVClientStructs.FFXIV.Client.UI.UI3DModule.ObjectInfo; 8 | 9 | 10 | namespace AetherCompass.Compasses 11 | { 12 | public unsafe sealed class CompassManager 13 | { 14 | private readonly HashSet standardCompasses = new(); 15 | private readonly HashSet experimentalCompasses = new(); 16 | #if DEBUG 17 | private readonly HashSet debugCompasses = new(); 18 | #endif 19 | 20 | private readonly SortedSet allAddedCompasses 21 | = new(new CompassSortComp()); 22 | private readonly List workingCompasses = new(); 23 | 24 | private bool hasMapFlagToProcess; 25 | private System.Numerics.Vector2 mapFlagCoord; 26 | 27 | 28 | public void Init() 29 | { 30 | foreach (var t in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()) 31 | { 32 | if (t.IsSubclassOf(typeof(Compass)) && !t.IsAbstract) 33 | { 34 | var ctor = t.GetConstructor(Type.EmptyTypes); 35 | if (ctor != null) AddCompass((Compass)ctor.Invoke(null)); 36 | } 37 | } 38 | } 39 | 40 | public bool AddCompass(Compass c) 41 | { 42 | var added = false; 43 | switch (c.CompassType) 44 | { 45 | case CompassType.Standard: 46 | added = standardCompasses.Add(c); 47 | break; 48 | case CompassType.Experimental: 49 | added = experimentalCompasses.Add(c); 50 | break; 51 | case CompassType.Debug: 52 | # if DEBUG 53 | added = debugCompasses.Add(c); 54 | #endif 55 | break; 56 | default: 57 | LogError($"Failed to enable compass {c.GetType().Name}: no valid compass type"); 58 | return false; 59 | } 60 | if (added) 61 | { 62 | allAddedCompasses.Add(c); 63 | Plugin.DetailsWindow.RegisterCompass(c); 64 | if (c.IsEnabledInCurrentTerritory()) 65 | workingCompasses.Add(c); 66 | } 67 | return true; 68 | } 69 | 70 | public void RemoveCompass(Compass c) 71 | { 72 | c.Reset(); 73 | Plugin.DetailsWindow.UnregisterCompass(c); 74 | workingCompasses.Remove(c); 75 | switch (c.CompassType) 76 | { 77 | case CompassType.Standard: 78 | standardCompasses.Remove(c); 79 | break; 80 | case CompassType.Experimental: 81 | experimentalCompasses.Remove(c); 82 | break; 83 | #if DEBUG 84 | case CompassType.Debug: 85 | debugCompasses.Remove(c); 86 | break; 87 | #endif 88 | default: break; 89 | }; 90 | allAddedCompasses.Remove(c); 91 | } 92 | 93 | 94 | public void RegisterMapFlag(System.Numerics.Vector2 flagCoord) 95 | { 96 | hasMapFlagToProcess = true; 97 | mapFlagCoord = flagCoord; 98 | } 99 | 100 | public void OnTick() 101 | { 102 | Plugin.Overlay.Clear(); 103 | Plugin.DetailsWindow.Clear(); 104 | 105 | if (workingCompasses.Count > 0) 106 | { 107 | foreach (var compass in workingCompasses) 108 | if (compass.CompassEnabled) compass.CancelLastUpdate(); 109 | 110 | #if DEBUG 111 | var debugTestAll = Plugin.Config.DebugTestAllGameObjects; 112 | void* array = debugTestAll 113 | ? GameObjects.ObjectListFiltered 114 | : GameObjects.SortedObjectInfoPointerArray; 115 | int count = debugTestAll 116 | ? GameObjects.ObjectListFilteredCount 117 | : GameObjects.SortedObjectInfoCount; 118 | #else 119 | var array = GameObjects.SortedObjectInfoPointerArray; 120 | var count = GameObjects.SortedObjectInfoCount; 121 | #endif 122 | 123 | if (array == null) return; 124 | 125 | foreach (var compass in workingCompasses) 126 | { 127 | if (compass.CompassEnabled) 128 | { 129 | #if DEBUG 130 | if (debugTestAll) 131 | compass.ProcessLoopDebugAllObjects((GameObject**)array, count); 132 | else 133 | #endif 134 | compass.ProcessLoop((ObjectInfo**)array, count); 135 | } 136 | 137 | } 138 | } 139 | 140 | ProcessFlagOnTickEnd(); 141 | } 142 | 143 | private void ProcessFlagOnTickEnd() 144 | { 145 | if (!hasMapFlagToProcess) return; 146 | 147 | // NOTE: Dirty fix 148 | // Currently Dalamud's MapLinkPayload internally does not take into account Map's X/Y-offset, 149 | // so in map with non-zero offsets (e.g., Mist subdivision) it's always incorrect. 150 | // Tweak it with a FixedMapLinkPayload that has the original raw X/Y 151 | // but our calcualted map coord to fix this issue. 152 | var terrId = Plugin.ClientState.TerritoryType; 153 | //var maplink = new MapLinkPayload(terrId, ZoneWatcher.CurrentMapId, 154 | // mapFlagCoord.X, mapFlagCoord.Y, fudgeFactor: 0.01f); 155 | var map = ZoneWatcher.CurrentMap; 156 | if (map != null) 157 | { 158 | var fixedMapLink = FixedMapLinkPayload.FromMapCoord(terrId, ZoneWatcher.CurrentMapId, 159 | mapFlagCoord.X, mapFlagCoord.Y, map.SizeFactor, map.OffsetX, map.OffsetY); 160 | #if DEBUG 161 | LogDebug($"Create MapLinkPayload from {mapFlagCoord}: {fixedMapLink}"); 162 | #endif 163 | //if (Plugin.GameGui.OpenMapWithMapLink(maplinkFix)) 164 | //{ 165 | // var msg = Chat.CreateMapLink(terrId, ZoneWatcher.CurrentMapId, maplink.XCoord, maplink.YCoord).PrependText("Flag set: "); 166 | // Chat.PrintChat(msg); 167 | // hasMapFlagToProcess = false; 168 | //} 169 | if (Plugin.GameGui.OpenMapWithMapLink(fixedMapLink)) 170 | { 171 | var msg = Chat.CreateMapLink(fixedMapLink).PrependText("Flag set: "); 172 | Chat.PrintChat(msg); 173 | } 174 | } 175 | 176 | hasMapFlagToProcess = false; 177 | } 178 | 179 | public void OnZoneChange() 180 | { 181 | try 182 | { 183 | workingCompasses.Clear(); 184 | foreach (var compass in allAddedCompasses) 185 | { 186 | if (compass.IsEnabledInCurrentTerritory()) 187 | workingCompasses.Add(compass); 188 | compass.OnZoneChange(); 189 | } 190 | } catch (ObjectDisposedException) { } 191 | } 192 | 193 | public void DrawCompassConfigUi() 194 | { 195 | foreach (var compass in allAddedCompasses) 196 | { 197 | compass.DrawConfigUi(); 198 | ImGui.NewLine(); 199 | } 200 | } 201 | 202 | 203 | private class CompassSortComp : IComparer 204 | { 205 | public int Compare(Compass? x, Compass? y) 206 | { 207 | if (x == null && y == null) return 0; 208 | if (x == null) return int.MinValue; 209 | if (y == null) return int.MaxValue; 210 | var ret = x.CompassType.CompareTo(y.CompassType); 211 | return ret != 0 ? ret 212 | : x.GetType().Name.CompareTo(y.GetType().Name); 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/UI/GUI/UiHelper.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Game.SeFunctions; 3 | using Dalamud.Interface; 4 | using Dalamud.Interface.Utility; 5 | using ImGuiNET; 6 | using System.Numerics; 7 | 8 | 9 | namespace AetherCompass.UI.GUI 10 | { 11 | public static class UiHelper 12 | { 13 | public static Vector2 GetScreenCentre() 14 | => ImGuiHelpers.MainViewport.GetCenter(); 15 | 16 | // offset is L/B/R/T, added directly to the position of each side of viewport 17 | public static bool IsScreenPosInsideMainViewport( 18 | Vector2 screenPos, Vector4 offset) 19 | { 20 | var pos = ImGuiHelpers.MainViewport.Pos; 21 | var size = ImGuiHelpers.MainViewport.Size; 22 | return MathUtil.IsBetween(screenPos.X, pos.X + offset.X, pos.X + size.X + offset.Z) 23 | && MathUtil.IsBetween(screenPos.Y, pos.Y + offset.W, pos.Y + size.Y + offset.Y); 24 | } 25 | 26 | public static bool IsScreenPosInsideMainViewport(Vector2 screenPos) 27 | => IsScreenPosInsideMainViewport(screenPos, new(0, 0, 0, 0)); 28 | 29 | public static bool WorldToScreenPos(Vector3 worldPos, out Vector2 screenPos) 30 | => Projection.WorldToScreen(worldPos, out screenPos); 31 | 32 | internal static bool WorldToScreenPos(Vector3 worldPos, out Vector2 screenPos, out Vector3 pCoordsRaw) 33 | => Projection.WorldToScreen(worldPos, out screenPos, out pCoordsRaw); 34 | 35 | // NDC used in UI3DModule.ObjectInfo for object screen positions 36 | public static Vector3 TranslateNormalisedCoordinates( 37 | Vector3 pos3norm, bool reverseY = true) 38 | { 39 | var mSizeHalf = ImGuiHelpers.MainViewport.Size / 2; 40 | var mCentrePos = ImGuiHelpers.MainViewport.Pos + mSizeHalf; 41 | return new(mCentrePos.X + pos3norm.X * mSizeHalf.X, 42 | reverseY ? mCentrePos.Y - pos3norm.Y * mSizeHalf.Y 43 | : mCentrePos.Y + pos3norm.Y * mSizeHalf.Y, 44 | pos3norm.Z); 45 | } 46 | 47 | public static Vector2 GetConstrainedScreenPos( 48 | Vector2 screenPos, Vector4 screenConstraint, Vector2 extraConstraint) 49 | { 50 | var constraintUL = ImGuiHelpers.MainViewport.Pos + extraConstraint; 51 | var constraintBR = ImGuiHelpers.MainViewport.Pos + ImGuiHelpers.MainViewport.Size - extraConstraint; 52 | var x = Math.Clamp(screenPos.X, constraintUL.X + screenConstraint.X, constraintBR.X - screenConstraint.Z); 53 | var y = Math.Clamp(screenPos.Y, constraintUL.Y + screenConstraint.W, constraintBR.Y - screenConstraint.Y); 54 | return new Vector2(x, y); 55 | } 56 | 57 | public static bool IsScreenPosInsideConstraint( 58 | Vector2 screenPos, Vector4 screenConstraint, Vector2 extraConstraint) 59 | { 60 | var constraintUL = ImGuiHelpers.MainViewport.Pos 61 | + new Vector2(screenConstraint.X, screenConstraint.W) + extraConstraint; 62 | var constraintBR = ImGuiHelpers.MainViewport.Pos + ImGuiHelpers.MainViewport.Size 63 | - new Vector2(screenConstraint.Z, screenConstraint.Y) - extraConstraint; 64 | return MathUtil.IsBetween(screenPos.X, constraintUL.X, constraintBR.X, true) 65 | && MathUtil.IsBetween(screenPos.Y, constraintUL.Y, constraintBR.Y, true); 66 | } 67 | 68 | public static float GetAngleOnScreen(Vector2 origin, Vector2 point, bool flipped = false) 69 | => flipped ? MathF.Atan2(origin.X - point.X, origin.Y - point.Y) 70 | : MathF.Atan2(point.X - origin.X, point.Y - origin.Y); 71 | 72 | public static float GetAngleOnScreen(Vector2 point, bool flipped = false) 73 | => GetAngleOnScreen(GetScreenCentre(), point, flipped); 74 | 75 | public static (Vector2 P1, Vector2 P2, Vector2 P3, Vector2 P4) 76 | GetRectPointsOnScreen(Vector2 screenPos, Vector2 rectHalfSize) 77 | { 78 | // p1~p4 is UL, UR, BR, BL of the image 79 | Vector2 p1 = screenPos - rectHalfSize; 80 | Vector2 p2 = new(screenPos.X + rectHalfSize.X, screenPos.Y - rectHalfSize.Y); 81 | Vector2 p3 = screenPos + rectHalfSize; 82 | Vector2 p4 = new(screenPos.X - rectHalfSize.X, screenPos.Y + rectHalfSize.Y); 83 | return (p1, p2, p3, p4); 84 | } 85 | 86 | // rotation = 0 points upwards to make things intuitive 87 | public static (Vector2 P1, Vector2 P2, Vector2 P3, Vector2 P4) 88 | GetRotatedRectPointsOnScreen(Vector2 screenPos, Vector2 rectHalfSize, float rotation) 89 | { 90 | var (p1, p2, p3, p4) = GetRectPointsOnScreen(screenPos, rectHalfSize); 91 | 92 | // Rotate 93 | p1 = RotatePointOnPlane(p1, screenPos, rotation); 94 | p2 = RotatePointOnPlane(p2, screenPos, rotation); 95 | p3 = RotatePointOnPlane(p3, screenPos, rotation); 96 | p4 = RotatePointOnPlane(p4, screenPos, rotation); 97 | 98 | return (p1, p2, p3, p4); 99 | } 100 | 101 | public static Vector2 RotatePointOnPlane(Vector2 p, Vector2 rotationCentre, float rotation) 102 | { 103 | p -= rotationCentre; 104 | var a = MathF.Atan2(p.X, p.Y); 105 | var di = Vector2.Distance(p, Vector2.Zero); 106 | return new Vector2( 107 | di * MathF.Sin(a + rotation) + rotationCentre.X, 108 | di * MathF.Cos(a + rotation) + rotationCentre.Y); 109 | } 110 | 111 | public static Vector4 GenerateShadowColour(Vector4 colour, float lightness) 112 | { 113 | ImGui.ColorConvertRGBtoHSV(colour.X, colour.Y, colour.Z, out float h, out float _, out float _); 114 | float s = -lightness * lightness + 1; 115 | float v = lightness; 116 | ImGui.ColorConvertHSVtoRGB(h, s, v, out float r, out float g, out float b); 117 | return new Vector4(r, g, b, colour.W); 118 | } 119 | 120 | 121 | public static Vector2 GetTextSize(string text, ImFontPtr font, float fontsize) 122 | { 123 | var split = text.Split('\n'); 124 | float maxLineW = 0; 125 | int lineCount = 0; 126 | foreach (var s in split) 127 | { 128 | float lineW = 0; 129 | foreach (var c in s) 130 | lineW += font.GetCharAdvance(c); 131 | maxLineW = MathF.Max(maxLineW, lineW); 132 | lineCount++; 133 | } 134 | return new Vector2(maxLineW * fontsize / font.FontSize, fontsize * lineCount); 135 | } 136 | 137 | public static void DrawTextWithShadow(ImDrawListPtr drawList, string text, Vector2 pos, 138 | ImFontPtr font, float fontsizeRaw, float scale, Vector4 colour, float shadowLightness) 139 | { 140 | var fontsize = fontsizeRaw * scale; 141 | var col_uint = ImGui.ColorConvertFloat4ToU32(colour); 142 | var shadowCol_uint = ImGui.ColorConvertFloat4ToU32(GenerateShadowColour(colour, shadowLightness)); 143 | // showdow R 144 | pos.X += scale; 145 | drawList.AddText(font, fontsize, pos, shadowCol_uint, text); 146 | // showdow D 147 | pos.X -= scale; 148 | pos.Y += scale; 149 | drawList.AddText(font, fontsize, pos, shadowCol_uint, text); 150 | // content 151 | pos.Y -= scale; 152 | drawList.AddText(font, fontsize, pos, col_uint, text); 153 | } 154 | 155 | public static void DrawMultilineTextWithShadow(ImDrawListPtr drawList, string text, 156 | Vector2 pos, ImFontPtr font, float fontsizeRaw, float scale, Vector4 colour, 157 | float shadowLightness, bool rightAligned = false) 158 | { 159 | if (!rightAligned) 160 | { 161 | DrawTextWithShadow(drawList, text, pos, font, fontsizeRaw, scale, colour, shadowLightness); 162 | return; 163 | } 164 | var fontsize = fontsizeRaw * scale; 165 | var lines = text.Split('\n'); 166 | var lineTextSize = new Vector2[lines.Length]; 167 | var maxSizeX = float.MinValue; 168 | for (int i = 0; i < lines.Length; i++) 169 | { 170 | lineTextSize[i] = GetTextSize(lines[i], font, fontsize); 171 | maxSizeX = MathF.Max(maxSizeX, lineTextSize[i].X); 172 | } 173 | var linePos = pos; 174 | for (int i = 0; i < lines.Length; i++) 175 | { 176 | linePos.X = pos.X + maxSizeX - lineTextSize[i].X; 177 | DrawTextWithShadow(drawList, lines[i], linePos, font, fontsizeRaw, scale, colour, shadowLightness); 178 | linePos.Y += lineTextSize[i].Y; 179 | } 180 | } 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/Plugin.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using static AetherCompass.PluginUtil; 4 | global using Sheets = Lumina.Excel.GeneratedSheets; 5 | using AetherCompass.Compasses; 6 | using AetherCompass.Game; 7 | using AetherCompass.UI; 8 | using AetherCompass.UI.GUI; 9 | using Dalamud.Game; 10 | using Dalamud.Game.ClientState.Conditions; 11 | using Dalamud.IoC; 12 | using Dalamud.Plugin; 13 | using Dalamud.Plugin.Services; 14 | 15 | namespace AetherCompass 16 | { 17 | public class Plugin : IDalamudPlugin 18 | { 19 | // Plugin Services 20 | [PluginService] 21 | internal static DalamudPluginInterface PluginInterface { get; private set; } = null!; 22 | [PluginService] 23 | internal static ISigScanner SigScanner { get; private set; } = null!; 24 | [PluginService] 25 | internal static ICommandManager CommandManager { get; private set; } = null!; 26 | [PluginService] 27 | internal static IDataManager DataManager { get; private set; } = null!; 28 | [PluginService] 29 | internal static ITextureProvider TextureProvider { get; private set; } = null!; 30 | [PluginService] 31 | internal static IFramework Framework { get; private set; } = null!; 32 | [PluginService] 33 | internal static IClientState ClientState { get; private set; } = null!; 34 | [PluginService] 35 | internal static ICondition ClientCondition { get; private set; } = null!; 36 | [PluginService] 37 | internal static IGameGui GameGui { get; private set; } = null!; 38 | [PluginService] 39 | internal static IChatGui ChatGui { get; private set; } = null!; 40 | [PluginService] 41 | internal static IToastGui ToastGui { get; private set; } = null!; 42 | [PluginService] 43 | internal static IPluginLog PluginLog { get; private set; } = null!; 44 | 45 | 46 | public string Name => "Aether Compass" 47 | #if PRE 48 | + " [PREVIEW]" 49 | #endif 50 | #if DEBUG 51 | + " [DEV]" 52 | #elif TEST 53 | + " [TEST]" 54 | #endif 55 | ; 56 | 57 | internal static readonly IconManager IconManager = new(); 58 | internal static readonly CompassManager CompassManager = new(); 59 | internal static readonly CompassOverlay Overlay = new(); 60 | internal static readonly CompassDetailsWindow DetailsWindow = new(); 61 | 62 | internal static PluginConfig Config { get; private set; } = null!; 63 | 64 | private static bool _enabled = false; 65 | public static bool Enabled 66 | { 67 | get => _enabled; 68 | internal set 69 | { 70 | _enabled = false; 71 | Overlay.Clear(); 72 | DetailsWindow.Clear(); 73 | if (!value) IconManager.DisposeAllIcons(); 74 | _enabled = value; 75 | if (Config != null) Config.Enabled = value; 76 | } 77 | } 78 | 79 | internal static bool InConfig; 80 | 81 | 82 | public Plugin() 83 | { 84 | Config = new(); 85 | Config.Load(); 86 | CompassManager.Init(); 87 | 88 | PluginCommands.AddCommands(); 89 | 90 | Framework.Update += OnFrameworkUpdate; 91 | PluginInterface.UiBuilder.Draw += OnDrawUi; 92 | PluginInterface.UiBuilder.OpenConfigUi += OnOpenConfigUi; 93 | ClientState.TerritoryChanged += OnZoneChange; 94 | 95 | Reload(); 96 | OnZoneChange(ClientState.TerritoryType); // update zone related stuff on init 97 | } 98 | 99 | public static void ShowError(string chatMsg, string logMsg) 100 | { 101 | Chat.PrintErrorChat(chatMsg); 102 | LogError(logMsg); 103 | } 104 | 105 | public static void OpenConfig(bool setFocus = false) 106 | { 107 | InConfig = true; 108 | if (setFocus) ConfigUi.IsFocus = true; 109 | } 110 | 111 | private void OnDrawUi() 112 | { 113 | if (ClientState.LocalContentId == 0) return; 114 | 115 | if (InConfig) ConfigUi.Draw(); 116 | 117 | if (Enabled && ZoneWatcher.IsInCompassWorkZone && !InNotDrawingConditions()) 118 | { 119 | if (ClientState.LocalPlayer != null) 120 | { 121 | try 122 | { 123 | if (Config.ShowScreenMark) Overlay.Draw(); 124 | if (Config.ShowDetailWindow) 125 | { 126 | if (!(Config.HideDetailInContents && ZoneWatcher.IsInDetailWindowHideZone)) 127 | DetailsWindow.Draw(); 128 | else DetailsWindow.Clear(); 129 | } 130 | } 131 | catch (Exception e) 132 | { 133 | //ShowError("Plugin encountered an error.", e.ToString()); 134 | LogError(e.ToString()); 135 | } 136 | } 137 | else 138 | { 139 | // Clear when should not draw to avoid any action remaining in queue be drawn later 140 | // which would cause game crash due to access violation etc. 141 | if (Config.ShowScreenMark) Overlay.Clear(); 142 | if (Config.ShowDetailWindow) DetailsWindow.Clear(); 143 | } 144 | } 145 | else if (InConfig && Config.ShowScreenMark) 146 | { 147 | // for drawing the marker display area when in config 148 | Overlay.Draw(); 149 | } 150 | } 151 | 152 | public static void Reload() 153 | { 154 | // Will clear prev drawings & dispose old icons 155 | Enabled = Config.Enabled; 156 | } 157 | 158 | internal static void SetEnabledIfConfigChanged() 159 | { 160 | if (Config.Enabled != _enabled) 161 | Enabled = Config.Enabled; // Clear&Reload iff Enabled changed 162 | } 163 | 164 | private void OnFrameworkUpdate(IFramework framework) 165 | { 166 | if (Enabled && ClientState.LocalContentId != 0 && ZoneWatcher.IsInCompassWorkZone) 167 | { 168 | try 169 | { 170 | CompassManager.OnTick(); 171 | } 172 | catch (Exception e) 173 | { 174 | //ShowError("Plugin encountered an error.", e.ToString()); 175 | LogError(e.ToString()); 176 | } 177 | } 178 | } 179 | 180 | private void OnOpenConfigUi() => OpenConfig(true); 181 | 182 | private void OnZoneChange( ushort terr) 183 | { 184 | ZoneWatcher.OnZoneChange(); 185 | if (terr == 0) return; 186 | // Local player is almost always null when this event fired 187 | if (Enabled && ClientState.LocalContentId != 0) 188 | CompassManager.OnZoneChange(); 189 | } 190 | 191 | private static bool InNotDrawingConditions() 192 | => Config.HideInEvent && 193 | ( ClientCondition[ConditionFlag.ChocoboRacing] 194 | || ClientCondition[ConditionFlag.CreatingCharacter] 195 | || ClientCondition[ConditionFlag.DutyRecorderPlayback] 196 | || ClientCondition[ConditionFlag.OccupiedInCutSceneEvent] 197 | || ClientCondition[ConditionFlag.OccupiedInEvent] 198 | || ClientCondition[ConditionFlag.OccupiedInQuestEvent] 199 | || ClientCondition[ConditionFlag.OccupiedSummoningBell] 200 | || ClientCondition[ConditionFlag.Performing] 201 | || ClientCondition[ConditionFlag.PlayingLordOfVerminion] 202 | || ClientCondition[ConditionFlag.PlayingMiniGame] 203 | || ClientCondition[ConditionFlag.WatchingCutscene] 204 | || ClientCondition[ConditionFlag.WatchingCutscene78] 205 | ) || Config.HideWhenCraftGather && 206 | ( ClientCondition[ConditionFlag.Crafting] 207 | || ClientCondition[ConditionFlag.Crafting40] 208 | || ClientCondition[ConditionFlag.Fishing] 209 | || ClientCondition[ConditionFlag.Gathering] 210 | || ClientCondition[ConditionFlag.Gathering42] 211 | || ClientCondition[ConditionFlag.PreparingToCraft] 212 | ); 213 | 214 | 215 | 216 | #region IDisposable Support 217 | 218 | protected virtual void Dispose(bool disposing) 219 | { 220 | if (!disposing) return; 221 | 222 | //config.Save(); 223 | 224 | PluginCommands.RemoveCommands(); 225 | IconManager.Dispose(); 226 | 227 | ClientState.TerritoryChanged -= OnZoneChange; 228 | 229 | PluginInterface.UiBuilder.Draw -= OnDrawUi; 230 | PluginInterface.UiBuilder.OpenConfigUi -= OnOpenConfigUi; 231 | 232 | Framework.Update -= OnFrameworkUpdate; 233 | } 234 | 235 | public void Dispose() 236 | { 237 | Dispose(true); 238 | GC.SuppressFinalize(this); 239 | } 240 | #endregion 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Common/CompassUtil.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Game; 2 | using FFXIVClientStructs.FFXIV.Client.Game.Character; 3 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 4 | using System.Numerics; 5 | using System.Runtime.InteropServices; 6 | 7 | 8 | namespace AetherCompass.Common 9 | { 10 | public static class CompassUtil 11 | { 12 | // Too slow to read the name as SeString 13 | // Just read it as plain UTF8 string for now, unless necessary 14 | public unsafe static string GetName(GameObject* o) 15 | => o == null ? string.Empty 16 | //: MemoryHelper.ReadSeString((IntPtr)o->Name, 64).TextValue; 17 | : Marshal.PtrToStringUTF8((IntPtr)o->Name) ?? string.Empty; 18 | 19 | public static string ToTitleCase(string s) 20 | => System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase(s); 21 | 22 | public unsafe static bool IsCharacter(GameObject* o) 23 | => o != null && o->IsCharacter(); 24 | 25 | public unsafe static byte GetCharacterLevel(GameObject* o) 26 | => IsCharacter(o) ? ((Character*)o)->CharacterData.Level : byte.MinValue; 27 | 28 | public unsafe static bool IsCharacterAlive(GameObject* o) 29 | //=> IsCharacter(o) && (Marshal.ReadByte((IntPtr)o + 0x197C) & 2) == 0; 30 | => IsCharacter(o) && !o->IsDead(); 31 | 32 | public unsafe static bool IsHostileCharacter(GameObject* o) 33 | => IsCharacter(o) && ((Character*)o)->IsHostile; 34 | 35 | 36 | public unsafe static float Get3DDistanceFromPlayer(GameObject* o) 37 | => o == null ? float.NaN : Get3DDistanceFromPlayer(o->Position); 38 | 39 | public unsafe static float Get3DDistanceFromPlayer(Vector3 gameObjPos) 40 | => Plugin.ClientState.LocalPlayer == null 41 | ? float.NaN : Vector3.Distance(gameObjPos, Plugin.ClientState.LocalPlayer.Position); 42 | 43 | public unsafe static float Get2DDistanceFromPlayer(GameObject* o) 44 | { 45 | if (o == null) return float.NaN; 46 | var player = Plugin.ClientState.LocalPlayer; 47 | if (player == null) return float.NaN; 48 | return MathF.Sqrt(MathF.Pow(o->Position.X - player.Position.X, 2) 49 | + MathF.Pow(o->Position.Z - player.Position.Z, 2)); 50 | } 51 | 52 | public static string DistanceToDescriptiveString(float dist, bool integer) 53 | => (integer ? $"{dist:0}" : $"{dist:0.0}") + Language.Unit.Yalm; 54 | 55 | public static string Get3DDistanceFromPlayerDescriptive(Vector3 gameObjPos, bool integer) 56 | => DistanceToDescriptiveString(Get3DDistanceFromPlayer(gameObjPos), integer); 57 | 58 | public unsafe static string Get3DDistanceFromPlayerDescriptive(GameObject* o, bool integer) 59 | => DistanceToDescriptiveString(Get3DDistanceFromPlayer(o), integer); 60 | 61 | public unsafe static float GetAltitudeDiffFromPlayer(GameObject* o) 62 | => o == null ? float.NaN : GetAltitudeDiffFromPlayer(o->Position); 63 | 64 | public static float GetAltitudeDiffFromPlayer(Vector3 gameObjPos) 65 | => Plugin.ClientState.LocalPlayer == null ? float.NaN : (gameObjPos.Y - Plugin.ClientState.LocalPlayer.Position.Y); 66 | 67 | public static string AltitudeDiffToDescriptiveString(float diff) 68 | { 69 | var diffAbs = MathF.Abs(diff); 70 | if (diffAbs < 1) return "At same altitude"; 71 | string s = DistanceToDescriptiveString(diffAbs, true); 72 | if (diff > 0) return s + " higher than you"; 73 | else return s + " lower than you"; 74 | } 75 | 76 | public unsafe static string GetAltitudeDiffFromPlayerDescriptive(GameObject* o) 77 | => AltitudeDiffToDescriptiveString(GetAltitudeDiffFromPlayer(o)); 78 | 79 | public static string GetAltitudeDiffFromPlayerDescriptive(Vector3 gameObjPos) 80 | => AltitudeDiffToDescriptiveString(GetAltitudeDiffFromPlayer(gameObjPos)); 81 | 82 | public unsafe static float GetRotationFromPlayer(GameObject* o) 83 | => o == null ? float.NaN : GetRotationFromPlayer(o->Position); 84 | 85 | public static float GetRotationFromPlayer(Vector3 gameObjPos) 86 | { 87 | var player = Plugin.ClientState.LocalPlayer; 88 | if (player == null) return float.NaN; 89 | return MathF.Atan2(gameObjPos.X - player.Position.X, gameObjPos.Z - player.Position.Z); 90 | } 91 | 92 | private static readonly float directionSpan = MathF.Sin(3 * MathF.PI / 8); 93 | 94 | public unsafe static CompassDirection GetDirectionFromPlayer(GameObject* o) 95 | => o == null ? CompassDirection.NaN : GetDirectionFromPlayer(o->Position); 96 | 97 | public static CompassDirection GetDirectionFromPlayer(Vector3 gameObjPos) 98 | { 99 | var player = Plugin.ClientState.LocalPlayer; 100 | if (player == null) return CompassDirection.NaN; 101 | var vec = Vector2.Normalize(new Vector2(gameObjPos.X - player.Position.X, gameObjPos.Z - player.Position.Z)); 102 | CompassDirection d = 0; 103 | if (MathF.Abs(vec.X) < directionSpan) 104 | d |= vec.Y > 0 ? CompassDirection.South : CompassDirection.North; 105 | if (MathF.Abs(vec.Y) < directionSpan) 106 | d |= vec.X > 0 ? CompassDirection.East : CompassDirection.West; 107 | return d; 108 | } 109 | 110 | 111 | public static short GetCurrentTerritoryZOffset() 112 | => ZoneWatcher.CurrentTerritoryTypeTransient?.OffsetZ ?? 0; 113 | 114 | public static string GetPlaceNameToString(uint placeNameRowId, string emptyPlaceName = "") 115 | { 116 | var name = ZoneWatcher.PlaceName?.GetRow(placeNameRowId)?.Name.ToString(); 117 | if (string.IsNullOrEmpty(name)) return emptyPlaceName; 118 | return name; 119 | } 120 | 121 | public static Vector3 GetMapCoord(Vector3 worldPos, ushort scale, 122 | short offsetXCoord, short offsetYCoord, short offsetZCoord) 123 | { 124 | // Altitude is y in world position but z in map coord 125 | float mx = WorldPositionToMapCoord(worldPos.X, scale, offsetXCoord); 126 | float my = WorldPositionToMapCoord(worldPos.Z, scale, offsetYCoord); 127 | float mz = WorldPositionToMapCoordZ(worldPos.Y, offsetZCoord); 128 | // Also truncate coords to one decimal place seems give closer results 129 | mx = MathUtil.TruncateToOneDecimalPlace(mx); 130 | my = MathUtil.TruncateToOneDecimalPlace(my); 131 | mz = MathUtil.TruncateToOneDecimalPlace(mz); 132 | return new Vector3(mx, my, mz); 133 | } 134 | 135 | // "-1" before divided by 2048 seems a more accurate result? 136 | private static float WorldPositionToMapCoord(float v, ushort scale, short offset) 137 | => 41f * ((MathF.Truncate(v) + offset) * (scale / 100f) + 1024f - 1) / 2048f / (scale / 100f) + 1; 138 | 139 | // Altitude seems pos:coord=10:.1 and ignoring map's sizefactor. 140 | // Z-coord offset seems coming from TerritoryTypeTransient sheet, 141 | // and *subtract* it from worldPos.Y 142 | private static float WorldPositionToMapCoordZ(float worldY, short offset = 0) 143 | => (worldY - offset) / 100f; 144 | 145 | public static Vector3 GetMapCoordInCurrentMap(Vector3 worldPos) 146 | { 147 | var map = ZoneWatcher.CurrentMap; 148 | if (map == null) return new Vector3(float.NaN, float.NaN, float.NaN); 149 | return GetMapCoord(worldPos, map.SizeFactor, map.OffsetX, map.OffsetY, GetCurrentTerritoryZOffset()); 150 | } 151 | 152 | // Among valid maps, all that officially has no Z coord has Z-offset of -10000 153 | public static bool CurrentHasZCoord() 154 | => GetCurrentTerritoryZOffset() > -10000; 155 | 156 | public static string MapCoordToFormattedString(Vector3 coord, bool showZ = true) 157 | => $"X:{coord.X:0.0}, Y:{coord.Y:0.0}{(showZ && CurrentHasZCoord() ? $", Z:{coord.Z:0.0}" : string.Empty)}"; 158 | //=> $"X:{coord.X}, Y:{coord.Y}{(showZ && CurrentHasZCoord() ? $", Z:{coord.Z}" : string.Empty)}"; 159 | 160 | public static string GetMapCoordInCurrentMapFormattedString(Vector3 worldPos, bool showZ = true) 161 | => MapCoordToFormattedString(GetMapCoordInCurrentMap(worldPos), showZ); 162 | 163 | public static Vector3 GetWorldPosition(Vector3 mapCoord, ushort scale, 164 | short offsetXCoord, short offsetYCoord, short offsetZCoord) 165 | => new(MapCoordToWorldPosition(mapCoord.X, scale, offsetXCoord), 166 | MapCoordToWorldPositionY(mapCoord.Z, offsetZCoord), 167 | MapCoordToWorldPosition(mapCoord.Y, scale, offsetYCoord)); 168 | 169 | private static float MapCoordToWorldPosition(float v, ushort scale, short offset) 170 | => ((v - 1) * (scale / 100f) * 2048f / 41f + 1 - 1024f) / (scale / 100f) - offset; 171 | 172 | private static float MapCoordToWorldPositionY(float coordZ, short offset = 0) 173 | => coordZ * 100f + offset; 174 | 175 | public static Vector3 GetWorldPositionFromMapCoordInCurrentMap(Vector3 mapCoord) 176 | { 177 | var map = ZoneWatcher.CurrentMap; 178 | if (map == null) return new Vector3(float.NaN, float.NaN, float.NaN); 179 | return GetWorldPosition(mapCoord, map.SizeFactor, map.OffsetX, map.OffsetY, GetCurrentTerritoryZOffset()); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/UI/GUI/ConfigUi.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Compasses; 2 | using AetherCompass.Game; 3 | using ImGuiNET; 4 | using System.Diagnostics; 5 | using System.Numerics; 6 | 7 | namespace AetherCompass.UI.GUI 8 | { 9 | public static class ConfigUi 10 | { 11 | internal static bool IsFocus; 12 | 13 | public static void Draw() 14 | { 15 | if (IsFocus) 16 | { 17 | ImGui.SetNextWindowCollapsed(false); 18 | ImGui.SetNextWindowFocus(); 19 | IsFocus = false; 20 | } 21 | 22 | if (ImGui.Begin("AetherCompass: Configuration")) // not collapsed 23 | { 24 | ImGuiEx.Checkbox("Enable plugin", ref Plugin.Config.Enabled, 25 | "Enable/Disable this plugin. \n" + 26 | "All compasses will auto pause in certain zones such as PvP zones regardless of this setting."); 27 | Plugin.SetEnabledIfConfigChanged(); 28 | if (Plugin.Config.Enabled) 29 | { 30 | ImGui.NewLine(); 31 | 32 | if (ImGui.BeginTabBar("AetherCompass_Configuration_MainTabBar")) 33 | { 34 | if (ImGui.BeginTabItem("Plugin Settings")) 35 | { 36 | ImGui.TreePush("AetherCompass_Configuration_TabPluginSettings"); 37 | ImGui.NewLine(); 38 | DrawPluginSettingsTab(); 39 | ImGui.TreePop(); 40 | ImGui.EndTabItem(); 41 | } 42 | if (ImGui.BeginTabItem("Compass Settings")) 43 | { 44 | ImGui.TreePush("AetherCompass_Configuration_TabCompassSettings"); 45 | ImGui.NewLine(); 46 | Plugin.CompassManager.DrawCompassConfigUi(); 47 | ImGui.TreePop(); 48 | ImGui.EndTabItem(); 49 | } 50 | ImGui.EndTabBar(); 51 | } 52 | } 53 | 54 | ImGuiEx.Separator(false, true); 55 | if (ImGui.Button("Save")) 56 | Plugin.Config.Save(); 57 | if (ImGui.Button("Save & Close")) 58 | { 59 | Plugin.Config.Save(); 60 | Plugin.InConfig = false; 61 | Plugin.Reload(); 62 | } 63 | ImGui.NewLine(); 64 | if (ImGui.Button("Close & Discard All Changes")) 65 | { 66 | Plugin.InConfig = false; 67 | Plugin.Config.Load(); 68 | Plugin.Reload(); 69 | } 70 | } 71 | ImGui.End(); 72 | 73 | Plugin.Config.CheckValueValidity(ImGui.GetMainViewport().Size); 74 | 75 | var displayArea = GetDisplayAreaFromConfigScreenMarkConstraint(); 76 | Plugin.Overlay.AddDrawAction(() => ImGui.GetWindowDrawList().AddRect( 77 | new(displayArea.X, displayArea.W), new(displayArea.Z, displayArea.Y), 78 | ImGui.ColorConvertFloat4ToU32(new(1, 0, 0, 1)), 0, ImDrawFlags.Closed, 4)); 79 | Plugin.Overlay.AddDrawAction(Compass.GenerateConfigDummyMarkerDrawAction( 80 | $"Marker size scale: {Plugin.Config.ScreenMarkSizeScale:0.00},\n" + 81 | $"Text rel size scale: {Plugin.Config.ScreenMarkTextRelSizeScale:0.0}", 82 | Plugin.Config.ScreenMarkSizeScale, Plugin.Config.ScreenMarkTextRelSizeScale)); 83 | } 84 | 85 | 86 | private static void DrawPluginSettingsTab() 87 | { 88 | // ScreenMark 89 | ImGuiEx.Checkbox( 90 | "Enable marking detected objects on screen", ref Plugin.Config.ShowScreenMark, 91 | "If enabled, will allow Compasses to mark objects detected by them on screen," + 92 | "showing the direction and distance.\n\n" + 93 | "You can configure this for each compass separately below."); 94 | if (Plugin.Config.ShowScreenMark) 95 | { 96 | ImGui.TreePush(); 97 | ImGuiEx.DragFloat("Marker size scale", 100, ref Plugin.Config.ScreenMarkSizeScale, 98 | .01f, PluginConfig.ScreenMarkSizeBound.Min, PluginConfig.ScreenMarkSizeBound.Max); 99 | ImGuiEx.DragFloat("Marker text size scale", 100, ref Plugin.Config.ScreenMarkTextRelSizeScale, 100 | .1f, PluginConfig.ScreenMarkTextRelSizeBound.Min, PluginConfig.ScreenMarkTextRelSizeBound.Max, "%.1f", 101 | tooltip: "Set the size scale for the markers' label text, relative to the marker size"); 102 | var viewport = ImGui.GetMainViewport().Pos; 103 | var vsize = ImGui.GetMainViewport().Size; 104 | Vector4 displayArea = GetDisplayAreaFromConfigScreenMarkConstraint(); 105 | ImGuiEx.DragFloat4("Marker display area (Left/Bottom/Right/Top)", ref displayArea, 106 | 1, PluginConfig.ScreenMarkConstraintMin, 9999, v_fmt: "%.0f", 107 | tooltip: "Set the display area for the markers.\n\n" + 108 | "The display area is shown as the red rectangle on the screen while configuration window is open.\n" + 109 | "Detected objects will be marked on screen within this area."); 110 | Plugin.Config.ScreenMarkConstraint = new( 111 | displayArea.X - viewport.X, // L 112 | viewport.Y + vsize.Y - displayArea.Y, // D 113 | viewport.X + vsize.X - displayArea.Z, // R 114 | displayArea.W - viewport.Y); // U 115 | ImGui.Text($"(* The full screen display area is " + 116 | $"<{viewport.X:0}, {viewport.Y + vsize.Y:0}, {viewport.X + vsize.X:0}, {viewport.Y:0}> )"); 117 | ImGuiEx.Checkbox("Hide marker when nameplate is inside the marker display area", 118 | ref Plugin.Config.HideScreenMarkIfNameplateInsideDisplayArea, 119 | "If enabled, markers will be hidden if the nameplate is inside the marker display area as set above\n" + 120 | "AND if the nameplate is also of certain size.\n\n" + 121 | "NOTE: if these two conditions are met but the nameplate is also hidden from view " + 122 | "due to it being behind other objects (e.g. a wall), \n" + 123 | "the plugin will not know this and will continue to hide the markers."); 124 | if (Plugin.Config.HideScreenMarkIfNameplateInsideDisplayArea) 125 | { 126 | ImGui.TreePush(); 127 | ImGuiEx.DragInt("Hide only when object is within", Language.Unit.Yalm, 128 | 50, ref Plugin.Config.HideScreenMarkEnabledDistance, 1, 129 | PluginConfig.HideScreenMarkEnabledDistanceBound.Min, 130 | PluginConfig.HideScreenMarkEnabledDistanceBound.Max, 131 | "Markers will be hidden only when the object is also\n" + 132 | "within this distance from player's character."); 133 | ImGui.TreePop(); 134 | } 135 | ImGui.TreePop(); 136 | } 137 | ImGui.NewLine(); 138 | 139 | // DetailWindow 140 | ImGuiEx.Checkbox("Show detected objects' details", ref Plugin.Config.ShowDetailWindow, 141 | "If enabled, will show a window listing details of detected objects.\n\n" + 142 | "You can configure this for each compass separately below."); 143 | if (Plugin.Config.ShowDetailWindow) 144 | { 145 | ImGui.TreePush(); 146 | ImGuiEx.Checkbox("Don't show in instanced contents", ref Plugin.Config.HideDetailInContents, 147 | "If enabled, will auto hide the detail window in instance contents such as dungeons, trials and raids."); 148 | ImGui.TreePop(); 149 | } 150 | ImGui.NewLine(); 151 | 152 | // Hiding options 153 | if (Plugin.Config.ShowScreenMark || Plugin.Config.ShowDetailWindow) 154 | { 155 | ImGuiEx.Checkbox("Hide compass UI when in event", ref Plugin.Config.HideInEvent, 156 | "If enabled, will auto hide both markers on screen and the detail window in certain conditions\n" + 157 | "such as in event, in cutscene and when using summoning bells"); 158 | ImGuiEx.Checkbox("Hide compass UI when crafting/gathering/fishing", ref Plugin.Config.HideWhenCraftGather); 159 | } 160 | ImGui.NewLine(); 161 | 162 | // Norification 163 | ImGuiEx.Checkbox("Enable chat notification", ref Plugin.Config.NotifyChat, 164 | "If enabled, will allow compasses to send notifications in game chat when detected an object.\n\n" + 165 | "You can configure this for each compass separately below. "); 166 | if (Plugin.Config.NotifyChat) 167 | { 168 | ImGui.TreePush(); 169 | ImGuiEx.Checkbox("Also enable sound notification", ref Plugin.Config.NotifySe, 170 | "If enabled, will allow compasses to make sound notification alongside chat notification.\n\n" + 171 | "You can configure this for each compass separately below."); 172 | ImGui.TreePop(); 173 | } 174 | ImGuiEx.Checkbox("Enable Toast notification", ref Plugin.Config.NotifyToast, 175 | "If enabled, will allow compasses to make Toast notifications on screen when detected an object.\n\n" + 176 | "You can configure this for each compass separately below."); 177 | ImGui.NewLine(); 178 | 179 | #if DEBUG 180 | // Debug 181 | ImGuiEx.Checkbox("[DEBUG] Test all GameObjects", ref Plugin.Config.DebugTestAllGameObjects); 182 | ImGui.NewLine(); 183 | #endif 184 | 185 | ImGui.Checkbox("Show Sponsor/Support button", ref Plugin.Config.ShowSponsor); 186 | if (Plugin.Config.ShowSponsor) 187 | { 188 | ImGui.Indent(); 189 | ImGui.PushStyleColor(ImGuiCol.Button, 0xFF000000 | 0x005E5BFF); 190 | ImGui.PushStyleColor(ImGuiCol.ButtonActive, 0xDD000000 | 0x005E5BFF); 191 | ImGui.PushStyleColor(ImGuiCol.ButtonHovered, 0xAA000000 | 0x005E5BFF); 192 | if (ImGuiEx.Button("Buy Yomishino a Coffee", 193 | "You can support me and buy me a coffee if you want.\n" + 194 | "(Will open external link to Ko-fi in your browser)")) 195 | { 196 | Process.Start(new ProcessStartInfo 197 | { 198 | FileName = "https://ko-fi.com/yomishino", 199 | UseShellExecute = true 200 | }); 201 | } 202 | ImGui.PopStyleColor(3); 203 | ImGui.Unindent(); 204 | } 205 | ImGui.NewLine(); 206 | } 207 | 208 | private static Vector4 GetDisplayAreaFromConfigScreenMarkConstraint() 209 | { 210 | var viewport = ImGui.GetMainViewport().Pos; 211 | var vsize = ImGui.GetMainViewport().Size; 212 | return new( 213 | viewport.X + Plugin.Config.ScreenMarkConstraint.X, // L 214 | viewport.Y + vsize.Y - Plugin.Config.ScreenMarkConstraint.Y, // D 215 | viewport.X + vsize.X - Plugin.Config.ScreenMarkConstraint.Z, // R 216 | viewport.Y + Plugin.Config.ScreenMarkConstraint.W); // U 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Compasses/GatheringPointCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Objectives; 4 | using AetherCompass.Game; 5 | using AetherCompass.Compasses.Configs; 6 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 7 | using ImGuiNET; 8 | using Lumina.Excel; 9 | using System.Numerics; 10 | 11 | 12 | namespace AetherCompass.Compasses 13 | { 14 | [CompassType(CompassType.Standard)] 15 | public class GatheringPointCompass : Compass 16 | { 17 | public override string CompassName => "Gathering Point Compass"; 18 | public override string Description => "Detecting nearby gathering points"; 19 | 20 | private protected override CompassConfig CompassConfig 21 | => Plugin.Config.GatheringConfig; 22 | private GatheringPointCompassConfig GatheringConfig 23 | => (GatheringPointCompassConfig)CompassConfig; 24 | 25 | private static readonly Vector4 infoTextColour = new(.55f, .98f, 1, 1); 26 | private static readonly float infoTextShadowLightness = .1f; 27 | 28 | 29 | public override bool IsEnabledInCurrentTerritory() 30 | => ZoneWatcher.CurrentTerritoryType?.TerritoryIntendedUse == 1 31 | || ZoneWatcher.CurrentTerritoryType?.TerritoryIntendedUse == 47 // diadem 32 | ; 33 | 34 | public override unsafe bool IsObjective(GameObject* o) 35 | => o != null && o->ObjectKind == (byte)ObjectKind.GatheringPoint; 36 | 37 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 38 | => objective.IsEmpty() ? null : new(() => 39 | { 40 | ImGui.Text($"{GetGatheringLevelDescription(objective.DataId)} {objective.Name}"); 41 | ImGui.BulletText($"{CompassUtil.MapCoordToFormattedString(objective.CurrentMapCoord)} (approx.)"); 42 | ImGui.BulletText($"{objective.CompassDirectionFromPlayer}, " + 43 | $"{CompassUtil.DistanceToDescriptiveString(objective.Distance3D, false)}"); 44 | ImGui.BulletText(CompassUtil.AltitudeDiffToDescriptiveString(objective.AltitudeDiff)); 45 | DrawFlagButton($"##{(long)objective.GameObject}", objective.CurrentMapCoord); 46 | ImGui.Separator(); 47 | }); 48 | 49 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 50 | { 51 | if (objective.IsEmpty()) return null; 52 | var iconId = GetGatheringPointIconId(objective.DataId); 53 | string descr = $"{GetGatheringLevelDescription(objective.DataId)} {objective.Name}, " + 54 | $"{CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}"; 55 | return GenerateDefaultScreenMarkerDrawAction(objective, iconId, 56 | DefaultMarkerIconSize, .9f, descr, infoTextColour, 57 | infoTextShadowLightness, out _, important: false); 58 | } 59 | 60 | private protected override unsafe string GetClosestObjectiveDescription(CachedCompassObjective objective) 61 | => objective.Name; 62 | 63 | 64 | private static readonly ExcelSheet? GatheringPointSheet 65 | = Plugin.DataManager.GetExcelSheet(); 66 | 67 | private static readonly ExcelSheet? GatheringPointBaseSheet 68 | = Plugin.DataManager.GetExcelSheet(); 69 | 70 | private static readonly ExcelSheet? GatheringTypeSheet 71 | = Plugin.DataManager.GetExcelSheet(); 72 | 73 | 74 | // True for those that use special icon; 75 | private static bool IsSpecialGatheringPointType(GatheringPointType type) 76 | => type == GatheringPointType.Unspoiled 77 | || type == GatheringPointType.Folklore 78 | || type == GatheringPointType.SFShadow 79 | || type == GatheringPointType.DiademClouded; 80 | 81 | // gatheringPointType is GatheringPoint.Type, not GatheringType sheet rows 82 | private static bool IsSpecialGatheringPointType(byte gatheringPointType) 83 | => IsSpecialGatheringPointType((GatheringPointType)gatheringPointType); 84 | 85 | private static bool IsSpecialGatheringPoint(uint dataId) 86 | { 87 | var gatherPointData = GatheringPointSheet?.GetRow(dataId); 88 | if (gatherPointData == null) return false; 89 | return IsSpecialGatheringPointType(gatherPointData.Type); 90 | } 91 | 92 | 93 | private static uint GetGatheringPointIconId( 94 | byte gatheringType, GatheringPointType gatheringPointType) 95 | { 96 | var typeRow = GatheringTypeSheet?.GetRow(gatheringType); 97 | if (typeRow == null) return 0; 98 | return (uint)(IsSpecialGatheringPointType(gatheringPointType) 99 | ? typeRow.IconOff : typeRow.IconMain); 100 | } 101 | 102 | private static uint GetGatheringPointIconId(uint dataId) 103 | { 104 | var gatherType = GatheringPointSheet?.GetRow(dataId)? 105 | .GatheringPointBase.Value?.GatheringType.Value; 106 | if (gatherType == null) return 0; 107 | return (uint)(IsSpecialGatheringPoint(dataId) 108 | ? gatherType.IconOff : gatherType.IconMain); 109 | } 110 | 111 | private static byte GetGatheringLevel(uint dataId) 112 | { 113 | var gatherPointBase = GatheringPointSheet?.GetRow(dataId)?.GatheringPointBase.Value; 114 | return gatherPointBase == null ? byte.MinValue : gatherPointBase.GatheringLevel; 115 | } 116 | 117 | private static string GetGatheringLevelDescription(uint dataId) 118 | { 119 | var lv = GetGatheringLevel(dataId); 120 | // Lv? in diadem etc. is Lv1 in excel 121 | return lv > 1 ? $"Lv{lv}" : $"Lv??"; 122 | } 123 | 124 | #region FUTURE/ ExportedGatheringPoint 125 | #if FUTURE 126 | private IEnumerable? exportedGatheringPointRowIds; 127 | 128 | private static ExcelSheet? ExportedGatheringPointSheet 129 | => Plugin.DataManager.GetExcelSheet(); 130 | 131 | private static ExcelSheet? GatheringPointNameSheet 132 | => Plugin.DataManager.GetExcelSheet(); 133 | 134 | 135 | private void UpdateTerritoryExportedGatheringPointsOnTerrInit(ushort terr) 136 | { 137 | exportedGatheringPointRowIds = GatheringPointSheet?.Where(p => p.TerritoryType.Row == terr) 138 | .Select(p => p.GatheringPointBase.Row); 139 | } 140 | 141 | private void DrawUiForTerritoryExportedGatheringPoints() 142 | { 143 | if (exportedGatheringPointRowIds == null) return; 144 | if (GatheringPointBaseSheet == null || ExportedGatheringPointSheet == null) return; 145 | foreach (var rowId in exportedGatheringPointRowIds) 146 | { 147 | var gatherPointBase = GatheringPointBaseSheet.GetRow(rowId); 148 | var exported = ExportedGatheringPointSheet.GetRow(rowId); 149 | if (gatherPointBase == null || exported == null || exported.GatheringPointType == 0) continue; 150 | var pos = new Vector3(exported.X, exported.Y, 0); 151 | var name = $"(Nodes Group: Lv{gatherPointBase!.GatheringLevel} " 152 | + InferGatheringPointName(exported.GatheringType, (GatheringPointType)exported.GatheringPointType) 153 | + ")"; 154 | var dist = CompassUtil.Get3DDistanceFromPlayer(pos); 155 | if (MarkScreen) 156 | overlay.AddDrawAction(new(() => 157 | { 158 | var icon = IconManager.GetGatheringMarkerIcon(GetGatheringPointIconId(exported.GatheringType, (GatheringPointType)exported.GatheringPointType)); 159 | string descr = $"{name}, {CompassUtil.DistanceToDescriptiveString(dist, true)}"; 160 | DrawScreenMarkerDefault(pos, 0, icon, IconManager.MarkerIconSize, .9f, descr, infoTextColour, infoTextShadowLightness, out _); 161 | }, dist < 200)); 162 | if (ShowDetail) 163 | detailsWindow.AddDrawAction(this, new(() => 164 | { 165 | ImGui.Text(name); 166 | ImGui.BulletText($"{CompassUtil.GetMapCoordInCurrentMapFormattedString(pos, false)} (approx.)"); 167 | ImGui.BulletText($"{CompassUtil.GetDirectionFromPlayer(pos)}, " + 168 | $"{CompassUtil.DistanceToDescriptiveString(dist, false)}"); 169 | ImGui.BulletText(CompassUtil.GetAltitudeDiffFromPlayerDescriptive(pos)); 170 | DrawFlagButton($"##NodesGroup_{rowId}", CompassUtil.GetMapCoordInCurrentMap(pos)); 171 | ImGui.Separator(); 172 | }, dist < 200)); 173 | } 174 | } 175 | 176 | private static string InferGatheringPointName(byte gatheringType, GatheringPointType gatheringPointType) 177 | => GatheringPointNameSheet?.GetRow( 178 | gatheringType switch 179 | { 180 | // mining 181 | 0 => gatheringPointType switch 182 | { 183 | GatheringPointType.Unspoiled => 5, 184 | GatheringPointType.Ephemeral => 9, // TODO: aetherial reduction one still this name? 185 | GatheringPointType.Folklore => 13, 186 | GatheringPointType.DiademClouded => 23, 187 | _ => 1 188 | }, 189 | // quarrying 190 | 1 => gatheringPointType switch 191 | { 192 | GatheringPointType.Unspoiled => 6, 193 | GatheringPointType.Ephemeral => 10, 194 | GatheringPointType.Folklore => 14, 195 | GatheringPointType.DiademClouded => 24, 196 | _ => 2 197 | }, 198 | // logging 199 | 2 => gatheringPointType switch 200 | { 201 | GatheringPointType.Unspoiled => 7, 202 | GatheringPointType.Ephemeral => 11, 203 | GatheringPointType.Folklore => 15, 204 | GatheringPointType.DiademClouded => 25, 205 | _ => 3 206 | }, 207 | // harvesting 208 | 3 => gatheringPointType switch 209 | { 210 | GatheringPointType.Unspoiled => 8, 211 | GatheringPointType.Ephemeral => 12, 212 | GatheringPointType.Folklore => 16, 213 | GatheringPointType.DiademClouded => 26, 214 | _ => 4 215 | }, 216 | // spearfishing 217 | 4 => gatheringPointType switch 218 | { 219 | GatheringPointType.SFShadow => 22, 220 | _ => 21 221 | }, 222 | _ => 0, 223 | })?.Singular ?? string.Empty; 224 | #endif 225 | #endregion 226 | 227 | } 228 | 229 | 230 | // mapping GatheringPoint.Type column, not GatheringType sheet rows 231 | public enum GatheringPointType: byte 232 | { 233 | None, 234 | Basic, 235 | Unspoiled, 236 | Leve, 237 | Ephemeral, // for aetherial reduction, 238 | Folklore, 239 | SFShadow, // spearfishing special 240 | DiademBasic, 241 | DiademClouded, // diadem special 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/PluginConfig.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Compasses.Configs; 3 | using Dalamud.Configuration; 4 | using Newtonsoft.Json; 5 | using System.Numerics; 6 | using System.IO; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace AetherCompass 10 | { 11 | [Serializable] 12 | public class PluginConfig : IPluginConfiguration 13 | { 14 | [JsonIgnore] 15 | public const int ActiveVersion = 2; // !! 16 | 17 | public int Version { get; set; } = ActiveVersion; 18 | 19 | public bool Enabled = true; 20 | public bool ShowScreenMark = true; 21 | public float ScreenMarkSizeScale = 1; 22 | public float ScreenMarkTextRelSizeScale = 1; 23 | [JsonIgnore] 24 | public static readonly (float Min, float Max) ScreenMarkSizeBound = (.1f, 10); 25 | [JsonIgnore] 26 | public static readonly (float Min, float Max) ScreenMarkTextRelSizeBound = (.5f, 2); 27 | // L,D,R,U; how much to squeeze into centre on each side, so generally should be positive 28 | public Vector4 ScreenMarkConstraint = new(80, 80, 80, 80); 29 | [JsonIgnore] 30 | public const float ScreenMarkConstraintMin = 2; 31 | public bool HideScreenMarkIfNameplateInsideDisplayArea = false; 32 | public int HideScreenMarkEnabledDistance = 30; 33 | [JsonIgnore] 34 | public static readonly (int Min, int Max) HideScreenMarkEnabledDistanceBound = (5, 50); 35 | public bool ShowDetailWindow = false; 36 | public bool HideDetailInContents = false; 37 | public bool HideInEvent = false; 38 | public bool HideWhenCraftGather = false; 39 | public bool NotifyChat = false; 40 | public bool NotifySe = false; 41 | public bool NotifyToast = false; 42 | 43 | public bool ShowSponsor = false; 44 | 45 | public AetherCurrentCompassConfig AetherCurrentConfig { get; private set; } = new(); 46 | public MobHuntCompassConfig MobHuntConfig { get; private set; } = new(); 47 | public GatheringPointCompassConfig GatheringConfig { get; private set; } = new(); 48 | public IslandSanctuaryCompassConfig IslandConfig { get; private set; } = new(); 49 | public QuestCompassConfig QuestConfig { get; private set; } = new(); 50 | public EurekanCompassConfig EurekanConfig { get; private set; } = new(); 51 | 52 | #if DEBUG 53 | [JsonIgnore] 54 | public bool DebugTestAllGameObjects = false; 55 | #endif 56 | [JsonIgnore] 57 | public DebugCompassConfig DebugConfig { get; private set; } = new(); 58 | 59 | public void CheckValueValidity(Vector2 screenSize) 60 | { 61 | ScreenMarkSizeScale = MathUtil.Clamp(ScreenMarkSizeScale, 62 | ScreenMarkSizeBound.Min, ScreenMarkSizeBound.Max); 63 | ScreenMarkTextRelSizeScale = MathUtil.Clamp(ScreenMarkTextRelSizeScale, 64 | ScreenMarkTextRelSizeBound.Min, ScreenMarkTextRelSizeBound.Max); 65 | 66 | ScreenMarkConstraint.X = MathUtil.Clamp(ScreenMarkConstraint.X, 67 | ScreenMarkConstraintMin, screenSize.X / 2 - 10); 68 | ScreenMarkConstraint.Y = MathUtil.Clamp(ScreenMarkConstraint.Y, 69 | ScreenMarkConstraintMin, screenSize.Y / 2 - 10); 70 | ScreenMarkConstraint.Z = MathUtil.Clamp(ScreenMarkConstraint.Z, 71 | ScreenMarkConstraintMin, screenSize.X / 2 - 10); 72 | ScreenMarkConstraint.W = MathUtil.Clamp(ScreenMarkConstraint.W, 73 | ScreenMarkConstraintMin, screenSize.Y / 2 - 10); 74 | 75 | HideScreenMarkEnabledDistance 76 | = (int)MathUtil.Clamp(HideScreenMarkEnabledDistance, 77 | HideScreenMarkEnabledDistanceBound.Min, 78 | HideScreenMarkEnabledDistanceBound.Max); 79 | 80 | AetherCurrentConfig.CheckValueValidity(); 81 | MobHuntConfig.CheckValueValidity(); 82 | GatheringConfig.CheckValueValidity(); 83 | IslandConfig.CheckValueValidity(); 84 | QuestConfig.CheckValueValidity(); 85 | EurekanConfig.CheckValueValidity(); 86 | #if DEBUG 87 | DebugConfig.CheckValueValidity(); 88 | #endif 89 | } 90 | 91 | private void LoadValues(PluginConfig config) 92 | { 93 | Version = ActiveVersion; 94 | 95 | Enabled = config.Enabled; 96 | ShowScreenMark = config.ShowScreenMark; 97 | ScreenMarkSizeScale = config.ScreenMarkSizeScale; 98 | ScreenMarkConstraint = config.ScreenMarkConstraint; 99 | ShowDetailWindow = config.ShowDetailWindow; 100 | HideDetailInContents = config.HideDetailInContents; 101 | HideInEvent = config.HideInEvent; 102 | HideWhenCraftGather = config.HideWhenCraftGather; 103 | NotifyChat = config.NotifyChat; 104 | NotifySe = config.NotifySe; 105 | NotifyToast = config.NotifyToast; 106 | HideScreenMarkEnabledDistance = config.HideScreenMarkEnabledDistance; 107 | HideScreenMarkIfNameplateInsideDisplayArea = config.HideScreenMarkIfNameplateInsideDisplayArea; 108 | 109 | AetherCurrentConfig.Load(config.AetherCurrentConfig); 110 | MobHuntConfig.Load(config.MobHuntConfig); 111 | GatheringConfig.Load(config.GatheringConfig); 112 | IslandConfig.Load(config.IslandConfig); 113 | QuestConfig.Load(config.QuestConfig); 114 | EurekanConfig.Load(config.EurekanConfig); 115 | } 116 | 117 | public void Save() 118 | { 119 | Plugin.PluginInterface.SavePluginConfig(this); 120 | } 121 | 122 | public void Load() 123 | => Load(PluginConfigHelper.GetSavedPluginConfig()); 124 | 125 | public void Load(PluginConfig config) 126 | { 127 | var versionMatched = PreloadCheck(config, out var checkedConfig); 128 | if (!versionMatched) 129 | PluginConfigHelper.BackupSavedPluginConfig(); 130 | LoadValues(checkedConfig); 131 | if (!versionMatched) Save(); 132 | } 133 | 134 | private static bool PreloadCheck(PluginConfig config, 135 | out PluginConfig checkedConfig) 136 | { 137 | if (ActiveVersion == config.Version) 138 | { 139 | LogDebug("Config version matched. Using saved config."); 140 | checkedConfig = config; 141 | return true; 142 | } 143 | else 144 | { 145 | var restored = PluginConfigHelper.RestoreBackupConfig( 146 | PluginConfigHelper.FindMatchingConfigBackup( 147 | ActiveVersion, GetPluginVersionAsString())); 148 | if (restored == null) 149 | { 150 | LogWarning("Config version not matched and no backup found. " 151 | + " Load saved config anyway."); 152 | checkedConfig = config; 153 | } 154 | else 155 | { 156 | LogWarning("Config version not matched. " 157 | + "Trying to restore from backup."); 158 | checkedConfig = restored; 159 | } 160 | return false; 161 | } 162 | } 163 | 164 | 165 | private static class PluginConfigHelper 166 | { 167 | const string ConfBkpFolderName = "confbkp"; 168 | const string ConfBkpFilenamePart1 = "conf_"; 169 | const string ConfBkpFileExt = ".json"; 170 | const string confBkpPatternConfigVerKey = "cver"; 171 | const string confBkpPatternPluginVerKey = "pver"; 172 | const string ConfBkpFilenamePattern 173 | = @ConfBkpFilenamePart1 174 | + @$"c(?<{confBkpPatternConfigVerKey}>[0-9]+)_" 175 | + @$"v(?<{confBkpPatternPluginVerKey}>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)" 176 | + @"\" + @ConfBkpFileExt; 177 | const string ConfBkpFileFullpathPattern 178 | = @".+[/\\]" + ConfBkpFilenamePattern; 179 | 180 | private static readonly Regex BackupConfigFullpathMatcher 181 | = new(ConfBkpFileFullpathPattern); 182 | 183 | private static string BackupConfigDirectoryPath 184 | => Path.Combine( 185 | Plugin.PluginInterface.GetPluginConfigDirectory(), 186 | ConfBkpFolderName); 187 | 188 | private static string GenerateConfBkpFileName(PluginConfig config) 189 | => ConfBkpFilenamePart1 190 | + $"c{config.Version}_v{GetPluginVersionAsString()}" 191 | + ConfBkpFileExt; 192 | 193 | internal static PluginConfig GetSavedPluginConfig() 194 | => GetSavedPluginConfigIfAny() ?? new(); 195 | 196 | internal static PluginConfig? GetSavedPluginConfigIfAny() 197 | => Plugin.PluginInterface.GetPluginConfig() as PluginConfig; 198 | 199 | internal static string GetSavedPluginConfigPath() 200 | => Plugin.PluginInterface.ConfigFile.FullName; 201 | 202 | internal static void BackupSavedPluginConfig() 203 | { 204 | try 205 | { 206 | var config = GetSavedPluginConfigIfAny(); 207 | if (config == null) 208 | { 209 | LogDebug("Ending config back up because no valid saved config is found"); 210 | return; 211 | } 212 | 213 | var dirpath = BackupConfigDirectoryPath; 214 | var dir = Directory.CreateDirectory(dirpath); 215 | 216 | var bkpFilename = GenerateConfBkpFileName(config); 217 | var bkpFilePath = Path.Combine(dirpath, bkpFilename); 218 | File.Copy(GetSavedPluginConfigPath(), bkpFilePath, true); 219 | LogInfo($"Created config back up at: {bkpFilePath}"); 220 | } 221 | catch (Exception e) 222 | { 223 | LogError("Failed to back up plugin config.\n" + e); 224 | } 225 | } 226 | 227 | internal static PluginConfig? RestoreBackupConfig(string? fullpath) 228 | { 229 | if (string.IsNullOrEmpty(fullpath)) return null; 230 | 231 | if (!File.Exists(fullpath)) 232 | { 233 | LogError($"Failed to restore backup config. " 234 | + $"File does not exist: {fullpath ?? ""}"); 235 | return null; 236 | } 237 | 238 | try 239 | { 240 | var contents = File.ReadAllText(fullpath); 241 | var restoredConfig 242 | = JsonConvert.DeserializeObject(contents); 243 | LogInfo($"Config restored from {fullpath}"); 244 | return restoredConfig; 245 | } 246 | catch (Exception e) 247 | { 248 | LogError("Failed to restore plugin config.\n" + e); 249 | } 250 | return null; 251 | } 252 | 253 | internal static string? FindMatchingConfigBackup( 254 | int desiredConfigVersion, string desiredPluginVersion) 255 | { 256 | var dirpath = BackupConfigDirectoryPath; 257 | if (!Directory.Exists(dirpath)) return null; 258 | (string Path, int CVer, string PVer) match = ("", -1, "0.0.0.0"); 259 | try 260 | { 261 | var filepaths = Directory.GetFiles(dirpath); 262 | foreach (var filepath in filepaths) 263 | { 264 | if (!BackupConfigFullpathMatcher.IsMatch(filepath)) 265 | continue; 266 | var groups = BackupConfigFullpathMatcher.Match(filepath).Groups; 267 | var bkpConfigVer 268 | = groups.TryGetValue(confBkpPatternConfigVerKey, out var g1) 269 | ? int.Parse(g1.Value) : -1; 270 | var bkpPluginVer 271 | = groups.TryGetValue(confBkpPatternPluginVerKey, out var g2) 272 | ? g2.Value : "0.0.0.0"; 273 | if (desiredConfigVersion != bkpConfigVer) continue; 274 | if (desiredPluginVersion == bkpPluginVer) 275 | { 276 | match = (filepath, bkpConfigVer, bkpPluginVer); 277 | } 278 | else if (ComparePluginVersion(bkpPluginVer, match.PVer) > 0 279 | && ComparePluginVersion(bkpPluginVer, desiredPluginVersion) <= 0) 280 | { 281 | match = (filepath, bkpConfigVer, bkpPluginVer); 282 | } 283 | if (match.CVer == desiredConfigVersion 284 | && match.PVer == desiredPluginVersion) 285 | return match.Path; // exact match 286 | } 287 | } 288 | catch (Exception e) 289 | { 290 | LogError("Failed when finding matching config backup.\n" + e); 291 | } 292 | if (!string.IsNullOrEmpty(match.Path)) 293 | return match.Path; 294 | return null; 295 | } 296 | } 297 | 298 | } 299 | 300 | 301 | } 302 | -------------------------------------------------------------------------------- /src/Compasses/IslandSanctuaryCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Configs; 4 | using AetherCompass.Compasses.Objectives; 5 | using AetherCompass.Game; 6 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 7 | using FFXIVClientStructs.FFXIV.Client.UI; 8 | using ImGuiNET; 9 | using Lumina.Excel; 10 | 11 | 12 | namespace AetherCompass.Compasses 13 | { 14 | [CompassType(CompassType.Experimental)] 15 | public class IslandSanctuaryCompass : Compass 16 | { 17 | public override string CompassName => "Island Sanctuary Compass"; 18 | public override string Description => 19 | "Detecting nearby gathering objects and animals in Island Sanctuary"; 20 | 21 | private protected override CompassConfig CompassConfig => Plugin.Config.IslandConfig; 22 | private IslandSanctuaryCompassConfig IslandConfig => (IslandSanctuaryCompassConfig)CompassConfig; 23 | 24 | private readonly Dictionary 25 | islandGatherDict = new(); // npcId => data 26 | private readonly List 27 | islandGatherList = new(); // ordered by row id 28 | private readonly Dictionary 29 | islandAnimalDict = new(); // dataId => data 30 | private readonly List 31 | islandAnimalList = new(); // ordered by row id 32 | 33 | private static readonly System.Numerics.Vector4 infoTextColourGather 34 | = new(.75f, .98f, .9f, 1); 35 | private static readonly System.Numerics.Vector4 infoTextColourAnimal 36 | = new(.98f, .8f, .85f, 1); 37 | private static readonly float infoTextShadowLightness = .1f; 38 | 39 | private const uint animalDefaultMarkerIconId = 63956; 40 | private static readonly System.Numerics.Vector2 41 | animalSpecificMarkerIconSize = new(25, 25); 42 | 43 | 44 | // TerritoryType RowId = 1055; TerritoryIntendedUse = 49 45 | public override bool IsEnabledInCurrentTerritory() 46 | => ZoneWatcher.CurrentTerritoryType?.TerritoryIntendedUse == 49; 47 | 48 | public override unsafe bool IsObjective(GameObject* o) 49 | { 50 | if (o == null) return false; 51 | if (IslandConfig.DetectGathering && o->ObjectKind == (byte)ObjectKind.MjiObject) 52 | return islandGatherDict.TryGetValue(o->GetNpcID(), out var data) 53 | && (IslandConfig.GatheringObjectsToShow & (1u << (int)data.SheetRowId)) != 0; 54 | if (IslandConfig.DetectAnimals && o->ObjectKind == (byte)ObjectKind.BattleNpc) 55 | return islandAnimalDict.TryGetValue(o->DataID, out var data) 56 | && (IslandConfig.AnimalsToShow & (1u << (int)data.SheetRowId)) != 0; 57 | return false; 58 | } 59 | 60 | protected override unsafe CachedCompassObjective 61 | CreateCompassObjective(GameObject* obj) 62 | { 63 | if (obj == null) 64 | return new IslandCachedCompassObjective(obj, 0); 65 | return obj->ObjectKind switch 66 | { 67 | (byte)ObjectKind.MjiObject => 68 | new IslandCachedCompassObjective(obj, IslandObjectType.Gathering), 69 | (byte)ObjectKind.BattleNpc => 70 | new IslandCachedCompassObjective(obj, IslandObjectType.Animal), 71 | _ => new IslandCachedCompassObjective(obj, 0), 72 | }; 73 | } 74 | 75 | protected override unsafe CachedCompassObjective 76 | CreateCompassObjective(UI3DModule.ObjectInfo* info) 77 | => CreateCompassObjective(info != null ? info->GameObject : null); 78 | //{ 79 | // var obj = info != null ? info->GameObject : null; 80 | // if (obj == null) return new IslandCachedCompassObjective(obj, 0); 81 | // return obj->ObjectKind switch 82 | // { 83 | // (byte)ObjectKind.MjiObject => 84 | // new IslandCachedCompassObjective(info, IslandObjectType.Gathering), 85 | // (byte)ObjectKind.BattleNpc => 86 | // new IslandCachedCompassObjective(info, IslandObjectType.Animal), 87 | // _ => new IslandCachedCompassObjective(info, 0), 88 | // }; 89 | //} 90 | 91 | private protected override unsafe string 92 | GetClosestObjectiveDescription(CachedCompassObjective objective) 93 | => objective.Name; 94 | 95 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 96 | => objective.IsEmpty() || objective is not IslandCachedCompassObjective islObjective 97 | ? null : new(() => 98 | { 99 | if (islObjective.Type == IslandObjectType.Gathering) 100 | ImGui.Text($"{islObjective.Name}, Type: {islObjective.Type}" + 101 | $" - {GetIslandGatherType(islObjective)}"); 102 | else ImGui.Text($"{islObjective.Name}, Type: {islObjective.Type}"); 103 | ImGui.BulletText($"{CompassUtil.MapCoordToFormattedString(islObjective.CurrentMapCoord)} (approx.)"); 104 | ImGui.BulletText($"{islObjective.CompassDirectionFromPlayer}, " + 105 | $"{CompassUtil.DistanceToDescriptiveString(islObjective.Distance3D, false)}"); 106 | ImGui.BulletText(CompassUtil.AltitudeDiffToDescriptiveString(islObjective.AltitudeDiff)); 107 | DrawFlagButton($"##{(long)islObjective.GameObject}", islObjective.CurrentMapCoord); 108 | ImGui.Separator(); 109 | }); 110 | 111 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 112 | { 113 | if (objective.IsEmpty() || objective is not IslandCachedCompassObjective islObjective) return null; 114 | var iconId = GetMarkerIconId(islObjective); 115 | var iconSize = islObjective.Type == IslandObjectType.Animal 116 | && IslandConfig.UseAnimalSpecificIcons 117 | ? animalSpecificMarkerIconSize : DefaultMarkerIconSize; 118 | var textColour = islObjective.Type == IslandObjectType.Gathering 119 | ? infoTextColourGather : infoTextColourAnimal; 120 | string descr = islObjective.Type switch 121 | { 122 | IslandObjectType.Animal => IslandConfig.ShowNameOnMarkerAnimals 123 | ? $"{objective.Name}, " : "", 124 | IslandObjectType.Gathering => IslandConfig.ShowNameOnMarkerGathering 125 | ? $"{objective.Name}, ": "", 126 | _ => "", 127 | } + $"{CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}"; 128 | var showIfOutOfScreen = islObjective.Type switch 129 | { 130 | IslandObjectType.Animal => !IslandConfig.HideMarkerWhenNotInScreenAnimals, 131 | IslandObjectType.Gathering => !IslandConfig.HideMarkerWhenNotInScreenGathering, 132 | _ => false 133 | }; 134 | return GenerateDefaultScreenMarkerDrawAction(objective, iconId, iconSize, 135 | .9f, descr, textColour, infoTextShadowLightness, out _, 136 | important: false, showIfOutOfScreen: showIfOutOfScreen); 137 | } 138 | 139 | public override void DrawConfigUiExtra() 140 | { 141 | ImGui.BulletText("More options:"); 142 | ImGui.Indent(); 143 | ImGui.Checkbox("Detect Gathering Objects", ref IslandConfig.DetectGathering); 144 | if (IslandConfig.DetectGathering) 145 | { 146 | ImGui.TreePush(); 147 | ImGui.Checkbox("Show gathering object names on the markers", 148 | ref IslandConfig.ShowNameOnMarkerGathering); 149 | ImGui.Checkbox("Hide markers for gathering objects that are out of screen", 150 | ref IslandConfig.HideMarkerWhenNotInScreenGathering); 151 | if(ImGui.CollapsingHeader("Detect only the following objects ...")) 152 | { 153 | ImGui.TreePush("DetectGatheringObjectFilterExpand"); 154 | if (ImGui.Button("Select all")) 155 | IslandConfig.GatheringObjectsToShow = uint.MaxValue; 156 | ImGui.SameLine(); 157 | if (ImGui.Button("Unselect all")) 158 | IslandConfig.GatheringObjectsToShow = uint.MinValue; 159 | if (ImGui.BeginListBox("##DetectGatheringObjectFilter")) 160 | { 161 | for (int i = 0; i < islandGatherList.Count; i++) 162 | { 163 | var data = islandGatherList[i]; 164 | if (data.NpcId == 0) continue; 165 | var flagval = 1u << i; 166 | ImGui.CheckboxFlags(data.Name, 167 | ref IslandConfig.GatheringObjectsToShow, flagval); 168 | } 169 | ImGui.EndListBox(); 170 | } 171 | ImGui.TreePop(); 172 | } 173 | ImGui.TreePop(); 174 | } 175 | ImGui.Checkbox("Detect Animals", ref IslandConfig.DetectAnimals); 176 | if (IslandConfig.DetectAnimals) 177 | { 178 | ImGui.TreePush("DetectAnimalFilterExpand"); 179 | ImGui.Checkbox("Show animal names on the markers", 180 | ref IslandConfig.ShowNameOnMarkerAnimals); 181 | ImGui.Checkbox("Hide markers for animals that are out of screen", 182 | ref IslandConfig.HideMarkerWhenNotInScreenAnimals); 183 | ImGui.Checkbox("Use different icons for different animals", 184 | ref IslandConfig.UseAnimalSpecificIcons); 185 | if (ImGui.CollapsingHeader("Detect only the following animals ...")) 186 | { 187 | ImGui.TreePush(); 188 | if (ImGui.Button("Select all")) 189 | IslandConfig.AnimalsToShow = uint.MaxValue; 190 | ImGui.SameLine(); 191 | if (ImGui.Button("Unselect all")) 192 | IslandConfig.AnimalsToShow = uint.MinValue; 193 | const int animalTableCols = 4; 194 | if (ImGui.BeginTable("##DetectAnimalFilterTable", animalTableCols, 195 | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.NoSavedSettings 196 | | ImGuiTableFlags.SizingFixedSame | ImGuiTableFlags.BordersInnerV)) 197 | { 198 | for (int i = 0; i < islandAnimalList.Count; i++) 199 | { 200 | if (i % animalTableCols == 0) ImGui.TableNextRow(); 201 | ImGui.TableNextColumn(); 202 | var data = islandAnimalList[i]; 203 | if (data.DataId == 0) continue; 204 | var flagval = 1u << i; 205 | var icon = Plugin.IconManager.GetIcon(data.IconId); 206 | ImGui.BeginGroup(); 207 | if (icon != null) 208 | ImGui.Image(icon.ImGuiHandle, animalSpecificMarkerIconSize); 209 | else ImGui.Text($"Animal#{i}"); 210 | ImGui.SameLine(); 211 | ImGui.CheckboxFlags($"##Animal#{i}", 212 | ref IslandConfig.AnimalsToShow, flagval); 213 | ImGui.EndGroup(); 214 | if (ImGui.IsItemHovered() && icon != null) 215 | { 216 | ImGui.BeginTooltip(); 217 | ImGui.Image(icon.ImGuiHandle, animalSpecificMarkerIconSize * 1.5f); 218 | ImGui.EndTooltip(); 219 | } 220 | } 221 | ImGui.EndTable(); 222 | } 223 | ImGui.TreePop(); 224 | } 225 | ImGui.TreePop(); 226 | } 227 | ImGui.Unindent(); 228 | } 229 | 230 | 231 | private static ExcelSheet? EObjNameSheet 232 | => Plugin.DataManager.GetExcelSheet(); 233 | 234 | // ExcelSheet "MJIGatheringObject" 235 | // Col#10 iconId 236 | // Col#11 NpcId == EObj's Row Id 237 | private static ExcelSheet? GatheringObjectSheet 238 | => Plugin.DataManager.GetExcelSheet(); 239 | 240 | private static ExcelSheet? AnimalSheet 241 | => Plugin.DataManager.GetExcelSheet(); 242 | 243 | private void BuildIslandGatherDict() 244 | { 245 | islandGatherDict.Clear(); 246 | var gatheringSheet = GatheringObjectSheet; 247 | var eObjNameSheet = EObjNameSheet; 248 | if (gatheringSheet == null) 249 | { 250 | LogWarningExcelSheetNotLoaded(typeof(Sheets.MJIGatheringObject).Name); 251 | return; 252 | } 253 | if (EObjNameSheet == null) 254 | { 255 | LogWarningExcelSheetNotLoaded(typeof(Sheets.EObjName).Name); 256 | } 257 | foreach (var row in gatheringSheet) 258 | { 259 | var name = Language.SanitizeText( 260 | eObjNameSheet?.GetRow(row.Name.Row)?.Singular.RawString ?? string.Empty); 261 | var data = new IslandGatheringObjectData( 262 | row.RowId, row.Name.Row, row.MapIcon, name); 263 | islandGatherDict.Add(row.Name.Row, data); 264 | islandGatherList.Add(data); 265 | } 266 | } 267 | 268 | private IslandGatherType GetIslandGatherType(IslandCachedCompassObjective islObjective) 269 | => islandGatherDict.TryGetValue(islObjective.NpcId, out var data) 270 | ? data.GatherType : 0; 271 | 272 | private uint GetIslandGatherIconId(IslandCachedCompassObjective islObjective) 273 | => islandGatherDict.TryGetValue(islObjective.NpcId, out var data) 274 | ? data.IconId : 0; 275 | 276 | private void BuildIslandAnimalDict() 277 | { 278 | islandAnimalDict.Clear(); 279 | var animalSheet = AnimalSheet; 280 | if (animalSheet == null) 281 | { 282 | LogWarningExcelSheetNotLoaded(typeof(Sheets.MJIAnimals).Name); 283 | return; 284 | } 285 | foreach (var row in animalSheet) 286 | { 287 | var dataId = row.BNpcBase.Row; 288 | var data = new IslandAnimalData( 289 | row.RowId, dataId, (uint)row.Icon); 290 | islandAnimalDict.Add(row.BNpcBase.Row, data); 291 | islandAnimalList.Add(data); 292 | } 293 | } 294 | 295 | private uint GetIslandAnimalIconId(IslandCachedCompassObjective islObjective) 296 | => islandAnimalDict.TryGetValue(islObjective.DataId, out var data) 297 | ? data.IconId : 0; 298 | 299 | private uint GetMarkerIconId(IslandCachedCompassObjective islObjective) 300 | => islObjective.Type switch 301 | { 302 | IslandObjectType.Gathering => GetIslandGatherIconId(islObjective), 303 | IslandObjectType.Animal 304 | => IslandConfig.UseAnimalSpecificIcons 305 | ? GetIslandAnimalIconId(islObjective) 306 | : animalDefaultMarkerIconId, 307 | _ => 0u 308 | }; 309 | 310 | 311 | public IslandSanctuaryCompass() : base() 312 | { 313 | BuildIslandGatherDict(); 314 | BuildIslandAnimalDict(); 315 | } 316 | 317 | } 318 | 319 | 320 | public class IslandAnimalData 321 | { 322 | public readonly uint SheetRowId; 323 | public readonly uint DataId; 324 | public readonly uint IconId; 325 | 326 | public IslandAnimalData(uint rowId, uint dataId, uint iconId) 327 | { 328 | SheetRowId = rowId; 329 | DataId = dataId; 330 | IconId = iconId; 331 | } 332 | } 333 | 334 | public class IslandGatheringObjectData 335 | { 336 | public readonly uint SheetRowId; 337 | public readonly uint NpcId; 338 | public readonly uint IconId; 339 | public readonly IslandGatherType GatherType; 340 | public readonly string Name; 341 | 342 | public IslandGatheringObjectData(uint rowId, uint npcId, uint iconId, string name) 343 | { 344 | SheetRowId = rowId; 345 | NpcId = npcId; 346 | IconId = iconId; 347 | GatherType = GetIslandGatherType(iconId); 348 | Name = name; 349 | } 350 | 351 | private static IslandGatherType GetIslandGatherType(uint iconId) 352 | => iconId switch 353 | { 354 | 63963 => IslandGatherType.Crops, 355 | 63964 => IslandGatherType.Trees, 356 | 63965 => IslandGatherType.Rocks, 357 | 63966 => IslandGatherType.Sands, 358 | 63967 => IslandGatherType.Sea, 359 | _ => 0 360 | }; 361 | } 362 | 363 | public enum IslandObjectType : byte 364 | { 365 | Animal = 1, 366 | Gathering = 2 367 | } 368 | 369 | // Classified according to icons 370 | public enum IslandGatherType : byte 371 | { 372 | Crops = 1, // 63963 373 | Trees = 2, // 63964 374 | Rocks = 3, // 63965 375 | Sands = 4, // 63966 376 | Sea = 5, // 63967 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/Compasses/QuestCompass.cs: -------------------------------------------------------------------------------- 1 | using AetherCompass.Common; 2 | using AetherCompass.Common.Attributes; 3 | using AetherCompass.Compasses.Objectives; 4 | using AetherCompass.Game; 5 | using AetherCompass.Game.SeFunctions; 6 | using AetherCompass.Compasses.Configs; 7 | using AetherCompass.UI.GUI; 8 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 9 | using ImGuiNET; 10 | using Lumina.Excel; 11 | 12 | namespace AetherCompass.Compasses 13 | { 14 | [CompassType(CompassType.Experimental)] 15 | public class QuestCompass : Compass 16 | { 17 | public override string CompassName => "Quest Compass"; 18 | public override string Description => 19 | "Detecting NPC/objects nearby relevant to your in-progress quests.\n" + 20 | "** Currently limited functionality: battle NPCs will not be detected, " + 21 | "and the compass sometimes gives inaccurate or, although more rarely, incorrect information."; 22 | 23 | private protected override CompassConfig CompassConfig => Plugin.Config.QuestConfig; 24 | private QuestCompassConfig QuestConfig => (QuestCompassConfig)CompassConfig; 25 | 26 | private static readonly System.Reflection.PropertyInfo?[,] cachedQuestSheetToDoLocMap = new System.Reflection.PropertyInfo[24, 8]; 27 | //private static readonly System.Reflection.PropertyInfo?[,] cachedQuestSheetToDoChildLocationMap = new System.Reflection.PropertyInfo[24, 7]; 28 | private readonly Dictionary objQuestMap = new(); 29 | private static readonly System.Numerics.Vector4 infoTextColour = new(.98f, .77f, .35f, 1); 30 | private static readonly float infoTextShadowLightness = .1f; 31 | 32 | private static readonly int ScreenMarkerQuestNameMaxLength 33 | = Language.GetAdjustedTextMaxLength(16); 34 | 35 | private const uint defaultQuestMarkerIconId = 71223; 36 | 37 | // NPC AnnounceIcon starts from 71200 38 | // Refer to Excel sheet EventIconType, 39 | // For types whose IconRange is 6, the 3rd is in-progress and 5th is last seq (checkmark icon), 40 | // because +0 is the dummy, so 1st icon in the range would start from +1. 41 | // Each type has available and locked ver, but rn idk how to accurately tell if a quest is avail or locked 42 | private static uint GetQuestMarkerIconId( 43 | uint baseIconId, byte iconRange, bool questLastSeq = false) 44 | => baseIconId + iconRange switch 45 | { 46 | 6 => questLastSeq ? 5u : 3u, 47 | 1 => 0, 48 | _ => 1, 49 | }; 50 | 51 | 52 | public override bool IsEnabledInCurrentTerritory() 53 | { 54 | var terr = ZoneWatcher.CurrentTerritoryType; 55 | // 0 is noninstance, 1 is solo instance 56 | return terr?.ExclusiveType == 0 57 | || ((QuestConfig?.EnabledInSoloContents ?? false) && terr?.ExclusiveType == 1) 58 | ; 59 | } 60 | 61 | public override unsafe bool IsObjective(GameObject* o) 62 | { 63 | if (o == null) return false; 64 | var kind = (ObjectKind)o->ObjectKind; 65 | uint dataId = o->DataID; 66 | if (dataId == 0) return false; 67 | // NOTE: already considered hidden quests or those not revealed by Todos when filling up objQuestMap 68 | // TODO: AreaObject??? 69 | if (kind == ObjectKind.EventNpc || kind == ObjectKind.EventObj || kind == ObjectKind.AreaObject) 70 | return objQuestMap.ContainsKey(o->DataID); 71 | return false; 72 | } 73 | 74 | private protected override unsafe string 75 | GetClosestObjectiveDescription(CachedCompassObjective objective) 76 | => objective.Name + " (Quest)"; 77 | 78 | 79 | public override unsafe DrawAction? CreateDrawDetailsAction(CachedCompassObjective objective) 80 | { 81 | if (objective.IsEmpty()) return null; 82 | if (!objQuestMap.TryGetValue(objective.DataId, out var mappedInfo)) return null; 83 | var questId = mappedInfo.RelatedQuest.QuestID; 84 | return new(() => 85 | { 86 | ImGui.Text($"{objective.Name} {(mappedInfo.TodoRevealed ? "★" : "")}"); 87 | ImGui.BulletText($"Quest: {Language.SanitizeText(GetQuestName(questId))}"); 88 | #if DEBUG 89 | var qItem = GetQuestRow(questId); 90 | if (qItem != null) 91 | { 92 | ImGui.BulletText( 93 | $"JournalGenre: {qItem.JournalGenre.Value?.Name ?? string.Empty} #{qItem.JournalGenre.Row}, " + 94 | $"Type: {qItem.Type}"); 95 | } 96 | #endif 97 | ImGui.BulletText($"{CompassUtil.MapCoordToFormattedString(objective.CurrentMapCoord)} (approx.)"); 98 | ImGui.BulletText($"{objective.CompassDirectionFromPlayer}, " + 99 | $"{CompassUtil.DistanceToDescriptiveString(objective.Distance3D, false)}"); 100 | ImGui.BulletText(CompassUtil.AltitudeDiffToDescriptiveString(objective.AltitudeDiff)); 101 | DrawFlagButton($"##{(long)objective.GameObject}", objective.CurrentMapCoord); 102 | ImGui.Separator(); 103 | }, mappedInfo.RelatedQuest.IsPriority); 104 | } 105 | 106 | public override unsafe DrawAction? CreateMarkScreenAction(CachedCompassObjective objective) 107 | { 108 | if (objective.IsEmpty()) return null; 109 | if (!objQuestMap.TryGetValue(objective.DataId, out var mappedInfo)) return null; 110 | var qRow = GetQuestRow(mappedInfo.RelatedQuest.QuestID); 111 | var iconId = qRow == null || qRow.EventIconType.Value == null 112 | ? defaultQuestMarkerIconId 113 | : GetQuestMarkerIconId(qRow.EventIconType.Value.NpcIconAvailable, 114 | qRow.EventIconType.Value.IconRange, 115 | mappedInfo.RelatedQuest.QuestSeq == questFinalSeqIdx); 116 | var descr = (mappedInfo.TodoRevealed ? "★ " : "") + $"{objective.Name}"; 117 | if (QuestConfig.ShowQuestName) 118 | { 119 | var questName = Language.SanitizeText(GetQuestName(mappedInfo.RelatedQuest.QuestID)); 120 | if (QuestConfig.MarkerTextInOneLine) 121 | { 122 | if (questName.Length > ScreenMarkerQuestNameMaxLength) 123 | questName = questName[..ScreenMarkerQuestNameMaxLength] + ".."; 124 | descr += $" (Quest: {questName}), {CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}"; 125 | } 126 | else descr += $", {CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}" + 127 | $"\n(Quest: {questName})"; 128 | } 129 | else descr += $", {CompassUtil.DistanceToDescriptiveString(objective.Distance3D, true)}"; 130 | return GenerateDefaultScreenMarkerDrawAction(objective, iconId, 131 | DefaultMarkerIconSize, .9f, descr, infoTextColour, infoTextShadowLightness, out _, 132 | important: objective.Distance3D < 55 || mappedInfo.RelatedQuest.IsPriority); 133 | } 134 | 135 | public override void DrawConfigUiExtra() 136 | { 137 | ImGui.BulletText("More options:"); 138 | ImGui.Indent(); 139 | ImGuiEx.Checkbox("Also enable this compass in solo instanced contents", ref QuestConfig.EnabledInSoloContents, 140 | "By default, this compass will not work in any type of instanced contents.\n" + 141 | "You can enable it in solo instanced contents if needed."); 142 | //ImGuiEx.Checkbox("Also detect quest related enemies", ref QuestConfig.DetectEnemy, 143 | // "By default, this compass will only detect event NPCs or objects, that is, NPCs/Objects that don't fight.\n" + 144 | // "You can enable this option to have the compass detect also quest related enemies."); 145 | ImGuiEx.Checkbox("Don't detect hidden quests", ref QuestConfig.HideHidden, 146 | "Hidden quests are those that you've marked as ignored in Journal.\n" + 147 | "If this option is enabled, will not detect NPC/Objects related to these hidden quests."); 148 | ImGuiEx.Checkbox("Detect all NPCs and objects relevant to in-progress quests", ref QuestConfig.ShowAllRelated, 149 | "By default, this compass only detects NPC/objects that are objectives of the quests " + 150 | "as shown in the quest Todos and on the Minimap.\n\n" + 151 | "If this option is enabled, NPC/objects that are spawned due to the quests will also " + 152 | "be detected by this compass, even if they may not be the objectives of the quests.\n" + 153 | "Additionally, for quests that require looking for NPC/objects in a certain area, " + 154 | "enabling this option may reveal the objectives' locations.\n\n" + 155 | "In either case, NPC/objects that are known to be quest objectives will have a \"★\" mark by their names."); 156 | if (MarkScreen) 157 | { 158 | ImGui.Checkbox("Show quest name by screen marker", ref QuestConfig.ShowQuestName); 159 | if (QuestConfig.ShowQuestName) 160 | ImGuiEx.Checkbox("Show screen marker text in one line", ref QuestConfig.MarkerTextInOneLine, 161 | "Display the whole label text in one line.\n" + 162 | "May only display part of the quest name to avoid the text being too long."); 163 | } 164 | ImGui.Unindent(); 165 | } 166 | 167 | 168 | public override void ProcessOnLoopStart() 169 | { 170 | objQuestMap.Clear(); 171 | ProcessQuestData(); 172 | 173 | base.ProcessOnLoopStart(); 174 | 175 | // TODO: may mark those location range in quest ToDOs if somehow we can know which ToDos are already done; 176 | // we can find those locations easily from Quest sheet but only if we can know which ToDo are done! 177 | // (If we can know that, we can also detect relevant BNpcs as well..) 178 | } 179 | 180 | 181 | private static uint QuestIdToQuestRowId(ushort questId) => questId + (uint)65536; 182 | private static ushort QuestRowIdToQuestId(uint questRowId) 183 | { 184 | if (questRowId <= 65536) return 0; 185 | var id = questRowId - 65536; 186 | if (id <= ushort.MaxValue) return (ushort)id; 187 | return 0; 188 | } 189 | 190 | 191 | private static readonly ExcelSheet? QuestSheet 192 | = Plugin.DataManager.GetExcelSheet(); 193 | private static readonly ExcelSheet? EObjSheet 194 | = Plugin.DataManager.GetExcelSheet(); 195 | private static readonly ExcelSheet? ENpcSheet 196 | = Plugin.DataManager.GetExcelSheet(); 197 | private static readonly ExcelSheet? LevelSheet 198 | = Plugin.DataManager.GetExcelSheet(); 199 | 200 | private static Sheets.Quest? GetQuestRow(ushort questId) 201 | => QuestSheet?.GetRow(QuestIdToQuestRowId(questId)); 202 | private static string GetQuestName(ushort questId) 203 | => GetQuestRow(questId)?.Name?.RawString ?? string.Empty; 204 | 205 | 206 | // ActorSpawn, ActorDespawn, Listener, etc. 207 | private const int questSheetActorArrayLength = 64; 208 | private const int questSheetToDoArrayLength = 24; 209 | private const int questSheetToDoChildMaxCount = 7; 210 | private const byte questFinalSeqIdx = byte.MaxValue; 211 | 212 | private unsafe void ProcessQuestData() 213 | { 214 | var questlist = Quests.GetQuestListArray(); 215 | if (questlist == null) 216 | { 217 | LogError("Failed to get QuestListArray"); 218 | return; 219 | } 220 | 221 | static bool ShouldExitActorArrayLoop(Sheets.Quest q, int idx) 222 | => (idx >= 0 && idx < questSheetActorArrayLength / 2 && q.QuestUInt8A[idx] == 0) 223 | || (idx >= questSheetActorArrayLength / 2 && idx < questSheetActorArrayLength 224 | && q.QuestUInt8B[idx - questSheetActorArrayLength / 2] == 0); 225 | 226 | for (int i = 0; i < Quests.QuestListArrayLength; i++) 227 | { 228 | var quest = questlist[i]; 229 | if (quest.QuestID == 0) continue; 230 | if (quest.IsHidden && QuestConfig.HideHidden) continue; 231 | var questRow = GetQuestRow(quest.QuestID); 232 | if (questRow == null) continue; 233 | 234 | // ToDos: find out objective gameobjs revealed by ToDos, if any 235 | HashSet todoRevealedObjs = new(); 236 | for (int j = 0; j < questSheetToDoArrayLength; j++) 237 | { 238 | // NOTE: ignore Level location for now, 239 | // because we cant tell if the ToDos are completed or not when there are multiple Todos 240 | if (questRow.ToDoCompleteSeq[j] == quest.QuestSeq) 241 | { 242 | //var mainLoc = questRow.ToDoMainLocation[j].Value; 243 | //if (mainLoc != null && mainLoc.Object != 0) 244 | // todoRevealedObjs.Add(mainLoc.Object); 245 | //for (int k = 0; k < questSheetToDoChildMaxCount; k++) 246 | //{ 247 | // var childLocRowId = GetQuestToDoChildLocationRowId(questRow, j, k); 248 | // if (childLocRowId != 0) 249 | // { 250 | // var childLoc = LevelSheet?.GetRow(childLocRowId); 251 | // if (childLoc != null && childLoc.Object != 0) 252 | // todoRevealedObjs.Add(childLoc.Object); 253 | // } 254 | //} 255 | 256 | // Main + Child 257 | for (int k = 0; k < questSheetToDoChildMaxCount + 1; k++) 258 | { 259 | var todoLocRowId = GetQuestToDoLocRowId(questRow, j, k); 260 | if (todoLocRowId > 0) 261 | { 262 | var todoLoc = LevelSheet?.GetRow(todoLocRowId); 263 | if (todoLoc != null && todoLoc.Object != 0) 264 | todoRevealedObjs.Add(todoLoc.Object); 265 | } 266 | } 267 | } 268 | if (questRow.ToDoCompleteSeq[j] == questFinalSeqIdx) break; 269 | } 270 | 271 | // Actor related arrays: find out related gameobjs 272 | List objsThisSeq = new(); // objectives (usually listeners) that will be deactivated when this Seq completes 273 | for (int j = 0; j < questSheetActorArrayLength; j++) 274 | { 275 | if (ShouldExitActorArrayLoop(questRow, j)) break; 276 | var listener = questRow.Listener[j]; 277 | // Track ConditionValue if ConditionType non-zero instead of listener itself; 278 | // this usually happens with BNpc etc. which we don't consider yet, but anyway 279 | var objToTrack = questRow.ConditionType[j] > 0 ? questRow.ConditionValue[j] : listener; 280 | bool todoRevealed = todoRevealedObjs.Contains(objToTrack); 281 | // Skip those not revealed by ToDos if option not enabled 282 | if (!QuestConfig.ShowAllRelated && !todoRevealed) continue; 283 | if (questRow.ActorSpawnSeq[j] == 0) continue; // Invalid? usually won't have this 284 | if (questRow.ActorSpawnSeq[j] > quest.QuestSeq) continue; // Not spawn/active yet 285 | if (questRow.ActorDespawnSeq[j] < questSheetToDoArrayLength) 286 | { 287 | // I think ActorDespawnSeq corresponds to ToDo idx, not Seq 288 | var despawnSeq = questRow.ToDoCompleteSeq[questRow.ActorDespawnSeq[j]]; 289 | if (despawnSeq < quest.QuestSeq) 290 | continue; // Despawned/deactivated when previous Seq ends 291 | // TODO: should we also check if it's spawned at start of this Seq? 292 | if (despawnSeq == quest.QuestSeq) 293 | objsThisSeq.Add(objToTrack); 294 | } 295 | if (!objQuestMap.ContainsKey(objToTrack) || quest.IsPriority) 296 | objQuestMap[objToTrack] = (quest, todoRevealed); 297 | } 298 | // Filter out already interacted ones 299 | if (quest.ObjectiveObjectsInteractedFlags != 0) 300 | { 301 | for (int k = 0; k < objsThisSeq.Count; k++) 302 | if (quest.IsObjectiveInteracted(k)) 303 | objQuestMap.Remove(objsThisSeq[k]); 304 | } 305 | } 306 | } 307 | 308 | // The uint stored in the cell is the corresponding row id in Level sheet 309 | private static uint GetQuestToDoLocRowId(Sheets.Quest? questRow, int row, int col) 310 | { 311 | if (row < 0 || row >= questSheetToDoArrayLength 312 | || col < 0 || col >= questSheetToDoChildMaxCount + 1 313 | || questRow == null) return 0; 314 | var val = cachedQuestSheetToDoLocMap[row, col]?.GetValue(questRow); 315 | // First 7 cells (from col 1221) is put in an array accessed via Unknown1221 316 | if (val == null) return 0; 317 | if (row <= 7 && col == 0) 318 | return (val as uint[])?[row] ?? 0; 319 | else return (uint)val; 320 | } 321 | 322 | //// ToDoChildLocation is 24 * 7 array containing row id of corresponding Level 323 | //// In sheet first col of every row get listed first, then 2nd col and so on. 324 | //// (So together with ToDoMainLocation, we can have at most 24 ToDos for a quest and each ToDo can have at most 8 Levels.) 325 | //public static uint GetQuestToDoChildLocationRowId(Sheets.Quest? questRow, int row, int col) 326 | //{ 327 | // if (row < 0 || row >= questSheetToDoArrayLength 328 | // || col < 0 || col >= questSheetToDoChildMaxCount || questRow == null) return 0; 329 | // var val = cachedQuestSheetToDoChildLocationMap[row, col]?.GetValue(questRow); 330 | // if (val == null) return 0; 331 | // if (row <= 6 && col == 0) // covered by uint array at col 1245 332 | // return (val as uint[])?[row] ?? 0; 333 | // else return (uint)val; 334 | //} 335 | 336 | //public static uint GetQuestToDoChildLocationRowId(uint questRowId, int row, int col) 337 | // => GetQuestToDoChildLocationRowId(QuestSheet?.GetRow(questRowId), row, col); 338 | 339 | // (0,0) is at col 1221. These first 24 cells used to be 340 | // represented as one array called ToDoMainlocation 341 | // The rest used to be a 24x7 array of ToDoChildLocation; 342 | // still row first (traverse through every row of one column then the next) 343 | // 344 | // Currently: Unknown1221 -> col 1221 ~ 1228 345 | // From Unknown1229 single cell, until Unknown1412 (incl.) 346 | private static void InitQuestSheetToDoLocMap() 347 | { 348 | var questSheetType = typeof(Sheets.Quest); 349 | int propIdx0 = 1221; 350 | for (int i = 0; i < questSheetToDoArrayLength; i++) 351 | { 352 | 353 | // Main 354 | cachedQuestSheetToDoLocMap[i, 0] = i <= 7 355 | ? questSheetType.GetProperty($"Unknown{propIdx0}") 356 | : questSheetType.GetProperty($"Unknown{propIdx0 + i}"); 357 | // Child 358 | for (int j = 0; j < questSheetToDoChildMaxCount; j++) 359 | cachedQuestSheetToDoLocMap[i, j] 360 | = questSheetType.GetProperty($"Unknown{propIdx0 + j * 24 + i}"); 361 | } 362 | } 363 | 364 | //// (0, 1) is at col 1245. Row first --- col 1246 is (1,1) etc. 365 | //// Used to be a 24x7 unit array in Lumina but now merged into one array with the previous ToDoMainLocation array 366 | //// So the Child part starts from the second columns of this big array 367 | //private static void InitQuestSheetToDoChildLocationMap() 368 | //{ 369 | // var questSheetType = typeof(Sheets.Quest); 370 | // int propIdx0 = 1245; 371 | // for (int i = 0; i < questSheetToDoArrayLength; i++) 372 | // { 373 | // for (int j = 0; j < questSheetToDoChildMaxCount; j++) 374 | // { 375 | // if (i <= 6 && j == 0) 376 | // // From uint array at col 1245 377 | // cachedQuestSheetToDoChildLocationMap[i, j] = questSheetType.GetProperty($"Unknown{propIdx0}"); 378 | // else 379 | // { 380 | // int propIdx = propIdx0 + j * questSheetToDoArrayLength + i; 381 | // cachedQuestSheetToDoChildLocationMap[i, j] = questSheetType.GetProperty($"Unknown{propIdx}"); 382 | // } 383 | // } 384 | // } 385 | //} 386 | 387 | 388 | public QuestCompass() : base() 389 | { 390 | InitQuestSheetToDoLocMap(); 391 | //InitQuestSheetToDoChildLocationMap(); 392 | } 393 | } 394 | } 395 | --------------------------------------------------------------------------------