876 lines
27 KiB
C#
876 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using LLib.GameData;
|
|
using LLib.Inventory;
|
|
using LLib.Shop;
|
|
using Lumina.Excel.Sheets;
|
|
using Microsoft.Extensions.Logging;
|
|
using Questionable.Controller.CustomDelivery;
|
|
using Questionable.Controller.Steps;
|
|
using Questionable.Controller.Steps.Common;
|
|
using Questionable.Controller.Steps.Shared;
|
|
using Questionable.External;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Gathering;
|
|
using Questionable.Model.Questing;
|
|
using Questionable.Navigation;
|
|
|
|
namespace Questionable.Controller;
|
|
|
|
internal sealed class CustomDeliveryController : MiniTaskController<CustomDeliveryController>
|
|
{
|
|
public enum EDeliveryState
|
|
{
|
|
Idle,
|
|
Executing,
|
|
WaitingForCraft,
|
|
WaitingForGather,
|
|
WaitingForFish,
|
|
TurningIn,
|
|
Complete
|
|
}
|
|
|
|
private const long GatherTimeoutMs = 180000L;
|
|
|
|
private const long CraftBusyGraceMs = 5000L;
|
|
|
|
private readonly DeliveryPlannerService _plannerService;
|
|
|
|
private readonly VendorResolverService _vendorResolver;
|
|
|
|
private readonly ArtisanIpc _artisanIpc;
|
|
|
|
private readonly AutoHookIpc _autoHookIpc;
|
|
|
|
private readonly GatheringController _gatheringController;
|
|
|
|
private readonly FishingController _fishingController;
|
|
|
|
private readonly GatheringPointRegistry _gatheringPointRegistry;
|
|
|
|
private readonly MovementController _movementController;
|
|
|
|
private readonly SmartNavRouteEnqueuer _routeEnqueuer;
|
|
|
|
private readonly IClientState _clientState;
|
|
|
|
private readonly IObjectTable _objectTable;
|
|
|
|
private readonly Configuration _configuration;
|
|
|
|
private readonly Dictionary<int, (ushort TerritoryId, Vector3 Position)> _npcPositions;
|
|
|
|
private readonly Dictionary<int, uint> _npcDataIds;
|
|
|
|
private readonly Dictionary<int, string> _npcNames;
|
|
|
|
private int _npcIndex;
|
|
|
|
private EDeliverySlot _slot;
|
|
|
|
private int _craftRetryCount;
|
|
|
|
private VendorResolverService.RecipeInfo? _pendingCraftRecipe;
|
|
|
|
private VendorResolverService.RecipeInfo? _currentCraftRecipe;
|
|
|
|
private EClassJob? _pendingGatherClass;
|
|
|
|
private bool _pendingFishingStart;
|
|
|
|
private long _craftBusyGraceUntilMs;
|
|
|
|
private long _gatherStartMs;
|
|
|
|
private int _lastGatherCount;
|
|
|
|
private long _fishStartMs;
|
|
|
|
private int _lastFishCount;
|
|
|
|
private int _usedDeliveriesAtPlan;
|
|
|
|
public EDeliveryState State { get; private set; }
|
|
|
|
public DeliveryPlan? CurrentPlan { get; private set; }
|
|
|
|
public bool IsRunning
|
|
{
|
|
get
|
|
{
|
|
if (State != EDeliveryState.Idle)
|
|
{
|
|
return State != EDeliveryState.Complete;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public TaskQueue TaskQueue => _taskQueue;
|
|
|
|
public string StatusLabel => State switch
|
|
{
|
|
EDeliveryState.Executing => "Navigating",
|
|
EDeliveryState.WaitingForCraft => _taskQueue.AllTasksComplete ? "Crafting" : "Purchasing",
|
|
EDeliveryState.WaitingForGather => "Gathering",
|
|
EDeliveryState.WaitingForFish => "Fishing",
|
|
EDeliveryState.TurningIn => "Turning in",
|
|
_ => State.ToString(),
|
|
};
|
|
|
|
public string? CurrentNpcName
|
|
{
|
|
get
|
|
{
|
|
if (!IsRunning)
|
|
{
|
|
return null;
|
|
}
|
|
return _npcNames.GetValueOrDefault(_npcIndex);
|
|
}
|
|
}
|
|
|
|
public int QueueRemaining => _configuration.CustomDeliveries.DeliveryQueue.Count;
|
|
|
|
public CustomDeliveryController(DeliveryPlannerService plannerService, VendorResolverService vendorResolver, ArtisanIpc artisanIpc, AutoHookIpc autoHookIpc, GatheringController gatheringController, FishingController fishingController, GatheringPointRegistry gatheringPointRegistry, MovementController movementController, SmartNavRouteEnqueuer routeEnqueuer, IChatGui chatGui, IClientState clientState, IObjectTable objectTable, ICondition condition, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, Configuration configuration, ILogger<CustomDeliveryController> logger)
|
|
: base(chatGui, condition, serviceProvider, interruptHandler, dataManager, configuration, logger)
|
|
{
|
|
_plannerService = plannerService;
|
|
_vendorResolver = vendorResolver;
|
|
_artisanIpc = artisanIpc;
|
|
_autoHookIpc = autoHookIpc;
|
|
_gatheringController = gatheringController;
|
|
_fishingController = fishingController;
|
|
_gatheringPointRegistry = gatheringPointRegistry;
|
|
_movementController = movementController;
|
|
_routeEnqueuer = routeEnqueuer;
|
|
_clientState = clientState;
|
|
_objectTable = objectTable;
|
|
_configuration = configuration;
|
|
_npcPositions = BuildNpcPositionLookup(dataManager);
|
|
_npcDataIds = BuildNpcDataIdLookup(dataManager);
|
|
_npcNames = BuildNpcNameLookup(dataManager);
|
|
}
|
|
|
|
public bool Start(int npcIndex, EDeliverySlot slot)
|
|
{
|
|
if (State != EDeliveryState.Idle)
|
|
{
|
|
_logger.LogWarning("Cannot start delivery - already in state {State}", State);
|
|
return false;
|
|
}
|
|
if (InventoryHelper.CountFreeInventorySlots() < 1)
|
|
{
|
|
_logger.LogWarning("No free inventory slots, cannot start custom delivery");
|
|
return false;
|
|
}
|
|
DeliveryPlan deliveryPlan = _plannerService.CreatePlan(npcIndex, slot);
|
|
if (deliveryPlan == null)
|
|
{
|
|
_logger.LogInformation("No delivery plan created for NPC {NpcIndex} slot {Slot}", npcIndex, slot);
|
|
return false;
|
|
}
|
|
_npcIndex = npcIndex;
|
|
_slot = slot;
|
|
_craftRetryCount = 0;
|
|
_pendingCraftRecipe = null;
|
|
_currentCraftRecipe = null;
|
|
_pendingGatherClass = null;
|
|
_pendingFishingStart = false;
|
|
_usedDeliveriesAtPlan = SatisfactionSupplyActions.GetNpcState(npcIndex).UsedDeliveries;
|
|
CurrentPlan = deliveryPlan;
|
|
State = EDeliveryState.Executing;
|
|
_logger.LogInformation("Starting custom delivery: NPC {NpcIndex}, slot {Slot}, item {ItemId} x{Count}", npcIndex, slot, deliveryPlan.ItemId, deliveryPlan.DeliveryCount);
|
|
PopulateTaskQueue(deliveryPlan);
|
|
return State != EDeliveryState.Idle;
|
|
}
|
|
|
|
public bool StartQueue()
|
|
{
|
|
if (_configuration.CustomDeliveries.DeliveryQueue.Count == 0)
|
|
{
|
|
_logger.LogInformation("Delivery queue is empty");
|
|
return false;
|
|
}
|
|
if (State == EDeliveryState.Complete)
|
|
{
|
|
Stop("Restart queue from Complete");
|
|
}
|
|
if (State != EDeliveryState.Idle)
|
|
{
|
|
_logger.LogWarning("Cannot start delivery queue - already in state {State}", State);
|
|
return false;
|
|
}
|
|
return StartNextFromQueue();
|
|
}
|
|
|
|
private bool StartNextFromQueue()
|
|
{
|
|
List<int> deliveryQueue = _configuration.CustomDeliveries.DeliveryQueue;
|
|
if (deliveryQueue.Count > 0 && InventoryHelper.CountFreeInventorySlots() < 1)
|
|
{
|
|
_logger.LogWarning("No free inventory slots, keeping {Count} queued deliveries", deliveryQueue.Count);
|
|
return false;
|
|
}
|
|
while (deliveryQueue.Count > 0)
|
|
{
|
|
int num = deliveryQueue[0];
|
|
deliveryQueue.RemoveAt(0);
|
|
EDeliverySlot? eDeliverySlot = _plannerService.AutoSelectSlot(num, _artisanIpc, _autoHookIpc);
|
|
if (!eDeliverySlot.HasValue)
|
|
{
|
|
_logger.LogInformation("No viable slot for NPC {NpcIndex}, skipping", num);
|
|
continue;
|
|
}
|
|
_logger.LogInformation("Queue: starting NPC {NpcIndex} with slot {Slot}", num, eDeliverySlot.Value);
|
|
if (!Start(num, eDeliverySlot.Value))
|
|
{
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
_logger.LogInformation("Delivery queue exhausted");
|
|
return false;
|
|
}
|
|
|
|
private void PopulateTaskQueue(DeliveryPlan plan)
|
|
{
|
|
_taskQueue.Reset();
|
|
_pendingGatherClass = null;
|
|
_pendingFishingStart = false;
|
|
switch (plan.Slot)
|
|
{
|
|
case EDeliverySlot.Craft:
|
|
{
|
|
if (HasEnoughItems(plan))
|
|
{
|
|
State = EDeliveryState.TurningIn;
|
|
EnqueueTurnInSequence(plan);
|
|
break;
|
|
}
|
|
Dictionary<(uint, uint), (ushort, Vector3, int, List<VendorPurchaseTask.PurchaseItem>)> dictionary = new Dictionary<(uint, uint), (ushort, Vector3, int, List<VendorPurchaseTask.PurchaseItem>)>();
|
|
foreach (IngredientPlan ingredientPlan in plan.IngredientPlans)
|
|
{
|
|
if (ingredientPlan.ToBuy > 0 && ingredientPlan.Vendor.HasValue)
|
|
{
|
|
VendorResolverService.VendorInfo value = ingredientPlan.Vendor.Value;
|
|
(uint, uint) key = (value.VendorNpcId, value.ShopId);
|
|
dictionary.TryAdd(key, (value.TerritoryId, value.Position, value.DialogOptionIndex, new List<VendorPurchaseTask.PurchaseItem>()));
|
|
dictionary[key].Item4.Add(new VendorPurchaseTask.PurchaseItem(ingredientPlan.ItemId, ingredientPlan.ToBuy));
|
|
}
|
|
}
|
|
foreach (var (tuple3, tuple4) in dictionary)
|
|
{
|
|
var (vendorNpcId, _) = tuple3;
|
|
var (territoryId, position, dialogOptionIndex, purchases) = tuple4;
|
|
EnqueueNavigateTo(territoryId, position);
|
|
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
|
|
_taskQueue.Enqueue(new VendorPurchaseTask(vendorNpcId, purchases, dialogOptionIndex));
|
|
}
|
|
(EClassJob Job, VendorResolverService.RecipeInfo? Recipe) tuple7 = ResolveCrafterClass(plan.ItemId);
|
|
EClassJob item = tuple7.Job;
|
|
VendorResolverService.RecipeInfo? recipeInfo = tuple7.Recipe ?? plan.Recipe;
|
|
EClassJob classJob = ((item != EClassJob.Adventurer) ? item : (recipeInfo?.CraftingClass ?? EClassJob.Carpenter));
|
|
_taskQueue.Enqueue(new SwitchClassJob.Task(classJob));
|
|
_pendingCraftRecipe = recipeInfo;
|
|
State = EDeliveryState.WaitingForCraft;
|
|
break;
|
|
}
|
|
case EDeliverySlot.Gather:
|
|
{
|
|
if (HasEnoughItems(plan))
|
|
{
|
|
State = EDeliveryState.TurningIn;
|
|
EnqueueTurnInSequence(plan);
|
|
break;
|
|
}
|
|
EClassJob eClassJob = ResolveGathererClass(plan.ItemId);
|
|
_taskQueue.Enqueue(new SwitchClassJob.Task(eClassJob));
|
|
if (EnqueueGatheringTravel(plan, eClassJob))
|
|
{
|
|
_pendingGatherClass = eClassJob;
|
|
State = EDeliveryState.WaitingForGather;
|
|
}
|
|
break;
|
|
}
|
|
case EDeliverySlot.Fish:
|
|
if (HasEnoughItems(plan))
|
|
{
|
|
State = EDeliveryState.TurningIn;
|
|
EnqueueTurnInSequence(plan);
|
|
}
|
|
else
|
|
{
|
|
_taskQueue.Enqueue(new SwitchClassJob.Task(EClassJob.Fisher));
|
|
_pendingFishingStart = true;
|
|
State = EDeliveryState.WaitingForFish;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private (EClassJob Job, VendorResolverService.RecipeInfo? Recipe) ResolveCrafterClass(uint craftedItemId)
|
|
{
|
|
Configuration.CustomDeliveryConfiguration customDeliveries = _configuration.CustomDeliveries;
|
|
if (customDeliveries.CraftJobMode == Configuration.CustomDeliveryConfiguration.EJobSelectionMode.Current)
|
|
{
|
|
uint? num = _objectTable.LocalPlayer?.ClassJob.RowId;
|
|
if (num.HasValue)
|
|
{
|
|
EClassJob value = (EClassJob)num.Value;
|
|
if (value.IsCrafter())
|
|
{
|
|
VendorResolverService.RecipeInfo? item = _vendorResolver.ResolveRecipe(craftedItemId, value);
|
|
if (item.HasValue)
|
|
{
|
|
return (Job: item.Value.CraftingClass, Recipe: item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VendorResolverService.RecipeInfo? item2 = _vendorResolver.ResolveRecipe(craftedItemId, customDeliveries.PreferredCraftJob);
|
|
if (item2.HasValue)
|
|
{
|
|
return (Job: item2.Value.CraftingClass, Recipe: item2);
|
|
}
|
|
VendorResolverService.RecipeInfo? item3 = _vendorResolver.ResolveRecipe(craftedItemId);
|
|
if (!item3.HasValue)
|
|
{
|
|
return (Job: EClassJob.Adventurer, Recipe: null);
|
|
}
|
|
return (Job: item3.Value.CraftingClass, Recipe: item3);
|
|
}
|
|
|
|
private EClassJob ResolveGathererClass(uint itemId)
|
|
{
|
|
Configuration.CustomDeliveryConfiguration customDeliveries = _configuration.CustomDeliveries;
|
|
if (customDeliveries.GatherJobMode == Configuration.CustomDeliveryConfiguration.EJobSelectionMode.Current)
|
|
{
|
|
uint? num = _objectTable.LocalPlayer?.ClassJob.RowId;
|
|
if (num.HasValue)
|
|
{
|
|
EClassJob value = (EClassJob)num.Value;
|
|
switch (value)
|
|
{
|
|
case EClassJob.Miner:
|
|
case EClassJob.Botanist:
|
|
if (CanGather(value))
|
|
{
|
|
return value;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (CanGather(customDeliveries.PreferredGatherJob))
|
|
{
|
|
return customDeliveries.PreferredGatherJob;
|
|
}
|
|
EClassJob eClassJob = ((customDeliveries.PreferredGatherJob == EClassJob.Miner) ? EClassJob.Botanist : EClassJob.Miner);
|
|
if (CanGather(eClassJob))
|
|
{
|
|
return eClassJob;
|
|
}
|
|
return EClassJob.Miner;
|
|
bool CanGather(EClassJob job)
|
|
{
|
|
GatheringPointId gatheringPointId;
|
|
return _gatheringPointRegistry.TryGetGatheringPointId(itemId, job, out gatheringPointId);
|
|
}
|
|
}
|
|
|
|
private bool EnqueueGatheringTravel(DeliveryPlan plan, EClassJob gathererClass)
|
|
{
|
|
if (!_gatheringPointRegistry.TryGetGatheringPointId(plan.ItemId, gathererClass, out GatheringPointId gatheringPointId) || !_gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot gatheringRoot))
|
|
{
|
|
_logger.LogError("No gathering path found for item {ItemId} as {ClassJob}", plan.ItemId, gathererClass);
|
|
Stop("No gathering path");
|
|
return false;
|
|
}
|
|
int num;
|
|
if (gatheringRoot.Steps.Count <= 0)
|
|
{
|
|
num = 0;
|
|
}
|
|
else
|
|
{
|
|
List<QuestStep> steps = gatheringRoot.Steps;
|
|
num = steps[steps.Count - 1].TerritoryId;
|
|
}
|
|
ushort num2 = (ushort)num;
|
|
GatheringLocation gatheringLocation = null;
|
|
int num3 = int.MaxValue;
|
|
foreach (GatheringNodeGroup group in gatheringRoot.Groups)
|
|
{
|
|
foreach (GatheringNode node in group.Nodes)
|
|
{
|
|
if (node.Locations.Count != 0 && node.Locations.Count < num3)
|
|
{
|
|
num3 = node.Locations.Count;
|
|
gatheringLocation = node.Locations[0];
|
|
}
|
|
}
|
|
}
|
|
Vector3? vector = ((gatheringLocation != null) ? new Vector3?(GatheringMath.CalculateLandingLocation(gatheringLocation).Item1) : ((Vector3?)null));
|
|
if (!vector.HasValue)
|
|
{
|
|
foreach (QuestStep step in gatheringRoot.Steps)
|
|
{
|
|
if (step.Position.HasValue)
|
|
{
|
|
vector = step.Position;
|
|
}
|
|
}
|
|
}
|
|
if (num2 == 0 || !vector.HasValue)
|
|
{
|
|
_logger.LogError("Gathering path {PointId} has no travel destination", gatheringPointId);
|
|
Stop("Invalid gathering path");
|
|
return false;
|
|
}
|
|
_logger.LogDebug("Navigating to gathering point {PointId} in territory {TerritoryId}", gatheringPointId, num2);
|
|
if (_clientState.TerritoryType != num2)
|
|
{
|
|
if (!_routeEnqueuer.TryEnqueueToTerritory(_taskQueue, num2, vector.Value))
|
|
{
|
|
_logger.LogError("No SmartNav route from territory {CurrentTerritoryId} to gathering territory {TerritoryId}", _clientState.TerritoryType, num2);
|
|
Stop("No route to gathering point");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("Already in gathering territory {TerritoryId}; leaving node movement to GatheringController", num2);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void StartCrafting(DeliveryPlan plan, VendorResolverService.RecipeInfo? recipeOverride = null)
|
|
{
|
|
VendorResolverService.RecipeInfo? recipeInfo = recipeOverride ?? plan.Recipe;
|
|
if (!recipeInfo.HasValue)
|
|
{
|
|
_logger.LogError("No recipe available for craft delivery of item {ItemId}", plan.ItemId);
|
|
Stop("No recipe");
|
|
return;
|
|
}
|
|
if (!_artisanIpc.IsAvailable())
|
|
{
|
|
_logger.LogError("Artisan is not available, cannot craft");
|
|
Stop("Artisan not available");
|
|
return;
|
|
}
|
|
_artisanIpc.SetEnduranceStatus(enabled: false);
|
|
_artisanIpc.SetStopRequest(stop: false);
|
|
int itemCount = GetItemCount(plan);
|
|
int num = plan.DeliveryCount - itemCount;
|
|
if (num > 0)
|
|
{
|
|
_logger.LogDebug("Requesting Artisan to craft {Count}x recipe {RecipeId} ({ClassJob})", num, recipeInfo.Value.RecipeId, recipeInfo.Value.CraftingClass);
|
|
if (!_artisanIpc.CraftItem((ushort)recipeInfo.Value.RecipeId, num))
|
|
{
|
|
_logger.LogError("Failed to start Artisan crafting for recipe {RecipeId}", recipeInfo.Value.RecipeId);
|
|
Stop("Artisan craft failed");
|
|
}
|
|
else
|
|
{
|
|
_craftBusyGraceUntilMs = Environment.TickCount64 + 5000;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StartGathering(DeliveryPlan plan, EClassJob gathererClass)
|
|
{
|
|
_gatherStartMs = Environment.TickCount64;
|
|
_lastGatherCount = GetItemCount(plan);
|
|
if (!_gatheringPointRegistry.TryGetGatheringPointId(plan.ItemId, gathererClass, out GatheringPointId gatheringPointId))
|
|
{
|
|
_logger.LogError("No gathering point found for item {ItemId} as {ClassJob}", plan.ItemId, gathererClass);
|
|
Stop("No gathering point");
|
|
return;
|
|
}
|
|
GatheringController.GatheringRequest gatheringRequest = new GatheringController.GatheringRequest(gatheringPointId, plan.ItemId, 0u, plan.DeliveryCount, plan.Collectability);
|
|
if (!_gatheringController.Start(gatheringRequest))
|
|
{
|
|
_logger.LogWarning("GatheringController.Start returned false for item {ItemId}", plan.ItemId);
|
|
if (HasEnoughItems(plan))
|
|
{
|
|
State = EDeliveryState.Executing;
|
|
EnqueueTurnInSequence(plan);
|
|
}
|
|
else
|
|
{
|
|
Stop("Gathering start failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StartFishing(DeliveryPlan plan)
|
|
{
|
|
_fishStartMs = Environment.TickCount64;
|
|
_lastFishCount = GetItemCount(plan);
|
|
FishingController.FishingRequest request = new FishingController.FishingRequest(plan.ItemId, plan.DeliveryCount, plan.Collectability);
|
|
if (!_fishingController.Start(request))
|
|
{
|
|
_logger.LogWarning("FishingController.Start returned false for item {ItemId}", plan.ItemId);
|
|
if (HasEnoughItems(plan))
|
|
{
|
|
State = EDeliveryState.Executing;
|
|
EnqueueTurnInSequence(plan);
|
|
}
|
|
else
|
|
{
|
|
Stop("Fishing start failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
public EDeliveryState Update()
|
|
{
|
|
if (State == EDeliveryState.Idle || State == EDeliveryState.Complete)
|
|
{
|
|
return State;
|
|
}
|
|
if (CurrentPlan == null)
|
|
{
|
|
Stop("No plan");
|
|
return EDeliveryState.Idle;
|
|
}
|
|
switch (State)
|
|
{
|
|
case EDeliveryState.WaitingForCraft:
|
|
return UpdateWaitingForCraft();
|
|
case EDeliveryState.WaitingForGather:
|
|
return UpdateWaitingForGather();
|
|
case EDeliveryState.WaitingForFish:
|
|
return UpdateWaitingForFish();
|
|
case EDeliveryState.Executing:
|
|
case EDeliveryState.TurningIn:
|
|
return UpdateExecuting();
|
|
default:
|
|
return State;
|
|
}
|
|
}
|
|
|
|
private EDeliveryState UpdateWaitingForCraft()
|
|
{
|
|
if (!_taskQueue.AllTasksComplete)
|
|
{
|
|
UpdateCurrentTask();
|
|
return State;
|
|
}
|
|
if (_pendingCraftRecipe.HasValue)
|
|
{
|
|
_currentCraftRecipe = _pendingCraftRecipe;
|
|
_pendingCraftRecipe = null;
|
|
StartCrafting(CurrentPlan, _currentCraftRecipe);
|
|
return State;
|
|
}
|
|
if (_artisanIpc.IsCrafting())
|
|
{
|
|
return State;
|
|
}
|
|
if (Environment.TickCount64 < _craftBusyGraceUntilMs)
|
|
{
|
|
return State;
|
|
}
|
|
if (_artisanIpc.GetStopRequest())
|
|
{
|
|
_logger.LogWarning("Artisan reported a stop request, checking items");
|
|
_artisanIpc.SetStopRequest(stop: false);
|
|
}
|
|
if (HasEnoughItems(CurrentPlan))
|
|
{
|
|
_logger.LogDebug("Crafting complete, transitioning to turn-in");
|
|
State = EDeliveryState.Executing;
|
|
EnqueueTurnInSequence(CurrentPlan);
|
|
}
|
|
else if (_craftRetryCount < 3)
|
|
{
|
|
_craftRetryCount++;
|
|
_logger.LogWarning("Crafting finished but insufficient items (have {Have}, need {Need}), retrying", GetItemCount(CurrentPlan), CurrentPlan.DeliveryCount);
|
|
StartCrafting(CurrentPlan, _currentCraftRecipe);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("Crafting finished but insufficient items after retry (have {Have}, need {Need})", GetItemCount(CurrentPlan), CurrentPlan.DeliveryCount);
|
|
Stop("Insufficient items after crafting");
|
|
}
|
|
return State;
|
|
}
|
|
|
|
private EDeliveryState UpdateWaitingForGather()
|
|
{
|
|
if (!_taskQueue.AllTasksComplete)
|
|
{
|
|
UpdateCurrentTask();
|
|
return State;
|
|
}
|
|
EClassJob? pendingGatherClass = _pendingGatherClass;
|
|
if (pendingGatherClass.HasValue)
|
|
{
|
|
EClassJob valueOrDefault = pendingGatherClass.GetValueOrDefault();
|
|
_pendingGatherClass = null;
|
|
StartGathering(CurrentPlan, valueOrDefault);
|
|
return State;
|
|
}
|
|
int itemCount = GetItemCount(CurrentPlan);
|
|
if (itemCount > _lastGatherCount)
|
|
{
|
|
_lastGatherCount = itemCount;
|
|
_gatherStartMs = Environment.TickCount64;
|
|
}
|
|
if (Environment.TickCount64 - _gatherStartMs > 180000)
|
|
{
|
|
_logger.LogWarning("Gathering made no progress for {Timeout}ms", 180000L);
|
|
_gatheringController.Stop("Delivery gather timeout");
|
|
Stop("Gathering timeout");
|
|
return State;
|
|
}
|
|
GatheringController.EStatus eStatus = _gatheringController.Update();
|
|
if (HasEnoughItems(CurrentPlan))
|
|
{
|
|
if (eStatus != GatheringController.EStatus.Complete)
|
|
{
|
|
return State;
|
|
}
|
|
_logger.LogDebug("Gathering complete, transitioning to turn-in");
|
|
State = EDeliveryState.Executing;
|
|
EnqueueTurnInSequence(CurrentPlan);
|
|
}
|
|
else if (eStatus == GatheringController.EStatus.Complete)
|
|
{
|
|
_logger.LogWarning("Gathering ended without enough items (have {Have}, need {Need})", itemCount, CurrentPlan.DeliveryCount);
|
|
Stop("Gathering ended without items");
|
|
}
|
|
return State;
|
|
}
|
|
|
|
private EDeliveryState UpdateWaitingForFish()
|
|
{
|
|
if (!_taskQueue.AllTasksComplete)
|
|
{
|
|
UpdateCurrentTask();
|
|
return State;
|
|
}
|
|
if (_pendingFishingStart)
|
|
{
|
|
_pendingFishingStart = false;
|
|
StartFishing(CurrentPlan);
|
|
return State;
|
|
}
|
|
int itemCount = GetItemCount(CurrentPlan);
|
|
if (itemCount > _lastFishCount)
|
|
{
|
|
_lastFishCount = itemCount;
|
|
_fishStartMs = Environment.TickCount64;
|
|
}
|
|
if (Environment.TickCount64 - _fishStartMs > 180000)
|
|
{
|
|
_logger.LogWarning("Fishing made no progress for {Timeout}ms", 180000L);
|
|
_fishingController.Stop("Delivery fishing timeout");
|
|
Stop("Fishing timeout");
|
|
return State;
|
|
}
|
|
FishingController.EFishingState state = _fishingController.State;
|
|
if (HasEnoughItems(CurrentPlan))
|
|
{
|
|
_logger.LogDebug("Fishing complete, transitioning to turn-in");
|
|
_fishingController.Stop("Delivery fishing complete");
|
|
State = EDeliveryState.Executing;
|
|
EnqueueTurnInSequence(CurrentPlan);
|
|
}
|
|
else if ((state == FishingController.EFishingState.Idle || state == FishingController.EFishingState.Complete) ? true : false)
|
|
{
|
|
_logger.LogWarning("Fishing ended without enough items (have {Have}, need {Need})", itemCount, CurrentPlan.DeliveryCount);
|
|
Stop("Fishing ended without items");
|
|
}
|
|
return State;
|
|
}
|
|
|
|
private EDeliveryState UpdateExecuting()
|
|
{
|
|
if (_taskQueue.AllTasksComplete)
|
|
{
|
|
OnAllTasksComplete();
|
|
return State;
|
|
}
|
|
UpdateCurrentTask();
|
|
return State;
|
|
}
|
|
|
|
private void OnAllTasksComplete()
|
|
{
|
|
if (CurrentPlan == null)
|
|
{
|
|
State = EDeliveryState.Complete;
|
|
return;
|
|
}
|
|
int usedDeliveries = SatisfactionSupplyActions.GetNpcState(CurrentPlan.NpcIndex).UsedDeliveries;
|
|
if (SatisfactionSupplyActions.GetRemainingAllowances() > 0)
|
|
{
|
|
if (usedDeliveries == _usedDeliveriesAtPlan)
|
|
{
|
|
_logger.LogWarning("Delivery batch completed without progress (used still {Used}), not re-planning", usedDeliveries);
|
|
}
|
|
else
|
|
{
|
|
DeliveryPlan deliveryPlan = _plannerService.CreatePlan(_npcIndex, _slot);
|
|
if (deliveryPlan != null)
|
|
{
|
|
_logger.LogDebug("Allowances remain (used: {Used}), re-planning", usedDeliveries);
|
|
_usedDeliveriesAtPlan = usedDeliveries;
|
|
CurrentPlan = deliveryPlan;
|
|
PopulateTaskQueue(deliveryPlan);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
_logger.LogInformation("NPC deliveries complete");
|
|
if (_configuration.CustomDeliveries.DeliveryQueue.Count > 0 && SatisfactionSupplyActions.GetRemainingAllowances() > 0)
|
|
{
|
|
State = EDeliveryState.Idle;
|
|
if (StartNextFromQueue())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
State = EDeliveryState.Complete;
|
|
}
|
|
|
|
protected override void OnTaskComplete(ITask task)
|
|
{
|
|
if (task is SatisfactionSupplyTurnInTask)
|
|
{
|
|
ResetStepRetryCounter();
|
|
}
|
|
}
|
|
|
|
protected override void OnRetryStep()
|
|
{
|
|
DeliveryPlan deliveryPlan = _plannerService.CreatePlan(_npcIndex, _slot);
|
|
if (deliveryPlan == null)
|
|
{
|
|
Stop("Re-plan on retry failed");
|
|
return;
|
|
}
|
|
_logger.LogInformation("Retrying delivery step with a fresh plan");
|
|
CurrentPlan = deliveryPlan;
|
|
PopulateTaskQueue(deliveryPlan);
|
|
}
|
|
|
|
public override void Stop(string label)
|
|
{
|
|
base.Stop(label);
|
|
if (State != EDeliveryState.Idle || CurrentPlan != null)
|
|
{
|
|
_logger.LogInformation("Stopping custom delivery controller: {Label}", label);
|
|
if (_artisanIpc.IsCrafting())
|
|
{
|
|
_artisanIpc.SetStopRequest(stop: true);
|
|
}
|
|
_fishingController.Stop("Delivery stop");
|
|
_gatheringController.Stop("Delivery stop");
|
|
_routeEnqueuer.ClearDestination();
|
|
_taskQueue.Reset();
|
|
CurrentPlan = null;
|
|
State = EDeliveryState.Idle;
|
|
}
|
|
}
|
|
|
|
private void EnqueueNavigateToNpc(int npcIndex)
|
|
{
|
|
if (!_npcPositions.TryGetValue(npcIndex, out (ushort, Vector3) value))
|
|
{
|
|
_logger.LogWarning("No position found for delivery NPC index {NpcIndex}, skipping navigation", npcIndex);
|
|
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
|
|
return;
|
|
}
|
|
uint valueOrDefault = _npcDataIds.GetValueOrDefault(npcIndex);
|
|
_routeEnqueuer.SetDestination(value.Item1, value.Item2);
|
|
if (_clientState.TerritoryType != value.Item1 && !_routeEnqueuer.TryEnqueueToTerritory(_taskQueue, value.Item1, value.Item2))
|
|
{
|
|
_logger.LogWarning("Could not route to delivery NPC territory {TerritoryId}; falling back to normal routing", value.Item1);
|
|
EnqueueNavigateTo(value.Item1, value.Item2);
|
|
}
|
|
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
|
|
_taskQueue.Enqueue(new DeliveryNpcApproachTask(value.Item1, value.Item2, valueOrDefault));
|
|
}
|
|
|
|
private void EnqueueNavigateTo(ushort territoryId, Vector3 position)
|
|
{
|
|
_routeEnqueuer.Enqueue(_taskQueue, territoryId, position);
|
|
}
|
|
|
|
private void EnqueueTurnInSequence(DeliveryPlan plan)
|
|
{
|
|
EnqueueNavigateToNpc(plan.NpcIndex);
|
|
if (!_npcDataIds.ContainsKey(plan.NpcIndex))
|
|
{
|
|
_logger.LogWarning("No DataId for delivery NPC index {NpcIndex}, turn-in may fail", plan.NpcIndex);
|
|
}
|
|
_taskQueue.Enqueue(new SatisfactionSupplyTurnInTask(plan.NpcIndex, plan.Slot, plan.DeliveryCount, _npcDataIds.GetValueOrDefault(plan.NpcIndex), plan.ItemId, plan.Collectability));
|
|
_taskQueue.Enqueue(new WaitAtEnd.WaitDelay());
|
|
}
|
|
|
|
private static Dictionary<int, (ushort TerritoryId, Vector3 Position)> BuildNpcPositionLookup(IDataManager dataManager)
|
|
{
|
|
Dictionary<int, (ushort, Vector3)> dictionary = new Dictionary<int, (ushort, Vector3)>();
|
|
Dictionary<uint, int> dictionary2 = new Dictionary<uint, int>();
|
|
foreach (SatisfactionNpc item in dataManager.GetExcelSheet<SatisfactionNpc>())
|
|
{
|
|
if (item.RowId != 0 && item.Npc.RowId != 0)
|
|
{
|
|
int value = (int)(item.RowId - 1);
|
|
dictionary2[item.Npc.RowId] = value;
|
|
}
|
|
}
|
|
foreach (Level item2 in dataManager.GetExcelSheet<Level>())
|
|
{
|
|
if (item2.RowId != 0 && item2.Territory.IsValid && dictionary2.TryGetValue(item2.Object.RowId, out var value2))
|
|
{
|
|
dictionary.TryAdd(value2, ((ushort)item2.Territory.RowId, new Vector3(item2.X, item2.Y, item2.Z)));
|
|
}
|
|
}
|
|
return dictionary;
|
|
}
|
|
|
|
private static Dictionary<int, uint> BuildNpcDataIdLookup(IDataManager dataManager)
|
|
{
|
|
Dictionary<int, uint> dictionary = new Dictionary<int, uint>();
|
|
foreach (SatisfactionNpc item in dataManager.GetExcelSheet<SatisfactionNpc>())
|
|
{
|
|
if (item.RowId != 0 && item.Npc.RowId != 0)
|
|
{
|
|
dictionary[(int)(item.RowId - 1)] = item.Npc.RowId;
|
|
}
|
|
}
|
|
return dictionary;
|
|
}
|
|
|
|
private static Dictionary<int, string> BuildNpcNameLookup(IDataManager dataManager)
|
|
{
|
|
Dictionary<int, string> dictionary = new Dictionary<int, string>();
|
|
foreach (SatisfactionNpc item in dataManager.GetExcelSheet<SatisfactionNpc>())
|
|
{
|
|
if (item.RowId != 0 && item.Npc.RowId != 0)
|
|
{
|
|
dictionary[(int)(item.RowId - 1)] = item.Npc.Value.Singular.ToString();
|
|
}
|
|
}
|
|
return dictionary;
|
|
}
|
|
|
|
private bool HasEnoughItems(DeliveryPlan plan)
|
|
{
|
|
return GetItemCount(plan) >= plan.DeliveryCount;
|
|
}
|
|
|
|
private unsafe int GetItemCount(DeliveryPlan plan)
|
|
{
|
|
return InventoryManager.Instance()->GetInventoryItemCount(plan.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)plan.Collectability);
|
|
}
|
|
}
|