Skip to content

Commit

Permalink
Merge pull request #581 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
some tweaks + merge vbm
  • Loading branch information
CarnifexOptimus authored Jan 25, 2025
2 parents 589ad08 + 2afc08b commit 30614d6
Show file tree
Hide file tree
Showing 26 changed files with 226 additions and 172 deletions.
2 changes: 1 addition & 1 deletion BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public async Task Execute(Actor player, Actor master)
var followTarget = _config.FollowTarget;
_followMaster = (_config.FollowDuringCombat || !master.InCombat || (_masterPrevPos - _masterMovementStart).LengthSq() > 100) && (_config.FollowDuringActiveBossModule || autorot.Bossmods.ActiveModule?.StateMachine.ActiveState == null) && (_config.FollowOutOfCombat || master.InCombat);
// 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)
{
var actorTarget = autorot.WorldState.Actors.Find(player.TargetID);
(var naviDecision, target) = followTarget && actorTarget != null
Expand Down
4 changes: 0 additions & 4 deletions BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 40 additions & 49 deletions BossMod/BossModule/AIHintsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public sealed class AIHintsBuilder : IDisposable
private readonly BossModuleManager _bmm;
private readonly ZoneModuleManager _zmm;
private readonly EventSubscriptions _subscriptions;
private readonly Dictionary<ulong, (Actor Caster, Actor? Target, AOEShape Shape, bool IsCharge)> _activeAOEs = [];
private readonly Dictionary<ulong, (Actor Caster, Actor? Target, AOEShape Shape)> _activeAOEs = [];
private ArenaBoundsCircle? _activeFateBounds;
private static readonly HashSet<uint> ignore = [27503, 33626]; // action IDs that the AI should ignore
private static readonly PartyRolesConfig _config = Service.Config.Get<PartyRolesConfig>();
private static readonly Dictionary<uint, (byte, byte)?> _fateCache = [];
private static readonly Dictionary<uint, (byte, byte, byte, uint, string?, string?, string?, int?, bool)?> _spellCache = [];
private static readonly Dictionary<uint, (byte, byte)> _fateCache = [];
private static readonly Dictionary<uint, (byte, byte, byte, uint, string, string, string, int, bool)> _spellCache = [];

public AIHintsBuilder(WorldState ws, BossModuleManager bmm, ZoneModuleManager zmm)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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}'...");
Expand All @@ -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<Lumina.Excel.Sheets.Fate>(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<Lumina.Excel.Sheets.Action>(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;
}
}
8 changes: 4 additions & 4 deletions BossMod/Components/ThinIce.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Func<WPos, float>>
var forbidden = new Func<WPos, float>[]
{
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);
}
Expand Down
18 changes: 15 additions & 3 deletions BossMod/Data/ActionID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint, (float, string)> _spellCache = [];

public readonly AID As<AID>() where AID : Enum => (AID)(object)ID;

public readonly string Name() => Type switch
{
ActionType.Spell => Service.LuminaRow<Lumina.Excel.Sheets.Action>(ID)?.Name.ToString() ?? "<not found>",
ActionType.Spell => GetSpellData(SpellId()).Name,
ActionType.Item => $"{Service.LuminaRow<Lumina.Excel.Sheets.Item>(ID % 1000000)?.Name ?? "<not found>"}{(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}",
_ => ""
Expand All @@ -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<Lumina.Excel.Sheets.Action>(ID)?.Cast100ms ?? 0) * 0.1f,
ActionType.Spell => GetSpellData(SpellId()).ExtraCastTime,
_ => 0
};

public readonly float CastTimeExtra() => Service.LuminaRow<Lumina.Excel.Sheets.Action>(SpellId())?.ExtraCastTime100ms * 0.1f ?? 0;
public readonly float CastTimeExtra() => GetSpellData(SpellId()).ExtraCastTime;

public readonly bool IsCasted() => CastTime() > 0;

Expand All @@ -85,4 +86,15 @@ public static ActionID MakeSpell<AID>(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<Lumina.Excel.Sheets.Action>(actionID);
(float, string)? data;

data = (row!.Value.ExtraCastTime100ms * 0.1f, row.Value.Name.ToString());
return _spellCache[actionID] = data!.Value;
}
}
4 changes: 3 additions & 1 deletion BossMod/Data/Actor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<PendingEffectDelta> PendingHPDifferences = []; // damage and heal effects applied to the target that were not confirmed yet
public List<PendingEffectDelta> PendingMPDifferences = [];
public List<PendingEffectStatusExtra> PendingStatuses = [];
public List<PendingEffectStatus> PendingDispels = [];
public List<PendingEffect> PendingKnockbacks = [];
public int PendingKnockbacks;

public Role Role => Class.GetRole();
public ClassCategory ClassCategory => Class.GetClassCategory();
Expand Down
Loading

0 comments on commit 30614d6

Please sign in to comment.