Skip to content

Commit

Permalink
Merge pull request #116 from FFXIV-CombatReborn/mergeWIP2
Browse files Browse the repository at this point in the history
more AI settings
  • Loading branch information
CarnifexOptimus authored Jun 5, 2024
2 parents 39542ea + 8a9ce38 commit 15f5ee6
Show file tree
Hide file tree
Showing 48 changed files with 1,501 additions and 573 deletions.
33 changes: 17 additions & 16 deletions BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ sealed class AIBehaviour(AIController ctrl, Autorotation autorot) : IDisposable
private WPos _masterMovementStart;
private DateTime _masterLastMoved;

public bool ForbidActions => _config.ForbidActions;
public bool ForbidMovement => _config.ForbidMovement;
public bool FollowDuringCombat => _config.FollowDuringCombat;
public bool FollowDuringActiveBossModule => _config.FollowDuringActiveBossModule;

public void Dispose()
{
}
Expand All @@ -34,7 +29,7 @@ public void Execute(Actor player, Actor master)
FocusMaster(master);

_afkMode = !master.InCombat && (autorot.WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 10;
bool forbidActions = _config.ForbidActions || ctrl.IsMounted || _afkMode || autorot.ClassActions == null || autorot.ClassActions.AutoAction >= CommonActions.AutoActionFirstCustom;
var forbidActions = _config.ForbidActions || ctrl.IsMounted || _afkMode || autorot.ClassActions == null || autorot.ClassActions.AutoAction >= CommonActions.AutoActionFirstCustom;

CommonActions.Targeting target = new();
if (!forbidActions)
Expand All @@ -46,14 +41,14 @@ public void Execute(Actor player, Actor master)
AdjustTargetPositional(player, ref target);
}

_followMaster = master != player && (_config.FollowDuringCombat || !master.InCombat || (_masterPrevPos - _masterMovementStart).LengthSq() > 100) && (_config.FollowDuringActiveBossModule || autorot.Bossmods.ActiveModule?.StateMachine.ActiveState == null);
_naviDecision = BuildNavigationDecision(player, master, ref target);
var followTarget = _config.FollowTarget;
_followMaster = master != player && (_config.FollowDuringCombat || !master.InCombat || (_masterPrevPos - _masterMovementStart).LengthSq() > 100) && (_config.FollowDuringActiveBossModule || autorot.Bossmods.ActiveModule?.StateMachine.ActiveState == null) && (_config.FollowOutOfCombat || master.InCombat);

bool masterIsMoving = TrackMasterMovement(master);
bool moveWithMaster = masterIsMoving && (master == player || _followMaster);
_naviDecision = followTarget && autorot.WorldState.Actors.Find(player.TargetID) != null ? BuildNavigationDecision(player, autorot.WorldState.Actors.Find(player.TargetID)!, ref target) : BuildNavigationDecision(player, master, ref target);
var masterIsMoving = TrackMasterMovement(master);
var moveWithMaster = masterIsMoving && (master == player || _followMaster);
_maxCastTime = moveWithMaster || ctrl.ForceFacing ? 0 : _naviDecision.LeewaySeconds;

// note: that there is a 1-frame delay if target and/or strategy changes - we don't really care?..
if (!forbidActions)
{
int actionStrategy = target.Target != null ? CommonActions.AutoActionAIFight : CommonActions.AutoActionAIIdle;
Expand Down Expand Up @@ -104,13 +99,15 @@ private void AdjustTargetPositional(Actor player, ref CommonActions.Targeting ta

private NavigationDecision BuildNavigationDecision(Actor player, Actor master, ref CommonActions.Targeting targeting)
{
if (_config.ForbidActions)
var target = autorot.WorldState.Actors.Find(player.TargetID);
if (_config.ForbidMovement)
return new() { LeewaySeconds = float.MaxValue };
if (_followMaster)
if (_followMaster && !_config.FollowTarget || _followMaster && _config.FollowTarget && target == null)
return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, master.Position, 1, new(), Positional.Any);
if (_followMaster && _config.FollowTarget && target != null)
return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, target.Position, target.HitboxRadius + player.HitboxRadius + 3, target.Rotation, _config.DesiredPositional);
if (targeting.Target == null)
return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, null, 0, new(), Positional.Any);

