Add Wrath Combo as combat module
authorLiza Carvelli <liza@carvel.li>
Fri, 27 Dec 2024 16:48:14 +0000 (17:48 +0100)
committerLiza Carvelli <liza@carvel.li>
Fri, 27 Dec 2024 16:48:14 +0000 (17:48 +0100)
12 files changed:
Questionable/Configuration.cs
Questionable/Controller/CombatController.cs
Questionable/Controller/CombatModules/BossModModule.cs
Questionable/Controller/CombatModules/ICombatModule.cs
Questionable/Controller/CombatModules/ItemUseModule.cs
Questionable/Controller/CombatModules/Mount128Module.cs
Questionable/Controller/CombatModules/Mount147Module.cs
Questionable/Controller/CombatModules/RotationSolverRebornModule.cs
Questionable/Controller/CombatModules/WrathComboModule.cs [new file with mode: 0644]
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigWindow.cs
Questionable/Windows/OneTimeSetupWindow.cs

index ce4c961659eca8cda2f1e79622e6c415fddde6b0..b4eb5a5b850dee5f10c6833ad99209c67c2a4d3f 100644 (file)
@@ -7,7 +7,7 @@ namespace Questionable;
 
 internal sealed class Configuration : IPluginConfiguration
 {
-    public const int PluginSetupVersion = 3;
+    public const int PluginSetupVersion = 4;
 
     public int Version { get; set; } = 1;
     public int PluginSetupCompleteVersion { get; set; }
@@ -23,7 +23,7 @@ internal sealed class Configuration : IPluginConfiguration
 
     internal sealed class GeneralConfiguration
     {
-        public ECombatModule CombatModule { get; set; } = ECombatModule.BossMod;
+        public ECombatModule CombatModule { get; set; } = ECombatModule.None;
         public uint MountId { get; set; } = 71;
         public GrandCompany GrandCompany { get; set; } = GrandCompany.None;
         public bool HideInAllInstances { get; set; } = true;
@@ -51,6 +51,7 @@ internal sealed class Configuration : IPluginConfiguration
     {
         None,
         BossMod,
+        WrathCombo,
         RotationSolverReborn,
     }
 }
index d7ca7fde79a63319f91d552fe96f3b28ff2cb5ad..f44c4934ddc1c45576fbd64ab52754475ceddefa 100644 (file)
@@ -13,8 +13,10 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using FFXIVClientStructs.FFXIV.Common.Math;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.CombatModules;
+using Questionable.Controller.Steps;
 using Questionable.Controller.Utils;
 using Questionable.Functions;
+using Questionable.Model;
 using Questionable.Model.Questing;
 
 namespace Questionable.Controller;
@@ -75,6 +77,7 @@ internal sealed class CombatController : IDisposable
             {
                 Module = combatModule,
                 Data = combatData,
+                LastDistanceCheck = DateTime.Now,
             };
             _wasInCombat = combatData.SpawnType is EEnemySpawnType.QuestInterruption or EEnemySpawnType.FinishCombatIfAny;
             return true;
@@ -129,7 +132,18 @@ internal sealed class CombatController : IDisposable
 
             if (nextTarget != null && nextTarget.Equals(target))
             {
-                _currentFight.Module.Update(target);
+                if (!IsMovingOrShouldMove(target))
+                {
+                    try
+                    {
+                        _currentFight.Module.Update(target);
+                    }
+                    catch (TaskException e)
+                    {
+                        _logger.LogWarning(e, "Combat was interrupted, stopping: {Exception}", e.Message);
+                        SetTarget(null);
+                    }
+                }
             }
             else if (nextTarget != null)
             {
@@ -323,14 +337,57 @@ internal sealed class CombatController : IDisposable
         {
             _logger.LogInformation("Moving to target, distance: {Distance:N2}",
                 Vector3.Distance(_clientState.LocalPlayer!.Position, target.Position));
-            _currentFight!.Module.MoveToTarget(target);
+            MoveToTarget(target);
         }
         else
         {
             _logger.LogInformation("Setting target to {TargetName} ({TargetId:X8})", target.Name.ToString(),
                 target.GameObjectId);
             _targetManager.Target = target;
-            _currentFight!.Module.MoveToTarget(target);
+            MoveToTarget(target);
+        }
+    }
+
+    private bool IsMovingOrShouldMove(IGameObject gameObject)
+    {
+        if (_movementController.IsPathfinding || _movementController.IsPathRunning)
+            return true;
+
+        if (DateTime.Now > _currentFight!.LastDistanceCheck.AddSeconds(10))
+        {
+            MoveToTarget(gameObject);
+            _currentFight!.LastDistanceCheck = DateTime.Now;
+            return true;
+        }
+
+        return false;
+    }
+
+    private void MoveToTarget(IGameObject gameObject)
+    {
+        var player = _clientState.LocalPlayer;
+        if (player == null)
+            return; // uh oh
+
+        float hitboxOffset = player.HitboxRadius + gameObject.HitboxRadius;
+        float actualDistance = Vector3.Distance(player.Position, gameObject.Position);
+        float maxDistance = player.ClassJob.ValueNullable?.Role is 3 or 4 ? 20f : 2.9f;
+        if (actualDistance - hitboxOffset >= maxDistance)
+        {
+            if (actualDistance - hitboxOffset <= 5)
+            {
+                _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack", gameObject.Name,
+                    gameObject.DataId);
+                _movementController.NavigateTo(EMovementType.Combat, null, [gameObject.Position], false, false,
+                    maxDistance + hitboxOffset - 0.25f, true);
+            }
+            else
+            {
+                _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack (with navmesh)", gameObject.Name,
+                    gameObject.DataId);
+                _movementController.NavigateTo(EMovementType.Combat, null, gameObject.Position, false, false,
+                    maxDistance + hitboxOffset - 0.25f, true);
+            }
         }
     }
 
