Skip to content

Commit

Permalink
Merge pull request #523 from xanunderscore/iloveai
Browse files Browse the repository at this point in the history
ChangelogNotice + remove AI options
  • Loading branch information
awgil authored Oct 13, 2024
2 parents 7ee3dc7 + 0361d16 commit 59c5449
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 144 deletions.
77 changes: 22 additions & 55 deletions BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
using BossMod.Autorotation;
using BossMod.Pathfinding;
using Dalamud.Interface.Utility;
using ImGuiNET;

namespace BossMod.AI;

public record struct Targeting(AIHints.Enemy Target, float PreferredRange = 3, Positional PreferredPosition = Positional.Any, bool PreferTanking = false);

// constantly follow master
sealed class AIBehaviour(AIController ctrl, RotationModuleManager autorot, Preset? aiPreset) : IDisposable
sealed class AIBehaviour(AIController ctrl, RotationModuleManager autorot) : IDisposable
{
public WorldState WorldState => autorot.Bossmods.WorldState;
public Preset? AIPreset = aiPreset;
public float ForceMovementIn { get; private set; } = float.MaxValue; // TODO: reconsider
private readonly AIConfig _config = Service.Config.Get<AIConfig>();
private readonly NavigationDecision.Context _naviCtx = new();
private NavigationDecision _naviDecision;
private bool _afkMode;
private bool _followMaster; // if true, our navigation target is master rather than primary target - this happens e.g. in outdoor or in dungeons during gathering trash
private WPos _masterPrevPos;
private WPos _masterMovementStart;
private DateTime _masterLastMoved;

public void Dispose()
Expand All @@ -36,29 +33,21 @@ public void Execute(Actor player, Actor master)
if (_config.FocusTargetMaster)
FocusMaster(master);

_afkMode = !master.InCombat && (WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 10;
_afkMode = master != player && !master.InCombat && (WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 10;
bool gazeImminent = autorot.Hints.ForbiddenDirections.Count > 0 && autorot.Hints.ForbiddenDirections[0].activation <= WorldState.FutureTime(0.5f);
bool pyreticImminent = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1);
bool forbidActions = _config.ForbidActions || _afkMode || gazeImminent || pyreticImminent || autorot.Preset != null && autorot.Preset != AIPreset;
bool forbidTargeting = _config.ForbidActions || _afkMode || gazeImminent || pyreticImminent;

Targeting target = new();
if (!forbidActions)
if (!forbidTargeting)
{
target = SelectPrimaryTarget(player, master);
if (target.Target != null || TargetIsForbidden(player.TargetID))
autorot.Hints.ForcedTarget ??= target.Target?.Actor;
AdjustTargetPositional(player, ref target);
}

if (_config.OverridePositional)
{
target.PreferredPosition = _config.DesiredPositional;
target.PreferTanking = _config.DesiredPositional != Positional.Any;
}
if (_config.OverrideRange)
target.PreferredRange = _config.MaxDistanceToTarget;

_followMaster = master != player && (autorot.Bossmods.ActiveModule?.StateMachine.ActiveState == null || _config.FollowDuringActiveBossModule) && (!master.InCombat || _config.FollowDuringCombat || (_masterPrevPos - _masterMovementStart).LengthSq() > 100) && (player.InCombat || _config.FollowOutOfCombat);
_followMaster = master != player;

// note: if there are pending knockbacks, don't update navigation decision to avoid fucking up positioning
if (!WorldState.PendingEffects.PendingKnockbacks(player.InstanceID))
Expand All @@ -69,24 +58,15 @@ public void Execute(Actor player, Actor master)
}

bool masterIsMoving = TrackMasterMovement(master);
bool moveWithMaster = masterIsMoving && (master == player || _followMaster);
bool moveWithMaster = masterIsMoving && _followMaster && master != player;
ForceMovementIn = moveWithMaster || gazeImminent || pyreticImminent ? 0 : _naviDecision.LeewaySeconds;

// note: that there is a 1-frame delay if target and/or strategy changes - we don't really care?..
if (!forbidActions)
{
autorot.Preset = target.Target != null ? AIPreset : null;
}

UpdateMovement(player, master, target, gazeImminent || pyreticImminent, !forbidActions ? autorot.Hints.ActionsToExecute : null);
UpdateMovement(player, master, target, gazeImminent || pyreticImminent, !forbidTargeting ? autorot.Hints.ActionsToExecute : null);
}