var adjRange = targeting.PreferredRange + player.HitboxRadius + targeting.Target.Actor.HitboxRadius;
if (targeting.PreferTanking)
{
Expand All @@ -123,8 +120,8 @@ private NavigationDecision BuildNavigationDecision(Actor player, Actor master, r
return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, dest, 0.5f, new(), Positional.Any);
}
}

var adjRotation = targeting.PreferTanking ? targeting.Target.DesiredRotation : targeting.Target.Actor.Rotation;

return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, targeting.Target.Actor.Position, adjRange, adjRotation, targeting.PreferredPosition);
}

Expand Down Expand Up @@ -206,8 +203,12 @@ public void DrawDebug()
ImGui.Checkbox("Forbid movement", ref _config.ForbidMovement);
ImGui.SameLine();
ImGui.Checkbox("Follow during combat", ref _config.FollowDuringCombat);
ImGui.SameLine();
ImGui.Spacing();
ImGui.Checkbox("Follow during active boss module", ref _config.FollowDuringActiveBossModule);
ImGui.SameLine();
ImGui.Checkbox("Follow out of combat", ref _config.FollowOutOfCombat);
ImGui.SameLine();
ImGui.Checkbox("Follow target", ref _config.FollowTarget);
var player = autorot.WorldState.Party.Player();
var dist = _naviDecision.Destination != null && player != null ? (_naviDecision.Destination.Value - player.Position).Length() : 0;
ImGui.TextUnformatted($"Max-cast={MathF.Min(_maxCastTime, 1000):f3}, afk={_afkMode}, follow={_followMaster}, algo={_naviDecision.DecisionType} {_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((autorot.WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}");
Expand Down
17 changes: 15 additions & 2 deletions BossMod/AI/AIConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ sealed class AIConfig : ConfigNode
[PropertyDisplay("Enable AI")]
public bool Enabled = false;

[PropertyDisplay("Show status in DTR bar")]
public bool ShowDTR = false;

[PropertyDisplay("Draw UI")]
public bool DrawUI = true;

[PropertyDisplay("Focus Target Leader")]
[PropertyDisplay("Focus target leader")]
public bool FocusTargetLeader = true;

[PropertyDisplay("Broadcast keypresses to other windows")]
public bool BroadcastToSlaves = false;

[PropertyDisplay("Follow Party Slot")]
[PropertyDisplay("Follow party slot")]
[PropertyCombo(["Slot 1", "Slot 2", "Slot 3", "Slot 4", "Slot 5", "Slot 6", "Slot 7", "Slot 8"])]
public int FollowSlot = 0;

Expand All @@ -30,4 +33,14 @@ sealed class AIConfig : ConfigNode

[PropertyDisplay("Follow during active boss module")]
public bool FollowDuringActiveBossModule = false;

[PropertyDisplay("Follow out of combat")]
public bool FollowOutOfCombat = true;

[PropertyDisplay("Follow target")]
public bool FollowTarget = false;