@@ -359,6 +416,7 @@ internal sealed class CombatController : IDisposable
     {
         public required ICombatModule Module { get; init; }
         public required CombatData Data { get; init; }
+        public required DateTime LastDistanceCheck { get; set; }
     }
 
     public sealed class CombatData
index f27a6f8104180ecb5f885815b5b22e453cf6ed63..ee25967f441041b7d342cbe7a3cef105f679192b 100644 (file)
@@ -16,8 +16,6 @@ internal sealed class BossModModule : ICombatModule, IDisposable
 {
     private const string Name = "BossMod";
     private readonly ILogger<BossModModule> _logger;
-    private readonly MovementController _movementController;
-    private readonly IClientState _clientState;
     private readonly Configuration _configuration;
     private readonly ICallGateSubscriber<string, string?> _getPreset;
     private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
@@ -25,18 +23,13 @@ internal sealed class BossModModule : ICombatModule, IDisposable
     private readonly ICallGateSubscriber<bool> _clearPreset;
 
     private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!;
-    private DateTime _lastDistanceCheck = DateTime.MinValue;
 
     public BossModModule(
         ILogger<BossModModule> logger,
-        MovementController movementController,
-        IClientState clientState,
         IDalamudPluginInterface pluginInterface,
         Configuration configuration)
     {
         _logger = logger;
-        _movementController = movementController;
-        _clientState = clientState;
         _configuration = configuration;
 
         _getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get");
@@ -70,7 +63,6 @@ internal sealed class BossModModule : ICombatModule, IDisposable
                 _logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _createPreset.InvokeFunc(reader.ReadToEnd(), true));
             }
             _setPreset.InvokeFunc("Questionable");
-            _lastDistanceCheck = DateTime.Now;
             return true;
         }
         catch (IpcError e)
@@ -94,46 +86,8 @@ internal sealed class BossModModule : ICombatModule, IDisposable
         }
     }
 
-    public void MoveToTarget(IGameObject gameObject)
-    {
-        var player = _clientState.LocalPlayer;
-        if (player == null)
-            return; // uh oh
-
-        float hitboxOffset = player.HitboxRadius + gameObject.HitboxRadius;
-        float actualDistance = Vector3.Distance(player.Position, gameObject.Position);
-        float maxDistance = player.ClassJob.ValueNullable?.Role is 3 or 4 ? 20f : 2.9f;
-        if (actualDistance - hitboxOffset >= maxDistance)
-        {
-            if (actualDistance - hitboxOffset <= 5)
-            {
-                _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack", gameObject.Name,
-                    gameObject.DataId);
-                _movementController.NavigateTo(EMovementType.Combat, null, [gameObject.Position], false, false,
-                    maxDistance + hitboxOffset - 0.25f, true);
-            }
-            else
-            {
-                _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack (with navmesh)", gameObject.Name,
-                    gameObject.DataId);
-                _movementController.NavigateTo(EMovementType.Combat, null, gameObject.Position, false, false,
-                    maxDistance + hitboxOffset - 0.25f, true);
-            }
-        }
-
-        _lastDistanceCheck = DateTime.Now;
-    }
-
     public void Update(IGameObject gameObject)
     {
-        if (_movementController.IsPathfinding || _movementController.IsPathRunning)
-            return;
-
-        if (DateTime.Now > _lastDistanceCheck.AddSeconds(10))
-        {
-            MoveToTarget(gameObject);
-            _lastDistanceCheck = DateTime.Now;
-        }
     }
 
     public bool CanAttack(IBattleNpc target) => true;
