From 5d1dd9dbf5612963a2dcd365f6e3c61f32d26507 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sat, 25 Jan 2025 19:14:00 +0000 Subject: [PATCH 1/3] Fixed pending knockbacks. --- BossMod/AI/AIBehaviour.cs | 2 +- BossMod/Data/Actor.cs | 4 +- BossMod/Data/ActorState.cs | 42 +++++++++++++++---- BossMod/Framework/WorldStateGameSync.cs | 17 ++++++++ .../Ultimate/FRU/P4CrystallizeTime.cs | 15 +++---- BossMod/Replay/ReplayParserLog.cs | 26 ++++++++---- BossMod/Replay/ReplayRecorder.cs | 13 ++++++ BossMod/Replay/Visualization/OpList.cs | 1 + .../Visualization/ReplayDetailsWindow.cs | 13 +++++- FFXIVClientStructs | 2 +- 10 files changed, 107 insertions(+), 28 deletions(-) diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index 994faaf6b0..1d3477fcd5 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -53,7 +53,7 @@ public void Execute(Actor player, Actor master) _followMaster = master != player; // note: if there are pending knockbacks, don't update navigation decision to avoid fucking up positioning - if (player.PendingKnockbacks.Count == 0) + if (player.PendingKnockbacks == 0) { _naviDecision = BuildNavigationDecision(player, master, ref target); // there is a difference between having a small positive leeway and having a negative one for pathfinding, prefer to keep positive diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index 6e13ea4919..a4271152b3 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -71,6 +71,7 @@ public record struct ActorStatus(uint ID, ushort Extra, DateTime ExpireAt, ulong public record struct ActorModelState(byte ModelState, byte AnimState1, byte AnimState2); +public readonly record struct ActorIncomingEffect(uint GlobalSequence, int TargetIndex, ulong SourceInstanceId, ActionID Action, ActionEffects Effects); public record struct PendingEffect(uint GlobalSequence, int TargetIndex, ulong SourceInstanceId, DateTime Expiration); public record struct PendingEffectDelta(PendingEffect Effect, int Value); public record struct PendingEffectStatus(PendingEffect Effect, uint StatusId); @@ -105,13 +106,14 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public ActorCastInfo? CastInfo; public ActorTetherInfo Tether; public ActorStatus[] Statuses = new ActorStatus[60]; // empty slots have ID=0 + public ActorIncomingEffect[] IncomingEffects = new ActorIncomingEffect[32]; // all pending lists are sorted by expiration time public List PendingHPDifferences = []; // damage and heal effects applied to the target that were not confirmed yet public List PendingMPDifferences = []; public List PendingStatuses = []; public List PendingDispels = []; - public List PendingKnockbacks = []; + public int PendingKnockbacks; public Role Role => Class.GetRole(); public ClassCategory ClassCategory => Class.GetClassCategory(); diff --git a/BossMod/Data/ActorState.cs b/BossMod/Data/ActorState.cs index 03599d603f..beb8fdab91 100644 --- a/BossMod/Data/ActorState.cs +++ b/BossMod/Data/ActorState.cs @@ -43,6 +43,9 @@ public IEnumerable CompareToInitial() for (int i = 0; i < act.Statuses.Length; ++i) if (act.Statuses[i].ID != 0) yield return new OpStatus(act.InstanceID, i, act.Statuses[i]); + for (int i = 0; i < act.IncomingEffects.Length; ++i) + if (act.IncomingEffects[i].GlobalSequence != 0) + yield return new OpIncomingEffect(act.InstanceID, i, act.IncomingEffects[i]); } } @@ -100,14 +103,6 @@ private void AddPendingEffects(Actor source, ActorCastEvent ev, DateTime timesta case ActionEffectType.LoseStatusEffectSource: effTarget.PendingDispels.Add(new(header, eff.Value)); break; - case ActionEffectType.Knockback: - case ActionEffectType.Attract1: - case ActionEffectType.Attract2: - case ActionEffectType.AttractCustom1: - case ActionEffectType.AttractCustom2: - case ActionEffectType.AttractCustom3: - effTarget.PendingKnockbacks.Add(header); - break; } } } @@ -120,7 +115,6 @@ private void RemovePendingEffects(Actor target, RemovePendingEffectPredicate pre target.PendingMPDifferences.RemoveAll(e => predicate(e.Effect)); target.PendingStatuses.RemoveAll(e => predicate(e.Effect)); target.PendingDispels.RemoveAll(e => predicate(e.Effect)); - target.PendingKnockbacks.RemoveAll(e => predicate(e)); } // implementation of operations @@ -444,6 +438,36 @@ public override void Write(ReplayRecorder.Output output) } } + public Event IncomingEffectAdd = new(); + public Event IncomingEffectRemove = new(); + public sealed record class OpIncomingEffect(ulong InstanceID, int Index, ActorIncomingEffect Value) : Operation(InstanceID) + { + protected override void ExecActor(WorldState ws, Actor actor) + { + ref var prev = ref actor.IncomingEffects[Index]; + if (prev.GlobalSequence != 0 && (prev.GlobalSequence != Value.GlobalSequence || prev.TargetIndex != Value.TargetIndex)) + { + if (prev.Effects.Any(eff => eff.Type is ActionEffectType.Knockback or ActionEffectType.Attract1 or ActionEffectType.Attract2 or ActionEffectType.AttractCustom1 or ActionEffectType.AttractCustom2 or ActionEffectType.AttractCustom3)) + --actor.PendingKnockbacks; + ws.Actors.IncomingEffectRemove.Fire(actor, Index); + } + actor.IncomingEffects[Index] = Value; + if (Value.GlobalSequence != 0) + { + if (Value.Effects.Any(eff => eff.Type is ActionEffectType.Knockback or ActionEffectType.Attract1 or ActionEffectType.Attract2 or ActionEffectType.AttractCustom1 or ActionEffectType.AttractCustom2 or ActionEffectType.AttractCustom3)) + ++actor.PendingKnockbacks; + ws.Actors.IncomingEffectAdd.Fire(actor, Index); + } + } + public override void Write(ReplayRecorder.Output output) + { + if (Value.GlobalSequence != 0) + output.EmitFourCC("AIE+"u8).EmitActor(InstanceID).Emit(Index).Emit(Value.GlobalSequence).Emit(Value.TargetIndex).EmitActor(Value.SourceInstanceId).Emit(Value.Action).Emit(Value.Effects); + else + output.EmitFourCC("AIE-"u8).EmitActor(InstanceID).Emit(Index); + } + } + // TODO: this should really be an actor field, but I have no idea what triggers icon clear... public Event IconAppeared = new(); public sealed record class OpIcon(ulong InstanceID, uint IconID, ulong TargetID) : Operation(InstanceID) diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index 8c95de6219..0d466e6059 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -352,6 +352,23 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) UpdateActorStatus(act, i, curStatus); } } + + var aeh = chr != null ? chr->GetActionEffectHandler() : null; + if (aeh != null) + { + for (int i = 0; i < aeh->IncomingEffects.Length; ++i) + { + ref var eff = ref aeh->IncomingEffects[i]; + ref var prev = ref act.IncomingEffects[i]; + if ((prev.GlobalSequence, prev.TargetIndex) != (eff.GlobalSequence != 0 ? (eff.GlobalSequence, eff.TargetIndex) : (0, 0))) + { + var effects = new ActionEffects(); + for (int j = 0; j < ActionEffects.MaxCount; ++j) + effects[j] = *(ulong*)eff.Effects.Effects.GetPointer(j); + _ws.Execute(new ActorState.OpIncomingEffect(act.InstanceID, i, new(eff.GlobalSequence, eff.TargetIndex, eff.Source, new((ActionType)eff.ActionType, eff.ActionId), effects))); + } + } + } } private void UpdateActorCastInfo(Actor act, ActorCastInfo? cast) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs index 6917cbeb2a..2bed294d91 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -387,12 +387,12 @@ public enum Hint private readonly P4CrystallizeTime? _ct = module.FindComponent(); private readonly P4CrystallizeTimeDragonHead? _heads = module.FindComponent(); private readonly P4CrystallizeTimeMaelstrom? _hourglass = module.FindComponent(); - private bool KnockbacksDone; + private DateTime KnockbacksResolve; // default before knockbacks are done, set to estimated resolve time after they are done private bool DarknessDone; public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (actor.PendingKnockbacks.Count > 0) + if (actor.PendingKnockbacks > 0) return; // don't move while waiting for kb to resolve... var hint = CalculateHint(slot); @@ -402,7 +402,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (hint.offset.LengthSq() > 18 * 18) hint.offset *= 19.5f / 19; - if (hint.hint.HasFlag(Hint.KnockbackFrom) && Raid.WithoutSlot().Any(p => p.PendingKnockbacks.Count > 0)) + if (hint.hint.HasFlag(Hint.KnockbackFrom) && Raid.WithoutSlot().Any(p => p.PendingKnockbacks > 0)) { return; // don't even try moving until all knockbacks are resolved, that can fuck up others... } @@ -454,7 +454,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) switch ((AID)spell.Action.ID) { case AID.CrystallizeTimeDarkAero: - KnockbacksDone = true; + KnockbacksResolve = WorldState.FutureTime(1); // it takes ~0.8s to resolve knockbacks break; case AID.UltimateRelativityUnholyDarkness: DarknessDone = true; @@ -494,7 +494,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) { if (numHourglassesDone < 2) return (SafeOffsetDodgeFirstHourglassSouth(clawSide), Hint.SafespotRough | Hint.Maelstrom); // dodge first hourglass by the south side - if (!KnockbacksDone) + if (KnockbacksResolve == default) return (SafeOffsetPreKnockbackSouth(clawSide, 19), Hint.SafespotPrecise); // preposition to knock party across if (numHourglassesDone < 4 && clawSide == northSlowSide) return (SafeOffsetDodgeSecondHourglassSouth(clawSide), Hint.SafespotRough | Hint.Maelstrom); // dodge second hourglass; note that player on the slow side can already go intercept the head @@ -504,7 +504,8 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) { var headOff = head.Position - Module.Center; var headDir = Angle.FromDirection(headOff) * clawSide; // always decreases as head moves - return (headDir.Rad > IdealSecondHeadBaitAngle.Rad ? SafeOffsetSecondHeadBait(clawSide) : headOff, Hint.SafespotPrecise | (clawSide != northSlowSide ? Hint.KnockbackFrom : Hint.None)); + var hint = clawSide != northSlowSide && WorldState.CurrentTime < KnockbacksResolve ? Hint.None : Hint.SafespotPrecise; // Hint.KnockbackFrom?.. depends on how new pending knockbacks work for others + return (headDir.Rad > IdealSecondHeadBaitAngle.Rad ? SafeOffsetSecondHeadBait(clawSide) : headOff, hint); } // head is done, so dodge between last two hourglasses return (SafeOffsetChillSouth(northSlowSide), Hint.Maelstrom | Hint.Heads | Hint.Mid); @@ -534,7 +535,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) { if (numHourglassesDone < 2) return (SafeOffsetDodgeFirstHourglassSouth(-northSlowSide), Hint.SafespotRough | Hint.Maelstrom); // dodge first hourglass by the south side - if (!KnockbacksDone) + if (KnockbacksResolve == default) return (SafeOffsetPreKnockbackSouth(-northSlowSide, 17), Hint.Knockback); // preposition to knockback across arena // from now on move together with eruption return HintFangEruption(northSlowSide, numHourglassesDone); diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index 9c2f43237f..48895d881b 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -37,6 +37,7 @@ protected virtual void Dispose(bool disposing) { } public abstract ActionID ReadAction(); public abstract Class ReadClass(); public abstract ActorStatus ReadStatus(); + public abstract ActionEffects ReadActionEffects(); public abstract void ReadTargets(List list); public abstract (float, float) ReadFloatPair(); public abstract (DateTime, float) ReadTimePair(); @@ -121,6 +122,13 @@ public override ActorStatus ReadStatus() int sep = sid.IndexOf(' ', StringComparison.Ordinal); return new(uint.Parse(sep >= 0 ? sid.AsSpan(0, sep) : sid.AsSpan()), ushort.Parse(ReadString(), NumberStyles.HexNumber), Timestamp.AddSeconds(ReadFloat()), ReadActorID()); } + public override ActionEffects ReadActionEffects() + { + var effects = new ActionEffects(); + for (int i = 0; i < ActionEffects.MaxCount; ++i) + effects[i] = ReadULong(true); + return effects; + } public override void ReadTargets(List list) { while (CanRead()) @@ -193,18 +201,19 @@ public override void ReadVoid() { } public override ActionID ReadAction() => new(_input.ReadUInt32()); public override Class ReadClass() => (Class)_input.ReadByte(); public override ActorStatus ReadStatus() => new(_input.ReadUInt32(), _input.ReadUInt16(), new(_input.ReadInt64()), _input.ReadUInt64()); + public override ActionEffects ReadActionEffects() + { + var effects = new ActionEffects(); + for (int i = 0; i < ActionEffects.MaxCount; ++i) + effects[i] = ReadULong(true); + return effects; + } public override void ReadTargets(List list) { var count = _input.ReadInt32(); list.Capacity = count; for (int i = 0; i < count; ++i) - { - var id = _input.ReadUInt64(); - var effects = new ActionEffects(); - for (int j = 0; j < ActionEffects.MaxCount; ++j) - effects[j] = _input.ReadUInt64(); - list.Add(new(id, effects)); - } + list.Add(new(_input.ReadUInt64(), ReadActionEffects())); } public override (float, float) ReadFloatPair() => (_input.ReadSingle(), _input.ReadSingle()); public override (DateTime, float) ReadTimePair() => (new(_input.ReadInt64()), _input.ReadSingle()); @@ -314,6 +323,8 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("STA+"u8)] = () => ParseActorStatus(true), [new("STA-"u8)] = () => ParseActorStatus(false), [new("STA!"u8)] = () => ParseActorStatus(true), + [new("AIE+"u8)] = () => ParseActorIncomingEffect(true), + [new("AIE-"u8)] = () => ParseActorIncomingEffect(false), [new("ICON"u8)] = ParseActorIcon, [new("ESTA"u8)] = ParseActorEventObjectStateChange, [new("EANM"u8)] = ParseActorEventObjectAnimation, @@ -594,6 +605,7 @@ private ActorState.OpCastEvent ParseActorCastEvent() private ActorState.OpEffectResult ParseActorEffectResult() => new(_input.ReadActorID(), _input.ReadUInt(false), _input.ReadInt()); private ActorState.OpStatus ParseActorStatus(bool gainOrUpdate) => new(_input.ReadActorID(), _input.ReadInt(), gainOrUpdate ? _input.ReadStatus() : default); + private ActorState.OpIncomingEffect ParseActorIncomingEffect(bool add) => new(_input.ReadActorID(), _input.ReadInt(), add ? new(_input.ReadUInt(false), _input.ReadInt(), _input.ReadActorID(), _input.ReadAction(), _input.ReadActionEffects()) : default); private ActorState.OpIcon ParseActorIcon() => new(_input.ReadActorID(), _input.ReadUInt(false), _version >= 22 ? _input.ReadActorID() : 0); private ActorState.OpEventObjectStateChange ParseActorEventObjectStateChange() => new(_input.ReadActorID(), _input.ReadUShort(true)); private ActorState.OpEventObjectAnimation ParseActorEventObjectAnimation() => new(_input.ReadActorID(), _input.ReadUShort(true), _input.ReadUShort(true)); diff --git a/BossMod/Replay/ReplayRecorder.cs b/BossMod/Replay/ReplayRecorder.cs index 81752006ab..002cf9ed76 100644 --- a/BossMod/Replay/ReplayRecorder.cs +++ b/BossMod/Replay/ReplayRecorder.cs @@ -36,6 +36,7 @@ public void Dispose() public abstract Output Emit(ActionID v); public abstract Output Emit(Class v); public abstract Output Emit(ActorStatus v); + public abstract Output Emit(in ActionEffects v); public abstract Output Emit(List v); public abstract Output EmitFloatPair(float t1, float t2); public abstract Output EmitActor(ulong instanceID); @@ -82,6 +83,12 @@ public override Output Emit(byte[] v) public override Output Emit(ActionID v) => WriteEntry(v.ToString()); public override Output Emit(Class v) => WriteEntry(v.ToString()); public override Output Emit(ActorStatus v) => WriteEntry(Utils.StatusString(v.ID)).WriteEntry(v.Extra.ToString("X4")).WriteEntry(Utils.StatusTimeString(v.ExpireAt, _curEntry)).EmitActor(v.SourceID); + public override Output Emit(in ActionEffects v) + { + for (int i = 0; i < ActionEffects.MaxCount; ++i) + Emit(v[i], "X16"); + return this; + } public override Output Emit(List v) { foreach (var t in v) @@ -142,6 +149,12 @@ public override void StartEntry(DateTime t) { } public override Output Emit(ActionID v) { _dest.Write(v.Raw); return this; } public override Output Emit(Class v) { _dest.Write((byte)v); return this; } public override Output Emit(ActorStatus v) { _dest.Write(v.ID); _dest.Write(v.Extra); _dest.Write(v.ExpireAt.Ticks); _dest.Write(v.SourceID); return this; } + public override Output Emit(in ActionEffects v) + { + for (int i = 0; i < ActionEffects.MaxCount; ++i) + _dest.Write(v[i]); + return this; + } public override Output Emit(List v) { _dest.Write(v.Count); diff --git a/BossMod/Replay/Visualization/OpList.cs b/BossMod/Replay/Visualization/OpList.cs index 1003a0310e..f96531a65b 100644 --- a/BossMod/Replay/Visualization/OpList.cs +++ b/BossMod/Replay/Visualization/OpList.cs @@ -116,6 +116,7 @@ private bool FilterOp(WorldState.Operation o) ActorState.OpCastEvent op => FilterInterestingActor(op.InstanceID, op.Timestamp, false) && !_filteredActions.Contains(op.Value.Action), ActorState.OpEffectResult => false, ActorState.OpStatus op => FilterInterestingStatuses(op.InstanceID, op.Index, op.Timestamp), + ActorState.OpIncomingEffect => false, PartyState.OpLimitBreakChange => false, ClientState.OpActionRequest => false, //ClientState.OpActionReject => false, diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index 87f8ec4f2e..d75d960d94 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -299,10 +299,11 @@ private void DrawCommonColumns(Actor actor) ImGui.TableNextColumn(); var numRealStatuses = actor.Statuses.Count(s => s.ID != 0); + var numIncoming = actor.IncomingEffects.Count(i => i.GlobalSequence != 0); var mouseOffset = ImGui.GetMousePos() - ImGui.GetWindowPos() - ImGui.GetCursorPos(); var mouseInColumn = mouseOffset.X >= 0 && mouseOffset.Y >= 0 && mouseOffset.X < ImGui.GetColumnWidth() && mouseOffset.Y < ImGui.GetFontSize() + 2 * ImGui.GetStyle().FramePadding.Y; - ImGui.TextUnformatted($"{(actor.PendingKnockbacks.Count > 0 ? "Knockbacks pending, " : "")}{(actor.MountId != 0 ? $"Mounted ({actor.MountId}), " : "")}{numRealStatuses} + {actor.PendingStatuses.Count} statuses, {actor.PendingDispels.Count} dispels"); - if (mouseInColumn && numRealStatuses + actor.PendingStatuses.Count + actor.PendingDispels.Count > 0) + ImGui.TextUnformatted($"{(actor.PendingKnockbacks > 0 ? "Knockbacks pending, " : "")}{(actor.MountId != 0 ? $"Mounted ({actor.MountId}), " : "")}{numRealStatuses} + {actor.PendingStatuses.Count} statuses, {actor.PendingDispels.Count} dispels, {numIncoming} incoming effects"); + if (mouseInColumn && numRealStatuses + actor.PendingStatuses.Count + actor.PendingDispels.Count + numIncoming > 0) { using var tooltip = ImRaii.Tooltip(); if (tooltip) @@ -324,6 +325,14 @@ private void DrawCommonColumns(Actor actor) { ImGui.TextUnformatted($"[dispel] {Utils.StatusString(s.StatusId)}{fromString("by", s.Effect.SourceInstanceId)}"); } + for (int i = 0; i < actor.IncomingEffects.Length; ++i) + { + ref var inc = ref actor.IncomingEffects[i]; + if (inc.GlobalSequence != 0) + { + ImGui.TextUnformatted($"[incoming {i}] {inc.GlobalSequence}/{inc.TargetIndex} {inc.Action}{fromString("from", inc.SourceInstanceId)}"); + } + } } } } diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 8a31ad51b4..fa26dd2b2d 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 8a31ad51b49b379d94787cc8488e79407e36b45d +Subproject commit fa26dd2b2df6d2bbb10737504432941c2e63fd8c From 9b86612be1ea26e137fb967109b1efcafe39f159 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Sat, 25 Jan 2025 21:30:10 +0100 Subject: [PATCH 2/3] some tweaks --- BossMod/BossModule/AIHints.cs | 4 - BossMod/BossModule/AIHintsBuilder.cs | 89 +++++++++---------- BossMod/Components/ThinIce.cs | 8 +- BossMod/Data/ActionID.cs | 18 +++- BossMod/Framework/IPCProvider.cs | 5 -- .../Dungeon/D02WorqorZormor/D023Gurfurlur.cs | 2 +- .../D03SkydeepCenote/D031FeatherRay.cs | 27 ++++-- .../TheWarmthOfTheFamily/AlphaAlligator.cs | 2 +- .../MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs | 2 +- .../Quest/RoleQuests/DreamsOfANewDay.cs | 2 +- .../Dawntrail/Ultimate/FRU/P2DiamondDust.cs | 2 +- .../T04PortaDecumana/T04PortaDecumana2.cs | 2 +- .../Dungeon/D02DohnMheg/D022Griaule.cs | 2 +- BossMod/Pathfinding/NavigationDecision.cs | 2 +- BossMod/Util/ShapeDistance.cs | 66 ++++++-------- BossMod/Util/WPosDir.cs | 26 +----- 16 files changed, 117 insertions(+), 142 deletions(-) diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index f345fe807e..d99a6c8066 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -72,10 +72,6 @@ public enum SpecialMode // positioning: next positional hint (TODO: reconsider, maybe it should be a list prioritized by in-gcds, and imminent should be in-gcds instead? or maybe it should be property of an enemy? do we need correct?) public (Actor? Target, Positional Pos, bool Imminent, bool Correct) RecommendedPositional; - public void SetPositional(Positional positional) - { - RecommendedPositional = new(RecommendedPositional.Target, positional, RecommendedPositional.Imminent, RecommendedPositional.Correct); - } // orientation restrictions (e.g. for gaze attacks): a list of forbidden orientation ranges, now or in near future // AI will rotate to face allowed orientation at last possible moment, potentially losing uptime diff --git a/BossMod/BossModule/AIHintsBuilder.cs b/BossMod/BossModule/AIHintsBuilder.cs index ccb011cef2..2c5dd92fb4 100644 --- a/BossMod/BossModule/AIHintsBuilder.cs +++ b/BossMod/BossModule/AIHintsBuilder.cs @@ -11,12 +11,12 @@ public sealed class AIHintsBuilder : IDisposable private readonly BossModuleManager _bmm; private readonly ZoneModuleManager _zmm; private readonly EventSubscriptions _subscriptions; - private readonly Dictionary _activeAOEs = []; + private readonly Dictionary _activeAOEs = []; private ArenaBoundsCircle? _activeFateBounds; private static readonly HashSet ignore = [27503, 33626]; // action IDs that the AI should ignore private static readonly PartyRolesConfig _config = Service.Config.Get(); - private static readonly Dictionary _fateCache = []; - private static readonly Dictionary _spellCache = []; + private static readonly Dictionary _fateCache = []; + private static readonly Dictionary _spellCache = []; public AIHintsBuilder(WorldState ws, BossModuleManager bmm, ZoneModuleManager zmm) { @@ -68,9 +68,8 @@ private void FillEnemies(AIHints hints, bool playerIsDefaultTank) var activeFateID = _ws.Client.ActiveFate.ID; if (activeFateID != 0) { - var activeFateRow = GetFateData(activeFateID); - var fate = activeFateRow!.Value; - var playerInFate = activeFateRow != null && (_ws.Party.Player()?.Level <= fate.ClassJobLevelMax || fate.EurekaFate == 1); + var fate = GetFateData(activeFateID); + var playerInFate = _ws.Party.Player()?.Level <= fate.ClassJobLevelMax || fate.EurekaFate == 1; allowedFateID = playerInFate ? activeFateID : 0; } foreach (var actor in _ws.Actors.Actors.Values) @@ -109,9 +108,8 @@ private void CalculateAutoHints(AIHints hints, Actor player) var activeFateID = _ws.Client.ActiveFate.ID; if (activeFateID != 0) { - var activeFateRow = GetFateData(activeFateID); - var fate = activeFateRow!.Value; - var playerInFate = activeFateRow != null && (_ws.Party.Player()?.Level <= fate.ClassJobLevelMax || fate.EurekaFate == 1); + var fate = GetFateData(activeFateID); + var playerInFate = _ws.Party.Player()?.Level <= fate.ClassJobLevelMax || fate.EurekaFate == 1; inFate = playerInFate; } @@ -167,65 +165,62 @@ private void CalculateAutoHints(AIHints hints, Actor player) var target = aoe.Target?.Position ?? aoe.Caster.CastInfo!.LocXZ; var rot = aoe.Caster.CastInfo!.Rotation; var finishAt = _ws.FutureTime(aoe.Caster.CastInfo.NPCRemainingTime); - if (aoe.IsCharge) - { - hints.AddForbiddenZone(ShapeDistance.Rect(aoe.Caster.Position, target, ((AOEShapeRect)aoe.Shape).HalfWidth), finishAt); - } - else - { - hints.AddForbiddenZone(aoe.Shape, target, rot, finishAt); - } + hints.AddForbiddenZone(aoe.Shape, target, rot, finishAt); } } private void OnCastStarted(Actor actor) { + if (_bmm.ActiveModule?.StateMachine.ActivePhase != null) // no need to do all of this if it won't be used anyway + return; if (actor.Type is not ActorType.Enemy and not ActorType.Helper || actor.IsAlly) return; var actionID = actor.CastInfo!.Action.ID; if (ignore.Contains(actionID)) return; - var data = actor.CastInfo!.IsSpell() ? GetSpellData(actionID) : null; - var dat = data!.Value; - if (data == null || dat.CastType == 1) + var spell = actor.CastInfo!.IsSpell(); + if (!spell) + return; + var data = GetSpellData(actionID); + if (data.CastType == 1) return; - if (dat.CastType is 2 or 5 && dat.EffectRange >= RaidwideSize) + if (data.CastType is 2 or 5 && data.EffectRange >= RaidwideSize) return; - AOEShape? shape = dat.CastType switch + AOEShape? shape = data.CastType switch { - 2 => new AOEShapeCircle(dat.EffectRange), // used for some point-blank aoes and enemy location-targeted - does not add caster hitbox - 3 => new AOEShapeCone(dat.EffectRange + actor.HitboxRadius, DetermineConeAngle(dat) * HalfWidth), - 4 => new AOEShapeRect(dat.EffectRange + actor.HitboxRadius, dat.XAxisModifier * HalfWidth), - 5 => new AOEShapeCircle(dat.EffectRange + actor.HitboxRadius), + 2 => new AOEShapeCircle(data.EffectRange), // used for some point-blank aoes and enemy location-targeted - does not add caster hitbox + 3 => new AOEShapeCone(data.EffectRange + actor.HitboxRadius, DetermineConeAngle(data) * HalfWidth), + 4 => new AOEShapeRect(data.EffectRange + actor.HitboxRadius, data.XAxisModifier * HalfWidth), + 5 => new AOEShapeCircle(data.EffectRange + actor.HitboxRadius), //6 => custom shapes //7 => new AOEShapeCircle(data.EffectRange), - used for player ground-targeted circles a-la asylum - 8 => new AOEShapeRect((actor.CastInfo!.LocXZ - actor.Position).Length(), dat.XAxisModifier * HalfWidth), - 10 => new AOEShapeDonut(3, dat.EffectRange), - 11 => new AOEShapeCross(dat.EffectRange, dat.XAxisModifier * HalfWidth), - 12 => new AOEShapeRect(dat.EffectRange, dat.XAxisModifier * HalfWidth), - 13 => new AOEShapeCone(dat.EffectRange, DetermineConeAngle(dat) * HalfWidth), + 8 => new AOEShapeRect((actor.CastInfo!.LocXZ - actor.Position).Length(), data.XAxisModifier * HalfWidth), + 10 => new AOEShapeDonut(3, data.EffectRange), + 11 => new AOEShapeCross(data.EffectRange, data.XAxisModifier * HalfWidth), + 12 => new AOEShapeRect(data.EffectRange, data.XAxisModifier * HalfWidth), + 13 => new AOEShapeCone(data.EffectRange, DetermineConeAngle(data) * HalfWidth), _ => null }; if (shape == null) { - Service.Log($"[AutoHints] Unknown cast type {dat.CastType} for {actor.CastInfo.Action}"); + Service.Log($"[AutoHints] Unknown cast type {data.CastType} for {actor.CastInfo.Action}"); return; } var target = _ws.Actors.Find(actor.CastInfo.TargetID); - _activeAOEs[actor.InstanceID] = (actor, target, shape, dat.CastType == 8); + _activeAOEs[actor.InstanceID] = (actor, target, shape); } private void OnCastFinished(Actor actor) => _activeAOEs.Remove(actor.InstanceID); - private static Angle DetermineConeAngle((byte, byte, byte, uint RowId, Lumina.Text.ReadOnly.ReadOnlySeString? Name, Lumina.Text.ReadOnly.ReadOnlySeString? PathAlly, string? Path, int? Pos, bool Omen) data) + private static Angle DetermineConeAngle((byte, byte, byte, uint RowId, string Name, string PathAlly, string Path, int Pos, bool Omen) data) { if (!data.Omen) { Service.Log($"[AutoHints] No omen data for {data.RowId} '{data.Name}'..."); return 180.Degrees(); } - var path = data.Path!; - var pos = data.Pos!.Value; + var path = data.Path; + var pos = data.Pos; if (pos < 0 || pos + 6 > path.Length || !int.TryParse(path.AsSpan(pos + 3, 3), out var angle)) { Service.Log($"[AutoHints] Can't determine angle from omen ({path}/{data.PathAlly}) for {data.RowId} '{data.Name}'..."); @@ -234,28 +229,24 @@ private static Angle DetermineConeAngle((byte, byte, byte, uint RowId, Lumina.Te return angle.Degrees(); } - private static (byte ClassJobLevelMax, byte EurekaFate)? GetFateData(uint fateID) + private static (byte ClassJobLevelMax, byte EurekaFate) GetFateData(uint fateID) { if (_fateCache.TryGetValue(fateID, out var fateRow)) return fateRow; var row = Service.LuminaRow(fateID); - (byte, byte)? data = null; - if (row != null) - data = (row.Value.ClassJobLevelMax, row.Value.EurekaFate); - return _fateCache[fateID] = data; + (byte, byte)? data; + data = (row!.Value.ClassJobLevelMax, row.Value.EurekaFate); + return _fateCache[fateID] = data.Value; } - private static (byte CastType, byte EffectRange, byte XAxisModifier, uint RowId, string? Name, string? PathAlly, string? path, int? pos, bool Omen)? GetSpellData(uint actionID) + private static (byte CastType, byte EffectRange, byte XAxisModifier, uint RowId, string Name, string PathAlly, string path, int pos, bool Omen) GetSpellData(uint actionID) { if (_spellCache.TryGetValue(actionID, out var actionRow)) return actionRow; var row = Service.LuminaRow(actionID); - (byte, byte, byte, uint, string?, string?, string?, int?, bool)? data = null; - if (row != null) - { - var omenPath = row.Value.Omen.Value.Path.ToString(); - data = (row.Value.CastType, row.Value.EffectRange, row.Value.XAxisModifier, row.Value.RowId, row.Value.Name.ToString(), row.Value.Omen.Value.PathAlly.ToString(), omenPath, omenPath.IndexOf("fan", StringComparison.Ordinal), row.Value.Omen.ValueNullable != null); - } - return _spellCache[actionID] = data; + (byte, byte, byte, uint, string, string, string, int, bool)? data; + var omenPath = row!.Value.Omen.Value.Path.ToString(); + data = (row.Value.CastType, row.Value.EffectRange, row.Value.XAxisModifier, row.Value.RowId, row.Value.Name.ToString(), row.Value.Omen.Value.PathAlly.ToString(), omenPath, omenPath.IndexOf("fan", StringComparison.Ordinal), row.Value.Omen.ValueNullable != null); + return _spellCache[actionID] = data!.Value; } } diff --git a/BossMod/Components/ThinIce.cs b/BossMod/Components/ThinIce.cs index 6c6b582094..58e2e326d8 100644 --- a/BossMod/Components/ThinIce.cs +++ b/BossMod/Components/ThinIce.cs @@ -46,11 +46,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { var pos = actor.Position; var ddistance = 2 * Distance; - var forbidden = new List> + var forbidden = new Func[] { - ShapeDistance.InvertedDonut(pos, Distance, Distance + 0.5f), - ShapeDistance.InvertedDonut(pos, ddistance, ddistance + 0.5f), - ShapeDistance.InvertedRect(pos, offset, 0.5f, 0.5f, 0.5f) + ShapeDistance.InvertedDonut(pos, Distance, Distance + 1.2f), + ShapeDistance.InvertedDonut(pos, ddistance, ddistance + 1.2f), + ShapeDistance.InvertedRect(pos, offset, 0.7f, 0.7f, 0.7f) }; hints.AddForbiddenZone(ShapeDistance.Intersection(forbidden), DateTime.MaxValue); } diff --git a/BossMod/Data/ActionID.cs b/BossMod/Data/ActionID.cs index c8faac2f72..e989bb331f 100644 --- a/BossMod/Data/ActionID.cs +++ b/BossMod/Data/ActionID.cs @@ -39,12 +39,13 @@ public ActionID(ActionType type, uint id) : this(((uint)type << 24) | id) { } public static implicit operator bool(ActionID x) => x.Raw != 0; public override readonly string ToString() => $"{Type} {ID} '{Name()}'"; + private static readonly Dictionary _spellCache = []; public readonly AID As() where AID : Enum => (AID)(object)ID; public readonly string Name() => Type switch { - ActionType.Spell => Service.LuminaRow(ID)?.Name.ToString() ?? "", + ActionType.Spell => GetSpellData(SpellId()).Name, ActionType.Item => $"{Service.LuminaRow(ID % 1000000)?.Name ?? ""}{(ID > 1000000 ? " (HQ)" : "")}", // see Dalamud.Game.Text.SeStringHandling.Payloads.GetAdjustedId; TODO: id > 500000 is "collectible", >2000000 is "event" ?? ActionType.BozjaHolsterSlot0 or ActionType.BozjaHolsterSlot1 => $"{(BozjaHolsterID)ID}", _ => "" @@ -65,11 +66,11 @@ public ActionID(ActionType type, uint id) : this(((uint)type << 24) | id) { } public readonly float CastTime() => Type switch { - ActionType.Spell => (Service.LuminaRow(ID)?.Cast100ms ?? 0) * 0.1f, + ActionType.Spell => GetSpellData(SpellId()).ExtraCastTime, _ => 0 }; - public readonly float CastTimeExtra() => Service.LuminaRow(SpellId())?.ExtraCastTime100ms * 0.1f ?? 0; + public readonly float CastTimeExtra() => GetSpellData(SpellId()).ExtraCastTime; public readonly bool IsCasted() => CastTime() > 0; @@ -85,4 +86,15 @@ public static ActionID MakeSpell(AID id) where AID : Enum 1 => new(ActionType.BozjaHolsterSlot1, (uint)id), _ => default }; + + private static (float ExtraCastTime, string Name) GetSpellData(uint actionID) + { + if (_spellCache.TryGetValue(actionID, out var actionRow)) + return actionRow; + var row = Service.LuminaRow(actionID); + (float, string)? data; + + data = (row!.Value.ExtraCastTime100ms * 0.1f, row.Value.Name.ToString()); + return _spellCache[actionID] = data!.Value; + } } diff --git a/BossMod/Framework/IPCProvider.cs b/BossMod/Framework/IPCProvider.cs index 00d1f60487..d6609cd72d 100644 --- a/BossMod/Framework/IPCProvider.cs +++ b/BossMod/Framework/IPCProvider.cs @@ -83,11 +83,6 @@ public IPCProvider(RotationModuleManager autorotation, ActionManagerEx amex, Mov Register("AI.SetPreset", (string name) => ai.SetAIPreset(autorotation.Database.Presets.VisiblePresets.FirstOrDefault(x => x.Name == name))); Register("AI.GetPreset", () => ai.GetAIPreset); - Register("AI.GetPotentialTargets", () => autorotation.Hints.PotentialTargets); - Register("AI.GetSpecialMode", () => autorotation.Hints.ImminentSpecialMode); - Register("AI.ForbiddenDirections", () => autorotation.Hints.ForbiddenDirections); - Register("AI.ForcedTarget", () => autorotation.Hints.ForcedTarget); - Register("AI.SetPositional", (Positional positional) => autorotation.Hints.SetPositional(positional)); } public void Dispose() => _disposeActions?.Invoke(); diff --git a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs index 77b6043367..2ff83d8277 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs @@ -86,7 +86,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme for (var i = 0; i < len; ++i) { var o = orbz[i]; - orbs[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0, 1), 0.75f, 0.75f, 0.75f); + orbs[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0, 1), 0.7f, 0.7f, 0.7f); } hints.AddForbiddenZone(ShapeDistance.Intersection(orbs)); } diff --git a/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs b/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs index 4006898abe..a77d24eea7 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs @@ -64,21 +64,34 @@ class AiryBubble(BossModule module) : Components.GenericAOEs(module) private static readonly AOEShapeCapsule capsule = new(Radius, Length); private readonly List bubbles = module.Enemies(OID.AiryBubble); private readonly List _aoes = new(36); + private bool active; public override IEnumerable ActiveAOEs(int slot, Actor actor) { var count = _aoes.Count; if (count == 0) return []; - List aoes = new(count); + var aoes = new AOEInstance[count]; for (var i = 0; i < count; ++i) { var o = _aoes[i]; - aoes.Add(new(capsule, o.Position, o.Rotation)); + aoes[i] = new(capsule, o.Position, o.Rotation); } return aoes; } + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.TroubleBubbles or AID.BlowingBubbles) + active = true; + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.TroubleBubbles or AID.BlowingBubbles) + active = false; + } + public override void OnActorPlayActionTimelineEvent(Actor actor, ushort id) { if (bubbles.Any(x => x.HitboxRadius == 1.1f && x == actor)) @@ -91,17 +104,19 @@ public override void OnActorPlayActionTimelineEvent(Actor actor, ushort id) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { var count = _aoes.Count; + if (active) + hints.AddForbiddenZone(ShapeDistance.Circle(Arena.Center, Module.PrimaryActor.HitboxRadius)); if (count == 0) return; - var forbidden = new List>(count + 1); + var forbidden = new Func[count + 1]; for (var i = 0; i < count; ++i) { var o = _aoes[i]; - forbidden.Add(ShapeDistance.Capsule(o.Position, o.Rotation, Length, Radius)); + forbidden[i] = ShapeDistance.Capsule(o.Position, o.Rotation, Length, Radius); } - forbidden.Add(ShapeDistance.Circle(Arena.Center, Module.PrimaryActor.HitboxRadius)); + forbidden[count] = ShapeDistance.Circle(Arena.Center, Module.PrimaryActor.HitboxRadius); - hints.AddForbiddenZone(ShapeDistance.Union(forbidden)); + hints.AddForbiddenZone(ShapeDistance.Union(forbidden), WorldState.FutureTime(1.1f)); } } diff --git a/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/AlphaAlligator.cs b/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/AlphaAlligator.cs index 22c3e31fe8..2c29829615 100644 --- a/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/AlphaAlligator.cs +++ b/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/AlphaAlligator.cs @@ -40,7 +40,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme return; var target = WorldState.Actors.Find(source.Tether.Target); if (target != null) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.5f), _activation); + hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.6f), _activation); } } } diff --git a/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs b/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs index 987c1a471e..c5bdc2acbe 100644 --- a/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs +++ b/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs @@ -117,7 +117,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme for (var i = 0; i < len; ++i) { var o = orbz[i]; - orbs[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0, 1), 0.75f, 0.75f, 0.75f); + orbs[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0, 1), 0.7f, 0.7f, 0.7f); } hints.AddForbiddenZone(ShapeDistance.Intersection(orbs)); } diff --git a/BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs b/BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs index 5f79981947..07be8f87b2 100644 --- a/BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs +++ b/BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs @@ -110,7 +110,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { var source = Module.PrimaryActor; var target = Module.Enemies(OID.TentoawaTheWideEye).FirstOrDefault()!; - hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.5f), _activation); + hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.6f), _activation); } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs index d3d4a9952f..a51b19526d 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs @@ -386,7 +386,7 @@ public override void Update() { if (AOEs.Count != 2) return; - foreach (var (i, p) in Raid.WithSlot().IncludedInMask(_thinIce)) + foreach (var (i, p) in Raid.WithSlot(false, true, true).IncludedInMask(_thinIce)) if (_slideBackPos[i] == default && p.LastFrameMovement != default) _slideBackPos[i] = p.PrevPosition; } diff --git a/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs b/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs index a0c8785167..f53a6e6f7b 100644 --- a/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs +++ b/BossMod/Modules/RealmReborn/Trial/T04PortaDecumana/T04PortaDecumana2.cs @@ -95,7 +95,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme for (var i = 0; i < len; ++i) { var o = orbz[i]; - orbs[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0, 1), 0.75f, 0.75f, 0.75f); + orbs[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0, 1), 0.7f, 0.7f, 0.7f); } hints.AddForbiddenZone(ShapeDistance.Intersection(orbs)); } diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs index c407ecf4a2..fdb20aa32c 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D022Griaule.cs @@ -33,7 +33,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { var source = Module.Enemies(OID.PaintedSapling)[slot]; var target = Module.PrimaryActor; - hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.5f), _activation); + hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.6f), _activation); } } } diff --git a/BossMod/Pathfinding/NavigationDecision.cs b/BossMod/Pathfinding/NavigationDecision.cs index 8eb643924a..0c9154f67f 100644 --- a/BossMod/Pathfinding/NavigationDecision.cs +++ b/BossMod/Pathfinding/NavigationDecision.cs @@ -44,7 +44,7 @@ public enum Decision public Decision DecisionType; private static readonly AI.AIConfig _config = Service.Config.Get(); - public const float DefaultForbiddenZoneCushion = 0.354f; // 0.5 * sqrt(2) + public const float DefaultForbiddenZoneCushion = 0.354f; // 0.25 * sqrt(2) = distance from center to a corner for the standard 0.5 map resolution public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints, Actor player, WPos? targetPos, float targetRadius, Angle targetRot, Positional positional, float playerSpeed = 6, float forbiddenZoneCushion = DefaultForbiddenZoneCushion) { diff --git a/BossMod/Util/ShapeDistance.cs b/BossMod/Util/ShapeDistance.cs index 6fd886ee0e..a40af6092f 100644 --- a/BossMod/Util/ShapeDistance.cs +++ b/BossMod/Util/ShapeDistance.cs @@ -20,34 +20,30 @@ public static Func HalfPlane(WPos point, WDir normal) public static Func Circle(WPos origin, float radius) { - var radiusSq = radius * radius; var originX = origin.X; var originZ = origin.Z; return radius <= 0 ? (_ => float.MaxValue) : (p => { var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - return pXoriginX * pXoriginX + pZoriginZ * pZoriginZ - radiusSq; + return MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ) - radius; }); } public static Func InvertedCircle(WPos origin, float radius) { - var radiusSq = radius * radius; var originX = origin.X; var originZ = origin.Z; return radius <= 0 ? (_ => float.MinValue) : (p => { var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - return radiusSq - (pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + return radius - MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); }); } public static Func Donut(WPos origin, float innerRadius, float outerRadius) { - var innerSq = innerRadius * innerRadius; - var outerSq = outerRadius * outerRadius; var originX = origin.X; var originZ = origin.Z; return outerRadius <= 0 || innerRadius >= outerRadius ? (_ => float.MaxValue) : innerRadius <= 0 ? Circle(origin, outerRadius) : (p => @@ -55,17 +51,15 @@ public static Func Donut(WPos origin, float innerRadius, float oute // intersection of outer circle and inverted inner circle var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; - var distSqOuter = distSqOrigin - outerSq; - var distSqInner = innerSq - distSqOrigin; + var distOrigin = MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + var distSqOuter = distOrigin - outerRadius; + var distSqInner = innerRadius - distOrigin; return distSqOuter > distSqInner ? distSqOuter : distSqInner; }); } public static Func InvertedDonut(WPos origin, float innerRadius, float outerRadius) { - var innerSq = innerRadius * innerRadius; - var outerSq = outerRadius * outerRadius; var originX = origin.X; var originZ = origin.Z; return outerRadius <= 0 || innerRadius >= outerRadius ? (_ => float.MaxValue) : innerRadius <= 0 ? Circle(origin, outerRadius) : (p => @@ -73,10 +67,10 @@ public static Func InvertedDonut(WPos origin, float innerRadius, fl // intersection of outer circle and inverted inner circle var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; - var distSqOuter = distSqOrigin - outerSq; - var distSqInner = innerSq - distSqOrigin; - return distSqOuter > distSqInner ? -distSqOuter : -distSqInner; + var distOrigin = MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + var distOuter = distOrigin - outerRadius; + var distInner = innerRadius - distOrigin; + return distOuter > distInner ? -distOuter : -distInner; }); } @@ -92,7 +86,6 @@ public static Func Cone(WPos origin, float radius, Angle centerDir, float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; var nl = coneFactor * (centerDir + halfAngle).ToDirection().OrthoL(); var nr = coneFactor * (centerDir - halfAngle).ToDirection().OrthoR(); - var radiusSq = radius * radius; var originX = origin.X; var originZ = origin.Z; var nlX = nl.X; @@ -103,14 +96,14 @@ public static Func Cone(WPos origin, float radius, Angle centerDir, { var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; - var distSqOuter = distSqOrigin - radiusSq; + var distOrigin = MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + var distOuter = distOrigin - radius; var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; var distRight = pXoriginX * nrX + pZoriginZ * nrZ; var maxSideDist = distLeft > distRight ? distLeft : distRight; var conef = coneFactor * maxSideDist; - return distSqOuter > conef ? distSqOuter : conef; + return distOuter > conef ? distOuter : conef; }; } @@ -126,7 +119,6 @@ public static Func InvertedCone(WPos origin, float radius, Angle ce float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; var nl = coneFactor * (centerDir + halfAngle).ToDirection().OrthoL(); var nr = coneFactor * (centerDir - halfAngle).ToDirection().OrthoR(); - var radiusSq = radius * radius; var originX = origin.X; var originZ = origin.Z; var nlX = nl.X; @@ -137,14 +129,14 @@ public static Func InvertedCone(WPos origin, float radius, Angle ce { var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; - var distSqOuter = distSqOrigin - radiusSq; + var distOrigin = MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + var distOuter = distOrigin - radius; var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; var distRight = pXoriginX * nrX + pZoriginZ * nrZ; var maxSideDist = distLeft > distRight ? distLeft : distRight; var conef = coneFactor * maxSideDist; - return distSqOuter > conef ? -distSqOuter : -conef; + return distOuter > conef ? -distOuter : -conef; }; } @@ -162,8 +154,6 @@ public static Func DonutSector(WPos origin, float innerRadius, floa float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; var nl = coneFactor * (centerDir + halfAngle + a90).ToDirection(); var nr = coneFactor * (centerDir - halfAngle - a90).ToDirection(); - var innerSq = innerRadius * innerRadius; - var outerSq = outerRadius * outerRadius; var originX = origin.X; var originZ = origin.Z; var nlX = nl.X; @@ -175,9 +165,9 @@ public static Func DonutSector(WPos origin, float innerRadius, floa { var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; - var distOuter = distSqOrigin - outerSq; - var distInner = innerSq - distSqOrigin; + var distOrigin = MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + var distOuter = distOrigin - outerRadius; + var distInner = innerRadius - distOrigin; var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; var distRight = pXoriginX * nrX + pZoriginZ * nrZ; @@ -202,8 +192,6 @@ public static Func InvertedDonutSector(WPos origin, float innerRadi float coneFactor = halfAngle.Rad > Angle.HalfPi ? -1 : 1; var nl = coneFactor * (centerDir + halfAngle + a90).ToDirection(); var nr = coneFactor * (centerDir - halfAngle - a90).ToDirection(); - var innerSq = innerRadius * innerRadius; - var outerSq = outerRadius * outerRadius; var originX = origin.X; var originZ = origin.Z; var nlX = nl.X; @@ -215,9 +203,9 @@ public static Func InvertedDonutSector(WPos origin, float innerRadi { var pXoriginX = p.X - originX; var pZoriginZ = p.Z - originZ; - var distSqOrigin = pXoriginX * pXoriginX + pZoriginZ * pZoriginZ; - var distOuter = distSqOrigin - outerSq; - var distInner = innerSq - distSqOrigin; + var distOrigin = MathF.Sqrt(pXoriginX * pXoriginX + pZoriginZ * pZoriginZ); + var distOuter = distOrigin - outerRadius; + var distInner = innerRadius - distOrigin; var distLeft = pXoriginX * nlX + pZoriginZ * nlZ; var distRight = pXoriginX * nrX + pZoriginZ * nrZ; @@ -496,7 +484,6 @@ public static Func InvertedRect(WPos from, WPos to, float halfWidth public static Func Capsule(WPos origin, WDir dir, float length, float radius) { - var radiusSq = radius * radius; var originX = origin.X; var originZ = origin.Z; var dirX = dir.X; @@ -511,13 +498,12 @@ public static Func Capsule(WPos origin, WDir dir, float length, flo var proj = origin + t * dir; var pXprojX = p.X - proj.X; var pZprojZ = p.Z - proj.Z; - return pXprojX * pXprojX + pZprojZ * pZprojZ - radiusSq; + return MathF.Sqrt(pXprojX * pXprojX + pZprojZ * pZprojZ) - radius; }; } public static Func Capsule(WPos origin, Angle direction, float length, float radius) { - var radiusSq = radius * radius; var dir = direction.ToDirection(); var originX = origin.X; var originZ = origin.Z; @@ -533,13 +519,12 @@ public static Func Capsule(WPos origin, Angle direction, float leng var proj = origin + t * dir; var pXprojX = p.X - proj.X; var pZprojZ = p.Z - proj.Z; - return pXprojX * pXprojX + pZprojZ * pZprojZ - radiusSq; + return MathF.Sqrt(pXprojX * pXprojX + pZprojZ * pZprojZ) - radius; }; } public static Func InvertedCapsule(WPos origin, WDir dir, float length, float radius) { - var radiusSq = radius * radius; var originX = origin.X; var originZ = origin.Z; var dirX = dir.X; @@ -554,13 +539,12 @@ public static Func InvertedCapsule(WPos origin, WDir dir, float len var proj = origin + t * dir; var pXprojX = p.X - proj.X; var pZprojZ = p.Z - proj.Z; - return radiusSq - (pXprojX * pXprojX + pZprojZ * pZprojZ); + return radius - MathF.Sqrt(pXprojX * pXprojX + pZprojZ * pZprojZ); }; } public static Func InvertedCapsule(WPos origin, Angle direction, float length, float radius) { - var radiusSq = radius * radius; var dir = direction.ToDirection(); var originX = origin.X; var originZ = origin.Z; @@ -576,7 +560,7 @@ public static Func InvertedCapsule(WPos origin, Angle direction, fl var proj = origin + t * dir; var pXprojX = p.X - proj.X; var pZprojZ = p.Z - proj.Z; - return radiusSq - (pXprojX * pXprojX + pZprojZ * pZprojZ); + return radius - MathF.Sqrt(pXprojX * pXprojX + pZprojZ * pZprojZ); }; } diff --git a/BossMod/Util/WPosDir.cs b/BossMod/Util/WPosDir.cs index c1b88fb319..ce012e1eb9 100644 --- a/BossMod/Util/WPosDir.cs +++ b/BossMod/Util/WPosDir.cs @@ -158,27 +158,9 @@ public readonly bool InRect(WPos origin, WDir startToEnd, float halfWidth) public readonly bool InCapsule(WPos origin, WDir direction, float radius, float length) { - var D = direction.Normalized(); - var OP = this - origin; - var t = WDir.Dot(OP, D); - - if (t <= 0) - { - // Closest point is at the origin - return OP.LengthSq() <= radius * radius; - } - else if (t >= length) - { - // Closest point is at the end point of the capsule - var EP = this - (origin + D * length); - return EP.LengthSq() <= radius * radius; - } - else - { - // Closest point is along the segment between origin and end point - var closestPoint = origin + D * t; - var CP = this - closestPoint; - return CP.LengthSq() <= radius * radius; - } + var offset = this - origin; + var t = Math.Clamp(offset.Dot(direction), 0, length); + var proj = origin + t * direction; + return (this - proj).LengthSq() <= radius * radius; } } From 2afc08bb6301a498389efb8ad16d0418a907cc2a Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Sat, 25 Jan 2025 21:49:10 +0100 Subject: [PATCH 3/3] merge fix --- BossMod/Data/ActorState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BossMod/Data/ActorState.cs b/BossMod/Data/ActorState.cs index e9006e5938..41ff026400 100644 --- a/BossMod/Data/ActorState.cs +++ b/BossMod/Data/ActorState.cs @@ -25,7 +25,7 @@ protected override void Exec(WorldState ws) public List CompareToInitial() { - List ops = new(Actors.Count * 2); + List ops = new(Actors.Count * 5); foreach (var act in Actors.Values) { @@ -53,7 +53,7 @@ public List CompareToInitial() if (status.ID != 0) ops.Add(new OpStatus(instanceID, j, status)); } - for (int i = 0; i < act.IncomingEffects.Length; ++i) + for (var i = 0; i < act.IncomingEffects.Length; ++i) if (act.IncomingEffects[i].GlobalSequence != 0) ops.Add(new OpIncomingEffect(act.InstanceID, i, act.IncomingEffects[i])); }