[PropertyDisplay("Desired positional when following target")]
[PropertyCombo(["Any", "Flank", "Rear", "Front"])]
public Positional DesiredPositional = Positional.Any;
}
86 changes: 76 additions & 10 deletions BossMod/AI/AIManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Dalamud.Game.Text;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using ImGuiNET;
Expand All @@ -10,6 +11,7 @@ sealed class AIManager : IDisposable
private readonly Autorotation _autorot;
private readonly AIController _controller;
private readonly AIConfig _config;
private readonly DtrBarEntry _dtrBarEntry;
private int _masterSlot = PartyState.PlayerSlot; // non-zero means corresponding player is master
private AIBehaviour? _beh;
private readonly UISimpleWindow _ui;
Expand All @@ -20,6 +22,7 @@ public AIManager(Autorotation autorot)
_controller = new();
_config = Service.Config.Get<AIConfig>();
_ui = new("AI", DrawOverlay, false, new(100, 100), ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoFocusOnAppearing) { RespectCloseHotkey = false };
_dtrBarEntry = Service.DtrBar.Get("Bossmod");
Service.ChatGui.ChatMessage += OnChatMessage;
Service.CommandManager.AddHandler("/bmrai", new Dalamud.Game.Command.CommandInfo(OnCommand) { HelpMessage = "Toggle AI mode" });
Service.CommandManager.AddHandler("/vbmai", new Dalamud.Game.Command.CommandInfo(OnCommand) { ShowInHelp = false });
Expand All @@ -29,6 +32,7 @@ public void Dispose()
{
SwitchToIdle();
_ui.Dispose();
_dtrBarEntry.Dispose();
Service.ChatGui.ChatMessage -= OnChatMessage;
Service.CommandManager.RemoveHandler("/bmrai");
Service.CommandManager.RemoveHandler("/vbmai");
Expand All @@ -55,19 +59,38 @@ public void Update()
_controller.Update(player);

_ui.IsOpen = _config.Enabled && player != null && _config.DrawUI;

DtrUpdate(_beh);
}

public void DtrUpdate(AIBehaviour? behaviour)
{
_dtrBarEntry.Shown = _config.ShowDTR;
if (_dtrBarEntry.Shown)
{
var status = behaviour != null ? "On" : "Off";
_dtrBarEntry.Text = "AI: " + status;
_dtrBarEntry.OnClick = () =>
{
if (behaviour != null)
SwitchToIdle();
else
SwitchToFollow(_config.FollowSlot);
};
}
}

private void DrawOverlay()
{
ImGui.TextUnformatted($"AI: {(_beh != null ? "on" : "off")}, master={_autorot.WorldState.Party[_masterSlot]?.Name}");
ImGui.TextUnformatted($"Navi={_controller.NaviTargetPos} / {_controller.NaviTargetRot}{(_controller.ForceFacing ? " forced" : "")}");
_beh?.DrawDebug();
if (ImGui.Button("Reset"))
SwitchToIdle();
ImGui.SameLine();
if (ImGui.Button("AI On - Follow selected slot"))
if (ImGui.Button("AI on"))
SwitchToFollow(_config.FollowSlot);
ImGui.Text("Follow Party Slot");
ImGui.SameLine();
if (ImGui.Button("AI off"))
SwitchToIdle();
ImGui.Text("Follow party slot");
ImGui.SameLine();
var partyMemberNames = new List<string>();
for (var i = 0; i < 8; i++)
Expand All @@ -81,6 +104,13 @@ private void DrawOverlay()
var partyMemberNamesArray = partyMemberNames.ToArray();

ImGui.Combo("##FollowPartySlot", ref _config.FollowSlot, partyMemberNamesArray, partyMemberNamesArray.Length);

ImGui.Text("Desired positional");
ImGui.SameLine();
var positionalOptions = Enum.GetNames(typeof(Positional));
var positionalIndex = (int)_config.DesiredPositional;
if (ImGui.Combo("##DesiredPositional", ref positionalIndex, positionalOptions, positionalOptions.Length))
_config.DesiredPositional = (Positional)positionalIndex;
}

private void SwitchToIdle()
Expand Down Expand Up @@ -113,10 +143,7 @@ private int FindPartyMemberSlotFromSender(SeString sender)

// Check for NPCs (Buddies)
var buddy = _autorot.WorldState.Party.WithSlot().FirstOrDefault(p => p.Item2.Name.Equals(source.PlayerName, StringComparison.OrdinalIgnoreCase));
if (buddy != default)
return buddy.Item1;

return -1;
return buddy != default ? buddy.Item1 : -1;
}

