Add Wrath Combo as combat module

This commit is contained in:
Liza 2024-12-27 17:48:14 +01:00
parent aa70f4c55a
commit 1578c5d197
Signed by: liza
GPG Key ID: 2C41B84815CF6445
12 changed files with 203 additions and 121 deletions

View File

@ -7,7 +7,7 @@ namespace Questionable;
internal sealed class Configuration : IPluginConfiguration internal sealed class Configuration : IPluginConfiguration
{ {
public const int PluginSetupVersion = 3; public const int PluginSetupVersion = 4;
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public int PluginSetupCompleteVersion { get; set; } public int PluginSetupCompleteVersion { get; set; }
@ -23,7 +23,7 @@ internal sealed class Configuration : IPluginConfiguration
internal sealed class GeneralConfiguration 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 uint MountId { get; set; } = 71;
public GrandCompany GrandCompany { get; set; } = GrandCompany.None; public GrandCompany GrandCompany { get; set; } = GrandCompany.None;
public bool HideInAllInstances { get; set; } = true; public bool HideInAllInstances { get; set; } = true;
@ -51,6 +51,7 @@ internal sealed class Configuration : IPluginConfiguration
{ {
None, None,
BossMod, BossMod,
WrathCombo,
RotationSolverReborn, RotationSolverReborn,
} }
} }

View File

@ -13,8 +13,10 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Common.Math; using FFXIVClientStructs.FFXIV.Common.Math;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.CombatModules; using Questionable.Controller.CombatModules;
using Questionable.Controller.Steps;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
namespace Questionable.Controller; namespace Questionable.Controller;
@ -75,6 +77,7 @@ internal sealed class CombatController : IDisposable
{ {
Module = combatModule, Module = combatModule,
Data = combatData, Data = combatData,
LastDistanceCheck = DateTime.Now,
}; };
_wasInCombat = combatData.SpawnType is EEnemySpawnType.QuestInterruption or EEnemySpawnType.FinishCombatIfAny; _wasInCombat = combatData.SpawnType is EEnemySpawnType.QuestInterruption or EEnemySpawnType.FinishCombatIfAny;
return true; return true;
@ -129,7 +132,18 @@ internal sealed class CombatController : IDisposable
if (nextTarget != null && nextTarget.Equals(target)) 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) else if (nextTarget != null)
{ {
@ -323,14 +337,57 @@ internal sealed class CombatController : IDisposable
{ {
_logger.LogInformation("Moving to target, distance: {Distance:N2}", _logger.LogInformation("Moving to target, distance: {Distance:N2}",
Vector3.Distance(_clientState.LocalPlayer!.Position, target.Position)); Vector3.Distance(_clientState.LocalPlayer!.Position, target.Position));
_currentFight!.Module.MoveToTarget(target); MoveToTarget(target);
} }
else else
{ {
_logger.LogInformation("Setting target to {TargetName} ({TargetId:X8})", target.Name.ToString(), _logger.LogInformation("Setting target to {TargetName} ({TargetId:X8})", target.Name.ToString(),
target.GameObjectId); target.GameObjectId);
_targetManager.Target = target; _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 ICombatModule Module { get; init; }
public required CombatData Data { get; init; } public required CombatData Data { get; init; }
public required DateTime LastDistanceCheck { get; set; }
} }
public sealed class CombatData public sealed class CombatData

View File

@ -16,8 +16,6 @@ internal sealed class BossModModule : ICombatModule, IDisposable
{ {
private const string Name = "BossMod"; private const string Name = "BossMod";
private readonly ILogger<BossModModule> _logger; private readonly ILogger<BossModModule> _logger;
private readonly MovementController _movementController;
private readonly IClientState _clientState;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ICallGateSubscriber<string, string?> _getPreset; private readonly ICallGateSubscriber<string, string?> _getPreset;
private readonly ICallGateSubscriber<string, bool, bool> _createPreset; private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
@ -25,18 +23,13 @@ internal sealed class BossModModule : ICombatModule, IDisposable
private readonly ICallGateSubscriber<bool> _clearPreset; private readonly ICallGateSubscriber<bool> _clearPreset;
private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!; private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!;
private DateTime _lastDistanceCheck = DateTime.MinValue;
public BossModModule( public BossModModule(
ILogger<BossModModule> logger, ILogger<BossModModule> logger,
MovementController movementController,
IClientState clientState,
IDalamudPluginInterface pluginInterface, IDalamudPluginInterface pluginInterface,
Configuration configuration) Configuration configuration)
{ {
_logger = logger; _logger = logger;
_movementController = movementController;
_clientState = clientState;
_configuration = configuration; _configuration = configuration;
_getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get"); _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)); _logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _createPreset.InvokeFunc(reader.ReadToEnd(), true));
} }
_setPreset.InvokeFunc("Questionable"); _setPreset.InvokeFunc("Questionable");
_lastDistanceCheck = DateTime.Now;
return true; return true;
} }
catch (IpcError e) 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) 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; public bool CanAttack(IBattleNpc target) => true;

View File

@ -12,7 +12,5 @@ internal interface ICombatModule
void Update(IGameObject nextTarget); void Update(IGameObject nextTarget);
void MoveToTarget(IGameObject nextTarget);
bool CanAttack(IBattleNpc target); bool CanAttack(IBattleNpc target);
} }

View File

@ -152,7 +152,5 @@ internal sealed class ItemUseModule : ICombatModule
return false; return false;
} }
public void MoveToTarget(IGameObject nextTarget) => _delegate!.MoveToTarget(nextTarget);
public bool CanAttack(IBattleNpc target) => _delegate!.CanAttack(target); public bool CanAttack(IBattleNpc target) => _delegate!.CanAttack(target);
} }

View File

@ -15,13 +15,10 @@ internal sealed class Mount128Module : ICombatModule
public const ushort MountId = 128; public const ushort MountId = 128;
private readonly EAction[] _actions = [EAction.MagitekThunder, EAction.MagitekPulse]; private readonly EAction[] _actions = [EAction.MagitekThunder, EAction.MagitekPulse];
private readonly MovementController _movementController;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
public Mount128Module(GameFunctions gameFunctions)
public Mount128Module(MovementController movementController, GameFunctions gameFunctions)
{ {
_movementController = movementController;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
} }
@ -33,9 +30,6 @@ internal sealed class Mount128Module : ICombatModule
public void Update(IGameObject gameObject) public void Update(IGameObject gameObject)
{ {
if (_movementController.IsPathfinding || _movementController.IsPathRunning)
return;
foreach (EAction action in _actions) foreach (EAction action in _actions)
{ {
if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false)) 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; public bool CanAttack(IBattleNpc target) => target.DataId is 7504 or 7505 or 14107;
} }

View File

@ -15,13 +15,11 @@ internal sealed class Mount147Module : ICombatModule
public const ushort MountId = 147; public const ushort MountId = 147;
private readonly EAction[] _actions = [EAction.Trample]; private readonly EAction[] _actions = [EAction.Trample];
private readonly MovementController _movementController;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
public Mount147Module(MovementController movementController, GameFunctions gameFunctions) public Mount147Module(GameFunctions gameFunctions)
{ {
_movementController = movementController;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
} }
@ -33,9 +31,6 @@ internal sealed class Mount147Module : ICombatModule
public void Update(IGameObject gameObject) public void Update(IGameObject gameObject)
{ {
if (_movementController.IsPathfinding || _movementController.IsPathRunning)
return;
foreach (EAction action in _actions) foreach (EAction action in _actions)
{ {
if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false)) 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; public bool CanAttack(IBattleNpc target) => target.DataId is 8593;
} }

View File

@ -14,19 +14,15 @@ namespace Questionable.Controller.CombatModules;
internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
{ {
private readonly ILogger<RotationSolverRebornModule> _logger; private readonly ILogger<RotationSolverRebornModule> _logger;
private readonly MovementController _movementController;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ICallGateSubscriber<string, object> _test; private readonly ICallGateSubscriber<string, object> _test;
private readonly ICallGateSubscriber<StateCommandType, object> _changeOperationMode; private readonly ICallGateSubscriber<StateCommandType, object> _changeOperationMode;
private DateTime _lastDistanceCheck = DateTime.MinValue;
public RotationSolverRebornModule(ILogger<RotationSolverRebornModule> logger, MovementController movementController, public RotationSolverRebornModule(ILogger<RotationSolverRebornModule> logger, MovementController movementController,
IClientState clientState, IDalamudPluginInterface pluginInterface, Configuration configuration) IClientState clientState, IDalamudPluginInterface pluginInterface, Configuration configuration)
{ {
_logger = logger; _logger = logger;
_movementController = movementController;
_clientState = clientState; _clientState = clientState;
_configuration = configuration; _configuration = configuration;
_test = pluginInterface.GetIpcSubscriber<string, object>("RotationSolverReborn.Test"); _test = pluginInterface.GetIpcSubscriber<string, object>("RotationSolverReborn.Test");
@ -55,7 +51,6 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
try try
{ {
_changeOperationMode.InvokeAction(StateCommandType.Manual); _changeOperationMode.InvokeAction(StateCommandType.Manual);
_lastDistanceCheck = DateTime.Now;
return true; return true;
} }
catch (IpcError e) catch (IpcError e)
@ -67,6 +62,9 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
public bool Stop() public bool Stop()
{ {
if (!_changeOperationMode.HasAction)
return true;
try try
{ {
_changeOperationMode.InvokeAction(StateCommandType.Off); _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) 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; public bool CanAttack(IBattleNpc target) => true;

View File

@ -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();
}
}

View File

@ -19,7 +19,6 @@ using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Leves; using Questionable.Controller.Steps.Leves;
using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
using Questionable.External; using Questionable.External;
using Questionable.Functions; using Questionable.Functions;
@ -249,6 +248,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<ICombatModule, Mount147Module>(); serviceCollection.AddSingleton<ICombatModule, Mount147Module>();
serviceCollection.AddSingleton<ICombatModule, ItemUseModule>(); serviceCollection.AddSingleton<ICombatModule, ItemUseModule>();
serviceCollection.AddSingleton<ICombatModule, BossModModule>(); serviceCollection.AddSingleton<ICombatModule, BossModModule>();
serviceCollection.AddSingleton<ICombatModule, WrathComboModule>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>(); serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
} }

View File

@ -29,7 +29,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
private readonly uint[] _mountIds; private readonly uint[] _mountIds;
private readonly string[] _mountNames; 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 = private readonly string[] _grandCompanyNames =
["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"]; ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];

View File

@ -54,6 +54,14 @@ internal sealed class OneTimeSetupWindow : LWindow
new Uri("https://github.com/awgil/ffxiv_bossmod"), new Uri("https://github.com/awgil/ffxiv_bossmod"),
new Uri("https://puni.sh/api/repository/veyn")) 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, Configuration.ECombatModule.RotationSolverReborn,
new("Rotation Solver Reborn", new("Rotation Solver Reborn",
@ -143,6 +151,7 @@ internal sealed class OneTimeSetupWindow : LWindow
} }
DrawCombatPlugin(Configuration.ECombatModule.BossMod, checklistPadding); DrawCombatPlugin(Configuration.ECombatModule.BossMod, checklistPadding);
DrawCombatPlugin(Configuration.ECombatModule.WrathCombo, checklistPadding);
DrawCombatPlugin(Configuration.ECombatModule.RotationSolverReborn, checklistPadding); DrawCombatPlugin(Configuration.ECombatModule.RotationSolverReborn, checklistPadding);
} }