├── src ├── Drawing │ ├── DrawTrigger.cs │ ├── Types │ │ ├── FacingDirectedConeAoEDrawData.cs │ │ ├── TargetDirectedLineAoEDrawData.cs │ │ ├── TargetDirectedConeAoEDrawData.cs │ │ ├── FacingDirectedLineAoEDrawData.cs │ │ ├── BidirectedLineAoEDrawData.cs │ │ ├── LineAoEDrawData.cs │ │ ├── CircleAoEDrawData.cs │ │ ├── DashAoEDrawData.cs │ │ ├── CrossAoEDrawData.cs │ │ ├── ConeAoEDrawData.cs │ │ └── DonutAoEDrawData.cs │ ├── Workers │ │ ├── IDrawWorker.cs │ │ ├── DrawWorker.cs │ │ └── CastingDrawWorker.cs │ ├── Projection.cs │ ├── DrawData.cs │ └── EffectRangeDrawing.cs ├── Actions │ ├── Data │ │ ├── Template │ │ │ ├── IDataItem.cs │ │ │ ├── BlacklistedActionDataItem.cs │ │ │ ├── AoETypeDataItem.cs │ │ │ └── ConeAoEAngleDataItem.cs │ │ ├── Predefined │ │ │ ├── RuledOutActions.cs │ │ │ ├── DonutAoERadiusMap.cs │ │ │ ├── NoCachedSeqActions.cs │ │ │ ├── AdditionalEffectsMap.cs │ │ │ ├── PetLikeActionMap.cs │ │ │ ├── DirectMap.cs │ │ │ ├── AoETypeOverridingMap.cs │ │ │ ├── EffectRangeCornerCases.cs │ │ │ ├── PetActionMap.cs │ │ │ ├── ConeAoEAngleMap.cs │ │ │ └── HarmfulnessMap.cs │ │ ├── Containers │ │ │ ├── IActionDataContainer.cs │ │ │ ├── AoETypeOverridingList.cs │ │ │ ├── ConeAoeAngleOverridingList.cs │ │ │ └── ActionBlacklist.cs │ │ └── ActionData.cs │ ├── Enums │ │ ├── ActionHarmfulness.cs │ │ ├── ActionCategory.cs │ │ └── ActionAoEType.cs │ ├── ActionSeqInfo.cs │ ├── ActionSeqRecord.cs │ ├── EffectRange │ │ ├── DashAoEEffectRangeData.cs │ │ ├── CircleAoEEffectRangeData.cs │ │ ├── NonAoEEffectRangeData.cs │ │ ├── BidirectedLineAoEEffectRangeData.cs │ │ ├── LineAoEEffectRangeData.cs │ │ ├── DonutAoEEffectRangeData.cs │ │ ├── CrossAoEEffectRangeData.cs │ │ ├── ConeAoEEffectRangeData.cs │ │ ├── EffectRangeData.cs │ │ └── EffectRangeDataManager.cs │ ├── SeqSnapshot.cs │ └── ActionWatcher.cs ├── Utils │ ├── Interop.cs │ └── Collections.cs ├── Game.cs ├── Helpers │ ├── ClassJobWatcher.cs │ ├── ActionManagerHelper.cs │ └── PetWatcher.cs ├── Configuration.cs ├── UI │ ├── ActionBlacklistEditUi.cs │ ├── ActionDataInterfacing.cs │ ├── AoETypeEditUi.cs │ ├── ImGuiExt.cs │ ├── ConeAoEAngleEditUI.cs │ ├── ActionDataEditUi.cs │ └── ConfigUi.cs └── Plugin.cs ├── DalamudPackager.targets ├── ActionEffectRange.json ├── .github └── FUNDING.yml ├── TODO.md ├── ActionEffectRange.sln ├── README.md ├── .gitattributes ├── ActionEffectRange.csproj ├── .editorconfig └── .gitignore /src/Drawing/DrawTrigger.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Drawing 2 | { 3 | public enum DrawTrigger : byte 4 | { 5 | Used = 1, 6 | Casting = 2 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Actions/Data/Template/IDataItem.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions.Data.Template 2 | { 3 | public interface IDataItem 4 | { 5 | public uint ActionId { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Actions/Enums/ActionHarmfulness.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ActionEffectRange.Actions.Enums 4 | { 5 | [Flags] 6 | public enum ActionHarmfulness 7 | { 8 | None = 0, 9 | Harmful = 1, 10 | Beneficial = 2, 11 | Both = Harmful | Beneficial, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Utils/Interop.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ActionEffectRange.Utils 4 | { 5 | internal static class Interop 6 | { 7 | public static float MarshalFloat(IntPtr p) 8 | => BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(p))); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DalamudPackager.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/RuledOutActions.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions.Data.Predefined 2 | { 3 | // ActionBlacklist - no processing 4 | public static class RuledOutActions 5 | { 6 | public static uint[] PredefinedList => new uint[] 7 | { 8 | 2262, // Shukuchi (NIN) 9 | 10 | 29513, // Shukuchi (NIN PvP) 11 | 29551, // Regress (RPR PvP) 12 | 13 | 11401, // Loom (BLU) 14 | 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/DonutAoERadiusMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace ActionEffectRange.Actions.Data.Predefined 5 | { 6 | public static class DonutAoERadiusMap 7 | { 8 | // InnerRadius 9 | public static readonly ImmutableDictionary Predefined 10 | = new KeyValuePair[] 11 | { 12 | new(11420, 8), // dragon's voice (BLU) 13 | 14 | }.ToImmutableDictionary(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Drawing/Types/FacingDirectedConeAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Drawing.Types 2 | { 3 | public class FacingDirectedConeAoEDrawData : ConeAoEDrawData 4 | { 5 | public FacingDirectedConeAoEDrawData( 6 | Vector3 origin, float rotation, byte baseEffectRange, byte xAxisModifier, 7 | float centralAngleCycles, uint ringColour, uint fillColour) 8 | : base(origin, baseEffectRange, xAxisModifier, 9 | rotation, centralAngleCycles, ringColour, fillColour) { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Actions/Data/Template/BlacklistedActionDataItem.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions.Data.Template 2 | { 3 | public class BlacklistedActionDataItem : IDataItem 4 | { 5 | public uint ActionId { get; } 6 | 7 | public BlacklistedActionDataItem(uint actionId) => ActionId = actionId; 8 | 9 | public static implicit operator uint(BlacklistedActionDataItem item) 10 | => item.ActionId; 11 | 12 | public static explicit operator BlacklistedActionDataItem(uint actionId) 13 | => new(actionId); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Actions/Enums/ActionCategory.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions.Enums 2 | { 3 | // Excel "ActionCategory" 4 | public enum ActionCategory 5 | { 6 | Invalid = 0, 7 | AA = 1, 8 | Spell = 2, 9 | WS = 3, 10 | Ability = 4, 11 | Item = 5, 12 | DoL = 6, 13 | DoH = 7, 14 | Event = 8, 15 | LB = 9, 16 | System1 = 10, 17 | System2 = 11, 18 | Mount = 12, 19 | Special = 13, 20 | ItemManipulation = 14, 21 | AR = 15, 22 | Artillery = 17 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Drawing/Types/TargetDirectedLineAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Drawing.Types 2 | { 3 | public class TargetDirectedLineAoEDrawData : LineAoEDrawData 4 | { 5 | public readonly Vector3 Target; 6 | 7 | public TargetDirectedLineAoEDrawData(Vector3 origin, Vector3 target, 8 | byte baseEffectRange, byte xAxisModifier, float rotationOffset, 9 | uint ringColour, uint fillColour) 10 | : base(origin, target, baseEffectRange, xAxisModifier, 11 | rotationOffset, ringColour, fillColour) { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Actions/Data/Template/AoETypeDataItem.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Enums; 2 | 3 | namespace ActionEffectRange.Actions.Data.Template 4 | { 5 | public class AoETypeDataItem : IDataItem 6 | { 7 | public uint ActionId { get; } 8 | public byte CastType { get; } 9 | public ActionHarmfulness Harmfulness { get; } 10 | 11 | public AoETypeDataItem( 12 | uint actionId, byte castType, ActionHarmfulness harmfulness) 13 | { 14 | ActionId = actionId; 15 | CastType = castType; 16 | Harmfulness = harmfulness; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Actions/Data/Template/ConeAoEAngleDataItem.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions.Data.Template 2 | { 3 | public class ConeAoEAngleDataItem : IDataItem 4 | { 5 | public uint ActionId { get; } 6 | public float CentralAngleCycles { get; } 7 | public float RotationOffset { get; } 8 | 9 | public ConeAoEAngleDataItem(uint actionId, 10 | float centralAngleCycles, float rotationOffset = 0) 11 | { 12 | ActionId = actionId; 13 | CentralAngleCycles = centralAngleCycles; 14 | RotationOffset = rotationOffset; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Actions/Enums/ActionAoEType.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions.Enums 2 | { 3 | // Action.CastType 4 | public enum ActionAoEType : byte 5 | { 6 | None, 7 | 8 | Circle = 2, 9 | Cone = 3, 10 | Line = 4, 11 | Circle2 = 5, 12 | 13 | GT = 7, 14 | DashAoE = 8, // Dash and do dmg on the route, e.g. soten 15 | // Actually may be Line AoE with length 16 | // adjusted to target position, not always dashing 17 | 18 | Donut = 10, 19 | Cross = 11, 20 | Line2 = 12, 21 | Cone2 = 13, 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/NoCachedSeqActions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace ActionEffectRange.Actions.Data.Predefined 5 | { 6 | // Actions here should not try to match cached SeqInfo 7 | // but always use current position info 8 | internal static class NoCachedSeqActions 9 | { 10 | public static ImmutableHashSet Set => new HashSet 11 | { 12 | 29557, // Relentless Rush (GNB PvP) (persistent effect following #29130) 13 | 29558, // Honing Dance (DNC PvP) (persistent effect following #29422) 14 | }.ToImmutableHashSet(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Drawing/Types/TargetDirectedConeAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Drawing.Types 2 | { 3 | public class TargetDirectedConeAoEDrawData : ConeAoEDrawData 4 | { 5 | public readonly Vector3 Target; 6 | 7 | public TargetDirectedConeAoEDrawData( 8 | Vector3 origin, Vector3 target, byte baseEffectRange, byte xAxisModifier, 9 | float centralAngleCycles, float rotationOffset, uint ringColour, uint fillColour) 10 | : base(origin, baseEffectRange, xAxisModifier, 11 | CalcRotation(origin, target) + rotationOffset, centralAngleCycles, 12 | ringColour, fillColour) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Drawing/Workers/IDrawWorker.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.EffectRange; 2 | using ImGuiNET; 3 | using System.Numerics; 4 | 5 | namespace ActionEffectRange.Drawing.Workers 6 | { 7 | public interface IDrawWorker 8 | { 9 | public DrawTrigger Trigger { get; } 10 | 11 | public void RefreshConfig(); 12 | public void Clear(); 13 | public void Reset(); 14 | public void QueueDrawing(uint sequence, EffectRangeData effectRangeData, 15 | Vector3 originPos, Vector3 targetPos, float rotation); 16 | public void CleanupOld(); 17 | public bool HasDataToDraw(); 18 | public void Draw(ImDrawListPtr drawList); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ActionEffectRange.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "ActionEffectRange", 3 | "Author": "yomishino", 4 | "Punchline": "Provides a visual cue on the effect range of the AoE action just used.", 5 | "Description": "This plugin provides a visual cue on the effect range of the AoE action you just used.\n\nMay be used as a supplement/replacement to the actions' VFXs in showing effect range related information, such as where has the action landed and how large an area it covered.", 6 | "RepoUrl": "https://github.com/yomishino/FFXIVActionEffectRange", 7 | "CategoryTags": [ 8 | "UI" 9 | ], 10 | "ImageUrls": "", 11 | "IconUrl": "", 12 | "Changelog": "Update: Dalamud API9 compatibility", 13 | "DalamudApiLevel": 9 14 | } -------------------------------------------------------------------------------- /src/Game.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange 2 | { 3 | internal class Game 4 | { 5 | public static bool IsPlayerLoaded 6 | => ClientState.LocalContentId != 0 && ClientState.LocalPlayer != null; 7 | 8 | public static Dalamud.Game.ClientState.Objects.SubKinds.PlayerCharacter? 9 | LocalPlayer => ClientState.LocalPlayer; 10 | 11 | public static bool IsPvPZone 12 | => DataManager.GetExcelSheet()? 13 | .GetRow(ClientState.TerritoryType)?.IsPvpZone ?? false; 14 | 15 | public const uint InvalidGameObjectId 16 | = Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Actions/Data/Containers/IActionDataContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ActionEffectRange.Actions.Data.Containers 4 | { 5 | interface IActionDataContainer 6 | { 7 | public int PredefinedCount { get; } 8 | public int CustomisedCount { get; } 9 | public int TotalCount => PredefinedCount + CustomisedCount; 10 | 11 | public bool Add(IDataItem item); 12 | public bool Remove(uint actionId); 13 | public bool Contains(uint actionId); 14 | public bool TryGet(uint actionId, out IDataItem? item); 15 | public IEnumerable CopyCustomised(); 16 | public void Reload(); 17 | public void Save(bool writeToFile = false); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Actions/ActionSeqInfo.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Actions 2 | { 3 | public class ActionSeqInfo 4 | { 5 | public readonly uint ActionId; 6 | public readonly SeqSnapshot SeqSnapshot = null!; 7 | public readonly bool IsPetAction; // incl. pet-like such as bunshin's 8 | public uint Seq => SeqSnapshot.Seq; 9 | public DateTime SnapshotTime => SeqSnapshot.CreatedTime; 10 | public double ElapsedSeconds => SeqSnapshot.ElapsedSeconds; 11 | 12 | public ActionSeqInfo(uint actionId, 13 | SeqSnapshot snapshot, bool isPetAction = false) 14 | { 15 | ActionId = actionId; 16 | SeqSnapshot = snapshot; 17 | IsPetAction = isPetAction; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Drawing/Types/FacingDirectedLineAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | namespace ActionEffectRange.Drawing.Types 2 | { 3 | public class FacingDirectedLineAoEDrawData : LineAoEDrawData 4 | { 5 | public FacingDirectedLineAoEDrawData(Vector3 origin, float rotation, 6 | byte baseEffectRange, byte xAxisModifier, float rotationOffset, 7 | uint ringColour, uint fillColour) 8 | : base(origin, GetDummyTarget(origin, rotation), baseEffectRange, 9 | xAxisModifier, rotationOffset, ringColour, fillColour) 10 | {} 11 | 12 | protected static Vector3 GetDummyTarget(Vector3 origin, float rotation) 13 | => CalcFarEndWorldPos( 14 | origin, new(MathF.Sin(rotation), 0, MathF.Cos(rotation)), 1); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/AdditionalEffectsMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace ActionEffectRange.Actions.Data.Predefined 5 | { 6 | // Map to actions for additional effects of the original actions 7 | public static class AdditionalEffectsMap 8 | { 9 | public static readonly ImmutableDictionary> 10 | Dictionary = new KeyValuePair>[] 11 | { 12 | // PvE 13 | 14 | new(24318, new uint[]{ 27524 }.ToImmutableArray()), // Pneuma (SGE) 15 | 16 | // PvP 17 | 18 | new(29260, new uint[]{ 29706 }.ToImmutableArray()), // Pneuma (SGE PvP) 19 | 20 | }.ToImmutableDictionary(); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | ## Planned 4 | 5 | 6 | ## Pending 7 | 8 | - [ ] Dancer's "Curing Waltz" (#16015), and PvP ver (#29429): 9 | Not showing effect range for additional effect (AoE heal around partner) 10 | - `ReceiveActionEffect` is triggered only once on action use; 11 | cannot know whether player has a partner without checking status effects; 12 | - Also cannot know the partner's position directly 13 | 14 | - [ ] Ninja's "Hollow Nozuchi" (#25776): 15 | Not showing effect range on Doton area 16 | - `ReceiveActionEffect` is triggered for #25776 but cannot get position information 17 | as it is not implemented as "pet". 18 | 19 | - [ ] Reaper's "Arcane Crest" (#24404): 20 | Not showing effect range when the barrier effect is triggered 21 | - `ReceiveActionEffect` is not triggered for the barrier effect 22 | 23 | - [ ] Configuration option to draw (or not draw) for auto-triggered action effects 24 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/PetLikeActionMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace ActionEffectRange.Actions.Data.Predefined 5 | { 6 | // Not used now 7 | 8 | // Actions implemented as pet actions 9 | public class PetLikeActionMap 10 | { 11 | public static readonly ImmutableDictionary> Dictionary = new KeyValuePair>[] 12 | { 13 | new(25774, new uint[]{ 25775 }.ToImmutableHashSet()), // phantom kamaitachi (NIN) 14 | 15 | new(8324, new uint[]{ 7440, 7441 }.ToImmutableHashSet()), // stellar detonation (player); stellar burst, stellar explosion (pet) (<- earthly star) (AST) 16 | 17 | new(28509, new uint[]{ 25863, 25864 }.ToImmutableHashSet()) // liturgy of the bell (consuming) (WHM) 18 | 19 | }.ToImmutableDictionary(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Utils/Collections.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ActionEffectRange.Utils 4 | { 5 | public static class Collections 6 | { 7 | public static List FlatMap( 8 | this IEnumerable s, Func?> map) 9 | { 10 | var s1 = new List(); 11 | foreach (var item in s) 12 | { 13 | var m = map(item); 14 | if (m != null) s1.AddRange(m); 15 | } 16 | return s1; 17 | } 18 | 19 | public static void AddAllMappedNotNull( 20 | this ICollection s, IEnumerable items, Func map) 21 | { 22 | foreach (var item in items) 23 | if (item != null) 24 | { 25 | var m = map(item); 26 | if (m != null) s.Add(m); 27 | } 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Actions/ActionSeqRecord.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace ActionEffectRange.Actions 5 | { 6 | public class ActionSeqRecord : IEnumerable 7 | { 8 | private readonly int bound; 9 | private readonly Queue queue; 10 | 11 | public ActionSeqRecord(int bound) 12 | { 13 | this.bound = bound; 14 | queue = new(bound); 15 | } 16 | 17 | 18 | public void Add(ActionSeqInfo info) 19 | { 20 | while (queue.Count >= bound) 21 | queue.Dequeue(); 22 | queue.Enqueue(info); 23 | } 24 | 25 | public void Clear() => queue.Clear(); 26 | 27 | IEnumerator IEnumerable.GetEnumerator() 28 | => queue.GetEnumerator(); 29 | 30 | public IEnumerator GetEnumerator() 31 | => queue.GetEnumerator(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Drawing/Types/BidirectedLineAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | public class BidirectedLineAoEDrawData : FacingDirectedLineAoEDrawData 6 | { 7 | public readonly Vector3 End2; 8 | 9 | // Draw like line AoE but extend on both directions 10 | // Using one end as the origin instead of the position of the actor 11 | public BidirectedLineAoEDrawData(Vector3 origin, float rotation, 12 | byte baseEffectRange, byte xAxisModifier, float rotationOffset, 13 | uint ringColour, uint fillColour) 14 | : base(origin, rotation, byte.DivRem(baseEffectRange, 2).Quotient, 15 | xAxisModifier, rotationOffset, ringColour, fillColour) 16 | { 17 | End2 = CalcFarEndWorldPos(origin, -Direction, Length); 18 | } 19 | 20 | public override void Draw(ImDrawListPtr drawList) 21 | => DrawRect(drawList, Width, End2, End, Direction); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/DirectMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace ActionEffectRange.Actions.Data.Predefined 5 | { 6 | // DEPRECATED: not used any more 7 | // A -> {A'}: on A used, directly mapped to and wait for any in {A'} 8 | public class DirectMap 9 | { 10 | public static readonly ImmutableDictionary> Dictionary = 11 | new KeyValuePair>[] 12 | { 13 | new(29470, BuildArray(29423, 29424, 29425, 29426, 29427)), // Honing Ovation (DNC PvP) (player -> auto) 14 | new(29499, BuildArray(29498)), // Sky Shatter (DRG PvP) (player -> auto) 15 | 16 | new(29469, BuildArray(29131)), // Terminal Trigger (GNB PvP) (player -> auto) 17 | }.ToImmutableDictionary(); 18 | 19 | private static ImmutableArray BuildArray(params uint[] actionId) 20 | => actionId.ToImmutableArray(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/AoETypeOverridingMap.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Template; 2 | using ActionEffectRange.Actions.Enums; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | 6 | using static ActionEffectRange.Actions.Enums.ActionAoEType; 7 | using static ActionEffectRange.Actions.Enums.ActionHarmfulness; 8 | 9 | namespace ActionEffectRange.Actions.Data.Predefined 10 | { 11 | public static class AoETypeOverridingMap 12 | { 13 | public static readonly ImmutableDictionary PredefinedSpecial 14 | = new KeyValuePair[] 15 | { 16 | GeneratePair(7385, Cone, Beneficial), // Passage of Arms (PLD) 17 | GeneratePair(7418, Cone, Harmful), // Flamethrower (MCH) 18 | }.ToImmutableDictionary(); 19 | 20 | private static KeyValuePair GeneratePair( 21 | uint actionId, ActionAoEType aoeType, ActionHarmfulness harmfulness) 22 | => new(actionId, new(actionId, (byte)aoeType, harmfulness)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/DashAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class DashAoEEffectRangeData : EffectRangeData 7 | { 8 | public DashAoEEffectRangeData(uint actionId, 9 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 10 | sbyte range, byte effectRange, byte xAxisModifier, 11 | byte castType, bool isOriginal = false) 12 | : base(actionId, actionCategory, isGT, harmfulness, 13 | range, effectRange, xAxisModifier, castType, isOriginal) 14 | { } 15 | 16 | public DashAoEEffectRangeData(Lumina.Excel.GeneratedSheets.Action actionRow) 17 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 18 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 19 | actionRow.EffectRange, actionRow.XAxisModifier, 20 | actionRow.CastType, isOriginal: true) 21 | { } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/CircleAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class CircleAoEEffectRangeData : EffectRangeData 7 | { 8 | public CircleAoEEffectRangeData(uint actionId, 9 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 10 | sbyte range, byte effectRange, byte xAxisModifier, 11 | byte castType, bool isOriginal = false) 12 | : base(actionId, actionCategory, isGT, harmfulness, 13 | range, effectRange, xAxisModifier, castType, isOriginal) 14 | { } 15 | 16 | public CircleAoEEffectRangeData(Lumina.Excel.GeneratedSheets.Action actionRow) 17 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 18 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 19 | actionRow.EffectRange, actionRow.XAxisModifier, 20 | actionRow.CastType, isOriginal: true) 21 | { } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/NonAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | // Temporarily holding data to allow overriding later 7 | public class NonAoEEffectRangeData : EffectRangeData 8 | { 9 | public NonAoEEffectRangeData(uint actionId, 10 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 11 | sbyte range, byte effectRange, byte xAxisModifier, 12 | byte castType, bool isOriginal = false) 13 | : base(actionId, actionCategory, isGT, harmfulness, 14 | range, effectRange, xAxisModifier, castType, isOriginal) 15 | { } 16 | 17 | public NonAoEEffectRangeData(Lumina.Excel.GeneratedSheets.Action actionRow) 18 | : this(actionRow.RowId, actionRow.ActionCategory.Row, 19 | actionRow.TargetArea, ActionData.GetActionHarmfulness(actionRow), 20 | actionRow.Range, actionRow.EffectRange, actionRow.XAxisModifier, 21 | actionRow.CastType, isOriginal: true) 22 | { } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ActionEffectRange.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.32002.261 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActionEffectRange", "ActionEffectRange.csproj", "{B4C318D9-2904-4F7B-9953-EFBB9BA601FD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Release|x64 = Release|x64 12 | TestRelease|x64 = TestRelease|x64 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {B4C318D9-2904-4F7B-9953-EFBB9BA601FD}.Debug|x64.ActiveCfg = Debug|x64 16 | {B4C318D9-2904-4F7B-9953-EFBB9BA601FD}.Debug|x64.Build.0 = Debug|x64 17 | {B4C318D9-2904-4F7B-9953-EFBB9BA601FD}.Release|x64.ActiveCfg = Release|x64 18 | {B4C318D9-2904-4F7B-9953-EFBB9BA601FD}.Release|x64.Build.0 = Release|x64 19 | {B4C318D9-2904-4F7B-9953-EFBB9BA601FD}.TestRelease|x64.ActiveCfg = TestRelease|x64 20 | {B4C318D9-2904-4F7B-9953-EFBB9BA601FD}.TestRelease|x64.Build.0 = TestRelease|x64 21 | EndGlobalSection 22 | GlobalSection(SolutionProperties) = preSolution 23 | HideSolutionNode = FALSE 24 | EndGlobalSection 25 | GlobalSection(ExtensibilityGlobals) = postSolution 26 | SolutionGuid = {F84C13BD-D515-411B-BF9E-DF08C06186BD} 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /src/Drawing/Types/LineAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | public abstract class LineAoEDrawData : DrawData 6 | { 7 | public readonly Vector3 Origin; 8 | public readonly Vector3 Direction; 9 | public readonly float Rotation; 10 | public readonly float Length; 11 | public readonly float Width; 12 | public readonly Vector3 End; 13 | 14 | 15 | // Length (depth) of LineAoE seems has a small factor added to Action.EffectRange 16 | // so its slightly longer: maybe 0.5? 17 | // Sometimes visually different on diffent enemies/hitbox radii. 18 | // Seems not applied to dummies (except for dummies in instances in explore mode). 19 | public LineAoEDrawData(Vector3 origin, Vector3 target, 20 | byte baseEffectRange, byte xAxisModifier, float rotationOffset, 21 | uint ringColour, uint fillColour) 22 | : base(ringColour, fillColour) 23 | { 24 | Origin = origin; 25 | Direction = CalcDirection(origin, target, rotationOffset); 26 | Length = baseEffectRange + .5f; 27 | Width = xAxisModifier; 28 | End = CalcFarEndWorldPos(origin, Direction, Length); 29 | } 30 | 31 | public override void Draw(ImDrawListPtr drawList) 32 | => DrawRect(drawList, Width, Origin, End, Direction); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/BidirectedLineAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class LineAoEEffectRangeData : EffectRangeData 7 | { 8 | public float RotationOffset; 9 | public byte Width => XAxisModifier; 10 | 11 | public LineAoEEffectRangeData(uint actionId, 12 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 13 | sbyte range, byte effectRange, byte xAxisModifier, byte castType, 14 | float rotationOffset = 0, bool isOriginal = false) 15 | : base(actionId, actionCategory, isGT, harmfulness, 16 | range, effectRange, xAxisModifier, castType, isOriginal) 17 | { 18 | RotationOffset = rotationOffset; 19 | } 20 | 21 | public LineAoEEffectRangeData(Lumina.Excel.GeneratedSheets.Action actionRow, 22 | float rotationOffset = 0) 23 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 24 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 25 | actionRow.EffectRange, actionRow.XAxisModifier, actionRow.CastType, 26 | rotationOffset, isOriginal: true) 27 | { } 28 | 29 | protected override string AdditionalFieldsToString() 30 | => $"RotationOffset: {RotationOffset}"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/LineAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class BidirectedLineAoEEffectRangeData : EffectRangeData 7 | { 8 | public float RotationOffset; 9 | public byte Width => XAxisModifier; 10 | 11 | public BidirectedLineAoEEffectRangeData(uint actionId, 12 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 13 | sbyte range, byte effectRange, byte xAxisModifier, byte castType, 14 | float rotationOffset = 0, bool isOriginal = false) 15 | : base(actionId, actionCategory, isGT, harmfulness, 16 | range, effectRange, xAxisModifier, castType, isOriginal) 17 | { 18 | RotationOffset = rotationOffset; 19 | } 20 | 21 | public BidirectedLineAoEEffectRangeData( 22 | Lumina.Excel.GeneratedSheets.Action actionRow, float rotationOffset = 0) 23 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 24 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 25 | actionRow.EffectRange, actionRow.XAxisModifier, actionRow.CastType, 26 | rotationOffset, isOriginal: true) 27 | { } 28 | 29 | protected override string AdditionalFieldsToString() 30 | => $"RotationOffset: {RotationOffset}"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Helpers/ClassJobWatcher.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Plugin.Services; 2 | 3 | namespace ActionEffectRange.Helpers 4 | { 5 | internal static class ClassJobWatcher 6 | { 7 | private static uint classJobId; 8 | 9 | public delegate void OnClassJobChangedDelegate(uint currentClassJob); 10 | public static event OnClassJobChangedDelegate ClassJobChanged = delegate { }; 11 | 12 | public static uint CurrentClassJobId => LocalPlayer?.ClassJob.Id ?? 0; 13 | 14 | public static bool IsCurrentClassJobACNRelated() 15 | => CurrentClassJobId == 26 // ACN 16 | || CurrentClassJobId == 27 // SMN 17 | || CurrentClassJobId == 28; // SCH 18 | 19 | private static void CheckClassJobChange(uint currentClassJobId) 20 | { 21 | if (classJobId != currentClassJobId) 22 | { 23 | classJobId = currentClassJobId; 24 | ClassJobChanged.Invoke(classJobId); 25 | } 26 | } 27 | 28 | private static void OnFrameworkUpdate(IFramework framework) 29 | { 30 | if (ClientState.LocalContentId == 0) return; 31 | if (LocalPlayer == null) return; 32 | CheckClassJobChange(CurrentClassJobId); 33 | } 34 | 35 | public static void Dispose() 36 | { 37 | Framework.Update -= OnFrameworkUpdate; 38 | } 39 | 40 | static ClassJobWatcher() 41 | { 42 | Framework.Update += OnFrameworkUpdate; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/DonutAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class DonutAoEEffectRangeData : EffectRangeData 7 | { 8 | public readonly byte InnerRadius; 9 | 10 | public DonutAoEEffectRangeData(uint actionId, 11 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 12 | sbyte range, byte effectRange, byte xAxisModifier, byte castType, 13 | byte innerRadius = 0, bool isOriginal = false) 14 | : base(actionId, actionCategory, isGT, harmfulness, 15 | range, effectRange, xAxisModifier, castType, isOriginal) 16 | { 17 | InnerRadius = innerRadius; 18 | } 19 | 20 | public DonutAoEEffectRangeData( 21 | Lumina.Excel.GeneratedSheets.Action actionRow, byte innerRadius = 0) 22 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 23 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 24 | actionRow.EffectRange, actionRow.XAxisModifier, actionRow.CastType, 25 | innerRadius, isOriginal: true) 26 | { } 27 | 28 | public DonutAoEEffectRangeData(EffectRangeData original, 29 | byte innerRadius, bool isOriginal = false) 30 | : this(original.ActionId, (uint)original.Category, original.IsGTAction, 31 | original.Harmfulness, original.Range, original.EffectRange, 32 | original.XAxisModifier, original.CastType, innerRadius, isOriginal) 33 | { } 34 | 35 | protected override string AdditionalFieldsToString() 36 | => $"InnerRadius: {InnerRadius}"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/CrossAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class CrossAoEEffectRangeData : EffectRangeData 7 | { 8 | public byte Width => XAxisModifier; 9 | public readonly float RotationOffset; 10 | 11 | public CrossAoEEffectRangeData(uint actionId, 12 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 13 | sbyte range, byte effectRange, byte xAxisModifier, byte castType, 14 | float rotationOffset = 0, bool isOriginal = false) 15 | : base(actionId, actionCategory, isGT, harmfulness, 16 | range, effectRange, xAxisModifier, castType, isOriginal) 17 | { 18 | RotationOffset = rotationOffset; 19 | } 20 | 21 | public CrossAoEEffectRangeData(Lumina.Excel.GeneratedSheets.Action actionRow, 22 | float rotationOffset = 0) 23 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 24 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 25 | actionRow.EffectRange, actionRow.XAxisModifier, actionRow.CastType, 26 | rotationOffset, isOriginal: true) 27 | { } 28 | 29 | public CrossAoEEffectRangeData(EffectRangeData original, 30 | float rotationOffset) 31 | : this(original.ActionId, (uint)original.Category, 32 | original.IsGTAction, original.Harmfulness, 33 | original.Range, original.EffectRange, 34 | original.XAxisModifier, original.CastType, 35 | rotationOffset, isOriginal: false) 36 | { } 37 | 38 | protected override string AdditionalFieldsToString() 39 | => $"RotationOffset: {RotationOffset}"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Drawing/Types/CircleAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | public class CircleAoEDrawData : DrawData 6 | { 7 | public readonly Vector3 Centre; 8 | public readonly int Radius; 9 | 10 | public CircleAoEDrawData(Vector3 centre, byte baseEffectRange, 11 | byte xAxisModifier, uint ringColour, uint fillColour) 12 | : base(ringColour, fillColour) 13 | { 14 | Centre = centre; 15 | Radius = baseEffectRange + xAxisModifier; 16 | } 17 | 18 | public override void Draw(ImDrawListPtr drawList) 19 | { 20 | if (Config.LargeDrawOpt == 1 && Radius >= Config.LargeThreshold) 21 | return; // no draw large 22 | 23 | var points = new Vector2[Config.NumSegments]; 24 | var seg = 2 * MathF.PI / Config.NumSegments; 25 | for (int i = 0; i < Config.NumSegments; i++) 26 | { 27 | Projection.WorldToScreen( 28 | new(Centre.X + Radius * MathF.Sin(i * seg), 29 | Centre.Y, 30 | Centre.Z + Radius * MathF.Cos(i * seg)), 31 | out var p, out var pr); 32 | 33 | // Dont add points that may be projected to weird positions 34 | if (pr.Z < -.1f) points[i] = new(float.NaN, float.NaN); 35 | else 36 | { 37 | points[i] = p; 38 | drawList.PathLineTo(p); 39 | } 40 | } 41 | 42 | if (Config.Filled 43 | && (Config.LargeDrawOpt == 0 || Radius < Config.LargeThreshold)) 44 | { 45 | drawList.PathFillConvex(FillColour); 46 | foreach (var p in points) 47 | if (!float.IsNaN(p.X)) drawList.PathLineTo(p); 48 | } 49 | if (Config.OuterRing) 50 | drawList.PathStroke(RingColour, ImDrawFlags.Closed, Config.Thickness); 51 | drawList.PathClear(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Configuration.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Template; 2 | using Dalamud.Configuration; 3 | using Newtonsoft.Json; 4 | namespace ActionEffectRange 5 | { 6 | [Serializable] 7 | public partial class Configuration : IPluginConfiguration 8 | { 9 | public int Version { get; set; } = 1; 10 | 11 | public bool Enabled = true; 12 | public bool EnabledPvP = true; 13 | 14 | public bool DrawBeneficial = true; 15 | public Vector4 BeneficialColour = new(.5f, .5f, 1, 1); 16 | public bool DrawHarmful = true; 17 | public Vector4 HarmfulColour = new(1, .5f, .5f, 1); 18 | 19 | public bool DrawACNPets = true; 20 | public bool DrawSummonedCompanions = true; 21 | public bool DrawGT = true; 22 | public bool DrawEx = false; 23 | 24 | public int LargeDrawOpt = 0; // 0 - normal, 1 - no draw, 2 - ring only 25 | public int LargeThreshold = 15; 26 | [JsonIgnore] public static readonly string[] LargeDrawOptions 27 | = new string[] { "Draw normally", "Do not draw", "Draw outline (outer ring) only" }; 28 | 29 | public bool OuterRing = true; 30 | public int Thickness = 2; 31 | 32 | public bool Filled = true; 33 | public float FillAlpha = .1f; 34 | 35 | public int NumSegments = 100; // smoothness 36 | 37 | public float DrawDelay = .4f; 38 | public float PersistSeconds = 1; 39 | 40 | public bool DrawWhenCasting = false; 41 | public Vector4 DrawWhenCastingColour = new(1f, 1f, .5f, 1); 42 | public bool DrawWhenCastingUntilCastEnd = true; 43 | 44 | public uint[] ActionBlacklist 45 | = Array.Empty(); 46 | public AoETypeDataItem[] AoETypeList 47 | = Array.Empty(); 48 | public ConeAoEAngleDataItem[] ConeAoeAngleList 49 | = Array.Empty(); 50 | 51 | public bool LogDebug = false; 52 | public bool ShowSponsor = false; 53 | 54 | 55 | public void Save() 56 | { 57 | PluginInterface.SavePluginConfig(this); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/ConeAoEEffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | 4 | namespace ActionEffectRange.Actions.EffectRange 5 | { 6 | public class ConeAoEEffectRangeData : EffectRangeData 7 | { 8 | public readonly float CentralAngleCycles; 9 | public readonly float RotationOffset; 10 | 11 | public ConeAoEEffectRangeData(uint actionId, 12 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 13 | sbyte range, byte effectRange, byte xAxisModifier, byte castType, 14 | float centralAngleCycles = 0, float rotationOffset = 0, 15 | bool isOriginal = false) 16 | : base(actionId, actionCategory, isGT, harmfulness, 17 | range, effectRange, xAxisModifier, castType, isOriginal) 18 | { 19 | CentralAngleCycles = centralAngleCycles; 20 | RotationOffset = rotationOffset; 21 | } 22 | 23 | public ConeAoEEffectRangeData(Lumina.Excel.GeneratedSheets.Action actionRow, 24 | float centralAngleCycles = 0, float rotationOffset = 0) 25 | : this(actionRow.RowId, actionRow.ActionCategory.Row, actionRow.TargetArea, 26 | ActionData.GetActionHarmfulness(actionRow), actionRow.Range, 27 | actionRow.EffectRange, actionRow.XAxisModifier, actionRow.CastType, 28 | centralAngleCycles, rotationOffset, isOriginal: true) 29 | { } 30 | 31 | public ConeAoEEffectRangeData(EffectRangeData original, 32 | float centralAngleCycles = 0, float rotationOffset = 0, 33 | bool isOriginal = true) 34 | : this(original.ActionId, (uint)original.Category, 35 | original.IsGTAction, original.Harmfulness, 36 | original.Range, original.EffectRange, 37 | original.XAxisModifier, original.CastType, 38 | centralAngleCycles, rotationOffset, isOriginal) 39 | { } 40 | 41 | protected override string AdditionalFieldsToString() 42 | => $"Angle: {CentralAngleCycles}, RotationOffset: {RotationOffset}"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Helpers/ActionManagerHelper.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ActionEffectRange.Helpers 5 | { 6 | internal unsafe static class ActionManagerHelper 7 | { 8 | private static readonly IntPtr actionMgrPtr; 9 | 10 | internal static IntPtr FpUseAction => 11 | (IntPtr)ActionManager.MemberFunctionPointers.UseAction; 12 | internal static IntPtr FpUseActionLocation => 13 | (IntPtr)ActionManager.MemberFunctionPointers.UseActionLocation; 14 | 15 | public static ushort CurrentSeq => actionMgrPtr != IntPtr.Zero 16 | ? (ushort)Marshal.ReadInt16(actionMgrPtr + 0x110) : (ushort)0; 17 | public static ushort LastRecievedSeq => actionMgrPtr != IntPtr.Zero 18 | ? (ushort)Marshal.ReadInt16(actionMgrPtr + 0x112) : (ushort)0; 19 | 20 | public static bool IsCasting => actionMgrPtr != IntPtr.Zero 21 | && Marshal.ReadByte(actionMgrPtr + 0x28) != 0; 22 | public static uint CastingActionId => actionMgrPtr != IntPtr.Zero 23 | ? (uint)Marshal.ReadInt32(actionMgrPtr + 0x24) : 0u; 24 | public static uint CastTargetObjectId => actionMgrPtr != IntPtr.Zero 25 | ? (uint)Marshal.ReadInt32(actionMgrPtr + 0x38) : 0u; 26 | public static float CastTargetPosX => actionMgrPtr != IntPtr.Zero 27 | ? Interop.MarshalFloat(actionMgrPtr + 0x40) : 0f; 28 | public static float CastTargetPosY => actionMgrPtr != IntPtr.Zero 29 | ? Interop.MarshalFloat(actionMgrPtr + 0x44) : 0f; 30 | public static float CastTargetPosZ => actionMgrPtr != IntPtr.Zero 31 | ? Interop.MarshalFloat(actionMgrPtr + 0x48) : 0f; 32 | // The player rotation when casting 33 | public static float CastRotation => actionMgrPtr != IntPtr.Zero 34 | ? Interop.MarshalFloat(actionMgrPtr + 0x50) : 0f; 35 | 36 | static ActionManagerHelper() 37 | { 38 | actionMgrPtr = (IntPtr)ActionManager.Instance(); 39 | if (actionMgrPtr == IntPtr.Zero) 40 | PluginLog.Warning("Ptr to ActionManager is 0"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Actions/Data/Containers/AoETypeOverridingList.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Predefined; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ActionEffectRange.Actions.Data.Containers 7 | { 8 | public class AoETypeOverridingList : IActionDataContainer 9 | { 10 | private readonly IDictionary predefinedDict; 11 | private readonly Dictionary customisedDict; 12 | private readonly Configuration config; 13 | 14 | public int PredefinedCount => predefinedDict.Count; 15 | public int CustomisedCount => customisedDict.Count; 16 | 17 | public AoETypeOverridingList(Configuration config) 18 | { 19 | this.config = config; 20 | predefinedDict = AoETypeOverridingMap.PredefinedSpecial; 21 | customisedDict = new(); 22 | Reload(); 23 | } 24 | 25 | public void Reload() 26 | { 27 | customisedDict.Clear(); 28 | foreach (var item in config.AoETypeList) 29 | Add(item); 30 | } 31 | 32 | public bool Add(AoETypeDataItem item) 33 | => item.ActionId > 0 && customisedDict.TryAdd(item.ActionId, item); 34 | 35 | public bool Remove(uint actionId) 36 | => customisedDict.Remove(actionId); 37 | 38 | public bool Contains(uint actionId) 39 | => predefinedDict.ContainsKey(actionId) 40 | || customisedDict.ContainsKey(actionId); 41 | 42 | // Get customised data first 43 | public bool TryGet(uint actionId, out AoETypeDataItem? item) 44 | => customisedDict.TryGetValue(actionId, out item) 45 | || predefinedDict.TryGetValue(actionId, out item); 46 | 47 | public IEnumerable CopyCustomised() 48 | => new List(customisedDict.Values).AsEnumerable(); 49 | 50 | public void Save(bool writeToFile = false) 51 | { 52 | config.AoETypeList = CopyCustomised().ToArray(); 53 | if (writeToFile) config.Save(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Actions/Data/Containers/ConeAoeAngleOverridingList.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Predefined; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ActionEffectRange.Actions.Data.Containers 7 | { 8 | public class ConeAoeAngleOverridingList : IActionDataContainer 9 | { 10 | private readonly IDictionary predefinedDict; 11 | private readonly Dictionary customisedDict; 12 | private readonly Configuration config; 13 | 14 | public int PredefinedCount => predefinedDict.Count; 15 | public int CustomisedCount => customisedDict.Count; 16 | 17 | 18 | public ConeAoeAngleOverridingList(Configuration config) 19 | { 20 | this.config = config; 21 | predefinedDict = ConeAoEAngleMap.PredefinedActionMap; 22 | customisedDict = new(); 23 | Reload(); 24 | } 25 | 26 | public void Reload() 27 | { 28 | customisedDict.Clear(); 29 | foreach (var item in config.ConeAoeAngleList) 30 | Add(item); 31 | } 32 | 33 | public bool Add(ConeAoEAngleDataItem item) 34 | => item.ActionId > 0 && customisedDict.TryAdd(item.ActionId, item); 35 | 36 | public bool Remove(uint actionId) 37 | => customisedDict.Remove(actionId); 38 | 39 | public bool Contains(uint actionId) 40 | => predefinedDict.ContainsKey(actionId) 41 | || customisedDict.ContainsKey(actionId); 42 | 43 | // Get customised data first 44 | public bool TryGet(uint actionId, out ConeAoEAngleDataItem? item) 45 | => customisedDict.TryGetValue(actionId, out item) 46 | || predefinedDict.TryGetValue(actionId, out item); 47 | 48 | public IEnumerable CopyCustomised() 49 | => new List(customisedDict.Values).AsEnumerable(); 50 | 51 | public void Save(bool writeToFile = false) 52 | { 53 | config.ConeAoeAngleList = CopyCustomised().ToArray(); 54 | if (writeToFile) config.Save(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActionEffectRange 2 | 3 | A FFXIV Dalamud plugin that provides a visual cue on the effect range of the AoE action the player has just used. 4 | 5 | May be used as a supplement/replacement to the actions' VFXs in showing effect range related information, 6 | such as where has the action landed and how large an area it covered. 7 | 8 | 16 | 17 | 18 | ## How to Install 19 | 20 | [XIVLauncher](https://github.com/goatcorp/FFXIVQuickLauncher) is required to install and run the plugin. 21 | 22 | Add my [Dalamud plugin repo](https://github.com/yomishino/MyDalamudPlugins) to Dalamud's Custom Plugin Repositories. 23 | 24 | Once added, look for the plugin "ActionEffectRange" in Plugin Installer's available plugins. 25 | 26 | 27 | ## Disclaimer 28 | 29 | 1. Because the visuals are drawn on an overlay without any current context/knowledge about the in-game geographical features etc., 30 | it can sometimes look distorted or "hovered in the air" depending on the terrain and/or camera angle. 31 | 32 | 2. Please expect errors in calculation. 33 | There are minor ones due to network latency that are not possible to fix. 34 | For other errors, please feel free to open issues to report them. 35 | 36 | 3. Some data (such as Cone AoE angles) are not found in the client (as far as I know). 37 | For these, I have to find out by myself, but I am unable to guarantee when this could be done 38 | after each game update when new actions or changes to existing actions are introduced 39 | (especially since I do not have much time to work on the plugin or even for the game itself now). 40 | So any help is welcome and appreciated! 41 | 42 | 43 | ## Known Issues 44 | 45 | - Dancer's "Curing Waltz" (PvE #16015, PvP #29429): Not showing effect range for additional effect (AoE heal around partner) 46 | - Ninja's "Hollow Nozuchi" (#25776): Not showing effect range on Doton area 47 | - Reaper's "Arcane Crest" (#24404): Not showing effect range when the barrier effect is triggered 48 | -------------------------------------------------------------------------------- /src/Helpers/PetWatcher.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Objects.Types; 2 | using Lumina.Excel; 3 | using GeneratedSheets = Lumina.Excel.GeneratedSheets; 4 | 5 | namespace ActionEffectRange.Helpers 6 | { 7 | internal static class PetWatcher 8 | { 9 | private static readonly ExcelSheet? petSheet 10 | = DataManager.GetExcelSheet(); 11 | 12 | public static bool HasPetPresent => BuddyList.PetBuddy != null; 13 | 14 | public static GameObject? GetPet() 15 | => BuddyList.PetBuddy?.GameObject; 16 | 17 | public static uint GetPetObjectId() 18 | => GetPet()?.ObjectId ?? 0; 19 | 20 | public static Vector3 GetPetPosition() 21 | => GetPet()?.Position ?? new(); 22 | 23 | public static float GetPetRotation() 24 | => GetPet()?.Rotation ?? 0; 25 | 26 | // Pet sheet key id 27 | public static uint GetPetType() 28 | => BuddyList.PetBuddy?.DataID ?? 0; 29 | 30 | public static bool IsNamelessPet(uint petType) 31 | => petType > 0 32 | && string.IsNullOrEmpty(petSheet?.GetRow(petType)?.Name.RawString); 33 | 34 | public static bool IsBunshin(uint petType) 35 | => petType == 19 || petType == 22; 36 | 37 | // Bunshin is named but has no interactable models 38 | public static bool IsNamelessPetOrBunshin(uint petType) 39 | => IsNamelessPet(petType) || IsBunshin(petType); 40 | 41 | public static bool IsCurrentPetNameless() 42 | => IsNamelessPet(GetPetType()); 43 | 44 | public static bool IsCurrentPetNamelessOrBunshin() 45 | => IsNamelessPetOrBunshin(GetPetType()); 46 | 47 | // Can't find out how to reliably check from pets so just check the job 48 | public static bool IsCurrentPetACNPet() 49 | => !IsCurrentPetNamelessOrBunshin() 50 | && ClassJobWatcher.IsCurrentClassJobACNRelated(); 51 | 52 | public static bool IsCurrentPetNonACNNamedPet() 53 | => !IsCurrentPetNamelessOrBunshin() 54 | && !ClassJobWatcher.IsCurrentClassJobACNRelated(); 55 | 56 | public static bool IsCurrentPet(uint objectId) 57 | => GetPetObjectId() == objectId; 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Drawing/Types/DashAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | public class DashAoEDrawData : DrawData 6 | { 7 | public readonly Vector3 Origin; 8 | public readonly Vector3 Target; 9 | public readonly Vector3 Direction; 10 | public readonly byte EffectRange; 11 | public readonly float Width; 12 | 13 | // No padding to length of the AoE which is unlike LineAoE. 14 | // Seems for pvp actions, an actor can actually hit the an enemy only if 15 | // the targeted position is at least (exactly) the same or further past 16 | // the enemy's position. (Tested on duelling ground.) 17 | // Dummies seem to still get dmg as if the AoE had the padded length. 18 | // Actually, not sure about player enemies in instances... 19 | // 20 | // Patch 6.1~: for SAM's new Soten, while action user will pass through 21 | // the target entirely, there seems to be no dmg or other effects happen 22 | // within the extra area from target's position to user's final position? 23 | // Perhaps it is just the effect calculation as usual for this type 24 | // and then character somehow "warps" to the final position??? 25 | // 26 | // TODO: Width for new Soten? 27 | // Old Soten has EffectRange=1 and XAxisModifier=4 while the new one has 0 and 4, 28 | // but the EffectRange for old Soten may be for the GT action hitbox circle instead. 29 | // The old Soten seemed to have a width of 2; not sure about the new one. 30 | // Using XAxisModifier/2 for now just to be consistent numerically; 31 | // but this may be incorrect... 32 | public DashAoEDrawData(Vector3 origin, Vector3 target, 33 | byte effectRange, byte xAxisModifier, uint ringColour, uint fillColour) 34 | : base(ringColour, fillColour) 35 | { 36 | Origin = origin; 37 | Target = target; 38 | Direction = target - origin; 39 | EffectRange = effectRange; 40 | //Width = effectRange * 2; 41 | Width = xAxisModifier / 2; 42 | } 43 | 44 | 45 | public override void Draw(ImDrawListPtr drawList) 46 | => DrawRect(drawList, Width, Origin, Target, Direction); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/EffectRangeCornerCases.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.EffectRange; 2 | 3 | namespace ActionEffectRange.Actions.Data.Predefined 4 | { 5 | // The remaining cases for EffectRangeData overriding 6 | public static class EffectRangeCornerCases 7 | { 8 | public static EffectRangeData UpdateEffectRangeData(EffectRangeData erdata) 9 | => erdata.ActionId switch 10 | { 11 | // PvE 12 | 13 | // Liturgy of the bell (placing) (WHM) 14 | // Use the effect range from #25863 15 | 25862 => new CircleAoEEffectRangeData( 16 | erdata.ActionId, (uint)erdata.Category, 17 | erdata.IsGTAction, erdata.Harmfulness, 18 | 0, ActionData.GetActionExcelRow(25863)?.EffectRange ?? 0, 19 | 0, erdata.CastType, false), 20 | 21 | // PvP 22 | 23 | // Eventide (DRK PvP) 24 | // Change to bidirected line AoE (front+back) 25 | // The original effect range is for front + back in total. 26 | 29097 => new BidirectedLineAoEEffectRangeData( 27 | erdata.ActionId, (uint)erdata.Category, 28 | erdata.IsGTAction, erdata.Harmfulness, erdata.Range, 29 | erdata.EffectRange, erdata.XAxisModifier, 30 | erdata.CastType, 0, isOriginal: true), 31 | 32 | // Hissatsu: Soten (SAM PvP) 33 | // Providing EffectRange 34 | // EffectRange of 1 is from Excel data for the old PvP Soten skill 35 | 29532 => new DashAoEEffectRangeData( 36 | erdata.ActionId, (uint)erdata.Category, 37 | erdata.IsGTAction, erdata.Harmfulness, 38 | erdata.Range, 1, erdata.XAxisModifier, 39 | erdata.CastType, isOriginal: false), 40 | 41 | // Southern Cross (White/Black) (RDM PvP) 42 | // Southern Cross (Black) (RDM PvP) 43 | // Seems to have a 45-degree rotation offset 44 | 29704 or 29705 => new CrossAoEEffectRangeData( 45 | erdata, -MathF.PI / 4), 46 | 47 | _ => erdata 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Drawing/Projection.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Utility; 2 | using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace ActionEffectRange.Drawing 6 | { 7 | internal unsafe static class Projection 8 | { 9 | private delegate IntPtr GetMatrixSingletonDelegate(); 10 | private static readonly GetMatrixSingletonDelegate getMatrixSingleton; 11 | 12 | private static readonly Device* device; 13 | 14 | static Projection() 15 | { 16 | IntPtr addr = Plugin.SigScanner.ScanText( 17 | "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) 23 | => WorldToScreen(worldPos, out screenPos, out _); 24 | 25 | internal static unsafe bool WorldToScreen( 26 | 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 | if (device == null) 36 | throw new InvalidOperationException("Cannot get Device instance"); 37 | 38 | var windowPos = ImGuiHelpers.MainViewport.Pos; 39 | 40 | var viewProjectionMatrix = *(Matrix4x4*)(matrixSingleton + 0x1b4); 41 | 42 | float width = device->Width; 43 | float height = device->Height; 44 | 45 | var pCoords = Vector3.Transform(worldPos, viewProjectionMatrix); 46 | pCoordsRaw = pCoords; 47 | 48 | screenPos = new Vector2( 49 | pCoords.X / MathF.Abs(pCoords.Z), 50 | pCoords.Y / MathF.Abs(pCoords.Z)); 51 | 52 | screenPos.X = (0.5f * width * (screenPos.X + 1f)) + windowPos.X; 53 | screenPos.Y = (0.5f * height * (1f - screenPos.Y)) + windowPos.Y; 54 | 55 | return pCoords.Z > 0 56 | && screenPos.X > windowPos.X && screenPos.X < windowPos.X + width 57 | && screenPos.Y > windowPos.Y && screenPos.Y < windowPos.Y + height; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Actions/SeqSnapshot.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Helpers; 2 | using System.Numerics; 3 | using System; 4 | 5 | namespace ActionEffectRange.Actions 6 | { 7 | public class SeqSnapshot 8 | { 9 | public readonly ushort Seq; 10 | public readonly Vector3 PlayerPosition; 11 | public readonly float PlayerRotation; 12 | public readonly uint TargetObjectId; 13 | public readonly Vector3 TargetPosition; 14 | public readonly uint PetObjectId; 15 | public readonly Vector3 PetPosition; 16 | public readonly float PetRotation; 17 | public readonly DateTime CreatedTime = DateTime.Now; 18 | public double ElapsedSeconds 19 | => (DateTime.Now - CreatedTime).TotalSeconds; 20 | public bool HadValidTarget => TargetObjectId != 0 21 | && TargetObjectId != InvalidGameObjectId; 22 | public bool WasPetPresent => PetObjectId != 0 23 | && PetObjectId != InvalidGameObjectId; 24 | 25 | public SeqSnapshot(ushort seq, Vector3 playerPos, float playerRotation, 26 | uint targetObjId, Vector3 targetPos, 27 | uint petObjId, Vector3 petPos, float petRotation) 28 | { 29 | Seq = seq; 30 | PlayerPosition = playerPos; 31 | PlayerRotation = playerRotation; 32 | TargetObjectId = targetObjId; 33 | TargetPosition = targetPos; 34 | PetObjectId = petObjId; 35 | PetPosition = petPos; 36 | PetRotation = petRotation; 37 | } 38 | 39 | public SeqSnapshot(ushort seq, uint targetObjId, Vector3 targetPos) 40 | : this(seq, LocalPlayer?.Position ?? new(), LocalPlayer?.Rotation ?? 0, 41 | targetObjId, targetPos, 42 | PetWatcher.GetPetObjectId(), 43 | PetWatcher.GetPetPosition(), 44 | PetWatcher.GetPetRotation()) 45 | { } 46 | 47 | public SeqSnapshot(ushort seq) 48 | : this(seq, LocalPlayer?.ObjectId ?? 0, LocalPlayer?.Position ?? new()) 49 | { } 50 | 51 | public SeqSnapshot(ushort seq, uint targetObjId, Vector3 targetPos, 52 | uint petObjId, Vector3 petPos, float petRotation) 53 | : this(seq, LocalPlayer?.Position ?? new(), LocalPlayer?.Rotation ?? 0, 54 | targetObjId, targetPos, petObjId, petPos, petRotation) 55 | { } 56 | 57 | public SeqSnapshot(ushort seq, uint petObjId, Vector3 petPos, float petRotation) 58 | : this(seq, petObjId, petPos, petObjId, petPos, petRotation) 59 | { } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/UI/ActionBlacklistEditUi.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using Dalamud.Interface; 4 | using Dalamud.Interface.Components; 5 | using ImGuiNET; 6 | 7 | namespace ActionEffectRange.UI 8 | { 9 | 10 | public class ActionBlacklistEditUI : ActionDataEditUi 11 | { 12 | protected override Vector2 InitialUiSize => new(400, 400); 13 | protected override string UiName => "Action Blacklist"; 14 | 15 | protected override DataTableModel DataTableViewModel { get; set; } = null!; 16 | 17 | public ActionBlacklistEditUI() 18 | { 19 | DataTableViewModel = new( 20 | "ActionEffectRange_Tbl_ActionBlacklistConfig", 21 | actionId => ActionData.RemoveFromActionBlacklist(actionId)); 22 | } 23 | 24 | public override void DrawContents() 25 | { 26 | DrawIntro(); 27 | DataTableViewModel.DrawTable( 28 | ActionData.GetCustomisedActionBlacklistCopy()); 29 | DrawActionSearchUi(); 30 | DrawActionInputPreviewUi(); 31 | 32 | } 33 | 34 | private static void DrawIntro() 35 | { 36 | ImGuiExt.MultiTextWrapped( 37 | "Action in this blacklist will not be drawn.", 38 | "You can use this blacklist to prevent a particular action " + 39 | "from being drawn regardless of other settings."); 40 | ImGui.NewLine(); 41 | } 42 | 43 | private void DrawActionInputPreviewUi() 44 | { 45 | ImGui.PushID("ActionBlacklistInputPreview"); 46 | if (selectedMatchedActionRow != null) 47 | { 48 | ImGui.Text($"Add this action to Blacklist?"); 49 | ImGui.Indent(); 50 | ImGui.Text(ActionDataInterfacing.GetActionDescription( 51 | selectedMatchedActionRow)); 52 | ImGui.Unindent(); 53 | if (ImGuiExt.IconButton(1, FontAwesomeIcon.Plus, "Add to Blacklist")) 54 | { 55 | ActionData.AddToActionBlacklist(selectedMatchedActionRow.RowId); 56 | ClearEditInput(); 57 | } 58 | ImGui.SameLine(); 59 | if (ImGuiExt.IconButton(2, FontAwesomeIcon.Times, "Clear Input")) 60 | ClearEditInput(); 61 | } 62 | else 63 | ImGuiComponents.DisabledButton(FontAwesomeIcon.Plus); 64 | ImGui.NewLine(); 65 | ImGui.PopID(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Actions/Data/Containers/ActionBlacklist.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Predefined; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ActionEffectRange.Actions.Data.Containers 7 | { 8 | public class ActionBlacklist : IActionDataContainer 9 | { 10 | private readonly Configuration config; 11 | private readonly HashSet predefinedBlacklist; 12 | private readonly HashSet customisedBlacklist; 13 | 14 | public int PredefinedCount => predefinedBlacklist.Count; 15 | public int CustomisedCount => customisedBlacklist.Count; 16 | 17 | 18 | public ActionBlacklist(Configuration config) 19 | { 20 | this.config = config; 21 | predefinedBlacklist = new(RuledOutActions.PredefinedList); 22 | customisedBlacklist = new(); 23 | Reload(); 24 | } 25 | 26 | public void Reload() 27 | { 28 | customisedBlacklist.Clear(); 29 | customisedBlacklist.UnionWith(config.ActionBlacklist); 30 | } 31 | 32 | public bool Contains(uint actionId) 33 | => customisedBlacklist.Contains(actionId) 34 | || predefinedBlacklist.Contains(actionId); 35 | 36 | public bool Add(uint actionId) 37 | => actionId > 0 && Add(PackDataItem(actionId)); 38 | 39 | public bool Add(BlacklistedActionDataItem item) 40 | => customisedBlacklist.Add(item.ActionId); 41 | 42 | public bool Remove(uint actionId) 43 | => customisedBlacklist.Remove(actionId); 44 | 45 | public bool TryGet(uint actionId, out uint item) 46 | => customisedBlacklist.TryGetValue(actionId, out item) 47 | || predefinedBlacklist.TryGetValue(actionId, out item); 48 | 49 | public bool TryGet(uint actionId, out BlacklistedActionDataItem? item) 50 | { 51 | item = Contains(actionId) ? PackDataItem(actionId) : null; 52 | return item != null; 53 | } 54 | 55 | public IEnumerable CopyCustomisedRaw() 56 | => new List(customisedBlacklist).AsEnumerable(); 57 | 58 | public IEnumerable CopyCustomised() 59 | => CopyCustomisedRaw().Select(PackDataItem).ToList().AsEnumerable(); 60 | 61 | public void Save(bool writeToFile = false) 62 | { 63 | config.ActionBlacklist = CopyCustomisedRaw().ToArray(); 64 | if (writeToFile) config.Save(); 65 | } 66 | 67 | private static BlacklistedActionDataItem PackDataItem(uint actionId) 68 | => new(actionId); 69 | 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /.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/Actions/Data/Predefined/PetActionMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace ActionEffectRange.Actions.Data.Predefined 5 | { 6 | // Not used now 7 | public static class PetActionMap 8 | { 9 | public static readonly ImmutableDictionary> Dictionary = new KeyValuePair>[] 10 | { 11 | // PvE 12 | 13 | new(25799, new uint[]{ 25841 }.ToImmutableHashSet()), // radiant aegis 14 | new(25801, new uint[]{ 25842 }.ToImmutableHashSet()), // Searing light 15 | 16 | //new(25802, new uint[]{}.ToImmutableHashSet()), // summon ruby 17 | //new(25803, new uint[]{}.ToImmutableHashSet()), // summon topaz 18 | //new(25804, new uint[]{}.ToImmutableHashSet()), // summon emerald 19 | new(25805, new uint[]{ 25852 }.ToImmutableHashSet()), // summon ifrit -> inferno 20 | new(25838, new uint[]{ 25852 }.ToImmutableHashSet()), // summon ifrit ii -> inferno 21 | new(25806, new uint[]{ 25853 }.ToImmutableHashSet()), // summon titan -> earthen fury 22 | new(25839, new uint[]{ 25853 }.ToImmutableHashSet()), // summon titan ii -> earthen fury 23 | new(25807, new uint[]{ 25854 }.ToImmutableHashSet()), // summon garuda -> aerial blast 24 | new(25840, new uint[]{ 25854 }.ToImmutableHashSet()), // summon garuda ii -> aerial blast 25 | 26 | new(25831, new uint[]{ 16517 }.ToImmutableHashSet()), // summon phoenix -> everlasting flight 27 | 28 | new(7429, new uint[]{ 7449 }.ToImmutableHashSet()), // enkindle bahamut -> akh morn 29 | new(16516, new uint[]{ 16518 }.ToImmutableHashSet()), // enkindle phoenix -> revelation 30 | 31 | new(16537, new uint[]{ 803, 16550 }.ToImmutableHashSet()), // whispering dawn -> ~ / angel's whisper 32 | new(16538, new uint[]{ 805, 16551 }.ToImmutableHashSet()), // fey illumination -> ~ / seraphic illumination 33 | new(7437, new uint[]{ 7438 }.ToImmutableHashSet()), // aetherpact -> fey union 34 | new(16543, new uint[]{ 16544 }.ToImmutableHashSet()), // fey blessing 35 | new(16546, new uint[]{ 16547 }.ToImmutableHashSet()), // consolation 36 | 37 | // PvP 38 | 39 | new(29674, new uint[] { 29677 }.ToImmutableHashSet()), // Enkindle Bahamut -> Megaflare (SMN PvP) 40 | new(29679, new uint[] { 29682 }.ToImmutableHashSet()), // Enkindle Pheonix -> Revelation (SMN PvP) 41 | new(29238, new uint[] { 29241 }.ToImmutableHashSet()), // Consolation -> Consolation (SCH PvP) 42 | 43 | }.ToImmutableDictionary(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Drawing/Types/CrossAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | 6 | public class CrossAoEDrawData : DrawData 7 | { 8 | public readonly Vector3 Origin; 9 | public readonly Vector3 Direction; 10 | public readonly float Length; 11 | public readonly float Width; 12 | public readonly Vector3 End; 13 | public readonly Vector3 End2; 14 | public readonly Vector3 CrossDirection; 15 | public readonly Vector3 CrossEnd; 16 | public readonly Vector3 CrossEnd2; 17 | 18 | 19 | public CrossAoEDrawData(Vector3 origin, Vector3 target, 20 | byte baseEffectRange, byte xAxisModifier, float rotationOffset, 21 | uint ringColour, uint fillColour) 22 | : base(ringColour, fillColour) 23 | { 24 | // TODO: do cross's have the extra +.5 as do lines? 25 | Length = baseEffectRange + .5f; 26 | Width = xAxisModifier; 27 | 28 | Origin = origin; 29 | // Basic direction is from origin to target 30 | // then further rotate by the offset. 31 | // (This assumes the action's direction is target-based) 32 | Direction = CalcDirection(origin, target, rotationOffset); 33 | CrossDirection = new Vector3( 34 | Direction.Z, Direction.Y, -Direction.X); // perpendicular 35 | 36 | // Endpoint is target based 37 | End = CalcFarEndWorldPos(target, Direction, Length); 38 | End2 = CalcFarEndWorldPos(target, -Direction, Length); 39 | CrossEnd = CalcFarEndWorldPos(target, CrossDirection, Length); 40 | CrossEnd2 = CalcFarEndWorldPos(target, -CrossDirection, Length); 41 | } 42 | 43 | public override void Draw(ImDrawListPtr drawList) 44 | { 45 | #if DEBUG 46 | Projection.WorldToScreen(End, out var pe, out _); 47 | drawList.AddCircleFilled(pe, Config.Thickness * 2, 48 | RingColour); 49 | Projection.WorldToScreen(End2, out var pe2, out _); 50 | drawList.AddCircleFilled(pe2, Config.Thickness * 2, 51 | ImGui.ColorConvertFloat4ToU32(new(0,0,1,1))); 52 | Projection.WorldToScreen(CrossEnd, out var pc, out _); 53 | drawList.AddCircleFilled(pc, Config.Thickness * 2, 54 | ImGui.ColorConvertFloat4ToU32(new(1, 0, 0, 1))); 55 | Projection.WorldToScreen(CrossEnd2, out var pc2, out _); 56 | drawList.AddCircleFilled(pc2, Config.Thickness * 2, 57 | ImGui.ColorConvertFloat4ToU32(new(1, 1, 0, 1))); 58 | #endif 59 | // TODO: Making the shapes not overlapped 60 | var w2 = Width / 2; 61 | DrawRect(drawList, w2, End, End2, Direction); 62 | DrawRect(drawList, w2, CrossEnd, CrossEnd2, CrossDirection); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/UI/ActionDataInterfacing.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Enums; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | using ExcSheets = Lumina.Excel.GeneratedSheets; 7 | 8 | namespace ActionEffectRange.UI 9 | { 10 | public static class ActionDataInterfacing 11 | { 12 | public static IEnumerable? GetAllPartialMatchActionExcelRows( 13 | string input, bool alsoMatchId, int maxCount, 14 | bool playerCombatActionOnly, Func? filter) 15 | => ActionData.ActionExcelSheet?.Where(row => row != null 16 | && (row.Name.RawString.Contains(input, 17 | StringComparison.CurrentCultureIgnoreCase) 18 | || alsoMatchId && row.RowId.ToString().Contains(input)) 19 | && (!playerCombatActionOnly || ActionData.IsPlayerCombatAction(row)) 20 | && (filter == null || filter(row))) 21 | .Take(maxCount); 22 | 23 | public static string GetActionDescription(ExcSheets.Action row) 24 | { 25 | var classjobRow = row.ClassJob.Value; 26 | var classjob = classjobRow != null && classjobRow.RowId > 0 27 | ? $" [{classjobRow.Abbreviation}]" : string.Empty; 28 | var pvp = row.IsPvP ? " [PvP]" : string.Empty; 29 | return $"#{row.RowId} {row.Name}{classjob}{pvp}"; 30 | } 31 | 32 | public static string GetAoETypeLabel(ActionAoEType type) 33 | => type switch 34 | { 35 | ActionAoEType.None => "N/A", 36 | ActionAoEType.Circle or ActionAoEType.Circle2 => "Circle", 37 | ActionAoEType.Cone or ActionAoEType.Cone2 => "Cone", 38 | ActionAoEType.Line or ActionAoEType.Line2 => "Line", 39 | ActionAoEType.GT => "Circle (GT)", 40 | ActionAoEType.DashAoE => "Dash (Line)", 41 | ActionAoEType.Donut => "Donut", 42 | _ => "?" 43 | }; 44 | 45 | public static string GetAoETypeLabel(byte castType) 46 | => GetAoETypeLabel((ActionAoEType)castType); 47 | 48 | public static ActionAoEType[] AoETypeSelections 49 | => new ActionAoEType[] 50 | { 51 | ActionAoEType.Circle, 52 | ActionAoEType.Cone, 53 | ActionAoEType.Line, 54 | ActionAoEType.DashAoE, 55 | ActionAoEType.Donut, 56 | ActionAoEType.GT 57 | }; 58 | 59 | public static ActionHarmfulness[] ActionHarmfulnessesSelections 60 | => Enum.GetValues(); 61 | 62 | public static float DegToCycle(float deg) => deg / 360; 63 | 64 | public static float CycleToDeg(float cycle) => cycle * 360; 65 | 66 | public static float DegToRad(float deg) 67 | => MathF.PI * deg / 180; 68 | 69 | public static float RadToDeg(float rad) 70 | => 180 * rad / MathF.PI; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/ConeAoEAngleMap.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Template; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | 5 | namespace ActionEffectRange.Actions.Data.Predefined 6 | { 7 | // May be inaccurate 8 | public static class ConeAoEAngleMap 9 | { 10 | // All central angles used internally are in cycles (1 cycle = 2pi) 11 | // RotaionOffsets are in radians though, 12 | // to be consistent and directly applicable with character rotation 13 | 14 | public const float DefaultAngleCycles = 1f / 3f; 15 | 16 | public static readonly ImmutableDictionary DefaultAnglesByRange 17 | = new KeyValuePair[] 18 | { 19 | new(6, .25f), 20 | new(8, 1f / 3f), 21 | new(12, .25f) 22 | }.ToImmutableDictionary(); 23 | 24 | public static ImmutableDictionary PredefinedActionMap 25 | => new KeyValuePair[] 26 | { 27 | GeneratePair(7385, 1f / 3f, MathF.PI), // Passage of Arms (PLD) 28 | GeneratePair(24392, .5f), // Grim Swathe (RPR) 29 | GeneratePair(24384, .5f), // Guillotine (RPR) 30 | GeneratePair(24397, .5f), // Grim Reaping (RPR) 31 | GeneratePair(24400, .5f), // Lemure's Scythe (RPR) 32 | GeneratePair(7418, .25f), // Flamethrower (MCH) 33 | GeneratePair(25791, 1f / 3f), // Fan Dance IV (DNC) 34 | 35 | GeneratePair(29428, .25f), // Fan Dance (DNC PvP) 36 | // --- idk why the dev team decided to make it smaller than PvE version but nah 37 | GeneratePair(29547, .5f), // Grim Swathe (RPR PvP) 38 | GeneratePair(29548, .5f), // Lemure's Slice (RPR PvP) 39 | 40 | // BLU up to #104 41 | // Most BLU cone aoe with effect range 6 seems 90-degree, exceptions exist 42 | GeneratePair(11390, .25f), // aqua breath (BLU) #3 43 | GeneratePair(11403, .25f), // faze (BLU) #23 44 | GeneratePair(11399, 1f / 3f), // the look (BLU) #27 45 | GeneratePair(11388, .25f), // bad breath (BLU) #28 46 | GeneratePair(11430, 2f / 3f), // glass dance (BLU) #48 47 | GeneratePair(18296, 1f / 12f), // protean wave (BLU) #51 48 | GeneratePair(18323, 1f / 3f), // surpanakha (BLU) #78 49 | GeneratePair(23288, .25f), // phantom flurry (BLU) #103 50 | GeneratePair(23289, .5f), // phantom flurry (2nd phase) (BLU) 51 | }.ToImmutableDictionary(); 52 | 53 | private static KeyValuePair 54 | GeneratePair(uint actionId, float angle, float rotationOffset = 0) 55 | => new(actionId, new(actionId, angle, rotationOffset)); 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Actions/Data/Predefined/HarmfulnessMap.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Enums; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | 5 | using static ActionEffectRange.Actions.Enums.ActionHarmfulness; 6 | 7 | namespace ActionEffectRange.Actions.Data.Predefined 8 | { 9 | public class HarmfulnessMap 10 | { 11 | public static readonly ImmutableDictionary Dictionary = 12 | new KeyValuePair[] 13 | { 14 | new(2270, Harmful), // Doton (NIN) 15 | new(3571, Both), // Assize (WHM) 16 | new(3639, Harmful), // salted earth (DRK) 17 | new(7439, Both), // earthly star (AST) (player action of placing earthly star) 18 | // #8324 - stellar detonation (AST) (player action of "explosion" of earthly star; mapped to 7440 or 7441) 19 | new(7440, Both), // stellar burst (Pet-like action from player executing #8324 stellar detonation) 20 | new(7441, Both), // stellar explosion (Pet-like action from player executing #8324 stellar detonation) 21 | new(16193, Both), // Single Technical Finish (DNC) 22 | new(16194, Both), // Double Technical Finish (DNC) 23 | new(16195, Both), // Triple Technical Finish (DNC) 24 | new(16196, Both), // Quadruple Technical Finish (DNC) 25 | new(16544, Beneficial), // Fey Blessing (SCH) (action by pet) 26 | new(16553, Beneficial), // celestial opposition (AST) 27 | new (25874, Both), // macrocosmos (AST) 28 | 29 | new(29094, Both), // Salted Earth (DRK PvP) 30 | new(29130, Harmful), // Relentless Rush (GNB PvP) 31 | new(29229, Both), // Seraph Strike (WHM PvP) 32 | new(29234, Both), // Deployment Tactics (SCH PvP) 33 | new(29253, Both), // Macrocosmos (AST PvP) 34 | new(29255, Both), // Celestial River (AST PvP) 35 | new(29266, Both), // Mesotes (SGE PvP) 36 | new(29267, Both), // Mesotes (SGE PvP) (after #29266) 37 | new(29412, Both), // Bishop Autoturret (MCH PvP) 38 | new(29413, Both), // Aether Mortar (MCH PvP) (after #29412) 39 | new(29422, Harmful), // Honing Dance (DNC PvP) 40 | // (#29423~29427 triggered on #29422 ended or on #29470 used), effect based on stacks 41 | new(29423, Both), // Honing Ovation (DNC PvP) 42 | new(29424, Both), // Honing Ovation (DNC PvP) 43 | new(29425, Both), // Honing Ovation (DNC PvP) 44 | new(29426, Both), // Honing Ovation (DNC PvP) 45 | new(29427, Both), // Honing Ovation (DNC PvP) 46 | new(29514, Harmful), // Doton (NIN PvP) 47 | new(29673, Harmful), // Summon Bahamut (SMN PvP) 48 | new(29685, Both), // Verholy (RDM PvP) (white) 49 | new(29704, Both), // Southern Cross (RDM PvP) (white) 50 | new(29705, Both), // Southern Cross (RDM PvP) (black) 51 | new(29706, Beneficial), // Pneuma (SGE PvP) additional circle beneficial effect from #29260 52 | }.ToImmutableDictionary(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Drawing/Workers/DrawWorker.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.EffectRange; 2 | using ActionEffectRange.Actions.Enums; 3 | using ImGuiNET; 4 | using System.Collections.Generic; 5 | 6 | namespace ActionEffectRange.Drawing.Workers 7 | { 8 | public class DrawWorker : IDrawWorker 9 | { 10 | private readonly Queue drawDataQueue = new(); 11 | 12 | private uint beneficialRingColour; 13 | private uint beneficialFillColour; 14 | private uint harmfulRingColour; 15 | private uint harmfulFillColour; 16 | 17 | public DrawTrigger Trigger => DrawTrigger.Used; 18 | 19 | public DrawWorker() 20 | { 21 | RefreshConfig(); 22 | } 23 | 24 | public void Clear() 25 | { 26 | drawDataQueue.Clear(); 27 | } 28 | 29 | public void CleanupOld() 30 | { 31 | while (drawDataQueue.TryPeek(out var head) 32 | && head.ElapsedSeconds > Config.DrawDelay + Config.PersistSeconds) 33 | drawDataQueue.Dequeue(); 34 | } 35 | 36 | public void Draw(ImDrawListPtr drawList) 37 | { 38 | foreach (var data in drawDataQueue) 39 | { 40 | if (data.ElapsedSeconds < Config.DrawDelay) continue; 41 | data.Draw(ImGui.GetWindowDrawList()); 42 | } 43 | } 44 | 45 | public bool HasDataToDraw() => drawDataQueue.Count > 0; 46 | 47 | public void QueueDrawing(uint sequence, EffectRangeData effectRangeData, 48 | Vector3 originPos, Vector3 targetPos, float rotation) 49 | { 50 | if (!IsPlayerLoaded) return; 51 | LogUserDebug($"{GetType().Name}.QueueDrawing => " + 52 | $"{effectRangeData}, orig={originPos}, target={targetPos}, rotation={rotation}"); 53 | 54 | if (effectRangeData.Harmfulness.HasFlag(ActionHarmfulness.Harmful) 55 | && Config.DrawHarmful) 56 | { 57 | var drawData = EffectRangeDrawing.GenerateDrawData( 58 | effectRangeData, harmfulRingColour, harmfulFillColour, 59 | originPos, targetPos, rotation); 60 | if (drawData != null) drawDataQueue.Enqueue(drawData); 61 | } 62 | 63 | if (effectRangeData.Harmfulness.HasFlag(ActionHarmfulness.Beneficial) 64 | && Config.DrawBeneficial) 65 | { 66 | var drawData = EffectRangeDrawing.GenerateDrawData( 67 | effectRangeData, beneficialRingColour, beneficialFillColour, 68 | originPos, targetPos, rotation); 69 | if (drawData != null) drawDataQueue.Enqueue(drawData); 70 | } 71 | } 72 | 73 | public void RefreshConfig() 74 | { 75 | beneficialRingColour = ImGui.ColorConvertFloat4ToU32( 76 | Config.BeneficialColour); 77 | beneficialFillColour = ImGui.ColorConvertFloat4ToU32(new( 78 | Config.BeneficialColour.X, Config.BeneficialColour.Y, 79 | Config.BeneficialColour.Z, Config.FillAlpha)); 80 | harmfulRingColour = ImGui.ColorConvertFloat4ToU32( 81 | Config.HarmfulColour); 82 | harmfulFillColour = ImGui.ColorConvertFloat4ToU32(new( 83 | Config.HarmfulColour.X, Config.HarmfulColour.Y, 84 | Config.HarmfulColour.Z, Config.FillAlpha)); 85 | } 86 | 87 | public void Reset() => Clear(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ActionEffectRange.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | yomishino 5 | 2.0.1.1 6 | A FFXIV Dalamud plugin that provides a visual cue on the effect range of the AoE action just used. 7 | 8 | Debug;Release;TestRelease 9 | 10 | 11 | 12 | net7.0 13 | x64 14 | enable 15 | latest 16 | true 17 | false 18 | false 19 | 20 | CA1416 21 | 22 | 23 | 24 | $(AppData)\XIVLauncher\addon\Hooks\dev 25 | 26 | 27 | 28 | 29 | TRACE;TEST 30 | true 31 | x64 32 | 33 | 34 | 35 | 36 | 37 | $(DalamudLibPath)\Dalamud.dll 38 | false 39 | 40 | 41 | $(DalamudLibPath)\ImGui.NET.dll 42 | false 43 | 44 | 45 | $(DalamudLibPath)\ImGuiScene.dll 46 | false 47 | 48 | 49 | $(DalamudLibPath)\Lumina.dll 50 | false 51 | 52 | 53 | $(DalamudLibPath)\Lumina.Excel.dll 54 | false 55 | 56 | 57 | $(DalamudLibPath)\FFXIVClientStructs.dll 58 | false 59 | 60 | 61 | $(DalamudLibPath)\Newtonsoft.Json.dll 62 | false 63 | 64 | 65 | $(DalamudLibPath)\Reloaded.Hooks.dll 66 | false 67 | 68 | 69 | $(DalamudLibPath)\Reloaded.Hooks.Definitions.dll 70 | false 71 | 72 | 73 | $(DalamudLibPath)\SharpDX.Mathematics.dll 74 | false 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/Drawing/Workers/CastingDrawWorker.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.EffectRange; 2 | using ActionEffectRange.Helpers; 3 | using ImGuiNET; 4 | using System.Collections.Generic; 5 | 6 | namespace ActionEffectRange.Drawing.Workers 7 | { 8 | public class CastingDrawWorker : IDrawWorker 9 | { 10 | private readonly Queue drawDataQueue = new(); 11 | 12 | private uint sequence = 0; 13 | 14 | private uint castingRingColour; 15 | private uint castingFillColour; 16 | 17 | private const float drawDelay = .1f; 18 | 19 | public DrawTrigger Trigger => DrawTrigger.Casting; 20 | 21 | public CastingDrawWorker() 22 | { 23 | RefreshConfig(); 24 | } 25 | 26 | public void RefreshConfig() 27 | { 28 | castingRingColour = ImGui.ColorConvertFloat4ToU32( 29 | Config.DrawWhenCastingColour); 30 | castingFillColour = ImGui.ColorConvertFloat4ToU32(new( 31 | Config.DrawWhenCastingColour.X, 32 | Config.DrawWhenCastingColour.Y, 33 | Config.DrawWhenCastingColour.Z, 34 | Config.DrawWhenCastingColour.W * Config.FillAlpha)); 35 | } 36 | 37 | public void Clear() 38 | { 39 | drawDataQueue.Clear(); 40 | } 41 | 42 | public void Reset() 43 | { 44 | Clear(); 45 | sequence = 0; 46 | } 47 | 48 | public bool HasDataToDraw() 49 | => DrawWhenCasting && drawDataQueue.Count > 0 50 | && ActionManagerHelper.IsCasting; 51 | 52 | public void CleanupOld() 53 | { 54 | // Always clear all when at least one DrawData too old 55 | // as we only allow this worker to draw for one action at a time 56 | 57 | if (Config.DrawWhenCastingUntilCastEnd) 58 | { 59 | CheckSequence(ActionManagerHelper.CurrentSeq); 60 | } 61 | else 62 | { 63 | if (drawDataQueue.TryPeek(out var head) 64 | && head.ElapsedSeconds > drawDelay + Config.PersistSeconds) 65 | { 66 | Clear(); 67 | } 68 | } 69 | } 70 | 71 | public void Draw(ImDrawListPtr drawList) 72 | { 73 | if (!ActionManagerHelper.IsCasting) return; 74 | foreach (var data in drawDataQueue) 75 | { 76 | if (data.ElapsedSeconds < drawDelay) continue; 77 | data.Draw(ImGui.GetWindowDrawList()); 78 | } 79 | } 80 | 81 | public void QueueDrawing(uint sequence, EffectRangeData effectRangeData, 82 | Vector3 originPos, Vector3 targetPos, float rotation) 83 | { 84 | if (!IsPlayerLoaded || !DrawWhenCasting) return; 85 | LogUserDebug($"{GetType().Name}.QueueDrawing => " + 86 | $"{effectRangeData}, orig={originPos}, target={targetPos}, rotation={rotation}"); 87 | CheckSequence(sequence); 88 | var drawData = EffectRangeDrawing.GenerateDrawData( 89 | effectRangeData, castingRingColour, castingFillColour, 90 | originPos, targetPos, rotation); 91 | if (drawData != null) drawDataQueue.Enqueue(drawData); 92 | } 93 | 94 | private void CheckSequence(uint sequence) 95 | { 96 | if (sequence > this.sequence) 97 | { 98 | Clear(); 99 | this.sequence = sequence; 100 | } 101 | } 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Drawing/Types/ConeAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | public abstract class ConeAoEDrawData : DrawData 6 | { 7 | public readonly Vector3 Origin; 8 | public readonly float Rotation; 9 | public readonly float Radius; 10 | public readonly byte Width; 11 | public readonly Vector3 End; 12 | public readonly float CentralAngleCycles; 13 | 14 | 15 | public ConeAoEDrawData(Vector3 origin, byte baseEffectRange, byte xAxisModifier, 16 | float rotation, float centralAngleCycles, uint ringColour, uint fillColour) 17 | : base(ringColour, fillColour) 18 | { 19 | Origin = origin; 20 | Radius = baseEffectRange + .5f; 21 | Width = xAxisModifier; 22 | Rotation = rotation; 23 | var direction = new Vector3(MathF.Sin(Rotation), 0, MathF.Cos(Rotation)); 24 | End = CalcFarEndWorldPos(Origin, direction, Radius); 25 | 26 | CentralAngleCycles = centralAngleCycles; 27 | } 28 | 29 | private void DrawHalfCone(ImDrawListPtr drawList, Vector2 projectedOrigin, 30 | Vector2 projectedEnd, int numSegments, bool drawClockwise) 31 | { 32 | var points = new Vector2[numSegments]; 33 | // rotation +/- (angleCycles * 2 * pi) / 2 34 | var rot = drawClockwise 35 | ? Rotation - CentralAngleCycles * MathF.PI 36 | : Rotation + CentralAngleCycles * MathF.PI; 37 | drawList.PathLineTo(projectedOrigin); 38 | for (int i = 0; i < numSegments; i++) 39 | { 40 | var a = drawClockwise 41 | ? i * ArcSegmentAngle + rot : rot - i * ArcSegmentAngle; 42 | Projection.WorldToScreen( 43 | new(Origin.X + Radius * MathF.Sin(a), Origin.Y, 44 | Origin.Z + Radius * MathF.Cos(a)), 45 | out var p, out var pr); 46 | // Don't draw the whole range if some of the points may be 47 | // projected to a weird position. 48 | // We cannot simply ignore it like when we draw Circle AoE 49 | // because that may truncate part of the cone 50 | if (pr.Z < -1) 51 | { 52 | drawList.PathClear(); 53 | return; 54 | } 55 | points[i] = p; 56 | drawList.PathLineTo(p); 57 | } 58 | drawList.PathLineTo(projectedEnd); 59 | if (Config.Filled) 60 | drawList.PathFillConvex(FillColour); 61 | if (Config.OuterRing) 62 | { 63 | if (Config.Filled) 64 | { 65 | drawList.PathLineTo(projectedOrigin); 66 | foreach (var p in points) 67 | drawList.PathLineTo(p); 68 | drawList.PathLineTo(projectedEnd); 69 | } 70 | drawList.PathStroke( 71 | RingColour, ImDrawFlags.None, Config.Thickness); 72 | } 73 | drawList.PathClear(); 74 | } 75 | 76 | public override void Draw(ImDrawListPtr drawList) 77 | { 78 | Projection.WorldToScreen(Origin, out var p0); 79 | Projection.WorldToScreen(End, out var pe); 80 | #if DEBUG 81 | drawList.AddCircleFilled(pe, Config.Thickness * 2, RingColour); 82 | #endif 83 | 84 | var numSegmentsHalf 85 | = (int)(CentralAngleCycles * Config.NumSegments / 2); 86 | 87 | DrawHalfCone(drawList, p0, pe, numSegmentsHalf, true); 88 | DrawHalfCone(drawList, p0, pe, numSegmentsHalf, false); 89 | return; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Drawing/Types/DonutAoEDrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing.Types 4 | { 5 | public class DonutAoEDrawData : DrawData 6 | { 7 | public readonly Vector3 Centre; 8 | public readonly int Radius; 9 | public readonly int InnerRadius; 10 | 11 | public DonutAoEDrawData(Vector3 centre, byte baseEffectRange, 12 | byte xAxisModifier, byte innerRadius, uint ringColour, uint fillColour) 13 | : base(ringColour, fillColour) 14 | { 15 | Centre = centre; 16 | Radius = baseEffectRange + xAxisModifier; 17 | InnerRadius = innerRadius; 18 | } 19 | 20 | public override void Draw(ImDrawListPtr drawList) 21 | { 22 | if (Config.LargeDrawOpt == 1 && Radius >= Config.LargeThreshold) 23 | return; // no draw large 24 | 25 | var outerPoints = new Vector2[Config.NumSegments]; 26 | var innerPoints = new Vector2[Config.NumSegments]; 27 | var seg = 2 * MathF.PI / Config.NumSegments; 28 | 29 | for (int i = 0; i < Config.NumSegments; i++) 30 | { 31 | Projection.WorldToScreen( 32 | new(Centre.X + Radius * MathF.Sin(i * seg), 33 | Centre.Y, 34 | Centre.Z + Radius * MathF.Cos(i * seg)), 35 | out var pOuter, out var prOuter); 36 | // Don't add points that may be projected to weird positions 37 | outerPoints[i] = prOuter.Z < -.5f 38 | ? new(float.NaN, float.NaN) : pOuter; 39 | Projection.WorldToScreen( 40 | new(Centre.X + InnerRadius * MathF.Sin(i * seg), 41 | Centre.Y, 42 | Centre.Z + InnerRadius * MathF.Cos(i * seg)), 43 | out var pInner, out var prInner); 44 | innerPoints[i] = prInner.Z < -.5f 45 | ? new(float.NaN, float.NaN) : pInner; 46 | } 47 | 48 | if (Config.Filled 49 | && (Config.LargeDrawOpt == 0 || Radius < Config.LargeThreshold)) 50 | { 51 | for (int i = 0; i < Config.NumSegments - 1; i++) 52 | { 53 | var j = i + 1; 54 | if (!float.IsNaN(outerPoints[i].X) 55 | && !float.IsNaN(outerPoints[j].X) 56 | && !float.IsNaN(innerPoints[i].X) 57 | && !float.IsNaN(innerPoints[j].X)) 58 | drawList.AddQuadFilled(outerPoints[i], outerPoints[j], 59 | innerPoints[j], innerPoints[i], FillColour); 60 | } 61 | if (!float.IsNaN(outerPoints[0].X) 62 | && !float.IsNaN(outerPoints[^1].X) 63 | && !float.IsNaN(innerPoints[0].X) 64 | && !float.IsNaN(innerPoints[^1].X)) 65 | drawList.AddQuadFilled(outerPoints[0], outerPoints[^1], 66 | innerPoints[^1], innerPoints[0], FillColour); 67 | } 68 | 69 | if (Config.OuterRing) 70 | { 71 | // outer 72 | for (int i = 0; i < Config.NumSegments; i++) 73 | if (!float.IsNaN(outerPoints[i].X)) 74 | drawList.PathLineTo(outerPoints[i]); 75 | drawList.PathStroke(RingColour, ImDrawFlags.Closed, Config.Thickness); 76 | 77 | // inner 78 | for (int i = 0; i < Config.NumSegments; i++) 79 | if (!float.IsNaN(innerPoints[i].X)) 80 | drawList.PathLineTo(innerPoints[i]); 81 | drawList.PathStroke(RingColour, ImDrawFlags.Closed, Config.Thickness); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Drawing/DrawData.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | namespace ActionEffectRange.Drawing 4 | { 5 | public abstract class DrawData 6 | { 7 | public readonly uint RingColour; 8 | public readonly uint FillColour; 9 | 10 | public DrawData(uint ringColour, uint fillColour) 11 | { 12 | RingColour = ringColour; 13 | FillColour = fillColour; 14 | } 15 | 16 | 17 | private readonly DateTime createTime = DateTime.Now; 18 | public float ElapsedSeconds 19 | => (float)(DateTime.Now - createTime).TotalSeconds; 20 | 21 | 22 | public abstract void Draw(ImDrawListPtr drawList); 23 | 24 | 25 | protected static float CalcRotation(Vector3 origin, Vector3 target) 26 | => MathF.Atan2(target.X - origin.X, target.Z - origin.Z); 27 | 28 | protected static float ArcSegmentAngle 29 | => 2 * MathF.PI / Config.NumSegments; 30 | 31 | 32 | protected static Vector3 CalcDirection(Vector3 origin, Vector3 target, 33 | float rotationOffset = 0, bool ignoreY = true) 34 | { 35 | var dir = Vector3.Normalize(target - origin); 36 | // Further rotate by offset 37 | if (rotationOffset > .0001f || rotationOffset < -.0001f) 38 | dir = new( 39 | MathF.Cos(rotationOffset) * dir.X 40 | - MathF.Sin(rotationOffset) * dir.Z, 41 | dir.Y, 42 | MathF.Cos(rotationOffset) * dir.Z 43 | + MathF.Sin(rotationOffset) * dir.X); 44 | if (ignoreY) dir.Y = 0; 45 | return dir; 46 | } 47 | 48 | // Return the world position of the far-end of the AoE shape; 49 | // for rectangular shape this is the midpoint on the farthest side 50 | protected static Vector3 CalcFarEndWorldPos( 51 | Vector3 origin, Vector3 direction, float length) 52 | => direction * length + origin; 53 | 54 | protected static (Vector3 P1, Vector3 P2, Vector3 P3, Vector3 P4) 55 | CalcRectCornersWorldPos( 56 | Vector3 nearEnd, Vector3 farEnd, Vector3 direction, float halfWidth) 57 | => (Vector3.Normalize(new(direction.Z, 0, -direction.X)) 58 | * halfWidth + nearEnd, 59 | Vector3.Normalize(new(direction.Z, 0, -direction.X)) 60 | * halfWidth + farEnd, 61 | Vector3.Normalize(new(-direction.Z, 0, direction.X)) 62 | * halfWidth + farEnd, 63 | Vector3.Normalize(new(-direction.Z, 0, direction.X)) 64 | * halfWidth + nearEnd); 65 | 66 | 67 | #region DrawComponent 68 | 69 | protected void DrawRect(ImDrawListPtr drawList, float width, 70 | Vector3 nearEndWorldPos, Vector3 farEndWorldPos, Vector3 direction) 71 | { 72 | #if DEBUG 73 | Projection.WorldToScreen(farEndWorldPos, out var pe, out var per); 74 | drawList.AddCircleFilled(pe, Config.Thickness * 2, RingColour); 75 | #endif 76 | 77 | var w2 = width / 2; 78 | var (p1w, p2w, p3w, p4w) = CalcRectCornersWorldPos( 79 | nearEndWorldPos, farEndWorldPos, direction, w2); 80 | 81 | Projection.WorldToScreen(p1w, out var p1s, out var p1r); 82 | Projection.WorldToScreen(p2w, out var p2s, out var p2r); 83 | Projection.WorldToScreen(p3w, out var p3s, out var p3r); 84 | Projection.WorldToScreen(p4w, out var p4s, out var p4r); 85 | 86 | // don't draw the whole range if some of the points 87 | // may be projected to a weird position 88 | if (p1r.Z < -1 || p2r.Z < -1 || p3r.Z < -1 || p4r.Z < -1) return; 89 | 90 | if (Config.Filled) 91 | drawList.AddQuadFilled(p1s, p2s, p3s, p4s, FillColour); 92 | if (Config.OuterRing) 93 | drawList.AddQuad(p1s, p2s, p3s, p4s, RingColour, Config.Thickness); 94 | } 95 | 96 | #endregion 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/EffectRangeData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Enums; 2 | 3 | namespace ActionEffectRange.Actions.EffectRange 4 | { 5 | public abstract class EffectRangeData 6 | { 7 | public readonly uint ActionId; 8 | public readonly ActionCategory Category; 9 | public readonly bool IsGTAction; 10 | public readonly ActionHarmfulness Harmfulness; 11 | public readonly sbyte Range; 12 | public readonly byte EffectRange; 13 | // XAxisModifier decides the width of line aoe? 14 | public readonly byte XAxisModifier; 15 | public readonly byte CastType; 16 | 17 | public readonly bool IsOriginal; 18 | 19 | 20 | protected EffectRangeData(uint actionId, uint actionCategory, bool isGT, 21 | ActionHarmfulness harmfulness, sbyte range, byte effectRange, 22 | byte xAxisModifier, byte castType, bool isOriginal = false) 23 | { 24 | ActionId = actionId; 25 | Category = (ActionCategory)actionCategory; 26 | IsGTAction = isGT; 27 | Harmfulness = harmfulness; 28 | Range = range; 29 | EffectRange = effectRange; 30 | XAxisModifier = xAxisModifier; 31 | CastType = castType; 32 | IsOriginal = isOriginal; 33 | } 34 | 35 | public static EffectRangeData Create(uint actionId, 36 | uint actionCategory, bool isGT, ActionHarmfulness harmfulness, 37 | sbyte range, byte effectRange, byte xAxisModifier, 38 | byte castType, bool isOriginal = false) 39 | => (ActionAoEType)castType switch 40 | { 41 | ActionAoEType.Circle or ActionAoEType.Circle2 or ActionAoEType.GT 42 | => new CircleAoEEffectRangeData(actionId, actionCategory, 43 | isGT, harmfulness, range, effectRange, xAxisModifier, 44 | castType, isOriginal: isOriginal), 45 | ActionAoEType.Cone or ActionAoEType.Cone2 46 | => new ConeAoEEffectRangeData(actionId, actionCategory, 47 | isGT, harmfulness, range, effectRange, xAxisModifier, 48 | castType, isOriginal: isOriginal), 49 | ActionAoEType.Line or ActionAoEType.Line2 50 | => new LineAoEEffectRangeData(actionId, actionCategory, 51 | isGT, harmfulness, range, effectRange, xAxisModifier, 52 | castType, isOriginal: isOriginal), 53 | ActionAoEType.DashAoE 54 | => new DashAoEEffectRangeData(actionId, actionCategory, 55 | isGT, harmfulness, range, effectRange, xAxisModifier, 56 | castType, isOriginal: isOriginal), 57 | ActionAoEType.Donut 58 | => new DonutAoEEffectRangeData(actionId, actionCategory, 59 | isGT, harmfulness, range, effectRange, xAxisModifier, 60 | castType, isOriginal: isOriginal), 61 | ActionAoEType.Cross 62 | => new CrossAoEEffectRangeData(actionId, actionCategory, 63 | isGT, harmfulness, range, effectRange, xAxisModifier, 64 | castType, isOriginal: isOriginal), 65 | _ => new NonAoEEffectRangeData(actionId, actionCategory, 66 | isGT, harmfulness, range, effectRange, xAxisModifier, 67 | castType, isOriginal: isOriginal) 68 | }; 69 | 70 | public override string ToString() 71 | => $"{GetType().Name}{{ ActionId: {ActionId}, Category: {Category}, " + 72 | $"IsGT: {IsGTAction}, Harmfulness: {Harmfulness}, " + 73 | $"Range: {Range}, EffectRange: {EffectRange}, XAxisModifier: {XAxisModifier}, " + 74 | $"CastType: {CastType}, {AdditionalFieldsToString()}, IsOriginal: {IsOriginal} }}"; 75 | 76 | protected virtual string AdditionalFieldsToString() => string.Empty; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/UI/AoETypeEditUi.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using Dalamud.Interface; 4 | using Dalamud.Interface.Components; 5 | using ImGuiNET; 6 | 7 | namespace ActionEffectRange.UI 8 | { 9 | public class AoETypeEditUi : ActionDataEditUi 10 | { 11 | private int selectedAoEType = 0; 12 | private int selectedHarmfulness = 0; 13 | 14 | private static readonly string[] aoeTypeSelectionsDisplayed 15 | = Array.ConvertAll( 16 | ActionDataInterfacing.AoETypeSelections, t => $"{t}"); 17 | private static readonly string[] harmfulnessSelectionsDisplayed 18 | = Array.ConvertAll( 19 | ActionDataInterfacing.ActionHarmfulnessesSelections, t => $"{t}"); 20 | 21 | protected override Vector2 InitialUiSize => new(500, 400); 22 | protected override string UiName => "AoE Types"; 23 | protected override DataTableModel DataTableViewModel { get; set; } = null!; 24 | 25 | public AoETypeEditUi() 26 | { 27 | BuildDataTableModel(); 28 | } 29 | 30 | private void BuildDataTableModel() 31 | { 32 | var model = new DataTableModel( 33 | "ActionEffectRange_Tbl_AoETypeEdit", 34 | actionId => ActionData.RemoveFromAoETypeList(actionId)); 35 | model.AddDataColumn("AoE Type", ImGuiTableColumnFlags.WidthFixed, 36 | false, 0, d => ActionDataInterfacing.GetAoETypeLabel(d.CastType)); 37 | model.AddDataColumn("Harmful/\nBeneficial", ImGuiTableColumnFlags.WidthFixed, 38 | false, 0, d => $"{d.Harmfulness}"); 39 | DataTableViewModel = model; 40 | } 41 | 42 | public override void DrawContents() 43 | { 44 | DrawIntro(); 45 | DataTableViewModel.DrawTable( 46 | ActionData.GetCustomisedAoETypeListCopy()); 47 | DrawActionSearchUi(); 48 | DrawTypeEditInputUi(); 49 | } 50 | 51 | private static void DrawIntro() 52 | { 53 | ImGuiExt.MultiTextWrapped( 54 | "Customising the AoE types of AoE actions.", 55 | "You can use this list to force an AoE action to be treated as another type."); 56 | ImGui.NewLine(); 57 | } 58 | 59 | private void DrawTypeEditInputUi() 60 | { 61 | ImGui.PushID("AoETypeEditInput"); 62 | 63 | if (selectedMatchedActionRow != null) 64 | { 65 | ImGui.Text("Editing for action: " + 66 | ActionDataInterfacing.GetActionDescription(selectedMatchedActionRow)); 67 | ImGui.Indent(); 68 | ImGuiExt.ComboWithTooltip("AoE type: ", "##comboAoETypeInput", 69 | ref selectedAoEType, aoeTypeSelectionsDisplayed, 70 | aoeTypeSelectionsDisplayed.Length, 280, null); 71 | ImGuiExt.ComboWithTooltip("Harmful/Beneficial: ", "##comboHarmfulnessInput", 72 | ref selectedHarmfulness, harmfulnessSelectionsDisplayed, 73 | harmfulnessSelectionsDisplayed.Length, 200, null); 74 | 75 | if (ImGuiExt.IconButton(1, FontAwesomeIcon.Plus, "Add to the list")) 76 | { 77 | ActionData.AddToAoETypeList(selectedMatchedActionRow.RowId, 78 | (byte)ActionDataInterfacing.AoETypeSelections[selectedAoEType], 79 | ActionDataInterfacing.ActionHarmfulnessesSelections[selectedHarmfulness]); 80 | ClearEditInput(); 81 | } 82 | ImGui.SameLine(); 83 | if (ImGuiExt.IconButton(2, FontAwesomeIcon.Times, "Clear Input")) 84 | ClearEditInput(); 85 | ImGui.Unindent(); 86 | } 87 | else 88 | ImGuiComponents.DisabledButton(FontAwesomeIcon.Plus); 89 | 90 | ImGui.PopID(); 91 | ImGui.NewLine(); 92 | } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Actions/EffectRange/EffectRangeDataManager.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.EffectRange; 3 | using ActionEffectRange.Actions.Enums; 4 | using CornerCases = ActionEffectRange.Actions.Data.Predefined.EffectRangeCornerCases; 5 | 6 | namespace ActionEffectRange.Actions 7 | { 8 | internal static class EffectRangeDataManager 9 | { 10 | 11 | public static EffectRangeData? NewData(uint actionId) 12 | { 13 | var row = ActionData.GetActionExcelRow(actionId); 14 | return row == null ? null : NewData(row); 15 | } 16 | 17 | public static EffectRangeData NewData( 18 | Lumina.Excel.GeneratedSheets.Action actionRow) 19 | => EffectRangeData.Create(actionRow.RowId, actionRow.ActionCategory.Row, 20 | actionRow.TargetArea, ActionData.GetActionHarmfulness(actionRow), 21 | actionRow.Range, actionRow.EffectRange, actionRow.XAxisModifier, 22 | actionRow.CastType, isOriginal: true); 23 | 24 | public static EffectRangeData NewDataChangeHarmfulness( 25 | EffectRangeData original, ActionHarmfulness harmfulness) 26 | => EffectRangeData.Create(original.ActionId, (uint)original.Category, 27 | original.IsGTAction, harmfulness, original.Range, original.EffectRange, 28 | original.XAxisModifier, original.CastType, isOriginal: false); 29 | 30 | 31 | public static bool IsHarmfulAction(EffectRangeData data) 32 | => data.Harmfulness.HasFlag(ActionHarmfulness.Harmful); 33 | 34 | public static bool IsBeneficialAction(EffectRangeData data) 35 | => data.Harmfulness.HasFlag(ActionHarmfulness.Beneficial); 36 | 37 | 38 | internal static EffectRangeData CustomiseEffectRangeData( 39 | EffectRangeData erdata) 40 | { 41 | erdata = CheckAoETypeOverriding(erdata); 42 | erdata = CheckConeAoEAngleOverriding(erdata); 43 | erdata = CheckDonutAoERadiusOverriding(erdata); 44 | erdata = CheckAoEHarmfulnessOverriding(erdata); 45 | 46 | erdata = CornerCases.UpdateEffectRangeData(erdata); 47 | return erdata; 48 | } 49 | 50 | private static EffectRangeData CheckAoETypeOverriding(EffectRangeData original) 51 | { 52 | if (ActionData.TryGetModifiedAoEType(original.ActionId, out var data) 53 | && data != null) 54 | return EffectRangeData.Create( 55 | original.ActionId, (uint)original.Category, original.IsGTAction, 56 | data.Harmfulness, original.Range, original.EffectRange, 57 | original.XAxisModifier, data.CastType, isOriginal: false); 58 | return original; 59 | } 60 | 61 | private static EffectRangeData CheckConeAoEAngleOverriding( 62 | EffectRangeData original) 63 | { 64 | if (original is not ConeAoEEffectRangeData) return original; 65 | 66 | if (ActionData.TryGetModifiedCone(original.ActionId, out var coneData) 67 | && coneData != null) 68 | return new ConeAoEEffectRangeData( 69 | original, coneData.CentralAngleCycles, coneData.RotationOffset); 70 | 71 | if (ActionData.TryGetConeAoEDefaultAngle( 72 | original.EffectRange, out var angle)) 73 | return new ConeAoEEffectRangeData(original, angle); 74 | 75 | return new ConeAoEEffectRangeData( 76 | original, ActionData.ConeAoEDefaultAngleCycles); 77 | } 78 | 79 | private static EffectRangeData CheckDonutAoERadiusOverriding( 80 | EffectRangeData original) 81 | => ActionData.TryGetDonutAoERadius(original.ActionId, out var radius) 82 | ? new DonutAoEEffectRangeData(original, radius) : original; 83 | 84 | private static EffectRangeData CheckAoEHarmfulnessOverriding( 85 | EffectRangeData original) 86 | => ActionData.TryGetHarmfulness(original.ActionId, out var harmfulness) 87 | ? NewDataChangeHarmfulness(original, harmfulness) : original; 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/UI/ImGuiExt.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface; 2 | using Dalamud.Interface.Components; 3 | using ImGuiNET; 4 | 5 | namespace ActionEffectRange.UI 6 | { 7 | public static class ImGuiExt 8 | { 9 | public static void SpacedSeparator() 10 | { 11 | ImGui.NewLine(); 12 | ImGui.Separator(); 13 | ImGui.NewLine(); 14 | } 15 | 16 | public static void MultiTextWrapped(params string[] paras) 17 | { 18 | foreach (var p in paras) ImGui.TextWrapped(p); 19 | } 20 | 21 | public static void BulletTextWrapped(string text) 22 | { 23 | ImGui.Bullet(); 24 | ImGui.SameLine(); 25 | ImGui.TextWrapped(text); 26 | } 27 | 28 | public static void BulletTextWrappedWithHelpMarker( 29 | string text, string helpText) 30 | { 31 | ImGui.Bullet(); 32 | ImGui.SameLine(); 33 | ImGui.TextWrapped(text); 34 | if (ImGui.IsItemHovered()) ImGui.SetTooltip(helpText); 35 | ImGui.SameLine(); 36 | ImGuiComponents.HelpMarker(helpText); 37 | } 38 | 39 | public static bool Button(string label, string? tooltip = null) 40 | { 41 | var ret = ImGui.Button(label); 42 | SetTooltipIfHovered(tooltip); 43 | return ret; 44 | } 45 | 46 | public static bool IconButton(int id, FontAwesomeIcon icon, 47 | string? tooltip = null) 48 | { 49 | var ret = ImGuiComponents.IconButton(id, icon); 50 | SetTooltipIfHovered(tooltip); 51 | return ret; 52 | } 53 | 54 | 55 | public static void CheckboxWithTooltip(string label, 56 | ref bool v, string? tooltip) 57 | { 58 | ImGui.Checkbox(label, ref v); 59 | SetTooltipIfHovered(tooltip); 60 | } 61 | 62 | public static void InputIntWithTooltip(string label, ref int v, 63 | int step, int stepFast, int min, int max, 64 | ImGuiInputTextFlags flags, float itemWidth, string? tooltip) 65 | { 66 | ImGui.Text(label); 67 | SetTooltipIfHovered(tooltip); 68 | ImGui.SameLine(); 69 | ImGui.SetNextItemWidth(itemWidth); 70 | ImGui.InputInt("##" + label, ref v, step, stepFast, flags); 71 | SetTooltipIfHovered(tooltip); 72 | if (v < min) v = min; 73 | if (v > max) v = max; 74 | } 75 | 76 | public static void DragIntWithTooltip(string label, ref int v, 77 | int spd, int min, int max, float itemWidth, string? tooltip) 78 | { 79 | ImGui.Text(label); 80 | SetTooltipIfHovered(tooltip); 81 | ImGui.SameLine(); 82 | ImGui.SetNextItemWidth(itemWidth); 83 | ImGui.DragInt("##" + label, ref v, spd, min, max); 84 | SetTooltipIfHovered(tooltip); 85 | } 86 | 87 | 88 | public static void DragFloatWithTooltip(string label, ref float v, float spd, 89 | float min, float max, string format, float itemWidth, string? tooltip) 90 | { 91 | ImGui.Text(label); 92 | SetTooltipIfHovered(tooltip); 93 | ImGui.SameLine(); 94 | ImGui.SetNextItemWidth(itemWidth); 95 | ImGui.DragFloat("##" + label, ref v, spd, min, max, format); 96 | SetTooltipIfHovered(tooltip); 97 | } 98 | 99 | public static void ComboWithTooltip(string prompt, string label, 100 | ref int selected, string[] items, int itemCount, 101 | float comboWidth, string? tooltip) 102 | { 103 | ImGui.Text(prompt); 104 | SetTooltipIfHovered(tooltip); 105 | ImGui.SameLine(); 106 | ImGui.SetNextItemWidth(comboWidth); 107 | ImGui.Combo(label, ref selected, items, itemCount); 108 | SetTooltipIfHovered(tooltip); 109 | } 110 | 111 | public static void SetTooltipIfHovered(string? tooltip) 112 | { 113 | if (tooltip != null && ImGui.IsItemHovered()) 114 | ImGui.SetTooltip(tooltip); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /.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 | dotnet_style_prefer_conditional_expression_over_return = true:silent 78 | dotnet_style_explicit_tuple_names = true:suggestion 79 | 80 | [*.cs] 81 | csharp_indent_labels = one_less_than_current 82 | csharp_using_directive_placement = outside_namespace:silent 83 | csharp_prefer_simple_using_statement = true:suggestion 84 | csharp_prefer_braces = true:silent 85 | csharp_style_namespace_declarations = block_scoped: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 -------------------------------------------------------------------------------- /src/UI/ConeAoEAngleEditUI.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using Dalamud.Interface; 4 | using Dalamud.Interface.Components; 5 | using ImGuiNET; 6 | 7 | namespace ActionEffectRange.UI 8 | { 9 | public class ConeAoEAngleEditUI : ActionDataEditUi 10 | { 11 | private int centralAngleDegInput; 12 | private int rotationOffsetDegInput; 13 | 14 | protected override Vector2 InitialUiSize => new(500, 400); 15 | protected override string UiName => "Cone AoE"; 16 | 17 | protected override DataTableModel DataTableViewModel { get; set; } = null!; 18 | 19 | 20 | public ConeAoEAngleEditUI() 21 | { 22 | BuildDataTableModel(); 23 | } 24 | 25 | public override void DrawContents() 26 | { 27 | DrawIntro(); 28 | DataTableViewModel.DrawTable( 29 | ActionData.GetCustomisedConeAoEAngleListCopy()); 30 | DrawActionSearchUi(); 31 | DrawAngleEditInputUi(); 32 | } 33 | 34 | private void BuildDataTableModel() 35 | { 36 | var model = new DataTableModel( 37 | "ActionEffectRange_Tbl_ConeAoEAngleEdit", 38 | actionId => ActionData.RemoveFromConeAoEAngleList(actionId)); 39 | model.AddDataColumn("Central\n Angle", ImGuiTableColumnFlags.WidthFixed, 40 | false, 0, d => $"{ActionDataInterfacing.CycleToDeg(d.CentralAngleCycles):0}"); 41 | model.AddDataColumn("Rotation\n Offset", ImGuiTableColumnFlags.WidthFixed, 42 | false, 0, d => $"{ActionDataInterfacing.RadToDeg(d.RotationOffset):0}"); 43 | DataTableViewModel = model; 44 | } 45 | 46 | private static void DrawIntro() 47 | { 48 | ImGuiExt.MultiTextWrapped( 49 | "Customising the drawing of the cone AoEs here.", 50 | "You can use this list to temporarily fix these errors " + 51 | "regarding cone-shaped AoEs, should they exist:"); 52 | ImGuiExt.BulletTextWrappedWithHelpMarker("Central Angle", 53 | "Central Angle: Sets the central angle of the sector drawn for the Cone AoE.\n\n" + 54 | "For example, set the Central Angle for an action to 90 if you want " + 55 | "the plugin to draw a sector of 90 degrees when you used that action."); 56 | ImGuiExt.BulletTextWrappedWithHelpMarker("Rotation Offset", 57 | "Rotation Offset: Adds certain degrees to the calculated rotation " + 58 | "(that is, direction) of the sector.\n\n" + 59 | "For example, set it to 180 so that the sector will be drawn " + 60 | "to the opposite direction.\n" + 61 | "(You probably won't need this though.)"); 62 | ImGuiExt.MultiTextWrapped( 63 | "Both Central Angle and Rotation Offset are specified in degrees.", 64 | "E.g., a value of 90 means 90 degrees\n"); 65 | ImGui.NewLine(); 66 | } 67 | 68 | private void DrawAngleEditInputUi() 69 | { 70 | ImGui.PushID("ConeAoEAngleEditInput"); 71 | 72 | if (selectedMatchedActionRow != null) 73 | { 74 | ImGui.Text("Editing for action: " + 75 | ActionDataInterfacing.GetActionDescription(selectedMatchedActionRow)); 76 | ImGui.Indent(); 77 | ImGuiExt.InputIntWithTooltip("Central angle: ", 78 | ref centralAngleDegInput, 1, 10, 0, 360, 79 | ImGuiInputTextFlags.None, 100, null); 80 | ImGuiExt.InputIntWithTooltip("Rotation offset: ", 81 | ref rotationOffsetDegInput, 1, 10, 0, 360, 82 | ImGuiInputTextFlags.None, 100, null); 83 | 84 | if (ImGuiExt.IconButton(1, FontAwesomeIcon.Plus, "Add to the list")) 85 | { 86 | ActionData.AddToConeAoEAngleList(selectedMatchedActionRow.RowId, 87 | ActionDataInterfacing.DegToCycle(centralAngleDegInput), 88 | ActionDataInterfacing.DegToRad(rotationOffsetDegInput)); 89 | ClearEditInput(); 90 | } 91 | ImGui.SameLine(); 92 | if (ImGuiExt.IconButton(2, FontAwesomeIcon.Times, "Clear Input")) 93 | ClearEditInput(); 94 | ImGui.Unindent(); 95 | } 96 | else 97 | ImGuiComponents.DisabledButton(FontAwesomeIcon.Plus); 98 | 99 | ImGui.PopID(); 100 | ImGui.NewLine(); 101 | } 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Plugin.cs: -------------------------------------------------------------------------------- 1 | global using static ActionEffectRange.Game; 2 | global using static ActionEffectRange.Plugin; 3 | global using ActionEffectRange.Utils; 4 | global using System; 5 | global using System.Numerics; 6 | 7 | using ActionEffectRange.Actions; 8 | using ActionEffectRange.Actions.Data; 9 | using ActionEffectRange.Drawing; 10 | using ActionEffectRange.Helpers; 11 | using ActionEffectRange.UI; 12 | using Dalamud.Game; 13 | using Dalamud.Game.Command; 14 | using Dalamud.IoC; 15 | using Dalamud.Plugin; 16 | using Dalamud.Plugin.Services; 17 | 18 | namespace ActionEffectRange 19 | { 20 | public class Plugin : IDalamudPlugin 21 | { 22 | [PluginService] 23 | internal static DalamudPluginInterface PluginInterface { 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 ISigScanner SigScanner { get; private set; } = null!; 30 | [PluginService] 31 | internal static IGameInteropProvider InteropProvider { get; private set; } = null!; 32 | [PluginService] 33 | internal static IFramework Framework { get; private set; } = null!; 34 | [PluginService] 35 | internal static IClientState ClientState { get; private set; } = null!; 36 | [PluginService] 37 | internal static IObjectTable ObejctTable { get; private set; } = null!; 38 | [PluginService] 39 | internal static IBuddyList BuddyList { get; private set; } = null!; 40 | [PluginService] 41 | internal static IPluginLog PluginLog { get; private set; } = null!; 42 | 43 | 44 | public string Name => "ActionEffectRange" 45 | #if DEBUG 46 | + " [DEV]"; 47 | #elif TEST 48 | + " [TEST]"; 49 | #else 50 | ; 51 | #endif 52 | 53 | private const string commandToggleConfig = "/actioneffectrange"; 54 | 55 | private static bool _enabled; 56 | internal static bool Enabled 57 | { 58 | get => _enabled; 59 | set 60 | { 61 | if (value != _enabled) 62 | { 63 | if (value) 64 | { 65 | EffectRangeDrawing.Reset(); 66 | ActionWatcher.Enable(); 67 | } 68 | else 69 | { 70 | EffectRangeDrawing.Reset(); 71 | ActionWatcher.Disable(); 72 | } 73 | _enabled = value; 74 | } 75 | } 76 | } 77 | internal static bool DrawWhenCasting; 78 | 79 | internal static Configuration Config = null!; 80 | internal static bool InConfig = false; 81 | 82 | public Plugin() 83 | { 84 | Config = PluginInterface.GetPluginConfig() as Configuration 85 | ?? new Configuration(); 86 | 87 | InitializeCommands(); 88 | 89 | PluginInterface.UiBuilder.OpenConfigUi += OnOpenConfigUi; 90 | PluginInterface.UiBuilder.Draw += ConfigUi.Draw; 91 | 92 | PluginInterface.UiBuilder.Draw += EffectRangeDrawing.OnTick; 93 | 94 | ClientState.Logout += OnLogOut; 95 | ClientState.TerritoryChanged += CheckTerritory; 96 | 97 | RefreshConfig(true); 98 | } 99 | 100 | private static void InitializeCommands() 101 | { 102 | CommandManager.AddHandler(commandToggleConfig, 103 | new CommandInfo((_, _) => InConfig = !InConfig) 104 | { 105 | HelpMessage = "Toggle the Configuration Window of ActionEffectRange", 106 | ShowInHelp = true 107 | }); 108 | } 109 | 110 | private static void OnOpenConfigUi() 111 | { 112 | InConfig = true; 113 | } 114 | 115 | private static void CheckTerritory(ushort terr) 116 | { 117 | if (IsPvPZone) 118 | { 119 | Enabled = Config.Enabled && Config.EnabledPvP; 120 | DrawWhenCasting = false; 121 | } 122 | else 123 | { 124 | Enabled = Config.Enabled; 125 | DrawWhenCasting = Config.DrawWhenCasting; 126 | } 127 | } 128 | 129 | private static void OnLogOut() 130 | { 131 | EffectRangeDrawing.Reset(); 132 | } 133 | 134 | internal static void RefreshConfig(bool reloadSavedList = false) 135 | { 136 | EffectRangeDrawing.RefreshConfig(); 137 | CheckTerritory(ClientState.TerritoryType); 138 | 139 | if (reloadSavedList) 140 | ActionData.ReloadCustomisedData(); 141 | } 142 | 143 | public static void LogUserDebug(string msg) 144 | { 145 | if (Config.LogDebug) PluginLog.Debug(msg); 146 | } 147 | 148 | 149 | #region IDisposable Support 150 | protected virtual void Dispose(bool disposing) 151 | { 152 | if (!disposing) return; 153 | 154 | PluginInterface.SavePluginConfig(Config); 155 | 156 | CommandManager.RemoveHandler(commandToggleConfig); 157 | 158 | PluginInterface.UiBuilder.Draw -= EffectRangeDrawing.OnTick; 159 | 160 | ClientState.Logout -= OnLogOut; 161 | ClientState.TerritoryChanged -= CheckTerritory; 162 | 163 | PluginInterface.UiBuilder.Draw -= ConfigUi.Draw; 164 | PluginInterface.UiBuilder.OpenConfigUi -= OnOpenConfigUi; 165 | 166 | ActionWatcher.Dispose(); 167 | ClassJobWatcher.Dispose(); 168 | } 169 | 170 | public void Dispose() 171 | { 172 | Dispose(true); 173 | GC.SuppressFinalize(this); 174 | } 175 | #endregion 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Drawing/EffectRangeDrawing.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.EffectRange; 2 | using ActionEffectRange.Drawing.Types; 3 | using ActionEffectRange.Drawing.Workers; 4 | using Dalamud.Interface.Utility; 5 | using ImGuiNET; 6 | using System.Collections.Generic; 7 | 8 | namespace ActionEffectRange.Drawing 9 | { 10 | public static class EffectRangeDrawing 11 | { 12 | private static readonly List workers = new(); 13 | 14 | 15 | static EffectRangeDrawing() 16 | { 17 | workers.Add(new DrawWorker()); 18 | workers.Add(new CastingDrawWorker()); 19 | } 20 | 21 | public static void RefreshConfig() 22 | => workers.ForEach(worker => worker.RefreshConfig()); 23 | 24 | private static void Clear() 25 | => workers.ForEach(worker => worker.Clear()); 26 | 27 | public static void Reset() 28 | => workers.ForEach(worker => worker.Reset()); 29 | 30 | public static void OnTick() 31 | { 32 | if (!Config.Enabled) return; 33 | 34 | if (!IsPlayerLoaded) 35 | { 36 | Clear(); 37 | return; 38 | } 39 | 40 | workers.ForEach(worker => worker.CleanupOld()); 41 | 42 | if (!HasDataToDraw()) return; 43 | 44 | ImGui.SetNextWindowSize(ImGui.GetMainViewport().Size); 45 | ImGuiHelpers.ForceNextWindowMainViewport(); 46 | ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero); 47 | ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); 48 | ImGui.Begin("EffectRangeOverlay", 49 | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoBringToFrontOnFocus 50 | | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking 51 | | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoInputs); 52 | try 53 | { 54 | // Unset the AntiAliasedFill flag so 55 | // compound shapes won't have ugly lines when filled 56 | ImGui.GetWindowDrawList().Flags &= ~ImDrawListFlags.AntiAliasedFill; 57 | 58 | workers.ForEach(worker => worker.Draw(ImGui.GetWindowDrawList())); 59 | } 60 | catch (Exception e) 61 | { 62 | PluginLog.Error($"{e}"); 63 | } 64 | finally 65 | { 66 | ImGui.End(); 67 | ImGui.PopStyleVar(); 68 | } 69 | } 70 | 71 | public static void AddEffectRangeToDraw(uint sequence, 72 | DrawTrigger trigger, EffectRangeData effectRangeData, 73 | Vector3 originPos, Vector3 targetPos, float rotation) 74 | => workers.ForEach(worker => 75 | { 76 | if (worker.Trigger == trigger) 77 | worker.QueueDrawing(sequence, effectRangeData, 78 | originPos, targetPos, rotation); 79 | }); 80 | 81 | public static DrawData? GenerateDrawData( 82 | EffectRangeData effectRangeData, uint ringCol, uint fillCol, 83 | Vector3 originPos, Vector3 targetPos, float rotation) 84 | { 85 | switch (effectRangeData) 86 | { 87 | case CircleAoEEffectRangeData circleData: 88 | return new CircleAoEDrawData( 89 | targetPos, circleData.EffectRange, circleData.XAxisModifier, 90 | ringCol, fillCol); 91 | case ConeAoEEffectRangeData coneData: 92 | return originPos == targetPos 93 | ? new FacingDirectedConeAoEDrawData(originPos, 94 | rotation + coneData.RotationOffset, 95 | coneData.EffectRange, coneData.XAxisModifier, 96 | coneData.CentralAngleCycles, ringCol, fillCol) 97 | : new TargetDirectedConeAoEDrawData(originPos, targetPos, 98 | coneData.EffectRange, coneData.XAxisModifier, 99 | coneData.CentralAngleCycles, coneData.RotationOffset, 100 | ringCol, fillCol); 101 | case LineAoEEffectRangeData lineData: 102 | return originPos == targetPos 103 | ? new FacingDirectedLineAoEDrawData( 104 | originPos, rotation, lineData.EffectRange, 105 | lineData.XAxisModifier, lineData.RotationOffset, 106 | ringCol, fillCol) 107 | : new TargetDirectedLineAoEDrawData( 108 | originPos, targetPos, lineData.EffectRange, 109 | lineData.XAxisModifier, lineData.RotationOffset, 110 | ringCol, fillCol); 111 | case BidirectedLineAoEEffectRangeData biDirLineData: 112 | return new BidirectedLineAoEDrawData( 113 | originPos, rotation, biDirLineData.EffectRange, 114 | biDirLineData.XAxisModifier, biDirLineData.RotationOffset, 115 | ringCol, fillCol); 116 | case DashAoEEffectRangeData dashData: 117 | return new DashAoEDrawData( 118 | originPos, targetPos, dashData.EffectRange, 119 | dashData.XAxisModifier, ringCol, fillCol); 120 | case DonutAoEEffectRangeData donutData: 121 | return new DonutAoEDrawData( 122 | targetPos, donutData.EffectRange, 123 | donutData.XAxisModifier, donutData.InnerRadius, 124 | ringCol, fillCol); 125 | case CrossAoEEffectRangeData crossData: 126 | return new CrossAoEDrawData( 127 | originPos, targetPos, crossData.EffectRange, 128 | crossData.XAxisModifier, crossData.RotationOffset, 129 | ringCol, fillCol); 130 | default: 131 | LogUserDebug( 132 | $"---No DrawData created for Action#{effectRangeData.ActionId}: " + 133 | $"created {effectRangeData.GetType().Name} from unknown AoE type"); 134 | return null; 135 | }; 136 | } 137 | 138 | private static bool HasDataToDraw() 139 | => workers.Exists(worker => worker.HasDataToDraw()); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /.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/Actions/Data/ActionData.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data.Containers; 2 | using ActionEffectRange.Actions.Data.Predefined; 3 | using ActionEffectRange.Actions.Data.Template; 4 | using ActionEffectRange.Actions.Enums; 5 | using Lumina.Excel; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using GeneratedSheets = Lumina.Excel.GeneratedSheets; 9 | 10 | 11 | namespace ActionEffectRange.Actions.Data 12 | { 13 | public static class ActionData 14 | { 15 | private static readonly ActionBlacklist actionBlacklist = new(Config); 16 | private static readonly AoETypeOverridingList aoeTypeOverridingList 17 | = new(Config); 18 | private static readonly ConeAoeAngleOverridingList coneAoeOverridingList 19 | = new(Config); 20 | 21 | 22 | #region Data sheet related 23 | 24 | internal static readonly ExcelSheet? ActionExcelSheet 25 | = DataManager.GetExcelSheet(); 26 | 27 | internal static readonly ExcelSheet? ActionCategoryExcelSheet 28 | = DataManager.GetExcelSheet(); 29 | 30 | public static GeneratedSheets.Action? GetActionExcelRow(uint actionId) 31 | => ActionExcelSheet?.GetRow(actionId); 32 | 33 | // Unk46 seems related to actions being harmful/beneficial: 34 | // 0 - No direct effect, e.g. nonattacking move action like 35 | // AM and En Avant, pet summon&ordering actions; 36 | // 1 - attacking/harmful; 37 | // 2 - healing/beneficial; 38 | // From observation, not sure. 39 | // But grounded attacking AoE (salted earth, doton etc.) are all 2, 40 | // Celetial Opposition(pve) is 1 (probably a legacy design where it used to stun enemies?) 41 | public static ActionHarmfulness GetActionHarmfulness( 42 | GeneratedSheets.Action actionRow) 43 | => actionRow.Unknown46 switch 44 | { 45 | 1 => ActionHarmfulness.Harmful, 46 | 2 => ActionHarmfulness.Beneficial, 47 | _ => ActionHarmfulness.None 48 | }; 49 | 50 | // Actions used by player and actions for addictional effects / pet actions etc. 51 | // following player using some action 52 | // ** ClassJobCategory=1 ("All Classes") seem to contain mostly special actions (not combat/doh/dol job related), 53 | // but also some that look like normal combat actions -- may be RP battle actions, not sure. 54 | // ** Meanwhile, actions from Eureka / Resistance items have ClassJobCategory>1 55 | // ** =0 ones are actions by enemies, Trust or quest battle NPCs, etc. 56 | // ** But PvP additional effect actions (e.g. #29706) may also have 0 here 57 | public static bool IsPlayerTriggeredAction(GeneratedSheets.Action actionRow) 58 | => actionRow.ClassJobCategory.Row > 0 || actionRow.IsPvP; 59 | 60 | public static bool IsPlayerCombatAction(GeneratedSheets.Action actionRow) 61 | => IsPlayerTriggeredAction(actionRow) 62 | && IsCombatActionCategory((ActionCategory)actionRow.ActionCategory.Row); 63 | 64 | public static ActionCategory GetActionCategory(uint actionId) 65 | => (ActionCategory)(GetActionExcelRow(actionId)?.ActionCategory.Row ?? 0); 66 | 67 | public static bool IsCombatActionCategory(ActionCategory actionCategory) 68 | => actionCategory is ActionCategory.Ability or ActionCategory.AR 69 | or ActionCategory.LB or ActionCategory.Spell or ActionCategory.WS; 70 | 71 | public static bool IsSpecialOrArtilleryActionCategory(ActionCategory actionCategory) 72 | => actionCategory is ActionCategory.Special or ActionCategory.Artillery; 73 | 74 | public static string GetActionCategoryName(ActionCategory actionCategory) 75 | => ActionCategoryExcelSheet?.GetRow((uint)actionCategory)?.Name ?? string.Empty; 76 | 77 | #endregion 78 | 79 | #region Customisation data managing 80 | 81 | public static void ReloadCustomisedData() 82 | { 83 | actionBlacklist.Reload(); 84 | aoeTypeOverridingList.Reload(); 85 | coneAoeOverridingList.Reload(); 86 | } 87 | 88 | public static void SaveCustomisedData(bool writeToFile = false) 89 | { 90 | actionBlacklist.Save(writeToFile); 91 | aoeTypeOverridingList.Save(writeToFile); 92 | coneAoeOverridingList.Save(writeToFile); 93 | } 94 | 95 | public static bool AddToActionBlacklist(uint actionId) 96 | => actionBlacklist.Add(actionId); 97 | 98 | public static bool RemoveFromActionBlacklist(uint actionId) 99 | => actionBlacklist.Remove(actionId); 100 | 101 | public static IEnumerable GetCustomisedActionBlacklistCopy() 102 | => actionBlacklist.CopyCustomised(); 103 | 104 | public static bool AddToAoETypeList( 105 | uint actionId, byte castType, ActionHarmfulness harmfulness) 106 | => aoeTypeOverridingList.Add(new(actionId, castType, harmfulness)); 107 | 108 | public static bool RemoveFromAoETypeList(uint actionId) 109 | => aoeTypeOverridingList.Remove(actionId); 110 | 111 | public static IEnumerable GetCustomisedAoETypeListCopy() 112 | => aoeTypeOverridingList.CopyCustomised(); 113 | 114 | public static bool AddToConeAoEAngleList( 115 | uint actionId, float centralAngleCycles, float rotationOffset) 116 | => coneAoeOverridingList.Add(new(actionId, centralAngleCycles, rotationOffset)); 117 | 118 | public static bool RemoveFromConeAoEAngleList(uint actionId) 119 | => coneAoeOverridingList.Remove(actionId); 120 | 121 | public static IEnumerable GetCustomisedConeAoEAngleListCopy() 122 | => coneAoeOverridingList.CopyCustomised(); 123 | 124 | #endregion 125 | 126 | 127 | #region Overriding processing 128 | 129 | public static bool IsActionBlacklisted(uint actionId) 130 | => actionBlacklist.Contains(actionId); 131 | 132 | // But partially based on heuristics... 133 | public static bool AreRelatedPlayerTriggeredActions( 134 | uint primaryActionId, uint secondaryActionId) 135 | { 136 | var row1 = GetActionExcelRow(primaryActionId); 137 | var row2 = GetActionExcelRow(secondaryActionId); 138 | if (row1 == null || row2 == null || !IsPlayerTriggeredAction(row1) 139 | || !IsPlayerTriggeredAction(row2)) return false; 140 | 141 | if (primaryActionId == secondaryActionId) return true; 142 | if (AdditionalEffectsMap.Dictionary.TryGetValue(primaryActionId, 143 | out var additionals) && additionals.Contains(secondaryActionId)) 144 | return true; 145 | 146 | // Case: Action with same name => possibly one is an additional effect of the other 147 | if (row1.Name.RawString == row2.Name.RawString) return true; 148 | 149 | // Case: Known pet/pet-like action 150 | // ** Not used so not checked 151 | //if (PetActionMap.Dictionary.TryGetValue(primaryActionId, out var secPetIds) 152 | // && secPetIds.Contains(secondaryActionId)) return true; 153 | //if (PetLikeActionMap.Dictionary.TryGetValue(primaryActionId, out var secPetlikeIds) 154 | // && secPetlikeIds.Contains(secondaryActionId)) return true; 155 | 156 | // Case: other? 157 | 158 | return false; 159 | } 160 | 161 | public static bool ShouldNotUseCachedSeq(uint actionId) 162 | => NoCachedSeqActions.Set.Contains(actionId); 163 | 164 | //public static bool TryGetDirectlyMapped(uint actionId, 165 | // out ImmutableArray mappedActionIds) 166 | // => DirectMap.Dictionary.TryGetValue(actionId, out mappedActionIds); 167 | 168 | //public static bool TryGetMappedPetActionIds(uint actionId, 169 | // out ImmutableHashSet? petActionIds) 170 | // => PetActionMap.Dictionary.TryGetValue(actionId, out petActionIds); 171 | 172 | //public static bool TryGetMappedPetlikeActionIds(uint actionId, 173 | // out ImmutableHashSet? petlikeActionIds) 174 | // => PetActionMap.Dictionary.TryGetValue(actionId, out petlikeActionIds); 175 | 176 | public static bool TryGetActionWithAdditionalEffects(uint actionId, 177 | out ImmutableArray additionalActionIds) 178 | => AdditionalEffectsMap.Dictionary.TryGetValue( 179 | actionId, out additionalActionIds); 180 | 181 | public static bool TryGetModifiedAoEType(uint actionId, out AoETypeDataItem? item) 182 | => aoeTypeOverridingList.TryGet(actionId, out item); 183 | 184 | public static bool TryGetHarmfulness(uint actionId, 185 | out ActionHarmfulness harmfulness) 186 | => HarmfulnessMap.Dictionary.TryGetValue(actionId, out harmfulness); 187 | 188 | public static bool TryGetModifiedCone(uint actionId, out ConeAoEAngleDataItem? item) 189 | => coneAoeOverridingList.TryGet(actionId, out item); 190 | 191 | public static bool TryGetConeAoEDefaultAngle(byte effectRange, out float angle) 192 | => ConeAoEAngleMap.DefaultAnglesByRange.TryGetValue(effectRange, out angle); 193 | 194 | public static float ConeAoEDefaultAngleCycles 195 | => ConeAoEAngleMap.DefaultAngleCycles; 196 | 197 | public static bool TryGetDonutAoERadius(uint actionId, out byte radius) 198 | => DonutAoERadiusMap.Predefined.TryGetValue(actionId, out radius); 199 | 200 | #endregion 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/UI/ActionDataEditUi.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.Data.Template; 3 | using Dalamud.Interface; 4 | using Dalamud.Interface.Utility; 5 | using ImGuiNET; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using ExcSheets = Lumina.Excel.GeneratedSheets; 9 | 10 | namespace ActionEffectRange.UI 11 | { 12 | public abstract class ActionDataEditUi where T : IDataItem 13 | { 14 | private bool isUiOpen; 15 | private bool shouldShowActionSearchMatches; 16 | private string actionSearchInput = string.Empty; 17 | private Vector2 actionSearchMatchesDisplayRectMin; 18 | private Vector2 actionSearchMatchesDisplayRectMax; 19 | protected IEnumerable? actionSearchMatchedActions = null; 20 | protected ExcSheets.Action? selectedMatchedActionRow = null; 21 | 22 | protected abstract Vector2 InitialUiSize { get; } 23 | protected abstract string UiName { get; } 24 | protected abstract DataTableModel DataTableViewModel { get; set; } 25 | protected virtual Func? 26 | ActionSearchExtraFilter { get; } = null; 27 | 28 | public void Draw() 29 | { 30 | if (!isUiOpen) return; 31 | 32 | ImGui.SetNextWindowSize( 33 | ImGuiHelpers.ScaledVector2(InitialUiSize.X, InitialUiSize.Y), 34 | ImGuiCond.FirstUseEver); 35 | ImGui.Begin($"ActionEffectRange: Configuration - {UiName}", 36 | ImGuiWindowFlags.NoCollapse); 37 | 38 | if (!ImGui.IsWindowFocused(ImGuiFocusedFlags.ChildWindows)) 39 | shouldShowActionSearchMatches = false; 40 | 41 | DrawContents(); 42 | 43 | if (ImGui.Button("Close")) 44 | CloseUI(); 45 | 46 | ImGui.End(); 47 | } 48 | 49 | public virtual void OpenUI() => isUiOpen = true; 50 | 51 | public virtual void CloseUI() 52 | { 53 | ClearEditInput(); 54 | isUiOpen = false; 55 | } 56 | 57 | public abstract void DrawContents(); 58 | 59 | protected void DrawActionSearchUi() 60 | { 61 | ImGui.PushID($"{UiName}_EditUiActionSearch"); 62 | 63 | ImGui.Text("Search an action to add to the list"); 64 | ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); 65 | var input = actionSearchInput; 66 | ImGui.InputTextWithHint("##actionSearchInput", 67 | "Enter Action ID or Action name ...", ref input, 64); 68 | 69 | // Check whether should show matching results 70 | if (ImGui.IsItemActivated() && !string.IsNullOrWhiteSpace(input)) 71 | shouldShowActionSearchMatches = true; 72 | if (ImGui.IsAnyMouseDown() && !ImGui.IsAnyItemHovered()) 73 | if (ImGui.IsAnyMouseDown() 74 | && !ImGui.IsMouseHoveringRect( 75 | actionSearchMatchesDisplayRectMin, 76 | actionSearchMatchesDisplayRectMax)) 77 | shouldShowActionSearchMatches = false; 78 | 79 | // Update input cache and search if input changed 80 | if (actionSearchInput != input) 81 | { 82 | actionSearchInput = input; 83 | actionSearchMatchedActions 84 | = string.IsNullOrWhiteSpace(input) ? null 85 | : ActionDataInterfacing.GetAllPartialMatchActionExcelRows( 86 | actionSearchInput, true, int.MaxValue, true, 87 | ActionSearchExtraFilter)? 88 | .ToList(); 89 | shouldShowActionSearchMatches 90 | = actionSearchMatchedActions?.Any() ?? false; 91 | } 92 | 93 | // Show matching results 94 | if (shouldShowActionSearchMatches) 95 | { 96 | actionSearchMatchesDisplayRectMin = ImGui.GetCursorScreenPos(); 97 | var displayRectSize = new Vector2( 98 | ImGui.GetContentRegionAvail().X, 99 | ImGui.GetTextLineHeight() * 10); 100 | actionSearchMatchesDisplayRectMax 101 | = actionSearchMatchesDisplayRectMin + displayRectSize; 102 | if (ImGui.BeginChildFrame( 103 | 1, displayRectSize, ImGuiWindowFlags.NoFocusOnAppearing)) 104 | { 105 | if (actionSearchMatchedActions != null) 106 | { 107 | foreach (var row in actionSearchMatchedActions) 108 | { 109 | if (ImGui.Selectable( 110 | ActionDataInterfacing.GetActionDescription(row))) 111 | { 112 | selectedMatchedActionRow = row; 113 | shouldShowActionSearchMatches = false; 114 | } 115 | } 116 | } 117 | ImGui.EndChildFrame(); 118 | } 119 | } 120 | 121 | ImGui.PopID(); 122 | ImGui.NewLine(); 123 | } 124 | 125 | protected void ClearEditInput() 126 | { 127 | actionSearchInput = string.Empty; 128 | shouldShowActionSearchMatches = false; 129 | actionSearchMatchedActions = null; 130 | selectedMatchedActionRow = null; 131 | } 132 | 133 | 134 | protected class DataTableModel 135 | { 136 | private readonly string label; 137 | private readonly Action deleteAction; 138 | private readonly List editDataColumns = new(); 139 | 140 | public int EditDataColumnCount => editDataColumns.Count; 141 | public int TotalColumnCount => EditDataColumnCount + 4; 142 | 143 | public DataTableModel(string label, Action deleteAction) 144 | { 145 | this.label = label; 146 | this.deleteAction = deleteAction; 147 | } 148 | 149 | public void DrawTable(IEnumerable data) 150 | { 151 | if (ImGui.BeginTable(label, TotalColumnCount, 152 | ImGuiTableFlags.BordersH | ImGuiTableFlags.SortMulti)) 153 | { 154 | DrawTableHeadersRow(); 155 | DrawTableContentRow(data); 156 | ImGui.TableNextRow(); // dummy 157 | ImGui.EndTable(); 158 | } 159 | ImGui.NewLine(); 160 | } 161 | 162 | private void DrawTableHeadersRow() 163 | { 164 | ImGui.TableSetupColumn("Action ID", ImGuiTableColumnFlags.WidthFixed); 165 | ImGui.TableSetupColumn("Action", ImGuiTableColumnFlags.WidthStretch); 166 | ImGui.TableSetupColumn("ClassJob", 167 | ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.WidthFixed); 168 | foreach (var col in editDataColumns) 169 | if (col.hasInitWidth) 170 | ImGui.TableSetupColumn(col.headerName, col.flags, 171 | col.initWidthOrWeight); 172 | else ImGui.TableSetupColumn(col.headerName, col.flags); 173 | ImGui.TableSetupColumn("##Delete", 174 | ImGuiTableColumnFlags.NoSort | ImGuiTableColumnFlags.WidthFixed, 175 | UiBuilder.IconFont.FontSize * ImGuiHelpers.GlobalScale * 2); 176 | ImGui.TableHeadersRow(); 177 | } 178 | 179 | private void DrawTableContentRow(IEnumerable data) 180 | { 181 | foreach (var entry in data) 182 | { 183 | var excelRow = ActionData.GetActionExcelRow(entry.ActionId); 184 | ImGui.TableNextRow(); 185 | // Action ID 186 | ImGui.TableSetColumnIndex(0); 187 | ImGui.Text(entry.ActionId.ToString()); 188 | // Action 189 | ImGui.TableNextColumn(); 190 | ImGui.Text(excelRow?.Name ?? string.Empty); 191 | // ClassJob 192 | ImGui.TableNextColumn(); 193 | ImGui.Text(excelRow?.ClassJob.Value?.Name ?? string.Empty); 194 | // Custom fields 195 | foreach (var col in editDataColumns) 196 | { 197 | ImGui.TableNextColumn(); 198 | ImGui.Text(col.dataGetter(entry)); 199 | } 200 | // Delete 201 | ImGui.TableNextColumn(); 202 | if (ImGuiExt.IconButton( 203 | (int)entry.ActionId, FontAwesomeIcon.Minus, "Delete")) 204 | deleteAction(entry.ActionId); 205 | } 206 | } 207 | 208 | public void AddDataColumn(string name, ImGuiTableColumnFlags flags, 209 | bool hasInitWidth, float initWidthOrWeight, 210 | Func dataGetter) 211 | { 212 | editDataColumns.Add( 213 | new(name, flags, hasInitWidth, initWidthOrWeight, dataGetter)); 214 | } 215 | 216 | private class EditDataColumn 217 | { 218 | public readonly string headerName = null!; 219 | public readonly ImGuiTableColumnFlags flags; 220 | public readonly bool hasInitWidth; 221 | public readonly float initWidthOrWeight; 222 | public readonly Func dataGetter; 223 | 224 | public EditDataColumn(string name, ImGuiTableColumnFlags flags, 225 | bool hasInitWidth, float initWidthOrWeight, 226 | Func dataGetter) 227 | { 228 | this.headerName = name; 229 | this.flags = flags; 230 | this.hasInitWidth = hasInitWidth; 231 | this.initWidthOrWeight = initWidthOrWeight; 232 | this.dataGetter = dataGetter; 233 | } 234 | } 235 | } 236 | 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/UI/ConfigUi.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ImGuiNET; 3 | using System.Diagnostics; 4 | 5 | namespace ActionEffectRange.UI 6 | { 7 | public static class ConfigUi 8 | { 9 | private static readonly ActionBlacklistEditUI actionBlacklistEditUI = new(); 10 | private static readonly AoETypeEditUi aoeTypeEditUI = new(); 11 | private static readonly ConeAoEAngleEditUI coneAoEAngleEditUI = new(); 12 | 13 | public static void Draw() 14 | { 15 | if (!InConfig) return; 16 | 17 | DrawMainConfigUi(); 18 | 19 | DrawSubUIs(); 20 | 21 | RefreshConfig(); 22 | } 23 | 24 | private static void DrawMainConfigUi() 25 | { 26 | ImGui.SetNextWindowSize(new(500, 400), ImGuiCond.FirstUseEver); 27 | if (ImGui.Begin("ActionEffectRange: Configuration")) 28 | { 29 | ImGui.TreePush(); 30 | ImGui.Checkbox("Enable plugin", ref Config.Enabled); 31 | ImGui.TreePop(); 32 | 33 | if (Config.Enabled) 34 | { 35 | ImGui.NewLine(); 36 | ImGui.TreePush(); 37 | ImGui.Checkbox("Enable in PvP zones", ref Config.EnabledPvP); 38 | ImGui.TreePop(); 39 | 40 | ImGuiExt.SpacedSeparator(); 41 | 42 | ImGui.Text("Drawing Options"); 43 | ImGui.NewLine(); 44 | ImGui.TreePush(); 45 | ImGui.Columns(2, "DrawingOptions", false); 46 | ImGuiExt.CheckboxWithTooltip("Enable for beneficial actions", 47 | ref Config.DrawBeneficial, 48 | "If enabled, will draw effect range for actions with beneficial effects, " + 49 | "\n such as heals and buffs."); 50 | if (Config.DrawBeneficial) 51 | { 52 | ImGui.Indent(); 53 | ImGui.Text("Colour: "); 54 | ImGui.ColorEdit4("##BeneficialColour", ref Config.BeneficialColour); 55 | ImGui.Unindent(); 56 | } 57 | ImGui.NextColumn(); 58 | ImGuiExt.CheckboxWithTooltip("Enable for harmful actions", 59 | ref Config.DrawHarmful, 60 | "If enabled, will draw effect range for actions with harmful effects, " + 61 | "\n such as attacks and debuffs."); 62 | if (Config.DrawHarmful) 63 | { 64 | ImGui.Indent(); 65 | ImGui.Text("Colour: "); 66 | ImGui.ColorEdit4("##HarmfulColour", ref Config.HarmfulColour); 67 | ImGui.Unindent(); 68 | } 69 | ImGui.Columns(1); 70 | ImGui.NewLine(); 71 | ImGuiExt.CheckboxWithTooltip("Enable drawing for ACN/SMN/SCH pets' actions", 72 | ref Config.DrawACNPets, 73 | "If enabled, will also draw effect range for actions used by your own pet." + 74 | "\nAffects only Arcanist/Summoner/Scholar pets' AoE actions."); 75 | ImGuiExt.CheckboxWithTooltip("Enable drawing for other summoned companions", 76 | ref Config.DrawSummonedCompanions, 77 | "If enabled, will also draw effect range for actions used by companions you summoned." + 78 | "\nAffects actions by those companions that " + 79 | "have interactable models (just like normal SMN/SCH pets) " + 80 | "\n and are summoned when you are NOT Arcanist/Summoner/Scholar." + 81 | "\nFor example, MCH's autoturrets."); 82 | ImGuiExt.CheckboxWithTooltip("Enable drawing for ground-targeted actions", 83 | ref Config.DrawGT, 84 | "If enabled, will also draw effect range for ground-targeted actions."); 85 | ImGuiExt.CheckboxWithTooltip("Enable drawing for Special/Artillery actions", 86 | ref Config.DrawEx, 87 | "If enabled, will also draw effect range for actions of category " + 88 | $"\"{ActionData.GetActionCategoryName(Actions.Enums.ActionCategory.Special)}\" or " + 89 | $"\"{ActionData.GetActionCategoryName(Actions.Enums.ActionCategory.Artillery)}\"." + 90 | $"\n\nActions of these categories are generally available in certain contents/duties, " + 91 | $"\nafter you mount something or transformed into something, etc." + 92 | $"\n\nWarning: effect range drawing for these actions may be very inaccurate."); 93 | ImGui.NewLine(); 94 | 95 | ImGui.Text("Actions with large effect range: "); 96 | ImGui.Indent(); 97 | ImGui.Combo("##LargeDrawOpt", ref Config.LargeDrawOpt, 98 | Configuration.LargeDrawOptions, 99 | Configuration.LargeDrawOptions.Length); 100 | ImGuiExt.SetTooltipIfHovered( 101 | $"If set to any option other than \"{Configuration.LargeDrawOptions[0]}\", " + 102 | "AoEs with effect range at least " + 103 | "\n as large as the number specified below will be drawn" + 104 | "\n (or not drawn at all) according to the set option." + 105 | "\n\nThis only applies to Circle or Donut AoEs (including Ground-targeted ones)." + 106 | "\nOther types of AoEs are not affected by this setting."); 107 | ImGui.Unindent(); 108 | if (Config.LargeDrawOpt > 0) 109 | { 110 | ImGuiExt.InputIntWithTooltip("Apply to actions with effect range >= ", 111 | ref Config.LargeThreshold, 1, 1, 5, 55, 0, 80, 112 | "The setting will be applied to actions with at least the specified effect range." + 113 | "\nFor example, if set to 15, AoE such as Medica and Medica II" + 114 | "\n will be affected by the setting, but not Cure III."); 115 | } 116 | ImGui.TreePop(); 117 | 118 | ImGuiExt.SpacedSeparator(); 119 | 120 | ImGui.Text("Style options"); 121 | ImGui.NewLine(); 122 | ImGui.TreePush(); 123 | ImGui.Columns(2, "StyleOptions", false); 124 | ImGui.Checkbox("Draw outline (outer ring)", ref Config.OuterRing); 125 | if (Config.OuterRing) 126 | { 127 | ImGuiExt.DragIntWithTooltip("Thickness: ", 128 | ref Config.Thickness, 1, 1, 50, 60, null); 129 | if (Config.Thickness < 1) Config.Thickness = 1; 130 | if (Config.Thickness > 50) Config.Thickness = 50; 131 | } 132 | ImGui.NextColumn(); 133 | ImGui.Checkbox("Fill colour", ref Config.Filled); 134 | if (Config.Filled) 135 | { 136 | ImGuiExt.DragFloatWithTooltip("Opacity: ", 137 | ref Config.FillAlpha, .01f, 0, 1, "%.2f", 60, null); 138 | if (Config.FillAlpha < 0) Config.FillAlpha = 0; 139 | if (Config.FillAlpha > 1) Config.FillAlpha = 1; 140 | } 141 | ImGui.Columns(1); 142 | ImGui.NewLine(); 143 | ImGuiExt.DragIntWithTooltip("Smoothness: ", 144 | ref Config.NumSegments, 10, 40, 500, 100, 145 | "The larger number, the smoothier"); 146 | if (Config.NumSegments < 40) Config.NumSegments = 40; 147 | if (Config.NumSegments > 500) Config.NumSegments = 500; 148 | ImGui.TreePop(); 149 | 150 | ImGuiExt.SpacedSeparator(); 151 | 152 | ImGui.TreePush(); 153 | ImGuiExt.DragFloatWithTooltip("Delay before drawing (sec): ", 154 | ref Config.DrawDelay, .1f, 0, 2, "%.3f", 80, 155 | "Delay (in seconds) to wait immediately after using an action " + 156 | "before drawing the effect range."); 157 | ImGuiExt.DragFloatWithTooltip("Remove drawing after time (sec): ", 158 | ref Config.PersistSeconds, .1f, .1f, 5, "%.3f", 80, 159 | "Allow the effect range drawn to last for the given time " + 160 | "(in seconds) before erased from screen."); 161 | ImGui.TreePop(); 162 | 163 | ImGuiExt.SpacedSeparator(); 164 | 165 | ImGui.TreePush(); 166 | ImGuiExt.CheckboxWithTooltip("Enable drawing during casting", 167 | ref Config.DrawWhenCasting, 168 | "If enabled, will also draw effect range when you are casting an AoE action.\n" + 169 | "Currently this only works in PvE areas."); 170 | if (Config.DrawWhenCasting) 171 | { 172 | ImGui.NewLine(); 173 | ImGui.Text("Colour: "); 174 | ImGui.SameLine(); 175 | ImGui.ColorEdit4("##DrawWhenCastingColour", 176 | ref Config.DrawWhenCastingColour); 177 | ImGuiExt.CheckboxWithTooltip("Draw until casting ends", 178 | ref Config.DrawWhenCastingUntilCastEnd, 179 | "If enabled, drawing of the casting action will last " + 180 | "until the casting is finished or cancelled.\n" + 181 | "Otherwise it will be removed after the duration set above."); 182 | } 183 | ImGui.TreePop(); 184 | 185 | ImGuiExt.SpacedSeparator(); 186 | 187 | ImGuiExt.BulletTextWrappedWithHelpMarker("Advanced Customisation Options", 188 | "Use these customisation options to control the drawing for specific actions.\n\n" + 189 | "This is mainly for providing a temporary fix to incorrect drawing, " + 190 | "such as incorrect Cone AoE angles.\n" + 191 | "Usually you don't need to care about any of these customisations."); 192 | ImGui.NewLine(); 193 | ImGui.TreePush(); 194 | if (ImGui.Button("Edit Action Blacklist")) 195 | actionBlacklistEditUI.OpenUI(); 196 | // TODO: this is too confusing and useless rn, unless 197 | // there're customisations for other data such as effect range after types being overriden. 198 | //if (ImGui.Button("Customise AoE Types")) 199 | // aoeTypeEditUI.OpenUI(); 200 | if (ImGui.Button("Customise Cone AoE Drawing")) 201 | coneAoEAngleEditUI.OpenUI(); 202 | ImGui.TreePop(); 203 | 204 | ImGuiExt.SpacedSeparator(); 205 | 206 | ImGui.TreePush(); 207 | ImGui.Checkbox($"[DEBUG] Log debug info to Dalamud Console", ref Config.LogDebug); 208 | ImGui.NewLine(); 209 | ImGui.Checkbox("Show Sponsor/Support button", ref Config.ShowSponsor); 210 | if (Config.ShowSponsor) 211 | { 212 | ImGui.Indent(); 213 | ImGui.PushStyleColor(ImGuiCol.Button, 0xFF000000 | 0x005E5BFF); 214 | ImGui.PushStyleColor(ImGuiCol.ButtonActive, 0xDD000000 | 0x005E5BFF); 215 | ImGui.PushStyleColor(ImGuiCol.ButtonHovered, 0xAA000000 | 0x005E5BFF); 216 | if (ImGuiExt.Button("Buy Yomishino a Coffee", 217 | "You can support me and buy me a coffee if you want.\n" + 218 | "(Will open external link to Ko-fi in your browser)")) 219 | { 220 | Process.Start(new ProcessStartInfo 221 | { 222 | FileName = "https://ko-fi.com/yomishino", 223 | UseShellExecute = true 224 | }); 225 | } 226 | ImGui.PopStyleColor(3); 227 | ImGui.Unindent(); 228 | } 229 | ImGui.TreePop(); 230 | } 231 | 232 | ImGuiExt.SpacedSeparator(); 233 | 234 | if (ImGui.Button("Save & Close")) 235 | { 236 | CloseSubUIs(); 237 | ActionData.SaveCustomisedData(); 238 | Config.Save(); 239 | InConfig = false; 240 | } 241 | 242 | ImGui.End(); 243 | } 244 | } 245 | 246 | private static void DrawSubUIs() 247 | { 248 | actionBlacklistEditUI.Draw(); 249 | aoeTypeEditUI.Draw(); 250 | coneAoEAngleEditUI.Draw(); 251 | } 252 | 253 | private static void CloseSubUIs() 254 | { 255 | actionBlacklistEditUI.CloseUI(); 256 | aoeTypeEditUI.CloseUI(); 257 | coneAoEAngleEditUI.CloseUI(); 258 | } 259 | 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Actions/ActionWatcher.cs: -------------------------------------------------------------------------------- 1 | using ActionEffectRange.Actions.Data; 2 | using ActionEffectRange.Actions.EffectRange; 3 | using ActionEffectRange.Drawing; 4 | using ActionEffectRange.Helpers; 5 | using Dalamud.Game.ClientState.Objects.Types; 6 | using Dalamud.Hooking; 7 | using System.Collections.Generic; 8 | using System.Runtime.InteropServices; 9 | using Vector3Struct = FFXIVClientStructs.FFXIV.Common.Math.Vector3; 10 | 11 | namespace ActionEffectRange.Actions 12 | { 13 | internal static class ActionWatcher 14 | { 15 | private const float SeqExpiry = 2.5f; // this is arbitrary... 16 | 17 | private static uint lastSendActionSeq = 0; 18 | private static uint lastReceivedMainSeq = 0; 19 | 20 | private static readonly ActionSeqRecord playerActionSeqs = new(5); 21 | private static readonly HashSet skippedSeqs = new(); 22 | 23 | // Send what is executed; won't be called if queued but not yet executed 24 | // or failed to execute (e.g. cast cancelled) 25 | // Information here also more accurate than from UseAction; handles combo/proc 26 | // and target issues esp. with other plugins being used. 27 | // Not called for GT actions 28 | private delegate void SendActionDelegate(long targetObjectId, 29 | byte actionType, uint actionId, ushort sequence, long a5, long a6, long a7, long a8, long a9); 30 | private static readonly Hook? SendActionHook; 31 | private static void SendActionDetour(long targetObjectId, 32 | byte actionType, uint actionId, ushort sequence, long a5, long a6, long a7, long a8, long a9) 33 | { 34 | SendActionHook!.Original(targetObjectId, actionType, actionId, sequence, a5, a6, a7, a8, a9); 35 | 36 | try 37 | { 38 | LogUserDebug($"SendAction => target={targetObjectId:X}, " + 39 | $"action={actionId}, type={actionType}, seq={sequence}"); 40 | #if DEBUG 41 | PluginLog.Debug($"** SendAction: targetId={targetObjectId:X}, " + 42 | $"actionType={actionType}, actionId={actionId}, seq={sequence}, " + 43 | $"a5={a5:X}, a6={a6:X}, a7={a7:X}, a8={a8:X}, a9={a9:X}"); 44 | PluginLog.Debug($"** ---AcMgr: currentSeq{ActionManagerHelper.CurrentSeq}, " + 45 | $"lastRecSeq={ActionManagerHelper.LastRecievedSeq}"); 46 | #endif 47 | lastSendActionSeq = sequence; 48 | 49 | if (!ShouldProcessAction(actionType, actionId)) 50 | { 51 | skippedSeqs.Add(sequence); 52 | return; 53 | } 54 | 55 | var actionCategory = ActionData.GetActionCategory(actionId); 56 | if (!ShouldDrawForActionCategory(actionCategory)) 57 | { 58 | LogUserDebug($"---Skip action#{actionId}: " + 59 | $"Not drawing for actions of category {actionCategory}"); 60 | skippedSeqs.Add(sequence); 61 | return; 62 | } 63 | 64 | if (targetObjectId == 0 || targetObjectId == InvalidGameObjectId) 65 | { 66 | LogUserDebug($"---Skip: Invalid target #{targetObjectId}"); 67 | return; 68 | } 69 | else if (targetObjectId == LocalPlayer!.ObjectId) 70 | { 71 | var snapshot = new SeqSnapshot(sequence); 72 | playerActionSeqs.Add(new(actionId, snapshot, false)); 73 | } 74 | else 75 | { 76 | var target = ObejctTable.SearchById((uint)targetObjectId); 77 | if (target != null) 78 | { 79 | var snapshot = new SeqSnapshot(sequence, 80 | target.ObjectId, target.Position); 81 | playerActionSeqs.Add(new(actionId, snapshot, false)); 82 | } 83 | else 84 | { 85 | PluginLog.Error($"Cannot find valid target #{targetObjectId:X} for action#{actionId}"); 86 | return; 87 | } 88 | } 89 | } 90 | catch (Exception e) 91 | { 92 | PluginLog.Error($"{e}"); 93 | } 94 | } 95 | 96 | private delegate byte UseActionLocationDelegate(IntPtr actionManager, 97 | byte actionType, uint actionId, long targetObjectId, IntPtr location, uint param); 98 | private static readonly Hook? UseActionLocationHook; 99 | private static byte UseActionLocationDetour(IntPtr actionManager, 100 | byte actionType, uint actionId, long targetObjectId, IntPtr location, uint param) 101 | { 102 | var ret = UseActionLocationHook!.Original(actionManager, 103 | actionType, actionId, targetObjectId, location, param); 104 | try 105 | { 106 | #if DEBUG 107 | PluginLog.Debug($"** UseActionLocation: actionType={actionType}, " + 108 | $"actionId={actionId}, targetId={targetObjectId:X}, " + 109 | $"loc={location:X} " + 110 | $"=> {(Vector3)Marshal.PtrToStructure(location)} " + 111 | $"param={param}; ret={ret}"); 112 | #endif 113 | if (ret == 0) return ret; 114 | 115 | var seq = ActionManagerHelper.CurrentSeq; 116 | 117 | // Skip if already processed in SendAction; these are not GT actions 118 | if (seq == lastSendActionSeq) return ret; 119 | 120 | LogUserDebug($"UseActionLocation => " + 121 | $"Possible GT action #{actionId}, type={actionType};" + 122 | $"Seq={ActionManagerHelper.CurrentSeq}"); 123 | 124 | if (!Config.DrawGT) 125 | { 126 | LogUserDebug($"---Skip: Config: disabed for GT actions"); 127 | skippedSeqs.Add(seq); 128 | return ret; 129 | } 130 | 131 | if (!ShouldProcessAction(actionType, actionId)) 132 | { 133 | skippedSeqs.Add(seq); 134 | return ret; 135 | } 136 | 137 | var actionCategory = ActionData.GetActionCategory(actionId); 138 | if (!ShouldDrawForActionCategory(actionCategory)) 139 | { 140 | LogUserDebug($"---Skip action#{actionId}: " + 141 | $"Not drawing for actions of category {actionCategory}"); 142 | skippedSeqs.Add(seq); 143 | return ret; 144 | } 145 | 146 | // NOTE: Should've checked if the action could be mapped to some pet/pet-like actions 147 | // but currently none for those actions if we've reached here so just omit it for now 148 | 149 | playerActionSeqs.Add(new(actionId, new(seq), false)); 150 | } 151 | catch (Exception e) 152 | { 153 | PluginLog.Error($"{e}"); 154 | } 155 | return ret; 156 | } 157 | 158 | // useType == 0 when queued; 159 | // If queued action not executed immediately, 160 | // useType == 1 when this function is called later to actually execute the action 161 | private delegate byte UseActionDelegate(IntPtr actionManager, 162 | byte actionType, uint actionId, long targetObjectId, uint param, uint useType, int pvp, IntPtr a8); 163 | private static readonly Hook? UseActionHook; 164 | // Detour used mainly for processing draw-when-casting 165 | // When applicable, drawing is triggered immediately 166 | private static byte UseActionDetour(IntPtr actionManager, 167 | byte actionType, uint actionId, long targetObjectId, uint param, uint useType, int pvp, IntPtr a8) 168 | { 169 | var ret = UseActionHook!.Original(actionManager, 170 | actionType, actionId, targetObjectId, param, useType, pvp, a8); 171 | 172 | try 173 | { 174 | LogUserDebug($"UseAction => actionType={actionType}, " + 175 | $"actionId={actionId}, targetId={targetObjectId:X}"); 176 | #if DEBUG 177 | PluginLog.Debug($"** UseAction: param={param}, useType={useType}, pvp={pvp}, a8={a8:X}; " + 178 | $"ret={ret}; CurrentSeq={ActionManagerHelper.CurrentSeq}"); 179 | #endif 180 | if (!DrawWhenCasting) return ret; 181 | 182 | if (!ActionManagerHelper.IsCasting) 183 | { 184 | LogUserDebug($"---Skip: not casting"); 185 | return ret; 186 | } 187 | 188 | if (ret == 0) 189 | { 190 | LogUserDebug($"---Skip: not drawing on useType={useType} && ret={ret}"); 191 | return ret; 192 | } 193 | 194 | var castActionId = ActionManagerHelper.CastingActionId; 195 | 196 | if (!ShouldProcessAction(actionType, castActionId)) 197 | return ret; 198 | 199 | var actionIdsToDraw = new List { castActionId }; 200 | if (ActionData.TryGetActionWithAdditionalEffects( 201 | castActionId, out var additionals)) 202 | actionIdsToDraw.AddRange(additionals); 203 | 204 | foreach (var a in actionIdsToDraw) 205 | { 206 | var erdata = EffectRangeDataManager.NewData(a); 207 | 208 | if (erdata == null) 209 | { 210 | PluginLog.Error($"Cannot get data for action#{a}"); 211 | continue; 212 | } 213 | 214 | if (!ShouldDrawForActionCategory(erdata.Category, true)) 215 | { 216 | LogUserDebug($"---Skip action#{erdata.ActionId}: " + 217 | $"Not drawing for actions of category {erdata.Category}"); 218 | continue; 219 | } 220 | 221 | erdata = EffectRangeDataManager.CustomiseEffectRangeData(erdata); 222 | if (!CheckShouldDrawPostCustomisation(erdata)) continue; 223 | 224 | var seq = ActionManagerHelper.CurrentSeq; 225 | float rotation = ActionManagerHelper.CastRotation; 226 | 227 | if (erdata.IsGTAction) 228 | { 229 | var targetPos = new Vector3( 230 | ActionManagerHelper.CastTargetPosX, 231 | ActionManagerHelper.CastTargetPosY, 232 | ActionManagerHelper.CastTargetPosZ); 233 | LogUserDebug($"UseAction => Triggering draw-when-casting, " + 234 | $"CastingActionId={castActionId}, GT action, " + 235 | $"CastPosition={targetPos}, CastRotation={rotation}"); 236 | LogUserDebug($"---Adding DrawData for action #{castActionId} " + 237 | $"from player, using cast position info"); 238 | EffectRangeDrawing.AddEffectRangeToDraw(seq, 239 | DrawTrigger.Casting, erdata, LocalPlayer!.Position, 240 | targetPos, rotation); 241 | } 242 | else 243 | { 244 | var castTargetId = ActionManagerHelper.CastTargetObjectId; 245 | LogUserDebug($"UseAction => Triggering draw-when-casting, " + 246 | $"CastingActionId={castActionId}, " + 247 | $"CastTargetObjectId={castTargetId}, CastRotation={rotation}"); 248 | 249 | GameObject? target = null; 250 | if (castTargetId == LocalPlayer!.ObjectId) 251 | target = LocalPlayer; 252 | else if (castTargetId != 0 253 | && castTargetId != InvalidGameObjectId) 254 | target = ObejctTable.SearchById(castTargetId); 255 | 256 | if (target != null) 257 | { 258 | LogUserDebug($"---Adding DrawData for action #{castActionId} " + 259 | $"from player, using cast position info"); 260 | // We do not have GT actions here 261 | EffectRangeDrawing.AddEffectRangeToDraw( 262 | ActionManagerHelper.CurrentSeq, DrawTrigger.Casting, 263 | erdata, LocalPlayer!.Position, target.Position, rotation); 264 | } 265 | else LogUserDebug($"---Failed: Target #{castTargetId:X} not found"); 266 | } 267 | } 268 | } 269 | catch (Exception e) 270 | { 271 | PluginLog.Error($"{e}"); 272 | } 273 | 274 | return ret; 275 | } 276 | 277 | 278 | private delegate void ReceiveActionEffectDelegate(int sourceObjectId, IntPtr sourceActor, 279 | IntPtr position, IntPtr effectHeader, IntPtr effectArray, IntPtr effectTrail); 280 | private static readonly Hook? ReceiveActionEffectHook; 281 | 282 | private static void ReceiveActionEffectDetour(int sourceObjectId, IntPtr sourceActor, 283 | IntPtr position, IntPtr effectHeader, IntPtr effectArray, IntPtr effectTrail) 284 | { 285 | ReceiveActionEffectHook!.Original(sourceObjectId, sourceActor, 286 | position, effectHeader, effectArray, effectTrail); 287 | 288 | try 289 | { 290 | #if DEBUG 291 | PluginLog.Debug($"** ReceiveActionEffect: src={sourceObjectId:X}, " + 292 | $"pos={(Vector3)Marshal.PtrToStructure(position)}; " + 293 | $"AcMgr: CurrentSeq={ActionManagerHelper.CurrentSeq}, " + 294 | $"LastRecSeq={ActionManagerHelper.LastRecievedSeq}"); 295 | #endif 296 | 297 | if (effectHeader == IntPtr.Zero) 298 | { 299 | PluginLog.Error("ReceiveActionEffect: effectHeader ptr is zero"); 300 | return; 301 | } 302 | var header = Marshal.PtrToStructure(effectHeader); 303 | LogUserDebug($"ReceiveActionEffect => " + 304 | $"source={sourceObjectId:X}, target={header.TargetObjectId:X}, " + 305 | $"action={header.ActionId}, seq={header.Sequence}"); 306 | #if DEBUG 307 | PluginLog.Debug($"** ---effectHeader: target={header.TargetObjectId:X}, " + 308 | $"action={header.ActionId}, unkObjId={header.UnkObjectId:X}, " + 309 | $"seq={header.Sequence}, unk={header.Unk_1A:X}"); 310 | #endif 311 | 312 | if (header.Sequence > 0) 313 | { 314 | lastReceivedMainSeq = header.Sequence; 315 | if (skippedSeqs.Contains(header.Sequence)) 316 | { 317 | LogUserDebug($"---Skip: not processing Seq#{header.Sequence}"); 318 | return; 319 | } 320 | } 321 | 322 | if (!IsPlayerLoaded) 323 | { 324 | LogUserDebug($"---Skip: PC not loaded"); 325 | return; 326 | } 327 | 328 | if (sourceObjectId != LocalPlayer!.ObjectId 329 | && (!PetWatcher.HasPetPresent 330 | || PetWatcher.GetPetObjectId() != sourceObjectId)) 331 | { 332 | LogUserDebug($"---Skip: Effect triggered by others"); 333 | return; 334 | } 335 | 336 | var erdata = EffectRangeDataManager.NewData(header.ActionId); 337 | if (erdata == null) 338 | { 339 | PluginLog.Error($"Cannot get data for action#{header.ActionId}"); 340 | return; 341 | } 342 | 343 | // Some additional effects (e.g. #29706 additional effect for Pneuma pvp) 344 | // have ActionCategory=0 345 | if (!ShouldDrawForActionCategory(erdata.Category, true)) 346 | { 347 | LogUserDebug($"---Skip action#{erdata.ActionId}: " + 348 | $"Not drawing for actions of category {erdata.Category}"); 349 | return; 350 | } 351 | 352 | erdata = EffectRangeDataManager.CustomiseEffectRangeData(erdata); 353 | 354 | if (!CheckShouldDrawPostCustomisation(erdata)) return; 355 | 356 | var mainSeq = header.Sequence > 0 357 | ? header.Sequence : lastReceivedMainSeq; 358 | 359 | if (sourceObjectId == LocalPlayer!.ObjectId) 360 | { 361 | // Source is pc 362 | 363 | // TODO: config on/off: auto triggered effects 364 | // (such as effects on time elapsed, receiving damage, ...) 365 | bool drawForAuto = true; // placeholder 366 | 367 | ActionSeqInfo? seqInfo = null; 368 | 369 | // For additional effects, received data always has seq=0 370 | // Match seq using predefined mapping and some heuristics 371 | if (!ActionData.ShouldNotUseCachedSeq(header.ActionId)) 372 | seqInfo = FindRecordedSeqInfo(header.Sequence, header.ActionId); 373 | 374 | if (seqInfo != null) 375 | { 376 | // Additional effect may have different target (e.g. self vs targeted enemy) 377 | Vector3 targetPos = erdata.IsGTAction 378 | ? Marshal.PtrToStructure(position) 379 | : (header.TargetObjectId == LocalPlayer.ObjectId 380 | ? seqInfo.SeqSnapshot.PlayerPosition 381 | : seqInfo.SeqSnapshot.TargetPosition); 382 | 383 | LogUserDebug($"---Adding DrawData for action #{header.ActionId} " + 384 | $"from player, using SeqSnapshot#{seqInfo.Seq}"); 385 | EffectRangeDrawing.AddEffectRangeToDraw(seqInfo.Seq, 386 | DrawTrigger.Used, erdata, seqInfo.SeqSnapshot.PlayerPosition, 387 | targetPos, seqInfo.SeqSnapshot.PlayerRotation); 388 | } 389 | else if (drawForAuto) 390 | { 391 | LogUserDebug($"---Adding DrawData for action #{header.ActionId} " + 392 | $"from player, using current position info"); 393 | 394 | if (erdata.IsGTAction) 395 | EffectRangeDrawing.AddEffectRangeToDraw(mainSeq, 396 | DrawTrigger.Used, erdata, LocalPlayer!.Position, 397 | Marshal.PtrToStructure(position), 398 | LocalPlayer!.Rotation); 399 | else 400 | { 401 | GameObject? target = null; 402 | if (header.TargetObjectId == sourceObjectId) // Self-targeting 403 | target = LocalPlayer; 404 | else if (header.TargetObjectId != 0 405 | && header.TargetObjectId != InvalidGameObjectId) 406 | target = ObejctTable.SearchById((uint)header.TargetObjectId); 407 | 408 | if (target != null) 409 | { 410 | EffectRangeDrawing.AddEffectRangeToDraw(mainSeq, 411 | DrawTrigger.Used, erdata, LocalPlayer!.Position, 412 | target.Position, LocalPlayer!.Rotation); 413 | } 414 | else LogUserDebug($"---Failed: Target #{header.TargetObjectId:X} not found"); 415 | } 416 | } 417 | else LogUserDebug($"---Skip: Not drawing for auto-triggered action #{header.ActionId}"); 418 | } 419 | else 420 | { 421 | // Source may be player's pet/pet-like object 422 | 423 | // NOTE: Always use current position infos here. 424 | // Due to potential delay, info snapshot at the time 425 | // player action is used is not accurate for pet actions as well. 426 | // E.g., when pet is moving, any other action will be delayed; 427 | // once the pet is settled on a location, the character positions 428 | // are snapshot and pet action is processed based on this snapshot; 429 | // but this also means the snapshot produced when player used 430 | // the "parent" action is already out-of-date. 431 | 432 | // Just ignore if the pet is no longer present at this point (e.g. due to delay). 433 | // Not very common as the game already defers removing pet objects 434 | // possibly to account for delays 435 | if (PetWatcher.HasPetPresent 436 | && PetWatcher.GetPetObjectId() == sourceObjectId) 437 | { 438 | if (PetWatcher.IsCurrentPetACNPet() && !Config.DrawACNPets) 439 | { 440 | LogUserDebug($"---Skip: Drawing for action#{header.ActionId} " + 441 | "from ACN/SMN/SCH pets configured OFF"); 442 | return; 443 | } 444 | if (PetWatcher.IsCurrentPetNonACNNamedPet() 445 | && !Config.DrawSummonedCompanions) 446 | { 447 | LogUserDebug($"---Skip: Drawing for action#{header.ActionId} " + 448 | "from summoned companions of non-ACN based jobs configured OFF"); 449 | return; 450 | } 451 | if (PetWatcher.IsCurrentPetNameless() 452 | && !Config.DrawGT) 453 | { 454 | // Assuming all nameless pets are ground placed objects ... 455 | LogUserDebug($"---Skip: Drawing for action#{header.ActionId} " + 456 | "from possibly ground placed object configured OFF"); 457 | return; 458 | } 459 | 460 | // TODO: Check if the effect is auto-triggered if it is from placed object? 461 | // (Assuming placed obj does not move, cached seq snapshot can be used.) 462 | // (Configurable opt) 463 | 464 | LogUserDebug($"---Add DrawData for action #{header.ActionId} " + 465 | $"from pet / pet-like object #{sourceObjectId:X}, using current position info"); 466 | 467 | if (erdata.IsGTAction) 468 | EffectRangeDrawing.AddEffectRangeToDraw(mainSeq, 469 | DrawTrigger.Used, erdata, LocalPlayer!.Position, 470 | Marshal.PtrToStructure(position), 471 | LocalPlayer!.Rotation); 472 | else 473 | { 474 | GameObject? target = null; 475 | if (header.TargetObjectId == sourceObjectId) // Pet self-targeting 476 | target = PetWatcher.GetPet(); 477 | else if (header.TargetObjectId == LocalPlayer.ObjectId) 478 | target = LocalPlayer; 479 | else if (header.TargetObjectId != 0 480 | && header.TargetObjectId != InvalidGameObjectId) 481 | target = ObejctTable.SearchById((uint)header.TargetObjectId); 482 | 483 | if (target != null) 484 | { 485 | var source = PetWatcher.GetPet(); 486 | EffectRangeDrawing.AddEffectRangeToDraw(mainSeq, 487 | DrawTrigger.Used, erdata, 488 | PetWatcher.GetPetPosition(), 489 | target.Position, PetWatcher.GetPetRotation()); 490 | } 491 | else LogUserDebug($"---Failed: Target #{header.TargetObjectId:X} not found"); 492 | } 493 | } 494 | else LogUserDebug($"---Skip: source actor #{sourceObjectId:X} not matching pc or pet"); 495 | } 496 | } 497 | catch (Exception e) 498 | { 499 | PluginLog.Error($"{e}"); 500 | } 501 | } 502 | 503 | 504 | #region Checks 505 | 506 | private static bool ShouldProcessAction(byte actionType, uint actionId) 507 | { 508 | if (!IsPlayerLoaded) 509 | { 510 | LogUserDebug($"---Skip: PC not loaded"); 511 | return false; 512 | } 513 | if (!ShouldProcessActionType(actionType) 514 | || !ShouldProcessAction(actionId)) 515 | { 516 | LogUserDebug($"---Skip: Not processing " + 517 | $"action#{actionId}, ActionType={actionType}"); 518 | return false; 519 | } 520 | return true; 521 | } 522 | 523 | private static bool ShouldProcessActionType(uint actionType) 524 | => actionType == 0x1 || actionType == 0xE; // pve 0x1, pvp 0xE 525 | 526 | private static bool ShouldProcessAction(uint actionId) 527 | => !ActionData.IsActionBlacklisted(actionId); 528 | 529 | 530 | private static bool ShouldDrawForActionCategory( 531 | Enums.ActionCategory actionCategory, bool allowCateogry0 = false) 532 | => ActionData.IsCombatActionCategory(actionCategory) 533 | || Config.DrawEx && ActionData.IsSpecialOrArtilleryActionCategory(actionCategory) 534 | || allowCateogry0 && actionCategory == 0; 535 | 536 | // Only check for circle and donut in Large EffectRange check 537 | private static bool ShouldDrawForEffectRange(EffectRangeData data) 538 | => data.EffectRange > 0 539 | && (!(data is CircleAoEEffectRangeData || data is DonutAoEEffectRangeData) 540 | || Config.LargeDrawOpt != 1 541 | || data.EffectRange < Config.LargeThreshold); 542 | 543 | // Note: will not draw for `None` (=0) 544 | private static bool ShouldDrawForHarmfulness(EffectRangeData data) 545 | => EffectRangeDataManager.IsHarmfulAction(data) && Config.DrawHarmful 546 | || EffectRangeDataManager.IsBeneficialAction(data) && Config.DrawBeneficial; 547 | 548 | 549 | private static bool CheckShouldDrawPostCustomisation(EffectRangeData data) 550 | { 551 | if (!ShouldDrawForEffectRange(data)) 552 | { 553 | LogUserDebug($"---Skip action #{data.ActionId}: " + 554 | $"Not drawing for actions of effect range = {data.EffectRange}"); 555 | return false; 556 | } 557 | 558 | if (!ShouldDrawForHarmfulness(data)) 559 | { 560 | LogUserDebug($"---Skip action #{data.ActionId}: " + 561 | $"Not drawing for harmful/beneficial actions = {data.Harmfulness}"); 562 | return false; 563 | } 564 | 565 | return true; 566 | } 567 | 568 | #endregion 569 | 570 | 571 | private static ActionSeqInfo? FindRecordedSeqInfo( 572 | ushort receivedSeq, uint receivedActionId) 573 | { 574 | foreach (var seqInfo in playerActionSeqs) 575 | { 576 | if (IsSeqExpired(seqInfo)) continue; 577 | if (receivedSeq > 0) // Primary effects from player actions 578 | { 579 | if (receivedSeq == seqInfo.Seq) 580 | { 581 | LogUserDebug($"---* Recorded sequence matched"); 582 | return seqInfo; 583 | } 584 | } 585 | else if (ActionData.AreRelatedPlayerTriggeredActions( 586 | seqInfo.ActionId, receivedActionId)) 587 | { 588 | LogUserDebug($"---* Related recorded sequence found"); 589 | return seqInfo; 590 | } 591 | } 592 | LogUserDebug($"---* No recorded sequence matched"); 593 | return null; 594 | } 595 | 596 | private static void ClearSeqRecordCache() 597 | { 598 | playerActionSeqs.Clear(); 599 | skippedSeqs.Clear(); 600 | } 601 | 602 | private static bool IsSeqExpired(ActionSeqInfo info) 603 | => info.ElapsedSeconds > SeqExpiry; 604 | 605 | private static void OnClassJobChangedClearCache(uint classJobId) 606 | => ClearSeqRecordCache(); 607 | 608 | private static void OnTerritoryChangedClearCache(ushort terr) 609 | => ClearSeqRecordCache(); 610 | 611 | 612 | static ActionWatcher() 613 | { 614 | UseActionHook ??= InteropProvider.HookFromAddress( 615 | ActionManagerHelper.FpUseAction, UseActionDetour); 616 | UseActionLocationHook ??= InteropProvider.HookFromAddress( 617 | ActionManagerHelper.FpUseActionLocation, UseActionLocationDetour); 618 | ReceiveActionEffectHook ??= InteropProvider.HookFromAddress( 619 | SigScanner.ScanText("E8 ?? ?? ?? ?? 48 8B 8D F0 03 00 00"), 620 | ReceiveActionEffectDetour); 621 | SendActionHook ??= InteropProvider.HookFromAddress( 622 | SigScanner.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? F3 0F 10 3D ?? ?? ?? ?? 48 8D 4D BF"), 623 | SendActionDetour); 624 | 625 | PluginLog.Information("ActionWatcher init:\n" + 626 | $"\tUseActionHook @{UseActionHook?.Address ?? IntPtr.Zero:X}\n" + 627 | $"\tUseActionLoactionHook @{UseActionLocationHook?.Address ?? IntPtr.Zero:X}\n" + 628 | $"\tReceiveActionEffectHook @{ReceiveActionEffectHook?.Address ?? IntPtr.Zero:X}\n" + 629 | $"\tSendActionHook @{SendActionHook?.Address ?? IntPtr.Zero:X}"); 630 | } 631 | 632 | public static void Enable() 633 | { 634 | UseActionHook?.Enable(); 635 | UseActionLocationHook?.Enable(); 636 | SendActionHook?.Enable(); 637 | ReceiveActionEffectHook?.Enable(); 638 | 639 | ClientState.TerritoryChanged += OnTerritoryChangedClearCache; 640 | ClassJobWatcher.ClassJobChanged += OnClassJobChangedClearCache; 641 | } 642 | 643 | public static void Disable() 644 | { 645 | UseActionHook?.Disable(); 646 | UseActionLocationHook?.Disable(); 647 | SendActionHook?.Disable(); 648 | ReceiveActionEffectHook?.Disable(); 649 | 650 | ClientState.TerritoryChanged -= OnTerritoryChangedClearCache; 651 | ClassJobWatcher.ClassJobChanged -= OnClassJobChangedClearCache; 652 | } 653 | 654 | public static void Dispose() 655 | { 656 | Disable(); 657 | UseActionHook?.Dispose(); 658 | UseActionLocationHook?.Dispose(); 659 | SendActionHook?.Dispose(); 660 | ReceiveActionEffectHook?.Dispose(); 661 | } 662 | 663 | } 664 | 665 | 666 | [StructLayout(LayoutKind.Explicit)] 667 | public struct ActionEffectHeader 668 | { 669 | [FieldOffset(0x0)] public long TargetObjectId; 670 | [FieldOffset(0x8)] public uint ActionId; 671 | // 0x14 Unk; but have some value keep accumulating here 672 | [FieldOffset(0x14)] public uint UnkObjectId; 673 | // 0x18 Corresponds exactly to the sequence of the action used; 674 | // AA, pet's action effect etc. will be 0 here 675 | [FieldOffset(0x18)] public ushort Sequence; 676 | // 0x1A Seems related to SendAction's arg a5, but not always the same value 677 | [FieldOffset(0x1A)] public ushort Unk_1A; 678 | // rest?? 679 | } 680 | } 681 | --------------------------------------------------------------------------------