"Z": 94.77368
},
"TerritoryId": 958,
- "InteractionType": "Instruction",
+ "InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [
14079
],
- "Comment": "TODO Needs item use?",
- "ItemId": 2003231,
- "ItemUseHealthMaxPercent": 10,
+ "CombatItemUse": {
+ "ItemId": 2003231,
+ "Condition": "Incapacitated"
+ },
"CompletionQuestVariablesFlags": [
null,
null,
"Z": 396.96338
},
"TerritoryId": 958,
- "InteractionType": "Instruction",
+ "InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [
14080
],
- "Comment": "TODO Needs item use?",
- "ItemId": 2003231,
- "ItemUseHealthMaxPercent": 10,
+ "CombatItemUse": {
+ "ItemId": 2003231,
+ "Condition": "Incapacitated"
+ },
"DisableNavmesh": true,
"CompletionQuestVariablesFlags": [
null,
]
}
},
+ "CombatItemUse": {
+ "description": "Unlike the 'AfterItemUse' condition that is used for spawning an enemy in the first place, interacting with an item at a certain stage of combat is required",
+ "type": "object",
+ "properties": {
+ "ItemId": {
+ "type": "integer"
+ },
+ "Condition": {
+ "type": "string",
+ "enum": [
+ "Incapacitated"
+ ]
+ }
+ },
+ "required": [
+ "ItemId",
+ "Condition"
+ ]
+ },
"CombatDelaySecondsAtStart": {
"type": "number"
}
--- /dev/null
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+public sealed class CombatItemUse
+{
+ public uint ItemId { get; set; }
+
+ [JsonConverter(typeof(CombatItemUseConditionConverter))]
+ public ECombatItemUseCondition Condition { get; set; }
+}
--- /dev/null
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class CombatItemUseConditionConverter() : EnumConverter<ECombatItemUseCondition>(Values)
+{
+ private static readonly Dictionary<ECombatItemUseCondition, string> Values = new()
+ {
+ { ECombatItemUseCondition.Incapacitated, "Incapacitated" },
+ };
+}
--- /dev/null
+namespace Questionable.Model.Questing;
+
+public enum ECombatItemUseCondition
+{
+ None,
+ Incapacitated,
+}
public EEnemySpawnType? EnemySpawnType { get; set; }
public List<uint> KillEnemyDataIds { get; set; } = [];
public List<ComplexCombatData> ComplexCombatData { get; set; } = [];
+ public CombatItemUse? CombatItemUse { get; set; }
public float? CombatDelaySecondsAtStart { get; set; }
public JumpDestination? JumpDestination { get; set; }
{
Stop("Starting combat");
- var combatModule = _combatModules.FirstOrDefault(x => x.IsLoaded);
+ var combatModule = _combatModules.FirstOrDefault(x => x.CanHandleFight(combatData));
if (combatModule == null)
return false;
- if (combatModule.Start())
+ if (combatModule.Start(combatData))
{
_currentFight = new CurrentFight
{
public required EEnemySpawnType SpawnType { get; init; }
public required List<uint> KillEnemyDataIds { get; init; }
public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
+ public required CombatItemUse? CombatItemUse { get; init; }
public HashSet<int> CompletedComplexDatas { get; } = new();
}
internal interface ICombatModule
{
- bool IsLoaded { get; }
+ bool CanHandleFight(CombatController.CombatData combatData);
- bool Start();
+ bool Start(CombatController.CombatData combatData);
bool Stop();
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Functions;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller.CombatModules;
+
+internal sealed class ItemUseModule : ICombatModule
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly GameFunctions _gameFunctions;
+ private readonly ICondition _condition;
+ private readonly ILogger<ItemUseModule> _logger;
+
+ private ICombatModule? _delegate;
+ private CombatController.CombatData? _combatData;
+ private bool _isDoingRotation;
+
+ public ItemUseModule(IServiceProvider serviceProvider, GameFunctions gameFunctions, ICondition condition,
+ ILogger<ItemUseModule> logger)
+ {
+ _serviceProvider = serviceProvider;
+ _gameFunctions = gameFunctions;
+ _condition = condition;
+ _logger = logger;
+ }
+
+ public bool CanHandleFight(CombatController.CombatData combatData)
+ {
+ if (combatData.CombatItemUse == null)
+ return false;
+
+ _delegate = _serviceProvider.GetRequiredService<IEnumerable<ICombatModule>>()
+ .Where(x => x is not ItemUseModule)
+ .FirstOrDefault(x => x.CanHandleFight(combatData));
+ _logger.LogInformation("ItemUse delegate: {Delegate}", _delegate?.GetType().Name);
+ return _delegate != null;
+ }
+
+ public bool Start(CombatController.CombatData combatData)
+ {
+ if (_delegate!.Start(combatData))
+ {
+ _combatData = combatData;
+ _isDoingRotation = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Stop()
+ {
+ if (_isDoingRotation)
+ {
+ _delegate!.Stop();
+ _isDoingRotation = false;
+ _combatData = null;
+ _delegate = null;
+ }
+
+ return true;
+ }
+
+ public void Update(IGameObject nextTarget)
+ {
+ if (_delegate == null)
+ return;
+
+ if (_combatData?.CombatItemUse == null)
+ {
+ _delegate.Update(nextTarget);
+ return;
+ }
+
+ if (_combatData.KillEnemyDataIds.Contains(nextTarget.DataId) ||
+ _combatData.ComplexCombatDatas.Any(x => x.DataId == nextTarget.DataId))
+ {
+ if (_isDoingRotation)
+ {
+ unsafe
+ {
+ InventoryManager* inventoryManager = InventoryManager.Instance();
+ if (inventoryManager->GetInventoryItemCount(_combatData.CombatItemUse.ItemId) == 0)
+ {
+ _isDoingRotation = false;
+ _delegate.Stop();
+ }
+ }
+
+ if (ShouldUseItem(nextTarget))
+ {
+ _isDoingRotation = false;
+ _delegate.Stop();
+ _gameFunctions.UseItem(nextTarget.DataId, _combatData.CombatItemUse.ItemId);
+ }
+ else
+ _delegate.Update(nextTarget);
+ }
+ else if (_condition[ConditionFlag.Casting])
+ {
+ // do nothing
+ }
+ else
+ {
+ _isDoingRotation = true;
+ _delegate.Start(_combatData);
+ }
+ }
+ else if (_isDoingRotation)
+ {
+ _delegate.Update(nextTarget);
+ }
+ }
+
+ private unsafe bool ShouldUseItem(IGameObject gameObject)
+ {
+ if (_combatData?.CombatItemUse == null)
+ return false;
+
+ if (gameObject is IBattleChara)
+ {
+ BattleChara* battleChara = (BattleChara*)gameObject.Address;
+ if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.Incapacitated)
+ return (battleChara->Flags2 & 128u) != 0;
+ }
+
+ return false;
+ }
+
+ public void MoveToTarget(IGameObject nextTarget) => _delegate!.MoveToTarget(nextTarget);
+
+ public bool CanAttack(IBattleNpc target) => _delegate!.CanAttack(target);
+}
_gameFunctions = gameFunctions;
}
- public bool IsLoaded => _gameFunctions.GetMountId() == MountId;
+ public bool CanHandleFight(CombatController.CombatData combatData) => _gameFunctions.GetMountId() == MountId;
- public bool Start() => true;
+ public bool Start(CombatController.CombatData combatData) => true;
public bool Stop() => true;
pluginInterface.GetIpcSubscriber<StateCommandType, object>("RotationSolverReborn.ChangeOperatingMode");
}
- public bool IsLoaded
+ public bool CanHandleFight(CombatController.CombatData combatData)
{
- get
+ try
{
- try
- {
- _test.InvokeAction("Validate RSR is callable from Questionable");
- return true;
- }
- catch (IpcError)
- {
- return false;
- }
+ _test.InvokeAction("Validate RSR is callable from Questionable");
+ return true;
+ }
+ catch (IpcError)
+ {
+ return false;
}
}
- public bool Start()
+ public bool Start(CombatController.CombatData combatData)
{
try
{
if (_condition[ConditionFlag.Mounted])
tasks.Add(new Mount.UnmountTask());
- tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
+ tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], [], null));
tasks.Add(new WaitAtEnd.WaitDelay());
_taskQueue.InterruptWith(tasks);
}
bool isLastStep = sequence.Steps.Last() == step;
return CreateTask(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
- step.CompletionQuestVariablesFlags, step.ComplexCombatData);
+ step.CompletionQuestVariablesFlags, step.ComplexCombatData, step.CombatItemUse);
}
internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
- IList<ComplexCombatData> complexCombatData)
+ IList<ComplexCombatData> complexCombatData, CombatItemUse? combatItemUse)
{
return new Task(new CombatController.CombatData
{
SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(),
+ CombatItemUse = combatItemUse,
}, completionQuestVariablesFlags, isLastStep);
}
}
serviceCollection.AddSingleton<LeveUiController>();
serviceCollection.AddSingleton<ICombatModule, Mount128Module>();
+ serviceCollection.AddSingleton<ICombatModule, ItemUseModule>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
}