punish v6.8.18.0
This commit is contained in:
commit
060278c1b7
317 changed files with 554155 additions and 0 deletions
492
Questionable/Questionable.Controller/CombatController.cs
Normal file
492
Questionable/Questionable.Controller/CombatController.cs
Normal file
|
@ -0,0 +1,492 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
|
||||
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;
|
||||
|
||||
internal sealed class CombatController : IDisposable
|
||||
{
|
||||
private sealed class CurrentFight
|
||||
{
|
||||
public required ICombatModule Module { get; init; }
|
||||
|
||||
public required CombatData Data { get; init; }
|
||||
|
||||
public required DateTime LastDistanceCheck { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CombatData
|
||||
{
|
||||
public required ElementId? ElementId { get; init; }
|
||||
|
||||
public required int Sequence { get; init; }
|
||||
|
||||
public required IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; init; }
|
||||
|
||||
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 HashSet<int>();
|
||||
}
|
||||
|
||||
public enum EStatus
|
||||
{
|
||||
NotStarted,
|
||||
InCombat,
|
||||
Moving,
|
||||
Complete
|
||||
}
|
||||
|
||||
private const float MaxTargetRange = 55f;
|
||||
|
||||
private const float MaxNameplateRange = 50f;
|
||||
|
||||
private readonly List<ICombatModule> _combatModules;
|
||||
|
||||
private readonly MovementController _movementController;
|
||||
|
||||
private readonly ITargetManager _targetManager;
|
||||
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
private readonly ICondition _condition;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
|
||||
private readonly ILogger<CombatController> _logger;
|
||||
|
||||
private CurrentFight? _currentFight;
|
||||
|
||||
private bool _wasInCombat;
|
||||
|
||||
private ulong? _lastTargetId;
|
||||
|
||||
private List<byte>? _previousQuestVariables;
|
||||
|
||||
public bool IsRunning => _currentFight != null;
|
||||
|
||||
public CombatController(IEnumerable<ICombatModule> combatModules, MovementController movementController, ITargetManager targetManager, IObjectTable objectTable, ICondition condition, IClientState clientState, QuestFunctions questFunctions, ILogger<CombatController> logger)
|
||||
{
|
||||
_combatModules = combatModules.ToList();
|
||||
_movementController = movementController;
|
||||
_targetManager = targetManager;
|
||||
_objectTable = objectTable;
|
||||
_condition = condition;
|
||||
_clientState = clientState;
|
||||
_questFunctions = questFunctions;
|
||||
_logger = logger;
|
||||
_clientState.TerritoryChanged += TerritoryChanged;
|
||||
}
|
||||
|
||||
public bool Start(CombatData combatData)
|
||||
{
|
||||
Stop("Starting combat");
|
||||
ICombatModule combatModule = _combatModules.FirstOrDefault((ICombatModule x) => x.CanHandleFight(combatData));
|
||||
if (combatModule == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (combatModule.Start(combatData))
|
||||
{
|
||||
_currentFight = new CurrentFight
|
||||
{
|
||||
Module = combatModule,
|
||||
Data = combatData,
|
||||
LastDistanceCheck = DateTime.Now
|
||||
};
|
||||
EEnemySpawnType spawnType = combatData.SpawnType;
|
||||
bool wasInCombat = (uint)(spawnType - 8) <= 1u;
|
||||
_wasInCombat = wasInCombat;
|
||||
UpdateLastTargetAndQuestVariables(null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public EStatus Update()
|
||||
{
|
||||
if (_currentFight == null)
|
||||
{
|
||||
return EStatus.Complete;
|
||||
}
|
||||
if (_movementController.IsPathfinding || _movementController.IsPathRunning || _movementController.MovementStartedAt > DateTime.Now.AddSeconds(-1.0))
|
||||
{
|
||||
return EStatus.Moving;
|
||||
}
|
||||
if (_currentFight.Data.SpawnType == EEnemySpawnType.OverworldEnemies)
|
||||
{
|
||||
if (_targetManager.Target != null)
|
||||
{
|
||||
_lastTargetId = _targetManager.Target?.GameObjectId;
|
||||
}
|
||||
else if (_lastTargetId.HasValue)
|
||||
{
|
||||
IGameObject gameObject = _objectTable.FirstOrDefault((IGameObject x) => x.GameObjectId == _lastTargetId);
|
||||
if (gameObject != null)
|
||||
{
|
||||
if (gameObject.IsDead)
|
||||
{
|
||||
ElementId elementId = _currentFight.Data.ElementId;
|
||||
QuestProgressInfo questProgressInfo = ((elementId != null) ? _questFunctions.GetQuestProgressInfo(elementId) : null);
|
||||
if (questProgressInfo != null && questProgressInfo.Sequence == _currentFight.Data.Sequence && QuestWorkUtils.HasCompletionFlags(_currentFight.Data.CompletionQuestVariablesFlags) && QuestWorkUtils.MatchesQuestWork(_currentFight.Data.CompletionQuestVariablesFlags, questProgressInfo))
|
||||
{
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
if (questProgressInfo == null || questProgressInfo.Sequence != _currentFight.Data.Sequence || _previousQuestVariables == null || questProgressInfo.Variables.SequenceEqual(_previousQuestVariables))
|
||||
{
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
UpdateLastTargetAndQuestVariables(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastTargetId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
IGameObject target = _targetManager.Target;
|
||||
if (target != null)
|
||||
{
|
||||
int item = GetKillPriority(target).Priority;
|
||||
IGameObject gameObject2 = FindNextTarget();
|
||||
int num = ((gameObject2 != null) ? GetKillPriority(gameObject2).Priority : 0);
|
||||
if (gameObject2 != null && gameObject2.Equals(target))
|
||||
{
|
||||
if (!IsMovingOrShouldMove(target))
|
||||
{
|
||||
try
|
||||
{
|
||||
_currentFight.Module.Update(target);
|
||||
}
|
||||
catch (TaskException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Combat was interrupted, stopping: {Exception}", ex.Message);
|
||||
SetTarget(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (gameObject2 != null)
|
||||
{
|
||||
if (num > item || item == 0)
|
||||
{
|
||||
SetTarget(gameObject2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetTarget(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IGameObject gameObject3 = FindNextTarget();
|
||||
if (gameObject3 != null && !gameObject3.IsDead)
|
||||
{
|
||||
SetTarget(gameObject3);
|
||||
}
|
||||
}
|
||||
if (_condition[ConditionFlag.InCombat])
|
||||
{
|
||||
_wasInCombat = true;
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
if (_wasInCombat)
|
||||
{
|
||||
return EStatus.Complete;
|
||||
}
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
|
||||
private unsafe IGameObject? FindNextTarget()
|
||||
{
|
||||
if (_currentFight == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
||||
if (complexCombatDatas.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < complexCombatDatas.Count; i++)
|
||||
{
|
||||
if (_currentFight.Data.CompletedComplexDatas.Contains(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ComplexCombatData complexCombatData = complexCombatDatas[i];
|
||||
if (complexCombatData.RewardItemId.HasValue && complexCombatData.RewardItemCount.HasValue && InventoryManager.Instance()->GetInventoryItemCount(complexCombatData.RewardItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0) >= complexCombatData.RewardItemCount.Value)
|
||||
{
|
||||
_logger.LogInformation("Complex combat condition fulfilled: itemCount({ItemId}) >= {ItemCount}", complexCombatData.RewardItemId, complexCombatData.RewardItemCount);
|
||||
_currentFight.Data.CompletedComplexDatas.Add(i);
|
||||
}
|
||||
else if (QuestWorkUtils.HasCompletionFlags(complexCombatData.CompletionQuestVariablesFlags) && _currentFight.Data.ElementId is QuestId elementId)
|
||||
{
|
||||
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(elementId);
|
||||
if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(complexCombatData.CompletionQuestVariablesFlags, questProgressInfo))
|
||||
{
|
||||
_logger.LogInformation("Complex combat condition fulfilled: QuestWork matches");
|
||||
_currentFight.Data.CompletedComplexDatas.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (from x in _objectTable
|
||||
select new
|
||||
{
|
||||
GameObject = x,
|
||||
Priority = GetKillPriority(x).Priority,
|
||||
Distance = Vector3.Distance(x.Position, _clientState.LocalPlayer.Position)
|
||||
} into x
|
||||
where x.Priority > 0
|
||||
orderby x.Priority descending, x.Distance
|
||||
select x.GameObject).FirstOrDefault();
|
||||
}
|
||||
|
||||
public unsafe (int Priority, string Reason) GetKillPriority(IGameObject gameObject)
|
||||
{
|
||||
var (num, text) = GetRawKillPriority(gameObject);
|
||||
if (!num.HasValue)
|
||||
{
|
||||
return (Priority: 0, Reason: text);
|
||||
}
|
||||
if (gameObject is IBattleNpc battleNpc && battleNpc.StatusFlags.HasFlag(StatusFlags.InCombat))
|
||||
{
|
||||
if (gameObject.TargetObjectId == _clientState.LocalPlayer?.GameObjectId)
|
||||
{
|
||||
return (Priority: num.Value + 150, Reason: text + "/Targeted");
|
||||
}
|
||||
Hater hater = UIState.Instance()->Hater;
|
||||
for (int i = 0; i < hater.HaterCount; i++)
|
||||
{
|
||||
if (hater.Haters[i].EntityId == gameObject.GameObjectId)
|
||||
{
|
||||
return (Priority: num.Value + 125, Reason: text + "/Enmity");
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Priority: num.Value, Reason: text);
|
||||
}
|
||||
|
||||
private unsafe (int? Priority, string Reason) GetRawKillPriority(IGameObject gameObject)
|
||||
{
|
||||
if (_currentFight == null)
|
||||
{
|
||||
return (Priority: null, Reason: "Not Fighting");
|
||||
}
|
||||
if (gameObject is IBattleNpc battleNpc)
|
||||
{
|
||||
if (!_currentFight.Module.CanAttack(battleNpc))
|
||||
{
|
||||
return (Priority: null, Reason: "Can't attack");
|
||||
}
|
||||
if (battleNpc.IsDead)
|
||||
{
|
||||
return (Priority: null, Reason: "Dead");
|
||||
}
|
||||
if (!battleNpc.IsTargetable)
|
||||
{
|
||||
return (Priority: null, Reason: "Untargetable");
|
||||
}
|
||||
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
||||
GameObject* address = (GameObject*)gameObject.Address;
|
||||
if (address->FateId != 0 && gameObject.TargetObjectId != _clientState.LocalPlayer?.GameObjectId)
|
||||
{
|
||||
return (Priority: null, Reason: "FATE mob");
|
||||
}
|
||||
Vector3 value = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
|
||||
bool flag = _currentFight.Data.SpawnType != EEnemySpawnType.FinishCombatIfAny && ((_currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies || !(Vector3.Distance(value, battleNpc.Position) >= 50f)) ? true : false);
|
||||
if (complexCombatDatas.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < complexCombatDatas.Count; i++)
|
||||
{
|
||||
if (!_currentFight.Data.CompletedComplexDatas.Contains(i) && (!flag || complexCombatDatas[i].IgnoreQuestMarker || address->NamePlateIconId != 0) && complexCombatDatas[i].DataId == battleNpc.DataId && (!complexCombatDatas[i].NameId.HasValue || complexCombatDatas[i].NameId == battleNpc.NameId))
|
||||
{
|
||||
return (Priority: 100, Reason: "CCD");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ((!flag || address->NamePlateIconId != 0) && _currentFight.Data.KillEnemyDataIds.Contains(battleNpc.DataId))
|
||||
{
|
||||
return (Priority: 90, Reason: "KED");
|
||||
}
|
||||
Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind battleNpcKind = battleNpc.BattleNpcKind;
|
||||
if ((battleNpcKind == Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind.BattleNpcPart || battleNpcKind == Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind.Enemy) ? true : false)
|
||||
{
|
||||
uint namePlateIconId = address->NamePlateIconId;
|
||||
if ((namePlateIconId == 60093 || namePlateIconId == 60732) ? true : false)
|
||||
{
|
||||
return (Priority: null, Reason: "FATE NPC");
|
||||
}
|
||||
return (Priority: 0, Reason: "Not part of quest");
|
||||
}
|
||||
return (Priority: null, Reason: "Wrong BattleNpcKind");
|
||||
}
|
||||
return (Priority: null, Reason: "Not BattleNpc");
|
||||
}
|
||||
|
||||
private void SetTarget(IGameObject? target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
if (_targetManager.Target != null)
|
||||
{
|
||||
_logger.LogInformation("Clearing target");
|
||||
_targetManager.Target = null;
|
||||
}
|
||||
}
|
||||
else if (Vector3.Distance(_clientState.LocalPlayer.Position, target.Position) > 55f)
|
||||
{
|
||||
_logger.LogInformation("Moving to target, distance: {Distance:N2}", Vector3.Distance(_clientState.LocalPlayer.Position, target.Position));
|
||||
MoveToTarget(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Setting target to {TargetName} ({TargetId:X8})", target.Name.ToString(), target.GameObjectId);
|
||||
_targetManager.Target = target;
|
||||
MoveToTarget(target);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsMovingOrShouldMove(IGameObject gameObject)
|
||||
{
|
||||
if (_movementController.IsPathfinding || _movementController.IsPathRunning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (DateTime.Now > _currentFight.LastDistanceCheck.AddSeconds(10.0))
|
||||
{
|
||||
MoveToTarget(gameObject);
|
||||
_currentFight.LastDistanceCheck = DateTime.Now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void MoveToTarget(IGameObject gameObject)
|
||||
{
|
||||
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float num = localPlayer.HitboxRadius + gameObject.HitboxRadius;
|
||||
float num2 = Vector3.Distance(localPlayer.Position, gameObject.Position);
|
||||
byte? b = localPlayer.ClassJob.ValueNullable?.Role;
|
||||
bool flag;
|
||||
if (b.HasValue)
|
||||
{
|
||||
byte valueOrDefault = b.GetValueOrDefault();
|
||||
if ((uint)(valueOrDefault - 3) <= 1u)
|
||||
{
|
||||
flag = true;
|
||||
goto IL_008e;
|
||||
}
|
||||
}
|
||||
flag = false;
|
||||
goto IL_008e;
|
||||
IL_008e:
|
||||
float num3 = (flag ? 20f : 2.9f);
|
||||
bool flag2 = num2 - num >= num3;
|
||||
bool flag3 = IsInLineOfSight(gameObject);
|
||||
if (flag2 || !flag3)
|
||||
{
|
||||
bool flag4 = num2 - num > 5f;
|
||||
if (!flag2 && !flag3)
|
||||
{
|
||||
num3 = Math.Min(num3, num2) / 2f;
|
||||
flag4 = true;
|
||||
}
|
||||
if (!flag4)
|
||||
{
|
||||
_logger.LogInformation("Moving to {TargetName} ({DataId}) to attack", gameObject.Name, gameObject.DataId);
|
||||
MovementController movementController = _movementController;
|
||||
int num4 = 1;
|
||||
List<Vector3> list = new List<Vector3>(num4);
|
||||
CollectionsMarshal.SetCount(list, num4);
|
||||
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
|
||||
int index = 0;
|
||||
span[index] = gameObject.Position;
|
||||
movementController.NavigateTo(EMovementType.Combat, null, list, fly: false, sprint: false, num3 + num - 0.25f, float.MaxValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Moving to {TargetName} ({DataId}) to attack (with navmesh)", gameObject.Name, gameObject.DataId);
|
||||
_movementController.NavigateTo(EMovementType.Combat, null, gameObject.Position, fly: false, sprint: false, num3 + num - 0.25f, float.MaxValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal unsafe bool IsInLineOfSight(IGameObject target)
|
||||
{
|
||||
Vector3 position = _clientState.LocalPlayer.Position;
|
||||
position.Y += 2f;
|
||||
Vector3 position2 = target.Position;
|
||||
position2.Y += 2f;
|
||||
Vector3 value = position2 - position;
|
||||
float maxDistance = value.Length();
|
||||
value = Vector3.Normalize(value);
|
||||
Vector3 vector = new Vector3(position.X, position.Y, position.Z);
|
||||
Vector3 vector2 = new Vector3(value.X, value.Y, value.Z);
|
||||
int* flags = stackalloc int[4] { 16384, 0, 16384, 0 };
|
||||
RaycastHit raycastHit = default(RaycastHit);
|
||||
return !Framework.Instance()->BGCollisionModule->RaycastMaterialFilter(&raycastHit, &vector, &vector2, maxDistance, 1, flags);
|
||||
}
|
||||
|
||||
private void UpdateLastTargetAndQuestVariables(IGameObject? target)
|
||||
{
|
||||
_lastTargetId = target?.GameObjectId;
|
||||
_previousQuestVariables = ((!(_currentFight.Data.ElementId != null)) ? null : _questFunctions.GetQuestProgressInfo(_currentFight.Data.ElementId)?.Variables);
|
||||
}
|
||||
|
||||
public void Stop(string label)
|
||||
{
|
||||
using (_logger.BeginScope(label))
|
||||
{
|
||||
if (_currentFight != null)
|
||||
{
|
||||
_logger.LogInformation("Stopping current fight");
|
||||
_currentFight.Module.Stop();
|
||||
}
|
||||
_currentFight = null;
|
||||
_wasInCombat = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TerritoryChanged(ushort territoryId)
|
||||
{
|
||||
Stop("TerritoryChanged");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_clientState.TerritoryChanged -= TerritoryChanged;
|
||||
Stop("Dispose");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue