├── 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 |
--------------------------------------------------------------------------------