private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
Expand Down Expand Up @@ -203,6 +230,10 @@ private void OnCommand(string cmd, string message)
_config.ForbidMovement = !_config.ForbidMovement;
Service.Log($"[AI] Forbid movement is now {(_config.ForbidMovement ? "enabled" : "disabled")}");
break;
case "followoutofcombat":
_config.FollowOutOfCombat = !_config.FollowOutOfCombat;
Service.Log($"[AI] Forbid movement is now {(_config.FollowOutOfCombat ? "enabled" : "disabled")}");
break;
case "followcombat":
if (_config.FollowDuringCombat)
{
Expand All @@ -229,12 +260,47 @@ private void OnCommand(string cmd, string message)
Service.Log($"[AI] Follow during active boss module is now {(_config.FollowDuringActiveBossModule ? "enabled" : "disabled")}");
Service.Log($"[AI] Follow during combat is now {(_config.FollowDuringCombat ? "enabled" : "disabled")}");
break;
case "followtarget":
_config.FollowTarget = !_config.FollowTarget;
Service.Log($"[AI] Following targets is now {(_config.FollowTarget ? "enabled" : "disabled")}");
break;
case "positional":
if (messageData.Length < 2)
{
Service.Log("[AI] Missing positional type.");
return;
}
SetPositional(messageData[1]);
break;
default:
Service.Log($"[AI] Unknown command: {messageData[0]}");
break;
}
}

private void SetPositional(string positional)
{
switch (positional.ToLower())
{
case "any":
_config.DesiredPositional = Positional.Any;
break;
case "flank":
_config.DesiredPositional = Positional.Flank;
break;
case "rear":
_config.DesiredPositional = Positional.Rear;
break;
case "front":
_config.DesiredPositional = Positional.Front;
break;
default:
Service.Log($"[AI] Unknown positional: {positional}");
return;
}
Service.Log($"[AI] Desired positional set to {_config.DesiredPositional}");
}

