From b2eb7ffd9712c7d1160b849ce683bb5b089f0f89 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 5 Jan 2025 10:49:30 +0000 Subject: [PATCH 1/4] Log splitting utility. --- .../Replay/Visualization/ReplayDetailsWindow.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index 31e055ac72..3139a5f78e 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -1,5 +1,6 @@ using BossMod.Autorotation; using ImGuiNET; +using System.IO; namespace BossMod.ReplayVisualization; @@ -211,6 +212,9 @@ private void DrawControlRow() ImGui.Checkbox("Show config", ref _showConfig); ImGui.SameLine(); ImGui.Checkbox("Show debug", ref _showDebug); + ImGui.SameLine(); + if (ImGui.Button("Split")) + SplitLog(); if (_showConfig) _config.Draw(); @@ -496,4 +500,17 @@ private void ResetPF() { _pfVisu = null; } + + private void SplitLog() + { + if (_player.Replay.Ops.Count == 0) + return; + + var player = new ReplayPlayer(_player.Replay); + player.WorldState.Frame.Timestamp = _player.Replay.Ops[0].Timestamp; // so that we get correct name etc. + using (var relogger = new ReplayRecorder(player.WorldState, ReplayLogFormat.BinaryCompressed, false, new FileInfo(_player.Replay.Path).Directory!, "Before")) + player.AdvanceTo(_curTime, () => { }); + using (var relogger = new ReplayRecorder(player.WorldState, ReplayLogFormat.BinaryCompressed, true, new FileInfo(_player.Replay.Path).Directory!, "After")) + player.AdvanceTo(DateTime.MaxValue, () => { }); + } } From de2a997c00f7ab74f7f993411bf48e883429ab0d Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 5 Jan 2025 11:27:45 +0000 Subject: [PATCH 2/4] FRU P4 WIP --- .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 7 +- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 16 +++-- .../Dawntrail/Ultimate/FRU/P4AkhMorn.cs | 5 +- .../Ultimate/FRU/P4CrystallizeTime.cs | 66 +++++++++++++++++-- .../Dawntrail/Ultimate/FRU/P4MornAfah.cs | 5 +- 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index a8817cf68a..65dd13703b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -227,7 +227,7 @@ public enum AID : uint AkhMornAOEOracle = 40303, // Helper->players, no cast, range 4 circle, 4-hit 4-man stack MornAfahUsurper = 40249, // UsurperOfFrostP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large) MornAfahOracle = 40304, // OracleOfDarknessP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large) - MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, wipe if hp difference check fails ? + MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, 8-man stack on usurper target, wipe if hp difference check fails CrystallizeTimeUsurper = 40240, // UsurperOfFrostP4->self, 10.0s cast, single-target, visual CrystallizeTimeOracle = 40298, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, raidwide @@ -240,8 +240,9 @@ public enum AID : uint LongingOfTheLost = 40241, // Helper->location, no cast, range 12 circle, aoe when head is touched DrachenWandererDisappear = 40244, // DrachenWanderer->self, no cast, single-target, visual (disappear) JoylessDragonsong = 40242, // Helper->self, no cast, range 40 circle, wipe if ??? - CrystallizeTimeHallowedWings = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (??? knockbacks?) - //_Weaponskill_HallowedWings = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect + CrystallizeTimeHallowedWings1 = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (first knockback) + CrystallizeTimeHallowedWings2 = 40230, // UsurperOfFrostP4->self, 0.5+1.3s cast, single-target, visual (second knockback) + CrystallizeTimeHallowedWingsAOE = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect, knockback 20, heavy damage on first target, vuln on first 4 targets } public enum SID : uint diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index ea93343627..c4028b2b9d 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -54,6 +54,7 @@ private void Phase34(uint id) P4DarklitDragonsong(id + 0x110000, 1.9f); P4AkhMornMornAfah(id + 0x120000, 5.8f); P4CrystallizeTime(id + 0x130000, 4.6f); + P4AkhMornMornAfah(id + 0x140000, 0.1f); SimpleState(id + 0xFF0000, 100, "???"); } @@ -593,18 +594,23 @@ private void P4CrystallizeTime(uint id, float delay) .ActivateOnEnter() .DeactivateOnExit() .SetHint(StateMachine.StateHint.Raidwide); - ComponentCondition(id + 0x80, 1.9f, comp => comp.Done, "Rewind") - .DeactivateOnExit() + ComponentCondition(id + 0x80, 1.9f, comp => comp.RewindDone, "Rewind place") .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit(); ActorCastStart(id + 0x90, _module.BossP4Oracle, AID.SpiritTaker, 0.4f); - ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings, 2.2f) + ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings1, 2.2f) .ActivateOnEnter(); ActorCastEnd(id + 0x92, _module.BossP4Oracle, 0.8f); ComponentCondition(id + 0x93, 0.3f, comp => comp.Spreads.Count == 0, "Jump") .DeactivateOnExit(); - ActorCastEnd(id + 0x94, _module.BossP4Usurper, 3.6f); - // TODO: knockbacks resolve, downtime end + ComponentCondition(id + 0x94, 3.3f, comp => comp.ReturnDone, "Rewind return") + .DeactivateOnExit(); + ActorCastEnd(id + 0x95, _module.BossP4Usurper, 0.3f); + ActorCast(id + 0xA0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWingsAOE, 1.4f, 0.5f, true); + ActorCast(id + 0xB0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings2, 2.1f, 0.5f, true); + ActorCast(id + 0xC0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWingsAOE, 1.4f, 0.5f, true); + ActorTargetable(id + 0xD0, _module.BossP4Usurper, true, 5.3f, "Bosses reappear") + .SetHint(StateMachine.StateHint.DowntimeEnd); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs index feea449556..f776b9110d 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs @@ -1,13 +1,14 @@ namespace BossMod.Dawntrail.Ultimate.FRU; +// TODO: can target change if boss is provoked mid cast? class P4AkhMorn(BossModule module) : Components.UniformStackSpread(module, 4, 0, 4) { public int NumCasts; public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.AkhMornOracle) - AddStacks(Raid.WithoutSlot(true).Where(p => p.Role == Role.Tank), Module.CastFinishAt(spell, 0.9f)); + if ((AID)spell.Action.ID is AID.AkhMornOracle or AID.AkhMornUsurper && WorldState.Actors.Find(caster.TargetID) is var target && target != null) + AddStack(target, Module.CastFinishAt(spell, 0.9f)); } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs index 12acfe6f97..a4318c618f 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -420,21 +420,77 @@ private WDir SafeOffsetFangOther(int numHourglassesDone, float northSlowX) private WDir SafeOffsetFinalNonAir(float northSlowX) => 6 * (northSlowX > 0 ? -150 : 150).Degrees().ToDirection(); } +// TODO: better positioning hints class P4CrystallizeTimeRewind(BossModule module) : BossComponent(module) { - public bool Done; + public bool RewindDone; + public bool ReturnDone; private readonly P4CrystallizeTime? _ct = module.FindComponent(); private readonly P4CrystallizeTimeTidalLight? _exalines = module.FindComponent(); + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[slot]) + { + var players = Raid.WithoutSlot(excludeNPCs: true).ToList(); + players.SortBy(p => p.Position.X); + var xOrder = players.IndexOf(actor); + players.SortBy(p => p.Position.Z); + var zOrder = players.IndexOf(actor); + if (xOrder >= 0 && zOrder >= 0) + { + if (_exalines.StartingOffset.X > 0) + xOrder = players.Count - 1 - xOrder; + if (_exalines.StartingOffset.Z > 0) + zOrder = players.Count - 1 - zOrder; + + var isFirst = xOrder == 0 || zOrder == 0; + var isTank = actor.Role == Role.Tank; + if (isFirst != isTank) + hints.Add(isTank ? "Stay in front of the group!" : "Hide behind tank!"); + var isFirstX = xOrder < 4; + var isFirstZ = zOrder < 4; + if (isFirstX == isFirstZ) + hints.Add("Position in group properly!"); + } + + if (KnockbackSpots(actor.Position).Any(p => !Module.Bounds.Contains(p - Module.Center))) + hints.Add("About to be knocked into wall!"); + } + } + public override void DrawArenaForeground(int pcSlot, Actor pc) { - if (_ct != null && _exalines != null && _ct.Cleansed[pcSlot]) - Arena.AddCircle(Module.Center + 0.5f * _exalines.StartingOffset, 1, ArenaColor.Safe); // TODO: better hints... + if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[pcSlot]) + { + var vertices = KnockbackSpots(pc.Position).ToList(); + Arena.AddQuad(pc.Position, vertices[0], vertices[2], vertices[1], ArenaColor.Danger); + Arena.AddCircle(Module.Center + 0.5f * _exalines.StartingOffset, 1, ArenaColor.Safe); + } } public override void OnStatusGain(Actor actor, ActorStatus status) { - if ((SID)status.ID == SID.Return) - Done = true; + switch ((SID)status.ID) + { + case SID.Return: + RewindDone = true; + break; + case SID.Stun: + ReturnDone = true; + break; + } + } + + private IEnumerable KnockbackSpots(WPos starting) + { + if (_exalines != null) + { + var dx = _exalines.StartingOffset.X > 0 ? -20 : +20; + var dz = _exalines.StartingOffset.Z > 0 ? -20 : +20; + yield return starting + new WDir(dx, 0); + yield return starting + new WDir(0, dz); + yield return starting + new WDir(dx, dz); + } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs index 1ec3b55c60..0a46ad3bf1 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs @@ -4,9 +4,8 @@ class P4MornAfah(BossModule module) : Components.UniformStackSpread(module, 4, 0 { public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.MornAfahOracle) + if ((AID)spell.Action.ID == AID.MornAfahUsurper) { - // note: target is random?.. var target = WorldState.Actors.Find(caster.TargetID); if (target != null) AddStack(target, Module.CastFinishAt(spell, 0.9f)); @@ -15,7 +14,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if ((AID)spell.Action.ID == AID.MornAfahAOE) // TODO: proper spell... + if ((AID)spell.Action.ID == AID.MornAfahAOE) Stacks.Clear(); } } From 03af9904c3c301ef120b861de67746cdd9da77e0 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:52:36 +0100 Subject: [PATCH 3/4] baldesions arsenal art module --- BossMod/AI/AIBehaviour.cs | 2 +- BossMod/BossModule/AIHints.cs | 1 + BossMod/BossModule/ArenaBounds.cs | 16 +- BossMod/BossModule/StateMachineBuilder.cs | 12 +- BossMod/Components/ChasingAOEs.cs | 49 ++++++ BossMod/Components/StayMove.cs | 9 +- .../Foray/BaldesionsArsenal/BA1Art/BA01Art.cs | 132 ++++++++++++++++ .../BaldesionsArsenal/BA1Art/BA01ArtStates.cs | 97 ++++++++++++ BossMod/ThirdParty/Earcut.cs | 20 +-- BossMod/Util/Polygon.cs | 145 +++++++++--------- UIDev/UIDev.csproj | 2 +- 11 files changed, 385 insertions(+), 100 deletions(-) create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index a19626d231..47ad45b248 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -143,7 +143,7 @@ private void AdjustTargetPositional(Actor player, ref Targeting targeting) private async Task<(NavigationDecision decision, Targeting updatedTargeting)> BuildNavigationDecision(Actor player, Actor master, Targeting targeting) { - if (_config.ForbidMovement || _config.ForbidAIMovementMounted && player.MountId != 0) + if (_config.ForbidMovement || _config.ForbidAIMovementMounted && player.MountId != 0 || autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.NoMovement && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1)) return (new NavigationDecision { LeewaySeconds = float.MaxValue }, targeting); if (_followMaster && (AIPreset == null || _config.OverrideAutorotation)) diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index a4855c3e68..f70fac72f0 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -28,6 +28,7 @@ public enum SpecialMode { Normal, Pyretic, // pyretic/acceleration bomb type of effects - no movement, no actions, no casting allowed at activation time + NoMovement, // no movement allowed Freezing, // should be moving at activation time Misdirection, // temporary misdirection - if current time is greater than activation, use special pathfinding codepath } diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index 202991a9dc..df62fee713 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -220,7 +220,7 @@ public ArenaBoundsCustom(float Radius, RelSimplifiedComplexPolygon Poly, float M { var part = Poly.Parts[i]; edgeList.AddRange(part.ExteriorEdges); - for (var j = 0; j < part.Holes.Count(); ++j) + for (var j = 0; j < part.Holes.Length; ++j) { edgeList.AddRange(part.InteriorEdges(j)); } @@ -258,7 +258,10 @@ public override WDir ClampToBounds(WDir offset) for (var i = 0; i < edges.Length; ++i) { var edge = edges[i]; - var nearest = NearestPointOnSegment(offset, edge.Item1, edge.Item2); + var segmentVector = edge.Item2 - edge.Item1; + var segmentLengthSq = segmentVector.LengthSq(); + var t = Math.Max(0, Math.Min(1, (offset - edge.Item1).Dot(segmentVector) / segmentLengthSq)); + var nearest = edge.Item1 + t * segmentVector; var distance = (nearest - offset).LengthSq(); if (distance < minDistance) @@ -267,18 +270,11 @@ public override WDir ClampToBounds(WDir offset) nearestPoint = nearest; } } + AddToInstanceCache(cacheKey, nearestPoint); return nearestPoint; } - private static WDir NearestPointOnSegment(WDir point, WDir segmentStart, WDir segmentEnd) - { - var segmentVector = segmentEnd - segmentStart; - var segmentLengthSq = segmentVector.LengthSq(); - var t = Math.Max(0, Math.Min(1, (point - segmentStart).Dot(segmentVector) / segmentLengthSq)); - return segmentStart + t * segmentVector; - } - private Pathfinding.Map BuildMap() { // faster than using the polygonwithholes distance method directly diff --git a/BossMod/BossModule/StateMachineBuilder.cs b/BossMod/BossModule/StateMachineBuilder.cs index a89b96ccce..c0177f0d55 100644 --- a/BossMod/BossModule/StateMachineBuilder.cs +++ b/BossMod/BossModule/StateMachineBuilder.cs @@ -135,8 +135,8 @@ public Phase DeathPhase(uint seqID, Action buildState) // create a simple state without any actions public State SimpleState(uint id, float duration, string name) { - if (_states.ContainsKey(id)) - throw new InvalidOperationException($"Duplicate state id {id:X}"); + // if (_states.ContainsKey(id)) + // throw new InvalidOperationException($"Duplicate state id {id:X}"); var state = _states[id] = new() { ID = id, Duration = duration, Name = name }; if (_lastState != null) @@ -297,7 +297,7 @@ public State CastStart(uint id, AID aid, float delay, string name = "") whe => ActorCastStart(id, () => Module.PrimaryActor, aid, delay, true, name); // create a state triggered by one of a set of expected casts by arbitrary actor; unexpected casts still trigger a transition, but log error - public State ActorCastStartMulti(uint id, Func actorAcc, IEnumerable aids, float delay, bool isBoss = false, string name = "") + public State ActorCastStartMulti(uint id, Func actorAcc, AID[] aids, float delay, bool isBoss = false, string name = "") where AID : Enum { var state = SimpleState(id, delay, name).SetHint(StateMachine.StateHint.BossCastStart, isBoss); @@ -315,7 +315,7 @@ public State ActorCastStartMulti(uint id, Func actorAcc, IEnumerabl } // create a state triggered by one of a set of expected casts by a primary actor; unexpected casts still trigger a transition, but log error - public State CastStartMulti(uint id, IEnumerable aids, float delay, string name = "") where AID : Enum + public State CastStartMulti(uint id, AID[] aids, float delay, string name = "") where AID : Enum => ActorCastStartMulti(id, () => Module.PrimaryActor, aids, delay, true, name); // create a state triggered by one of a set of expected casts by arbitrary actor, each of which forking to a separate subsequence @@ -360,7 +360,7 @@ public State Cast(uint id, AID aid, float delay, float castTime, string nam } // create a chain of states: ActorCastStartMulti -> ActorCastEnd; second state uses id+1 - public State ActorCastMulti(uint id, Func actorAcc, IEnumerable aids, float delay, float castTime, bool isBoss = false, string name = "", bool interruptible = false) + public State ActorCastMulti(uint id, Func actorAcc, AID[] aids, float delay, float castTime, bool isBoss = false, string name = "", bool interruptible = false) where AID : Enum { ActorCastStartMulti(id, actorAcc, aids, delay, isBoss, ""); @@ -368,7 +368,7 @@ public State ActorCastMulti(uint id, Func actorAcc, IEnumerable CastEnd; second state uses id+1 - public State CastMulti(uint id, IEnumerable aids, float delay, float castTime, string name = "", bool interruptible = false) + public State CastMulti(uint id, AID[] aids, float delay, float castTime, string name = "", bool interruptible = false) where AID : Enum { CastStartMulti(id, aids, delay, ""); diff --git a/BossMod/Components/ChasingAOEs.cs b/BossMod/Components/ChasingAOEs.cs index 4a7fc9c669..2042737960 100644 --- a/BossMod/Components/ChasingAOEs.cs +++ b/BossMod/Components/ChasingAOEs.cs @@ -145,3 +145,52 @@ public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) } } } + +// since open world players don't count towards party, we need to make a new component +public abstract class OpenWorldChasingAOEs(BossModule module, AOEShape shape, ActionID actionFirst, ActionID actionRest, float moveDistance, float secondsBetweenActivations, int maxCasts, bool resetExcludedTargets = false, uint icon = default, float activationDelay = 5.1f) : StandardChasingAOEs(module, shape, actionFirst, actionRest, moveDistance, secondsBetweenActivations, maxCasts, resetExcludedTargets, icon, activationDelay) +{ + public new HashSet ExcludedTargets = []; // any targets in this hashset aren't considered to be possible targets + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == ActionFirst) + { + var pos = spell.TargetID == caster.InstanceID ? caster.Position : WorldState.Actors.Find(spell.TargetID)?.Position ?? spell.LocXZ; + Actor? target = null; + var minDistanceSq = float.MaxValue; + + foreach (var actor in WorldState.Actors) + { + if (actor.OID == 0 && !ExcludedTargets.Contains(actor)) + { + var distanceSq = (actor.Position - pos).LengthSq(); + if (distanceSq < minDistanceSq) + { + minDistanceSq = distanceSq; + target = actor; + } + } + } + if (target != null) + { + Actors.Remove(target); + Chasers.Add(new(Shape, target, pos, 0, MaxCasts, Module.CastFinishAt(spell), SecondsBetweenActivations)); // initial cast does not move anywhere + ExcludedTargets.Add(target); + } + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == ActionFirst || spell.Action == ActionRest) + { + var pos = spell.MainTargetID == caster.InstanceID ? caster.Position : WorldState.Actors.Find(spell.MainTargetID)?.Position ?? spell.TargetXZ; + Advance(pos, MoveDistance, WorldState.CurrentTime); + if (Chasers.Count == 0 && ResetExcludedTargets) + { + ExcludedTargets.Clear(); + NumCasts = 0; + } + } + } +} diff --git a/BossMod/Components/StayMove.cs b/BossMod/Components/StayMove.cs index 51b22e30ad..18d3c09dad 100644 --- a/BossMod/Components/StayMove.cs +++ b/BossMod/Components/StayMove.cs @@ -4,7 +4,7 @@ // priorities can be used to simplify implementation when e.g. status changes at different stages of the mechanic (eg if prep status is replaced with pyretic, we want to allow them to happen in any sequence) public class StayMove(BossModule module, float maxTimeToShowHint = float.PositiveInfinity) : BossComponent(module) { - public enum Requirement { None, Stay, Move } + public enum Requirement { None, Stay, Stay2, Move } public record struct PlayerState(Requirement Requirement, DateTime Activation, int Priority = 0); public readonly PlayerState[] PlayerStates = new PlayerState[PartyState.MaxAllies]; @@ -18,6 +18,10 @@ public override void AddHints(int slot, Actor actor, TextHints hints) if (float.IsInfinity(MaxTimeToShowHint) || PlayerStates[slot].Activation <= WorldState.FutureTime(MaxTimeToShowHint)) hints.Add("Stop everything!", actor.PrevPosition != actor.PrevPosition || actor.CastInfo != null || actor.TargetID != 0); // note: assume if target is selected, we might autoattack... break; + case Requirement.Stay2: + if (float.IsInfinity(MaxTimeToShowHint) || PlayerStates[slot].Activation <= WorldState.FutureTime(MaxTimeToShowHint)) + hints.Add("Don't move!", actor.PrevPosition != actor.PrevPosition); // you are allowed to attack here, only moving is forbidden + break; case Requirement.Move: if (float.IsInfinity(MaxTimeToShowHint) || PlayerStates[slot].Activation <= WorldState.FutureTime(MaxTimeToShowHint)) hints.Add("Move!", actor.PrevPosition == actor.PrevPosition); @@ -36,6 +40,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme case Requirement.Stay: hints.AddSpecialMode(AIHints.SpecialMode.Pyretic, PlayerStates[slot].Activation); break; + case Requirement.Stay2: + hints.AddSpecialMode(AIHints.SpecialMode.NoMovement, PlayerStates[slot].Activation); + break; case Requirement.Move: hints.AddSpecialMode(AIHints.SpecialMode.Freezing, PlayerStates[slot].Activation); break; diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs new file mode 100644 index 0000000000..663c2f31b9 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs @@ -0,0 +1,132 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA01Art; + +public enum OID : uint +{ + // Boss = 0x265A, // R2.7 + Boss = 0x197, // R2.7 + Orlasrach = 0x265B, // R2.7 + Owain = 0x2662, // R2.7 + ShadowLinksHelper = 0x1EA1A1, // R2.0 (if pos -134.917, 750.44) + Helper = 0x265C +} + +public enum AID : uint +{ + AutoAttack = 14678, // Boss->player, no cast, single-target + + Thricecull = 14644, // Boss->player, 4.0s cast, single-target, tankbuster + Legendspinner = 14633, // Boss->self, 4.5s cast, range 7-22 donut + Legendcarver = 14632, // Boss->self, 4.5s cast, range 15 circle + AcallamNaSenorach = 14645, // Boss->self, 4.0s cast, range 60 circle + AcallamNaSenorachArt = 14628, // Boss->self, 7.0s cast, range 80 circle, enrage if Owain side does not get pulled, Owain teleports to Art + AcallamNaSenorachOwain = 14629, // Owain->self, 7.0s cast, range 80 circle + Mythcall = 14631, // Boss->self, 2.0s cast, single-target + Mythspinner = 14635, // Orlasrach->self, no cast, range 7-22 donut + Mythcarver = 14634, // Orlasrach->self, no cast, range 15 circle + LegendaryGeas = 14642, // Boss->location, 4.0s cast, range 8 circle + DefilersDeserts = 14643, // Helper->self, 3.5s cast, range 35+R width 8 rect + GloryUnearthedFirst = 14636, // Helper->location, 5.0s cast, range 10 circle + GloryUnearthedRest = 14637, // Helper->location, no cast, range 10 circle + Pitfall = 14639, // Boss->location, 5.0s cast, range 80 circle, damage fall off AOE + PiercingDarkVisual = 14640, // Boss->self, 2.5s cast, single-target + PiercingDark = 14641, // Helper->player, 5.0s cast, range 6 circle, spread +} + +public enum IconID : uint +{ + ChasingAOE = 92, // player->self +} + +class LegendMythSpinnerCarver(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCircle circle = new(15); + private static readonly AOEShapeDonut donut = new(7, 22); + private readonly List _aoes = new(5); + private bool mythcall; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + void AddAOE(AOEShape shape) => _aoes.Add(new(shape, caster.Position, default, Module.CastFinishAt(spell))); + void AddAOEs(AOEShape shape) + { + var orlasrach = Module.Enemies(OID.Orlasrach); + for (var i = 0; i < orlasrach.Count; ++i) + _aoes.Add(new(shape, orlasrach[i].Position, default, Module.CastFinishAt(spell, 2.6f))); + mythcall = false; + } + switch ((AID)spell.Action.ID) + { + case AID.Legendcarver: + AddAOE(circle); + if (mythcall) + AddAOEs(circle); + break; + case AID.Legendspinner: + AddAOE(donut); + if (mythcall) + AddAOEs(donut); + break; + case AID.Mythcall: + mythcall = true; + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (_aoes.Count != 0 && (AID)spell.Action.ID is AID.Legendcarver or AID.Legendspinner or AID.Mythcarver or AID.Mythspinner) + _aoes.RemoveAt(0); + } +} + +class Thricecull(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Thricecull)); +class AcallamNaSenorach(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AcallamNaSenorach)); +class DefilersDeserts(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DefilersDeserts), new AOEShapeRect(35.5f, 4)); +class Pitfall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Pitfall), 20); +class LegendaryGeasAOE(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.LegendaryGeas), 8); + +class DefilersDesertsPredict(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCross cross = new(35.5f, 4); + private readonly List _aoes = new(2); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + void AddAOE(Angle angle) => _aoes.Add(new(cross, spell.LocXZ, angle, Module.CastFinishAt(spell, 6.9f))); + if ((AID)spell.Action.ID == AID.LegendaryGeas) + { + AddAOE(45.Degrees()); + AddAOE(default); + } + else if ((AID)spell.Action.ID == AID.DefilersDeserts) + _aoes.Clear(); + } +} + +class LegendaryGeasStay(BossModule module) : Components.StayMove(module) +{ + public override void OnActorEAnim(Actor actor, uint state) + { + if ((OID)actor.OID == OID.ShadowLinksHelper && actor.Position.AlmostEqual(new(-134.917f, 750.44f), 1)) + { + if (state == 0x00010002) + Array.Fill(PlayerStates, new(Requirement.Stay2, WorldState.CurrentTime, 1)); + else if (state == 0x00040008) + Array.Clear(PlayerStates); + } + } +} + +class GloryUnearthed(BossModule module) : Components.OpenWorldChasingAOEs(module, new AOEShapeCircle(10), ActionID.MakeSpell(AID.GloryUnearthedFirst), ActionID.MakeSpell(AID.GloryUnearthedRest), 6.5f, 1.5f, 5, true, (uint)IconID.ChasingAOE); +class PiercingDark(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.PiercingDark), 6); +class Mythcounter(BossModule module) : Components.CastCounterMulti(module, [ActionID.MakeSpell(AID.Mythspinner), ActionID.MakeSpell(AID.Mythcarver)]); + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7968, PlanLevel = 70)] +public class BA01Art(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(-128.98f, 748), 29.5f, 64)], [new Rectangle(new(-129, 718), 20, 1.15f), new Rectangle(new(-129, 778), 20, 1.48f)]); +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs new file mode 100644 index 0000000000..056aedfddc --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs @@ -0,0 +1,97 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA01Art; + +class BA01ArtStates : StateMachineBuilder +{ + public BA01ArtStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } + // all timings seem to have upto 1s variation + private void SinglePhase(uint id) + { + Thricecull(id, 10); + LegendCarverSpinner(id + 0x10000); + AcallamNaSenorach(id + 0x20000, 4.1f); + Mythcall(id + 0x30000, 7); + AcallamNaSenorach(id + 0x40000, 3.2f); + Thricecull(id + 0x50000, 3.6f); + Mythcall(id + 0x60000, 5); + // from now on repeats until wipe or victory, this extends timeline until up around 20min since its theoretically possible to solo it as long as Owain is pulled + for (var i = 0; i < 10; ++i) + { + var pid = (uint)(i * 0x10000); + LegendaryGeas(id += 0x70000 + pid, 3.1f); + AcallamNaSenorach(id += 0x80000 + pid, 4.5f); + GloryUnearthedPitfall(id += 0x90000 + pid, 4.5f); + Thricecull(id += 0xA0000 + pid, 5); + AcallamNaSenorach(id += 0xB0000 + pid, 3.2f); + Mythcall(id += 0xC0000 + pid, 7.8f); + Thricecull(id += 0xD0000 + pid, 3); + AcallamNaSenorach(id += 0xE0000 + pid, 3.2f); + Mythcall2(id += 0xF0000 + pid, 6); + } + } + + private void Thricecull(uint id, float delay) + { + Cast(id, AID.Thricecull, delay, 4, "Tankbuster") + .SetHint(StateMachine.StateHint.Tankbuster); + } + + private void LegendCarverSpinner(uint id) + { + CastMulti(id, [AID.Legendcarver, AID.Legendspinner], 3.5f, 4.5f, "In/Out AOE"); + CastMulti(id + 0x10, [AID.Legendcarver, AID.Legendspinner], 4, 4.5f, "Inverse of previous AOE"); + } + + private void AcallamNaSenorach(uint id, float delay) + { + Cast(id, AID.AcallamNaSenorach, delay, 4, "Raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void Mythcall(uint id, float delay) + { + Cast(id, AID.Mythcall, delay, 2, "Spawn spears") + .ActivateOnEnter(); + CastMulti(id + 0x10, [AID.Legendcarver, AID.Legendspinner], 6, 4.5f, "In/Out AOE"); + ComponentCondition(id + 0x20, 3, comp => comp.NumCasts != 0, "Spears repeat AOE") + .DeactivateOnExit(); + } + + private void Mythcall2(uint id, float delay) + { + Cast(id, AID.Mythcall, delay, 2, "Spawn spears") + .ActivateOnEnter(); + Cast(id + 0x10, AID.PiercingDarkVisual, 6.1f, 2.5f, "Spreads"); + CastMulti(id + 0x20, [AID.Legendcarver, AID.Legendspinner], 1.6f, 4.5f, "In/Out AOE"); + ComponentCondition(id + 0x30, 0.4f, comp => comp.ActiveSpreads.Count == 0, "Spreads resolve"); + ComponentCondition(id + 0x40, 3, comp => comp.NumCasts != 0, "Spears repeat AOE") + .DeactivateOnExit(); + } + + private void LegendaryGeas(uint id, float delay) + { + Cast(id, AID.LegendaryGeas, delay, 4, "Circle AOE + stop moving"); + ComponentCondition(id + 0x10, 3, comp => comp.PlayerStates[0] == default, "Can move again + cross"); + ComponentCondition(id + 0x20, 0.5f, comp => comp.Casters.Count != 0, "Crosses"); + } + + private void GloryUnearthedPitfall(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Chasers.Count != 0, "Chasing AOE start"); + Cast(id + 0x10, AID.Pitfall, 2.5f, 5, "Distance based AOE start"); + CastEnd(id + 0x20, 5, "Distance AOE resolve"); + ComponentCondition(id + 0x30, 3.7f, comp => comp.Chasers.Count == 0, "Chasing AOE ends"); + } +} diff --git a/BossMod/ThirdParty/Earcut.cs b/BossMod/ThirdParty/Earcut.cs index a1efccdc03..332b2e5322 100644 --- a/BossMod/ThirdParty/Earcut.cs +++ b/BossMod/ThirdParty/Earcut.cs @@ -6,7 +6,7 @@ namespace EarcutNet; public class Earcut { - public static List Tessellate(ReadOnlySpan data, IList holeIndices) + public static List Tessellate(ReadOnlySpan data, List holeIndices) { var hasHoles = holeIndices.Count > 0; var outerLen = hasHoles ? holeIndices[0] * 2 : data.Length; @@ -18,10 +18,10 @@ public static List Tessellate(ReadOnlySpan data, IList holeInd return triangles; } - var minX = double.PositiveInfinity; - var minY = double.PositiveInfinity; - var maxX = double.NegativeInfinity; - var maxY = double.NegativeInfinity; + var minX = double.MaxValue; + var minY = double.MaxValue; + var maxX = double.MinValue; + var maxY = double.MinValue; var invSize = default(double); if (hasHoles) @@ -30,7 +30,7 @@ public static List Tessellate(ReadOnlySpan data, IList holeInd } // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox - if (data.Length > 80 * 2) + if (data.Length > 160) { for (int i = 0; i < outerLen; i += 2) { @@ -139,7 +139,7 @@ static Node FilterPoints(Node start, Node end = null) } // main ear slicing loop which triangulates a polygon (given as a linked list) - static void EarcutLinked(Node ear, IList triangles, double minX, double minY, double invSize, int pass = 0) + static void EarcutLinked(Node ear, List triangles, double minX, double minY, double invSize, int pass = 0) { if (ear == null) { @@ -312,7 +312,7 @@ static bool IsEarHashed(Node ear, double minX, double minY, double invSize) } // go through all polygon nodes and cure small local self-intersections - static Node CureLocalIntersections(Node start, IList triangles) + static Node CureLocalIntersections(Node start, List triangles) { var p = start; do @@ -340,7 +340,7 @@ static Node CureLocalIntersections(Node start, IList triangles) } // try splitting polygon into two and triangulate them independently - static void SplitEarcut(Node start, IList triangles, double minX, double minY, double invSize) + static void SplitEarcut(Node start, List triangles, double minX, double minY, double invSize) { // look for a valid diagonal that divides the polygon into two var a = start; @@ -370,7 +370,7 @@ static void SplitEarcut(Node start, IList triangles, double minX, double mi } // link every hole into the outer loop, producing a single-ring polygon without holes - static Node EliminateHoles(ReadOnlySpan data, IList holeIndices, Node outerNode) + static Node EliminateHoles(ReadOnlySpan data, List holeIndices, Node outerNode) { var queue = new List(); diff --git a/BossMod/Util/Polygon.cs b/BossMod/Util/Polygon.cs index 36c572eaba..3b07746c76 100644 --- a/BossMod/Util/Polygon.cs +++ b/BossMod/Util/Polygon.cs @@ -18,12 +18,15 @@ public RelPolygonWithHoles(List simpleVertices) : this(simpleVertices, []) public ReadOnlySpan AllVertices => Vertices.AsSpan(); public ReadOnlySpan Exterior => AllVertices[..ExteriorEnd]; public ReadOnlySpan Interior(int index) => AllVertices[HoleStarts[index]..HoleEnd(index)]; - public IEnumerable Holes + public ReadOnlySpan Holes { get { - for (var i = 0; i < HoleStarts.Count; ++i) - yield return i; + var count = HoleStarts.Count; + List result = new(count); + for (var i = 0; i < count; ++i) + result.Add(i); + return result.AsSpan(); } } @@ -38,7 +41,7 @@ public IEnumerable Holes private int HoleEnd(int index) => index + 1 < HoleStarts.Count ? HoleStarts[index + 1] : Vertices.Count; // add new hole; input is assumed to be a simple polygon - public void AddHole(IEnumerable simpleHole) + public void AddHole(List simpleHole) { HoleStarts.Add(Vertices.Count); Vertices.AddRange(simpleHole); @@ -48,7 +51,7 @@ public void AddHole(IEnumerable simpleHole) public bool Triangulate(List result) { var vertexCount = Vertices.Count; - Span pts = vertexCount <= 128 ? stackalloc double[vertexCount * 2] : new double[vertexCount * 2]; + Span pts = vertexCount <= 256 ? stackalloc double[vertexCount * 2] : new double[vertexCount * 2]; for (int i = 0, j = 0; i < vertexCount; ++i, j += 2) { var v = Vertices[i]; @@ -118,87 +121,87 @@ public bool Contains(WDir p) var original = Interlocked.CompareExchange(ref _edgeBuckets, newEdgeBuckets, null); edgeBuckets = original ?? newEdgeBuckets; + } - static ContourEdgeBuckets BuildEdgeBucketsForContour(ReadOnlySpan contour) - { - float minY = float.MaxValue, maxY = float.MinValue; - var count = contour.Length; + if (!InSimplePolygon(p, edgeBuckets.ExteriorEdgeBuckets)) + return false; - for (var i = 0; i < count; ++i) - { - var y = contour[i].Z; - if (y < minY) - minY = y; - if (y > maxY) - maxY = y; - } + for (var i = 0; i < edgeBuckets.HoleEdgeBuckets.Length; ++i) + { + if (InSimplePolygon(p, edgeBuckets.HoleEdgeBuckets[i])) + return false; + } + return true; + } - var invBucketHeight = BucketCount / (maxY - minY + Epsilon); + private static bool InSimplePolygon(WDir p, ContourEdgeBuckets buckets) + { + float x = p.X, y = p.Z; + var bucketIndex = (int)((y - buckets.MinY) * buckets.InvBucketHeight); + if ((uint)bucketIndex >= BucketCount) + return false; + var edges = buckets.EdgeBuckets[bucketIndex]; + var inside = false; + for (var i = 0; i < edges.Length; ++i) + { + var edge = edges[i]; + if ((edge.y0 > y) != (edge.y1 > y) && x < edge.x0 + edge.slopeX * (y - edge.y0)) + { + inside = !inside; + } + } + return inside; + } - var edgeBucketsArray = new List[BucketCount]; - for (var b = 0; b < BucketCount; ++b) - { - edgeBucketsArray[b] = []; - } + private static ContourEdgeBuckets BuildEdgeBucketsForContour(ReadOnlySpan contour) + { + float minY = float.MaxValue, maxY = float.MinValue; + var count = contour.Length; - var prev = contour[^1]; - for (var i = 0; i < count; ++i) - { - var curr = contour[i]; - var edge = new Edges(prev.X, prev.Z, curr.X, curr.Z); + for (var i = 0; i < count; ++i) + { + var y = contour[i].Z; + if (y < minY) + minY = y; + if (y > maxY) + maxY = y; + } - var bucketStart = (int)((Math.Min(edge.y0, edge.y1) - minY) * invBucketHeight); - var bucketEnd = (int)((Math.Max(edge.y0, edge.y1) - minY) * invBucketHeight); + var invBucketHeight = BucketCount / (maxY - minY + Epsilon); - bucketStart = Math.Clamp(bucketStart, 0, BucketCount - 1); - bucketEnd = Math.Clamp(bucketEnd, 0, BucketCount - 1); + var edgeBucketsArray = new List[BucketCount]; + for (var b = 0; b < BucketCount; ++b) + { + edgeBucketsArray[b] = []; + } - for (var b = bucketStart; b <= bucketEnd; ++b) - { - edgeBucketsArray[b].Add(edge); - } + var prev = contour[^1]; + for (var i = 0; i < count; ++i) + { + var curr = contour[i]; + var edge = new Edges(prev.X, prev.Z, curr.X, curr.Z); - prev = curr; - } + var bucketStart = (int)((Math.Min(edge.y0, edge.y1) - minY) * invBucketHeight); + var bucketEnd = (int)((Math.Max(edge.y0, edge.y1) - minY) * invBucketHeight); - var edgeBuckets = new Edges[BucketCount][]; - for (var b = 0; b < BucketCount; ++b) - { - edgeBuckets[b] = [.. edgeBucketsArray[b]]; - } + bucketStart = Math.Clamp(bucketStart, 0, BucketCount - 1); + bucketEnd = Math.Clamp(bucketEnd, 0, BucketCount - 1); - return new(edgeBuckets, minY, invBucketHeight); + for (var b = bucketStart; b <= bucketEnd; ++b) + { + edgeBucketsArray[b].Add(edge); } - } - - if (!InSimplePolygon(p, edgeBuckets.ExteriorEdgeBuckets)) - return false; - for (var i = 0; i < edgeBuckets.HoleEdgeBuckets.Length; ++i) - { - if (InSimplePolygon(p, edgeBuckets.HoleEdgeBuckets[i])) - return false; + prev = curr; } - return true; - static bool InSimplePolygon(WDir p, ContourEdgeBuckets buckets) + var edgeBuckets = new Edges[BucketCount][]; + for (var b = 0; b < BucketCount; ++b) { - float x = p.X, y = p.Z; - var bucketIndex = (int)((y - buckets.MinY) * buckets.InvBucketHeight); - if ((uint)bucketIndex >= BucketCount) - return false; - var edges = buckets.EdgeBuckets[bucketIndex]; - var inside = false; - for (var i = 0; i < edges.Length; ++i) - { - var edge = edges[i]; - if ((edge.y0 > y) != (edge.y1 > y) && x < edge.x0 + edge.slopeX * (y - edge.y0)) - { - inside = !inside; - } - } - return inside; + edgeBuckets[b] = [.. edgeBucketsArray[b]]; } + + return new(edgeBuckets, minY, invBucketHeight); } private readonly struct Edges @@ -542,7 +545,7 @@ public PolygonWithHolesDistanceFunction(WPos origin, RelSimplifiedComplexPolygon { var part = polygon.Parts[i]; edgeCount += part.ExteriorEdges.Count; - for (var j = 0; j < part.Holes.Count(); ++j) + for (var j = 0; j < part.Holes.Length; ++j) edgeCount += part.InteriorEdges(j).Count; } _edges = new Edge[edgeCount]; @@ -555,7 +558,7 @@ public PolygonWithHolesDistanceFunction(WPos origin, RelSimplifiedComplexPolygon Array.Copy(exteriorEdges, 0, _edges, edgeIndex, exteriorCount); edgeIndex += exteriorCount; - for (var j = 0; j < part.Holes.Count(); ++j) + for (var j = 0; j < part.Holes.Length; ++j) { var holeEdges = GetEdges(part.Interior(j), origin); var holeEdgesCount = holeEdges.Length; diff --git a/UIDev/UIDev.csproj b/UIDev/UIDev.csproj index 2dc6908ef5..582970dc93 100644 --- a/UIDev/UIDev.csproj +++ b/UIDev/UIDev.csproj @@ -69,6 +69,6 @@ - + From 83e35b27b59eb075932c5a61b89775e63f9930b0 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:53:02 +0100 Subject: [PATCH 4/4] uncommented line --- .../Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs index 663c2f31b9..1b0f59a9f0 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs @@ -2,8 +2,7 @@ namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA01Art; public enum OID : uint { - // Boss = 0x265A, // R2.7 - Boss = 0x197, // R2.7 + Boss = 0x265A, // R2.7 Orlasrach = 0x265B, // R2.7 Owain = 0x2662, // R2.7 ShadowLinksHelper = 0x1EA1A1, // R2.0 (if pos -134.917, 750.44)