// returns null if we're to be idle, otherwise target to attack
private Targeting SelectPrimaryTarget(Actor player, Actor master)
{
if (autorot.Hints.InteractWithTarget is Actor interact)
return new Targeting(new AIHints.Enemy(interact, false), 3);

// we prefer not to switch targets unnecessarily, so start with current target - it could've been selected manually or by AI on previous frames
// if current target is not among valid targets, clear it - this opens way for future target selection heuristics
var targetId = autorot.Hints.ForcedTarget?.InstanceID ?? player.TargetID;
Expand Down Expand Up @@ -138,16 +118,27 @@ private NavigationDecision BuildNavigationDecision(Actor player, Actor master, r
if (_config.ForbidMovement)
return new() { LeewaySeconds = float.MaxValue };

Actor? forceDestination = null;
float forceDestinationRange = 2;
if (_followMaster)
forceDestination = master;
else if (autorot.Hints.InteractWithTarget is Actor tar)
{
forceDestination = tar;
forceDestinationRange = 3.5f;
}

if (forceDestination != null && autorot.Hints.PathfindMapBounds.Contains(forceDestination.Position - autorot.Hints.PathfindMapCenter))
{
autorot.Hints.GoalZones.Clear();
autorot.Hints.GoalZones.Add(autorot.Hints.GoalSingleTarget(master.Position, _config.OverrideRange ? _config.MaxDistanceToSlot : 1));
autorot.Hints.GoalZones.Add(autorot.Hints.GoalSingleTarget(forceDestination, forceDestinationRange));
return NavigationDecision.Build(_naviCtx, WorldState, autorot.Hints, player);
}

// TODO: remove this once all rotation modules are fixed
if (autorot.Hints.GoalZones.Count == 0 && targeting.Target != null)
autorot.Hints.GoalZones.Add(autorot.Hints.GoalSingleTarget(targeting.Target.Actor, targeting.PreferredPosition, targeting.PreferredRange));

return NavigationDecision.Build(_naviCtx, WorldState, autorot.Hints, player);
}

Expand All @@ -157,7 +148,7 @@ private void FocusMaster(Actor master)
if (masterChanged)
{
ctrl.SetFocusTarget(master);
_masterPrevPos = _masterMovementStart = master.Position;
_masterPrevPos = master.Position;
_masterLastMoved = WorldState.CurrentTime.AddSeconds(-1);
}
}
Expand All @@ -174,8 +165,6 @@ private bool TrackMasterMovement(Actor master)
}
else if ((WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 0.5f)
{
// master has stopped, consider previous movement finished
_masterMovementStart = _masterPrevPos;
masterIsMoving = false;
}
// else: don't consider master to have stopped moving unless he's standing still for some small time
Expand Down Expand Up @@ -219,31 +208,9 @@ private void UpdateMovement(Actor player, Actor master, Targeting target, bool g

public void DrawDebug()
{
ImGui.Checkbox("Forbid actions", ref _config.ForbidActions);
ImGui.Checkbox("Disable autotarget", ref _config.ForbidActions);
ImGui.SameLine();
ImGui.Checkbox("Forbid movement", ref _config.ForbidMovement);
ImGui.SameLine();
ImGui.Checkbox("Show extra options", ref _config.ShowExtraUIOptions);
if (_config.ShowExtraUIOptions)
{
ImGui.Checkbox("Follow during combat", ref _config.FollowDuringCombat);
ImGui.SameLine();
ImGui.Checkbox("Follow during active boss module", ref _config.FollowDuringActiveBossModule);
ImGui.Spacing();
ImGui.Checkbox("Follow out of combat", ref _config.FollowOutOfCombat);
ImGui.SameLine();
ImGui.Checkbox("Follow target", ref _config.FollowTarget);
ImGui.SameLine();
ImGui.Checkbox("Override follow range", ref _config.OverrideRange);
ImGui.PushItemWidth(75 * ImGuiHelpers.GlobalScale);
ImGui.InputFloat("Follow slot range", ref _config.MaxDistanceToSlot);
ImGui.SameLine();
ImGui.InputFloat("Follow target range", ref _config.MaxDistanceToTarget);
ImGui.PopItemWidth();
}
var player = WorldState.Party.Player();
var dist = _naviDecision.Destination != null && player != null ? (_naviDecision.Destination.Value - player.Position).Length() : 0;
ImGui.TextUnformatted($"Max-cast={MathF.Min(ForceMovementIn, 1000):f3}, afk={_afkMode}, follow={_followMaster}, \n{_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}");
ImGui.Checkbox("Disable movement", ref _config.ForbidMovement);
}

private bool TargetIsForbidden(ulong actorId) => autorot.Hints.ForbiddenTargets.Any(e => e.Actor.InstanceID == actorId);
Expand Down
37 changes: 5 additions & 32 deletions BossMod/AI/AIConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,25 @@ public enum Slot { One, Two, Three, Four }
[PropertyDisplay("Show in-game UI")]
public bool DrawUI = true;

[PropertyDisplay("Show advanced options in the UI")]
public bool ShowExtraUIOptions = true;

[PropertyDisplay("Show AI status in the in-game UI's title bar")]
public bool ShowStatusOnTitlebar = true;

[PropertyDisplay("Show AI status in the server info bar")]
public bool ShowDTR = true;

// ai settings
[PropertyDisplay($"Override positional")]
public bool OverridePositional = false;

[PropertyDisplay("Desired positional")]
public Positional DesiredPositional = 0;

[PropertyDisplay($"Follow slot")]
public Slot FollowSlot = 0;

[PropertyDisplay($"Override follow range")]
public bool OverrideRange = false;

[PropertyDisplay($"Follow slot range")]
public float MaxDistanceToSlot = 1;

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

[PropertyDisplay($"Follow target range")]
public float MaxDistanceToTarget = 2.6f;

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

[PropertyDisplay("Follow during combat")]
public bool FollowDuringCombat = false;

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

[PropertyDisplay("Forbid movement")]
[PropertyDisplay("Disable movement")]
public bool ForbidMovement = false;

[PropertyDisplay("Forbid actions")]
[PropertyDisplay("Disable auto-target")]
public bool ForbidActions = false;

[PropertyDisplay("Automatically engage FATE mobs", since: "0.0.0.253")]
public bool AutoFate = true;

[PropertyDisplay("Focus target master")]
public bool FocusTargetMaster = false;

Expand Down
58 changes: 5 additions & 53 deletions BossMod/AI/AIManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using ImGuiNET;
using static Dalamud.Interface.Windowing.Window;

namespace BossMod.AI;

Expand All @@ -16,14 +15,11 @@ sealed class AIManager : IDisposable
private readonly AIController _controller;
private readonly AIConfig _config;
private int MasterSlot => (int)_config.FollowSlot; // non-zero means corresponding player is master
private Positional DesiredPositional => _config.DesiredPositional;
private Preset? _aiPreset;
private readonly UISimpleWindow _ui;
private WorldState WorldState => _autorot.Bossmods.WorldState;
private string _aiStatus = "";
private string _naviStatus = "";

public string GetAIPreset => _aiPreset?.Name ?? string.Empty;
public float ForceMovementIn => Behaviour?.ForceMovementIn ?? float.MaxValue;
public AIBehaviour? Behaviour { get; private set; }

Expand Down Expand Up @@ -78,7 +74,6 @@ public void Update()

var player = WorldState.Party.Player();
var master = WorldState.Party[MasterSlot];
var target = WorldState.Actors.Find(WorldState.Party.Player()?.TargetID ?? 0);

if (Behaviour != null && player != null && master != null)
{
Expand All @@ -90,19 +85,13 @@ public void Update()
}

_controller.Update(player, _autorot.Hints, WorldState.CurrentTime);
_aiStatus = $"AI: {(Behaviour != null ? $"on, {(_config.FollowTarget && target != null ? $"target={target.Name}" : $"master={master?.Name}[{((int)_config.FollowSlot) + 1}]")}" : "off")}";
_naviStatus = $"Navi={_controller.NaviTargetPos}";
_aiStatus = $"AI: {(Behaviour != null ? $"on, {$"master={master?.Name}[{(int)_config.FollowSlot + 1}]"}" : "off")}";
var dist = _controller.NaviTargetPos != null && player != null ? (_controller.NaviTargetPos.Value - player.Position).Length() : 0;
_naviStatus = $"Navi={_controller.NaviTargetPos?.ToString() ?? "<none>"} (d={dist:f3}, max-cast={MathF.Min(Behaviour?.ForceMovementIn ?? float.MaxValue, 1000):f3})";
_ui.IsOpen = player != null && _config.DrawUI;
_ui.WindowName = _config.ShowStatusOnTitlebar ? $"{_aiStatus}, {_naviStatus}###AI" : $"AI###AI";
}

public void SetAIPreset(Preset? p)
{
_aiPreset = p;
if (Behaviour != null)
Behaviour.AIPreset = p;
}

private void DrawOverlay()
{
if (!_config.ShowStatusOnTitlebar)
Expand All @@ -112,62 +101,25 @@ private void DrawOverlay()
}
Behaviour?.DrawDebug();

using (var leaderCombo = ImRaii.Combo("Follow", Behaviour == null ? "<idle>" : (_config.FollowTarget ? "<target>" : WorldState.Party[MasterSlot]?.Name ?? "<unknown>")))
using (var leaderCombo = ImRaii.Combo("Follow", Behaviour == null ? "<idle>" : WorldState.Party[MasterSlot]?.Name ?? "<unknown>"))
{
if (leaderCombo)
{
if (ImGui.Selectable("<idle>", Behaviour == null))
{
Enabled = false;
}
if (ImGui.Selectable("<target>", _config.FollowTarget))
{
_config.FollowSlot = 0;
_config.FollowTarget = true;
_config.Modified.Fire();
Enabled = true;
}
foreach (var (i, p) in WorldState.Party.WithSlot(true))
{
if (ImGui.Selectable(p.Name, MasterSlot == i))
{
_config.FollowSlot = (AIConfig.Slot)i;
_config.FollowTarget = false;
_config.Modified.Fire();
Enabled = true;
}
}
}
}

using (var positionalCombo = ImRaii.Combo("Positional", $"{DesiredPositional}"))
{
if (positionalCombo)
{
for (var i = 0; i < 4; i++)
{
if (ImGui.Selectable($"{(Positional)i}", DesiredPositional == (Positional)i))
{
_config.DesiredPositional = (Positional)i;
_config.Modified.Fire();
}
}
}
}

using (var presetCombo = ImRaii.Combo("AI preset", _aiPreset?.Name ?? ""))
{
if (presetCombo)
{
foreach (var p in _autorot.Database.Presets.VisiblePresets)
{
if (ImGui.Selectable(p.Name, p == _aiPreset))
{
SetAIPreset(p);
}
}
}
}
}

public void SwitchToIdle()
Expand All @@ -185,7 +137,7 @@ public void SwitchToFollow(int masterSlot)
SwitchToIdle();
_config.FollowSlot = (AIConfig.Slot)masterSlot;
_config.Modified.Fire();
Behaviour = new AIBehaviour(_controller, _autorot, _aiPreset);
Behaviour = new AIBehaviour(_controller, _autorot);
}

private unsafe int FindPartyMemberSlotFromSender(SeString sender)
Expand Down
Loading

0 comments on commit 59c5449

Please sign in to comment.