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 FFXIVClientStructs.FFXIV.Client.UI.Agent; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Model.Questing; namespace Questionable.Controller.CombatModules; internal sealed class ItemUseModule : ICombatModule { private readonly IServiceProvider _serviceProvider; private readonly ICondition _condition; private readonly ILogger _logger; private ICombatModule? _delegate; private CombatController.CombatData? _combatData; private bool _isDoingRotation; private long _continueAt; private bool _isWaitingToUseItem; private bool _itemUsePending; private long _itemUsePendingUntil; private int _itemCountBeforeUse; public ICombatModule? Delegate => _delegate; public bool IsItemUseInProgress { get { if (_isWaitingToUseItem || _itemUsePending) { return Environment.TickCount64 < _itemUsePendingUntil; } return false; } } public ItemUseModule(IServiceProvider serviceProvider, ICondition condition, ILogger logger) { _serviceProvider = serviceProvider; _condition = condition; _logger = logger; } public bool CanHandleFight(CombatController.CombatData combatData) { if (combatData.CombatItemUse == null) { return false; } _delegate = (from x in _serviceProvider.GetRequiredService>() where !(x is ItemUseModule) select x).FirstOrDefault((ICombatModule x) => x.CanHandleFight(combatData)); _logger.LogDebug("ItemUse delegate: {Delegate}", _delegate?.GetType().Name); return _delegate != null; } public bool Start(CombatController.CombatData combatData) { if (_delegate.Start(combatData)) { _combatData = combatData; _isDoingRotation = true; _continueAt = Environment.TickCount64; _isWaitingToUseItem = false; _itemUsePending = false; return true; } return false; } public bool Stop() { if (_isDoingRotation) { _delegate.Stop(); } _isDoingRotation = false; _isWaitingToUseItem = false; _itemUsePending = false; _combatData = null; _delegate = null; _continueAt = Environment.TickCount64; return true; } public unsafe void Update(IGameObject nextTarget) { if (_delegate == null || _continueAt > Environment.TickCount64) { return; } if (_combatData?.CombatItemUse == null) { _delegate.Update(nextTarget); } else if (_combatData.KillEnemyDataIds.Contains(nextTarget.BaseId) || _combatData.ComplexCombatDatas.Any((ComplexCombatData x) => x.DataId == nextTarget.BaseId && (!x.NameId.HasValue || (nextTarget is ICharacter character && x.NameId == character.NameId)))) { int inventoryItemCount = InventoryManager.Instance()->GetInventoryItemCount(_combatData.CombatItemUse.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0); if (_itemUsePending) { if (inventoryItemCount < _itemCountBeforeUse) { _itemUsePending = false; return; } if (_condition[ConditionFlag.Casting]) { _itemUsePendingUntil = Environment.TickCount64 + 1000; } if (Environment.TickCount64 < _itemUsePendingUntil) { _logger.LogDebug("Item use pending; inventory count={Count}", inventoryItemCount); return; } _logger.LogDebug("Item use was not observed; retrying"); _itemUsePending = false; _isWaitingToUseItem = true; _itemUsePendingUntil = Environment.TickCount64 + 4000; _continueAt = Environment.TickCount64 + 1000; } else if (_isDoingRotation) { if (inventoryItemCount == 0) { _delegate.Update(nextTarget); } else if (ShouldUseItem(nextTarget)) { _logger.LogDebug("Pausing rotation before using item {ItemId}", _combatData.CombatItemUse.ItemId); _isDoingRotation = false; _isWaitingToUseItem = true; _delegate.Stop(); _continueAt = Environment.TickCount64 + 3000; _itemUsePendingUntil = Environment.TickCount64 + 4000; } else { _delegate.Update(nextTarget); } } else if (_isWaitingToUseItem) { if (_condition[ConditionFlag.Casting]) { _continueAt = Environment.TickCount64 + 500; _itemUsePendingUntil = Environment.TickCount64 + 4000; return; } if (!ShouldUseItem(nextTarget) || inventoryItemCount == 0) { _isWaitingToUseItem = false; return; } _logger.LogDebug("Attempting to use item {ItemId}", _combatData.CombatItemUse.ItemId); long num = AgentInventoryContext.Instance()->UseItem(_combatData.CombatItemUse.ItemId, InventoryType.Invalid, 0u, 0); _logger.LogDebug("UseItem result: {Result}", num); bool itemUsePending = (((ulong)num <= 1uL) ? true : false); _itemUsePending = itemUsePending; _isWaitingToUseItem = !_itemUsePending; _itemCountBeforeUse = inventoryItemCount; _itemUsePendingUntil = Environment.TickCount64 + 3000; _continueAt = Environment.TickCount64 + (_itemUsePending ? 2000 : 500); } else if (_condition[ConditionFlag.Casting]) { long num2 = Environment.TickCount64 + 500; if (num2 > _continueAt) { _continueAt = num2; } } 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* address = (BattleChara*)gameObject.Address; if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.Incapacitated) { return (address->ActorControlFlags & 0x40) != 0; } if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.HealthPercent) { return 100f * (float)address->Health / (float)address->MaxHealth < (float)_combatData.CombatItemUse.Value; } if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.MissingStatus) { return !address->StatusManager.HasStatus((uint)_combatData.CombatItemUse.Value); } } return false; } public bool CanAttack(IBattleNpc target) { return _delegate.CanAttack(target); } }