private int FindPartyMemberByName(string name)
{
for (var i = 0; i < 8; i++)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace BossMod;

// TODO: rename
[ConfigDisplay(Name = "Action tweaks settings", Order = 4)]
class ActionManagerConfig : ConfigNode
public sealed class ActionManagerConfig : ConfigNode
{
// TODO: consider exposing max-delay to config; 0 would mean 'remove all delay', max-value would mean 'disable'
[PropertyDisplay("Remove extra lag-induced animation lock delay from instant casts (a-la xivalex)")]
Expand Down
43 changes: 43 additions & 0 deletions BossMod/ActionTweaks/AnimationLockTweak.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace BossMod;

// Effective animation lock reduction tweak (a-la xivalex/noclippy).
// The game handles instants and casted actions differently:
// * instants: on action request (e.g. on the frame the action button is pressed), animation lock is set to 0.5 (or 0.35 for some specific actions); it then ticks down every frame
// some time later (ping + server latency, typically 50-100ms if ping is good), we receive action effect packet - the packet contains action's animation lock (typically 0.6)
// the game then updates animation lock (now equal to 0.5 minus time since request) to the packet data
// so the 'effective' animation lock between action request and animation lock end is equal to action's animation lock + delay between request and response
// this tweak reduces effective animation lock by either removing extra delay completely or clamping it to specified min/max values
// * casts: on action request animation lock is not set (remains equal to 0), remaining cast time is set to action's cast time; remaining cast time then ticks down every frame
// some time later (cast time minus approximately 0.5s, aka slidecast window), we receive action effect packet - the packet contains action's animation lock (typically 0.1)
// the game then updates animation lock (still 0) to the packet data - however, since animation lock isn't ticking down while cast is in progress, there is no extra delay
// this tweak does nothing for casts, since they already work correctly
// The tweak also allows predicting the delay based on history (using exponential average).
// TODO: consider adding 'clamped delay' mode that doesn't reduce it straight to zero (a-la xivalex)?
public sealed class AnimationLockTweak
{
private readonly ActionManagerConfig _config = Service.Config.Get<ActionManagerConfig>();

public float DelaySmoothing = 0.8f; // TODO tweak
public float DelayAverage { get; private set; } = 0.1f; // smoothed delay between client request and server response
public float DelayEstimate => _config.RemoveAnimationLockDelay ? 0 : MathF.Min(DelayAverage * 1.5f, 0.1f); // this is a conservative estimate

// perform sanity check to detect conflicting plugins: disable the tweak if condition is false
public void SanityCheck(float originalAnimLock, float modifiedAnimLock)
{
if (!_config.RemoveAnimationLockDelay)
return; // nothing to do, tweak is already disabled
if (originalAnimLock == modifiedAnimLock && originalAnimLock % 0.01 is <= 0.0005f or >= 0.0095f)
return; // nothing changed the packet value, and it's original value is reasonable

Service.Log($"[ALT] Unexpected animation lock {originalAnimLock:f} -> {modifiedAnimLock:f}, disabling anim lock tweak feature");
_config.RemoveAnimationLockDelay = false; // disable the tweak (but don't save the config, in case this condition is temporary)
}

// apply tweak: given the delay, calculate how much it should be reduced
public float Apply(float current, float delay)
{
DelayAverage = delay * (1 - DelaySmoothing) + DelayAverage * DelaySmoothing; // update the average
// the result will be subtracted from current anim lock (and thus from adjusted lock delay)
return _config.RemoveCooldownDelay ? Math.Clamp(delay /* - DelayMax */, 0, current) : 0;
}
}
4 changes: 2 additions & 2 deletions BossMod/Autorotation/Autorotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ sealed class Autorotation : IDisposable
public Actor? SecondaryTarget; // this is usually a mouseover, but AI can override; typically used for heal and utility abilities
public AIHints Hints = new();
public float EffAnimLock => ActionManagerEx.Instance!.EffectiveAnimationLock;
public float AnimLockDelay => ActionManagerEx.Instance!.EffectiveAnimationLockDelay;
public float AnimLockDelay => ActionManagerEx.Instance!.AnimLockTweak.DelayEstimate;

private static readonly ActionID IDSprintGeneral = new(ActionType.General, 4);

Expand Down Expand Up @@ -96,7 +96,7 @@ public unsafe void Update()
if (Hints.ForcedTarget != null && PrimaryTarget != Hints.ForcedTarget)
{
PrimaryTarget = Hints.ForcedTarget;
var obj = Hints.ForcedTarget.SpawnIndex >= 0 ? FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager.Instance()->Objects.All[Hints.ForcedTarget.SpawnIndex].Value : null;
var obj = Hints.ForcedTarget.SpawnIndex >= 0 ? FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager.Instance()->Objects.IndexSorted[Hints.ForcedTarget.SpawnIndex].Value : null;
if (obj != null && obj->EntityId != Hints.ForcedTarget.InstanceID)
Service.Log($"[AR] Unexpected new target: expected {Hints.ForcedTarget.InstanceID:X} at #{Hints.ForcedTarget.SpawnIndex}, but found {obj->EntityId:X}");
FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem.Instance()->Target = obj;
Expand Down
2 changes: 1 addition & 1 deletion BossMod/Autorotation/CommonActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ protected void FillCommonPlayerState(CommonRotation.PlayerState s)
s.TargetingEnemy = Autorot.PrimaryTarget != null && Autorot.PrimaryTarget.Type is ActorType.Enemy or ActorType.Part && !Autorot.PrimaryTarget.IsAlly;
s.RangeToTarget = Autorot.PrimaryTarget != null ? (Autorot.PrimaryTarget.Position - Player.Position).Length() - Autorot.PrimaryTarget.HitboxRadius - Player.HitboxRadius : float.MaxValue;
s.AnimationLock = am.EffectiveAnimationLock;
s.AnimationLockDelay = am.EffectiveAnimationLockDelay;
s.AnimationLockDelay = am.AnimLockTweak.DelayEstimate;
s.ComboTimeLeft = am.ComboTimeLeft;
s.ComboLastAction = am.ComboLastMove;
s.LimitBreakLevel = Autorot.WorldState.Party.LimitBreakMax > 0 ? Autorot.WorldState.Party.LimitBreakCur / Autorot.WorldState.Party.LimitBreakMax : 0;
Expand Down
Loading

0 comments on commit 15f5ee6

Please sign in to comment.