index 06fe4ae684f2f44cf63749dc2743dc33fb53be5c..2a295ae030c781b3a836d08d73932a0ad8b3b8f4 100644 (file)
@@ -12,7 +12,5 @@ internal interface ICombatModule
 
     void Update(IGameObject nextTarget);
 
-    void MoveToTarget(IGameObject nextTarget);
-
     bool CanAttack(IBattleNpc target);
 }
index 65f829a1736e3decfc75d178322f7e8276f8e3d2..26a92ff32c4f6c7b31551f5b7f517572cd5b6289 100644 (file)
@@ -152,7 +152,5 @@ internal sealed class ItemUseModule : ICombatModule
         return false;
     }
 
-    public void MoveToTarget(IGameObject nextTarget) => _delegate!.MoveToTarget(nextTarget);
-
     public bool CanAttack(IBattleNpc target) => _delegate!.CanAttack(target);
 }
index e665163a0c3fcdd9cd2e59cef80b4d6f6429a927..9f8859639eef5a801182cb456fdebe81ff073736 100644 (file)
@@ -15,13 +15,10 @@ internal sealed class Mount128Module : ICombatModule
     public const ushort MountId = 128;
     private readonly EAction[] _actions = [EAction.MagitekThunder, EAction.MagitekPulse];
 
-    private readonly MovementController _movementController;
     private readonly GameFunctions _gameFunctions;
 
-
-    public Mount128Module(MovementController movementController, GameFunctions gameFunctions)
+    public Mount128Module(GameFunctions gameFunctions)
     {
-        _movementController = movementController;
         _gameFunctions = gameFunctions;
     }
 
@@ -33,9 +30,6 @@ internal sealed class Mount128Module : ICombatModule
 
     public void Update(IGameObject gameObject)
     {
-        if (_movementController.IsPathfinding || _movementController.IsPathRunning)
-            return;
-
         foreach (EAction action in _actions)
         {
             if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false))
@@ -43,9 +37,5 @@ internal sealed class Mount128Module : ICombatModule
         }
     }
 
-    public void MoveToTarget(IGameObject gameObject)
-    {
-    }
-
     public bool CanAttack(IBattleNpc target) => target.DataId is 7504 or 7505 or 14107;
 }
index f0c43176d9848680cbbe6180f47d698395d6566f..4d8202fcc6573292b8842f30390142661ef0df78 100644 (file)
@@ -15,13 +15,11 @@ internal sealed class Mount147Module : ICombatModule
     public const ushort MountId = 147;
     private readonly EAction[] _actions = [EAction.Trample];
 
-    private readonly MovementController _movementController;
     private readonly GameFunctions _gameFunctions;
 
 
-    public Mount147Module(MovementController movementController, GameFunctions gameFunctions)
+    public Mount147Module(GameFunctions gameFunctions)
     {
-        _movementController = movementController;
         _gameFunctions = gameFunctions;
     }
 
@@ -33,9 +31,6 @@ internal sealed class Mount147Module : ICombatModule
 
     public void Update(IGameObject gameObject)
     {
-        if (_movementController.IsPathfinding || _movementController.IsPathRunning)
-            return;
-
         foreach (EAction action in _actions)
         {
             if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false))
@@ -43,9 +38,5 @@ internal sealed class Mount147Module : ICombatModule
         }
     }
 
-    public void MoveToTarget(IGameObject gameObject)
-    {
-    }
-
     public bool CanAttack(IBattleNpc target) => target.DataId is 8593;
 }
index d5f13f4efae75840a2ff24972583695f9cf9f73f..f89ad334df4df725c10ee08505c727ca6c38430a 100644 (file)
@@ -14,19 +14,15 @@ namespace Questionable.Controller.CombatModules;
 internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
 {
     private readonly ILogger<RotationSolverRebornModule> _logger;
-    private readonly MovementController _movementController;
     private readonly IClientState _clientState;
     private readonly Configuration _configuration;
     private readonly ICallGateSubscriber<string, object> _test;
     private readonly ICallGateSubscriber<StateCommandType, object> _changeOperationMode;
 
-    private DateTime _lastDistanceCheck = DateTime.MinValue;
-
     public RotationSolverRebornModule(ILogger<RotationSolverRebornModule> logger, MovementController movementController,
         IClientState clientState, IDalamudPluginInterface pluginInterface, Configuration configuration)
     {
         _logger = logger;
-        _movementController = movementController;
         _clientState = clientState;
         _configuration = configuration;
         _test = pluginInterface.GetIpcSubscriber<string, object>("RotationSolverReborn.Test");
@@ -55,7 +51,6 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
         try
         {
             _changeOperationMode.InvokeAction(StateCommandType.Manual);
-            _lastDistanceCheck = DateTime.Now;
             return true;
         }
         catch (IpcError e)
@@ -67,6 +62,9 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
 
     public bool Stop()
     {
+        if (!_changeOperationMode.HasAction)
+            return true;
+
         try
         {
             _changeOperationMode.InvokeAction(StateCommandType.Off);
@@ -79,46 +77,8 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
         }
     }
 
-    public void MoveToTarget(IGameObject gameObject)
-    {
-        var player = _clientState.LocalPlayer;
-        if (player == null)
-            return; // uh oh
-
-        float hitboxOffset = player.HitboxRadius + gameObject.HitboxRadius;
-        float actualDistance = Vector3.Distance(player.Position, gameObject.Position);
-        float maxDistance = player.ClassJob.ValueNullable?.Role is 3 or 4 ? 20f : 2.9f;
-        if (actualDistance - hitboxOffset >= maxDistance)
-        {
-            if (actualDistance - hitboxOffset <= 5)
-            {
-                _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack", gameObject.Name,
-                    gameObject.DataId);
-                _movementController.NavigateTo(EMovementType.Combat, null, [gameObject.Position], false, false,
-                    maxDistance + hitboxOffset - 0.25f, true);
-            }
-            else
-            {
-                _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack (with navmesh)", gameObject.Name,
-                    gameObject.DataId);
-                _movementController.NavigateTo(EMovementType.Combat, null, gameObject.Position, false, false,
-                    maxDistance + hitboxOffset - 0.25f, true);
-            }
-        }
-
-        _lastDistanceCheck = DateTime.Now;
-    }
-
     public void Update(IGameObject gameObject)
     {
-        if (_movementController.IsPathfinding || _movementController.IsPathRunning)
-            return;
-
-        if (DateTime.Now > _lastDistanceCheck.AddSeconds(10))
-        {
-            MoveToTarget(gameObject);
-            _lastDistanceCheck = DateTime.Now;
-        }
     }
 
     public bool CanAttack(IBattleNpc target) => true;
