Experimentally handle item use in 'Strange Bedfellows'
authorLiza Carvelli <liza@carvel.li>
Sat, 12 Oct 2024 20:11:54 +0000 (22:11 +0200)
committerLiza Carvelli <liza@carvel.li>
Sat, 12 Oct 2024 20:11:54 +0000 (22:11 +0200)
14 files changed:
QuestPaths/6.x - Endwalker/MSQ/B-Garlemald/4393_Strange Bedfellows.json
QuestPaths/quest-v1.json
Questionable.Model/Questing/CombatItemUse.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/CombatItemUseConditionConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/ECombatItemUseCondition.cs [new file with mode: 0644]
Questionable.Model/Questing/QuestStep.cs
Questionable/Controller/CombatController.cs
Questionable/Controller/CombatModules/ICombatModule.cs
Questionable/Controller/CombatModules/ItemUseModule.cs [new file with mode: 0644]
Questionable/Controller/CombatModules/Mount128Module.cs
Questionable/Controller/CombatModules/RotationSolverRebornModule.cs
Questionable/Controller/MiniTaskController.cs
Questionable/Controller/Steps/Interactions/Combat.cs
Questionable/QuestionablePlugin.cs

index 1ed061f7d2044dccf5d49a165fa64d27dd2ca35f..36790c213a14ef685b926e9ad0b1c7ec935347a2 100644 (file)
             "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,
index aa74871eb6a947b9bdf8e1e6ae45432c4ba80d05..50d39d05261b6c738575604419a200940b64154e 100644 (file)
                   ]
                 }
               },
+              "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"
               }
diff --git a/Questionable.Model/Questing/CombatItemUse.cs b/Questionable.Model/Questing/CombatItemUse.cs
new file mode 100644 (file)
index 0000000..bbcfd10
--- /dev/null
@@ -0,0 +1,12 @@
+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; }
+}
diff --git a/Questionable.Model/Questing/Converter/CombatItemUseConditionConverter.cs b/Questionable.Model/Questing/Converter/CombatItemUseConditionConverter.cs
new file mode 100644 (file)
index 0000000..d2bca66
--- /dev/null
@@ -0,0 +1,12 @@
+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" },
+    };
+}
diff --git a/Questionable.Model/Questing/ECombatItemUseCondition.cs b/Questionable.Model/Questing/ECombatItemUseCondition.cs
new file mode 100644 (file)
index 0000000..dab191e
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Questionable.Model.Questing;
+
+public enum ECombatItemUseCondition
+{
+    None,
+    Incapacitated,
+}
index c5626a5d64c0c2de1f69ec50a888f5078497256d..dcb8042a9c0dc76686fd14598a9c411d70747dd7 100644 (file)
@@ -67,6 +67,7 @@ public sealed class QuestStep
     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; }
index d4b8ae4e6cbc6994d05564fba5e2b7217d177f47..9cdf3bcf00e28a738d6ec4f49f56b6bca122534e 100644 (file)
@@ -65,11 +65,11 @@ internal sealed class CombatController : IDisposable
     {
         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
             {
@@ -364,6 +364,7 @@ internal sealed class CombatController : IDisposable
         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();
     }
index 542e2d6f9461e177a593626b984310b2ffb31289..06fe4ae684f2f44cf63749dc2743dc33fb53be5c 100644 (file)
@@ -4,9 +4,9 @@ namespace Questionable.Controller.CombatModules;
 
 internal interface ICombatModule
 {
-    bool IsLoaded { get; }
+    bool CanHandleFight(CombatController.CombatData combatData);
 
-    bool Start();
+    bool Start(CombatController.CombatData combatData);
 
     bool Stop();
 
diff --git a/Questionable/Controller/CombatModules/ItemUseModule.cs b/Questionable/Controller/CombatModules/ItemUseModule.cs
new file mode 100644 (file)
index 0000000..cff4316
--- /dev/null
@@ -0,0 +1,142 @@
+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);
+}
index c2f1a6f88e731416581950a9995affbfc657958d..e665163a0c3fcdd9cd2e59cef80b4d6f6429a927 100644 (file)
@@ -25,9 +25,9 @@ internal sealed class Mount128Module : ICombatModule
         _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;
 
index 72e13ae70fd78605e0e6824da75d280528471b31..a0b43b2fdf98b9a67f98136e1166a31f85192099 100644 (file)
@@ -32,23 +32,20 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
             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
         {
index 4cf0828e3b139bdd0aadeb9214f7b72f7ff0a9ed..dfa0bfc9ee0e07209959ef6ce6f6f6d275e8f8a6 100644 (file)
@@ -169,7 +169,7 @@ internal abstract class MiniTaskController<T>
             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);
         }
index 12a41d605bedd1a795de5fdaedf6a24859314a75..c670e3bca6f97cef8a076208a1954303f2de676c 100644 (file)
@@ -97,12 +97,12 @@ internal static class Combat
 
             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
             {
@@ -110,6 +110,7 @@ internal static class Combat
                 SpawnType = enemySpawnType,
                 KillEnemyDataIds = killEnemyDataIds.ToList(),
                 ComplexCombatDatas = complexCombatData.ToList(),
+                CombatItemUse = combatItemUse,
             }, completionQuestVariablesFlags, isLastStep);
         }
     }
index b9a1c54eb8dd06069cb6e8d54403b88f2b8a082d..bdb639f450c6b41ec296d3ac9bd8a257887a72d7 100644 (file)
@@ -234,6 +234,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<LeveUiController>();
 
         serviceCollection.AddSingleton<ICombatModule, Mount128Module>();
+        serviceCollection.AddSingleton<ICombatModule, ItemUseModule>();
         serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
     }