diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index ce4c9616..b4eb5a5b 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -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, } } diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index d7ca7fde..f44c4934 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -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 diff --git a/Questionable/Controller/CombatModules/BossModModule.cs b/Questionable/Controller/CombatModules/BossModModule.cs index f27a6f81..ee25967f 100644 --- a/Questionable/Controller/CombatModules/BossModModule.cs +++ b/Questionable/Controller/CombatModules/BossModModule.cs @@ -16,8 +16,6 @@ internal sealed class BossModModule : ICombatModule, IDisposable { private const string Name = "BossMod"; private readonly ILogger _logger; - private readonly MovementController _movementController; - private readonly IClientState _clientState; private readonly Configuration _configuration; private readonly ICallGateSubscriber _getPreset; private readonly ICallGateSubscriber _createPreset; @@ -25,18 +23,13 @@ internal sealed class BossModModule : ICombatModule, IDisposable private readonly ICallGateSubscriber _clearPreset; private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!; - private DateTime _lastDistanceCheck = DateTime.MinValue; public BossModModule( ILogger logger, - MovementController movementController, - IClientState clientState, IDalamudPluginInterface pluginInterface, Configuration configuration) { _logger = logger; - _movementController = movementController; - _clientState = clientState; _configuration = configuration; _getPreset = pluginInterface.GetIpcSubscriber($"{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; diff --git a/Questionable/Controller/CombatModules/ICombatModule.cs b/Questionable/Controller/CombatModules/ICombatModule.cs index 06fe4ae6..2a295ae0 100644 --- a/Questionable/Controller/CombatModules/ICombatModule.cs +++ b/Questionable/Controller/CombatModules/ICombatModule.cs @@ -12,7 +12,5 @@ internal interface ICombatModule void Update(IGameObject nextTarget); - void MoveToTarget(IGameObject nextTarget); - bool CanAttack(IBattleNpc target); } diff --git a/Questionable/Controller/CombatModules/ItemUseModule.cs b/Questionable/Controller/CombatModules/ItemUseModule.cs index 65f829a1..26a92ff3 100644 --- a/Questionable/Controller/CombatModules/ItemUseModule.cs +++ b/Questionable/Controller/CombatModules/ItemUseModule.cs @@ -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); } diff --git a/Questionable/Controller/CombatModules/Mount128Module.cs b/Questionable/Controller/CombatModules/Mount128Module.cs index e665163a..9f885963 100644 --- a/Questionable/Controller/CombatModules/Mount128Module.cs +++ b/Questionable/Controller/CombatModules/Mount128Module.cs @@ -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; } diff --git a/Questionable/Controller/CombatModules/Mount147Module.cs b/Questionable/Controller/CombatModules/Mount147Module.cs index f0c43176..4d8202fc 100644 --- a/Questionable/Controller/CombatModules/Mount147Module.cs +++ b/Questionable/Controller/CombatModules/Mount147Module.cs @@ -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; } diff --git a/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs b/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs index d5f13f4e..f89ad334 100644 --- a/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs +++ b/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs @@ -14,19 +14,15 @@ namespace Questionable.Controller.CombatModules; internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable { private readonly ILogger _logger; - private readonly MovementController _movementController; private readonly IClientState _clientState; private readonly Configuration _configuration; private readonly ICallGateSubscriber _test; private readonly ICallGateSubscriber _changeOperationMode; - private DateTime _lastDistanceCheck = DateTime.MinValue; - public RotationSolverRebornModule(ILogger logger, MovementController movementController, IClientState clientState, IDalamudPluginInterface pluginInterface, Configuration configuration) { _logger = logger; - _movementController = movementController; _clientState = clientState; _configuration = configuration; _test = pluginInterface.GetIpcSubscriber("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 index 00000000..d9563683 --- /dev/null +++ b/Questionable/Controller/CombatModules/WrathComboModule.cs @@ -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 _logger; + private readonly Configuration _configuration; + private readonly ICallGateSubscriber _test; + private readonly ICallGateSubscriber _registerForLeaseWithCallback; + private readonly ICallGateSubscriber _releaseControl; + private readonly ICallGateSubscriber _setAutoRotationState; + private readonly ICallGateSubscriber _setCurrentJobAutoRotationReady; + private readonly ICallGateProvider _callback; + + private Guid? _lease; + + public WrathComboModule(ILogger logger, Configuration configuration, + IDalamudPluginInterface pluginInterface) + { + _logger = logger; + _configuration = configuration; + _test = pluginInterface.GetIpcSubscriber("WrathCombo.Test"); + _registerForLeaseWithCallback = + pluginInterface.GetIpcSubscriber("WrathCombo.RegisterForLeaseWithCallback"); + _releaseControl = pluginInterface.GetIpcSubscriber("WrathCombo.ReleaseControl"); + _setAutoRotationState = pluginInterface.GetIpcSubscriber("WrathCombo.SetAutoRotationState"); + _setCurrentJobAutoRotationReady = + pluginInterface.GetIpcSubscriber("WrathCombo.SetCurrentJobAutoRotationReady"); + + _callback = pluginInterface.GetIpcProvider($"{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(); + } +} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 6c89d5ad..5964eb45 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index 5184761c..f4414084 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -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"]; diff --git a/Questionable/Windows/OneTimeSetupWindow.cs b/Questionable/Windows/OneTimeSetupWindow.cs index 3ba35a09..3db85089 100644 --- a/Questionable/Windows/OneTimeSetupWindow.cs +++ b/Questionable/Windows/OneTimeSetupWindow.cs @@ -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); }