diff --git a/Questionable/Controller/CombatModules/WrathComboModule.cs b/Questionable/Controller/CombatModules/WrathComboModule.cs
new file mode 100644 (file)
index 0000000..d956368
--- /dev/null
@@ -0,0 +1,123 @@
+using System;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Ipc;
+using Dalamud.Plugin.Ipc.Exceptions;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps;
+
+namespace Questionable.Controller.CombatModules;
+
+internal sealed class WrathComboModule : ICombatModule, IDisposable
+{
+    private const string CallbackPrefix = "Questionable$Wrath";
+
+    private readonly ILogger<WrathComboModule> _logger;
+    private readonly Configuration _configuration;
+    private readonly ICallGateSubscriber<object> _test;
+    private readonly ICallGateSubscriber<string, string, string, Guid?> _registerForLeaseWithCallback;
+    private readonly ICallGateSubscriber<Guid, object> _releaseControl;
+    private readonly ICallGateSubscriber<Guid,bool,object> _setAutoRotationState;
+    private readonly ICallGateSubscriber<Guid,object> _setCurrentJobAutoRotationReady;
+    private readonly ICallGateProvider<int, string, object> _callback;
+
+    private Guid? _lease;
+
+    public WrathComboModule(ILogger<WrathComboModule> logger, Configuration configuration,
+        IDalamudPluginInterface pluginInterface)
+    {
+        _logger = logger;
+        _configuration = configuration;
+        _test = pluginInterface.GetIpcSubscriber<object>("WrathCombo.Test");
+        _registerForLeaseWithCallback =
+            pluginInterface.GetIpcSubscriber<string, string, string, Guid?>("WrathCombo.RegisterForLeaseWithCallback");
+        _releaseControl = pluginInterface.GetIpcSubscriber<Guid, object>("WrathCombo.ReleaseControl");
+        _setAutoRotationState = pluginInterface.GetIpcSubscriber<Guid, bool, object>("WrathCombo.SetAutoRotationState");
+        _setCurrentJobAutoRotationReady =
+            pluginInterface.GetIpcSubscriber<Guid, object>("WrathCombo.SetCurrentJobAutoRotationReady");
+
+        _callback = pluginInterface.GetIpcProvider<int, string, object>($"{CallbackPrefix}.WrathComboCallback");
+        _callback.RegisterAction(Callback);
+    }
+
+    public bool CanHandleFight(CombatController.CombatData combatData)
+    {
+        if (_configuration.General.CombatModule != Configuration.ECombatModule.WrathCombo)
+            return false;
+
+        try
+        {
+            _test.InvokeAction();
+            return true;
+        }
+        catch (IpcError)
+        {
+            return false;
+        }
+    }
+
+    public bool Start(CombatController.CombatData combatData)
+    {
+        try
+        {
+            _lease = _registerForLeaseWithCallback.InvokeFunc("Questionable", "Questionable", CallbackPrefix);
+            if (_lease != null)
+            {
+                _logger.LogDebug("Wrath combo lease: {Lease}", _lease.Value);
+
+                _setAutoRotationState.InvokeAction(_lease.Value, true);
+                _setCurrentJobAutoRotationReady.InvokeAction(_lease.Value);
+                return true;
+            }
+            else
+            {
+                _logger.LogError("Wrath combo did not return a lease");
+                return false;
+            }
+        }
+        catch (IpcError e)
+        {
+            _logger.LogError(e, "Unable to use wrath combo for combat");
+            return false;
+        }
+    }
+
+    public bool Stop()
+    {
+        try
+        {
+            if (_lease != null)
+            {
+                _releaseControl.InvokeAction(_lease.Value);
+                _lease = null;
+            }
+
+            return true;
+        }
+        catch (IpcError e)
+        {
+            _logger.LogWarning(e, "Could not turn off wrath combo");
+            return false;
+        }
+    }
+
+    public void Update(IGameObject nextTarget)
+    {
+        if (_lease == null)
+            throw new TaskException("Wrath Combo Lease is cancelled");
+    }
+
+    public bool CanAttack(IBattleNpc target) => true;
+
+    private void Callback(int reason, string additionalInfo)
+    {
+        _logger.LogWarning("WrathCombo callback: {Reason} ({Info})", reason, additionalInfo);
+        _lease = null;
+    }
+
+    public void Dispose()
+    {
+        Stop();
+        _callback.UnregisterAction();
+    }
+}
index 6c89d5ad5d8f69c5a5b463c5fcdea96369359337..5964eb45ad375a99c975a431642e3cb97f7ff6e5 100644 (file)
@@ -19,7 +19,6 @@ using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Steps.Gathering;
 using Questionable.Controller.Steps.Interactions;
 using Questionable.Controller.Steps.Leves;
-using Questionable.Controller.Utils;
 using Questionable.Data;
 using Questionable.External;
 using Questionable.Functions;
@@ -249,6 +248,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<ICombatModule, Mount147Module>();
         serviceCollection.AddSingleton<ICombatModule, ItemUseModule>();
         serviceCollection.AddSingleton<ICombatModule, BossModModule>();
+        serviceCollection.AddSingleton<ICombatModule, WrathComboModule>();
         serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
     }
 
index 5184761c59678e60dd30d5fba367c5ef614c5372..f4414084699aa36bd121c367a820f030b766ac89 100644 (file)
@@ -29,7 +29,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
     private readonly uint[] _mountIds;
     private readonly string[] _mountNames;
 
-    private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Rotation Solver Reborn"];
+    private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"];
 
     private readonly string[] _grandCompanyNames =
         ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
index 3ba35a09619d433691ce7debb9282e978bc1ad3a..3db85089d79fe2a46787053a7f5c8428c93167c9 100644 (file)
@@ -54,6 +54,14 @@ internal sealed class OneTimeSetupWindow : LWindow
                 new Uri("https://github.com/awgil/ffxiv_bossmod"),
                 new Uri("https://puni.sh/api/repository/veyn"))
         },
+        {
+            Configuration.ECombatModule.WrathCombo,
+            new PluginInfo("Wrath Combo",
+                "WrathCombo",
+                string.Empty,
+                new Uri("https://github.com/PunishXIV/WrathCombo"),
+                new Uri("https://puni.sh/api/plugins"))
+        },
         {
             Configuration.ECombatModule.RotationSolverReborn,
             new("Rotation Solver Reborn",
@@ -143,6 +151,7 @@ internal sealed class OneTimeSetupWindow : LWindow
             }
 
             DrawCombatPlugin(Configuration.ECombatModule.BossMod, checklistPadding);
+            DrawCombatPlugin(Configuration.ECombatModule.WrathCombo, checklistPadding);
             DrawCombatPlugin(Configuration.ECombatModule.RotationSolverReborn, checklistPadding);
         }