649 lines
23 KiB
C#
649 lines
23 KiB
C#
using AcDream.Plugin.Abstractions;
|
|
|
|
namespace AcDream.Plugins.MossTank;
|
|
|
|
internal readonly record struct CraftingPlan(
|
|
VtankCraftRecipe Recipe,
|
|
uint FirstObjectId,
|
|
uint SecondObjectId,
|
|
string DesiredResult)
|
|
{
|
|
public bool RequiresSplitFirstStack { get; init; }
|
|
public uint SplitContainerObjectId { get; init; }
|
|
}
|
|
|
|
internal static class ConsumableClassifier
|
|
{
|
|
private const uint HealingKitPublicFlag = 0x00010000u;
|
|
private const uint LockpickPublicFlag = 0x00020000u;
|
|
|
|
public static ConsumableCategory Classify(in PluginInventoryItem item)
|
|
{
|
|
if (item.Name.Equals(CraftingPlanner.AllPeas, StringComparison.Ordinal))
|
|
return ConsumableCategory.AllPeas;
|
|
if (item.Name.EndsWith(" Pea", StringComparison.Ordinal))
|
|
return ConsumableCategory.Pea;
|
|
if ((item.PublicFlags & LockpickPublicFlag) != 0u)
|
|
return ConsumableCategory.Lockpick;
|
|
if ((item.PublicFlags & HealingKitPublicFlag) != 0u)
|
|
return KitCategory(item.Name);
|
|
return item.BoosterVital switch
|
|
{
|
|
2 => ConsumableCategory.HealthFood,
|
|
4 => ConsumableCategory.StaminaFood,
|
|
6 => ConsumableCategory.ManaFood,
|
|
_ => ClassifyName(item.Name),
|
|
};
|
|
}
|
|
|
|
public static ConsumableCategory ClassifyName(string name)
|
|
{
|
|
if (name.Equals(CraftingPlanner.AllPeas, StringComparison.Ordinal))
|
|
return ConsumableCategory.AllPeas;
|
|
if (name.EndsWith(" Pea", StringComparison.Ordinal))
|
|
return ConsumableCategory.Pea;
|
|
return name.EndsWith(" Kit", StringComparison.Ordinal)
|
|
? KitCategory(name)
|
|
: ConsumableCategory.Other;
|
|
}
|
|
|
|
private static ConsumableCategory KitCategory(string name) => name switch
|
|
{
|
|
"Medicated Stamina Kit" or "Eternal Stamina Kit"
|
|
or "Greater Stamina Kit" or "Lesser Stamina Kit" =>
|
|
ConsumableCategory.StaminaKit,
|
|
"Medicated Mana Kit" or "Eternal Mana Kit"
|
|
or "Greater Mana Kit" or "Lesser Mana Kit" =>
|
|
ConsumableCategory.ManaKit,
|
|
_ => ConsumableCategory.HealthKit,
|
|
};
|
|
}
|
|
|
|
internal static class CraftingPlanner
|
|
{
|
|
public const string AllPeas = "[All Peas]";
|
|
|
|
public static CraftingPlan? Plan(
|
|
IReadOnlyList<PluginInventoryItem> inventory,
|
|
IEnumerable<string> desiredResults,
|
|
ICharacterInfo character,
|
|
int desiredCount = 1,
|
|
int arrowheadFletchDifficultyExcess = 10)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(inventory);
|
|
ArgumentNullException.ThrowIfNull(desiredResults);
|
|
ArgumentNullException.ThrowIfNull(character);
|
|
var counts = inventory
|
|
.GroupBy(static item => item.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(
|
|
static group => group.Key,
|
|
static group => group.Sum(item => Math.Max(1, item.StackSize)),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
foreach (string desired in desiredResults
|
|
.Where(static name => !string.IsNullOrWhiteSpace(name))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(static name => name, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
if (counts.GetValueOrDefault(desired) >= Math.Max(1, desiredCount))
|
|
continue;
|
|
CraftingPlan? plan = FindStep(
|
|
desired,
|
|
desired,
|
|
inventory,
|
|
character,
|
|
counts,
|
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
|
arrowheadFletchDifficultyExcess);
|
|
if (plan is not null)
|
|
return plan;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static CraftingPlan? PlanPeaSplit(
|
|
IReadOnlyList<PluginInventoryItem> inventory,
|
|
ISet<string> consumableProfile,
|
|
int minimumComponentCount)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(inventory);
|
|
ArgumentNullException.ThrowIfNull(consumableProfile);
|
|
int minimum = Math.Max(0, minimumComponentCount);
|
|
if (minimum == 0)
|
|
return null;
|
|
PluginInventoryItem tool = Find(inventory, "Splitting Tool");
|
|
if (tool.ObjectId == 0u)
|
|
return null;
|
|
bool allPeas = consumableProfile.Contains(AllPeas);
|
|
var counts = inventory
|
|
.GroupBy(static item => item.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(
|
|
static group => group.Key,
|
|
static group => group.Sum(item => Math.Max(1, item.StackSize)),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
foreach (VtankCraftRecipe recipe in VtankCraftDatabase.Recipes)
|
|
{
|
|
if (!recipe.FirstItem.Equals("Splitting Tool", StringComparison.Ordinal)
|
|
|| !recipe.SecondItem.EndsWith(" Pea", StringComparison.Ordinal)
|
|
|| (!allPeas && !consumableProfile.Contains(recipe.SecondItem))
|
|
|| counts.GetValueOrDefault(recipe.ResultItem) >= minimum)
|
|
{
|
|
continue;
|
|
}
|
|
PluginInventoryItem pea = Find(inventory, recipe.SecondItem);
|
|
if (pea.ObjectId == 0u)
|
|
continue;
|
|
return new CraftingPlan(
|
|
recipe,
|
|
tool.ObjectId,
|
|
pea.ObjectId,
|
|
recipe.ResultItem);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static CraftingPlan? FindStep(
|
|
string result,
|
|
string desiredResult,
|
|
IReadOnlyList<PluginInventoryItem> inventory,
|
|
ICharacterInfo character,
|
|
IReadOnlyDictionary<string, int> counts,
|
|
HashSet<string> visiting,
|
|
int arrowheadFletchDifficultyExcess)
|
|
{
|
|
if (!visiting.Add(result))
|
|
return null;
|
|
try
|
|
{
|
|
foreach (VtankCraftRecipe recipe in VtankCraftDatabase.ForResult(result))
|
|
{
|
|
if (!HasRequiredSkill(
|
|
character,
|
|
recipe.RequiredSkill,
|
|
recipe.Difficulty,
|
|
arrowheadFletchDifficultyExcess))
|
|
continue;
|
|
|
|
PluginInventoryItem first = Find(inventory, recipe.FirstItem);
|
|
if (first.ObjectId == 0u)
|
|
{
|
|
CraftingPlan? prerequisite = FindStep(
|
|
recipe.FirstItem,
|
|
desiredResult,
|
|
inventory,
|
|
character,
|
|
counts,
|
|
visiting,
|
|
arrowheadFletchDifficultyExcess);
|
|
if (prerequisite is not null)
|
|
return prerequisite;
|
|
continue;
|
|
}
|
|
|
|
PluginInventoryItem second = Find(
|
|
inventory,
|
|
recipe.SecondItem,
|
|
excludedObjectId: recipe.FirstItem.Equals(
|
|
recipe.SecondItem,
|
|
StringComparison.OrdinalIgnoreCase)
|
|
? first.ObjectId
|
|
: 0u);
|
|
if (second.ObjectId == 0u)
|
|
{
|
|
if (recipe.FirstItem.Equals(
|
|
recipe.SecondItem,
|
|
StringComparison.OrdinalIgnoreCase)
|
|
&& first.StackSize >= 2)
|
|
{
|
|
return new CraftingPlan(
|
|
recipe,
|
|
first.ObjectId,
|
|
0u,
|
|
desiredResult)
|
|
{
|
|
RequiresSplitFirstStack = true,
|
|
SplitContainerObjectId = first.ContainerObjectId,
|
|
};
|
|
}
|
|
CraftingPlan? prerequisite = FindStep(
|
|
recipe.SecondItem,
|
|
desiredResult,
|
|
inventory,
|
|
character,
|
|
counts,
|
|
visiting,
|
|
arrowheadFletchDifficultyExcess);
|
|
if (prerequisite is not null)
|
|
return prerequisite;
|
|
continue;
|
|
}
|
|
|
|
return new CraftingPlan(
|
|
recipe,
|
|
first.ObjectId,
|
|
second.ObjectId,
|
|
desiredResult);
|
|
}
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
visiting.Remove(result);
|
|
}
|
|
}
|
|
|
|
private static PluginInventoryItem Find(
|
|
IReadOnlyList<PluginInventoryItem> inventory,
|
|
string name,
|
|
uint excludedObjectId = 0u)
|
|
{
|
|
foreach (PluginInventoryItem item in inventory)
|
|
{
|
|
if (item.ObjectId != excludedObjectId
|
|
&& item.StackSize > 0
|
|
&& item.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
return default;
|
|
}
|
|
|
|
private static bool HasRequiredSkill(
|
|
ICharacterInfo character,
|
|
uint requiredSkill,
|
|
int difficulty,
|
|
int arrowheadFletchDifficultyExcess)
|
|
{
|
|
if (requiredSkill == 0u)
|
|
return true;
|
|
if (!character.TryGetSkill(requiredSkill, out PluginSkillInfo skill)
|
|
|| skill.Training is not (PluginSkillTraining.Trained
|
|
or PluginSkillTraining.Specialized))
|
|
{
|
|
return false;
|
|
}
|
|
return requiredSkill != 37u
|
|
|| skill.Current >= Math.Max(0, difficulty)
|
|
+ arrowheadFletchDifficultyExcess;
|
|
}
|
|
}
|
|
|
|
internal sealed class CraftingController
|
|
{
|
|
private const double SplitTimeoutSeconds = 10d;
|
|
|
|
private readonly IPluginHost _host;
|
|
private readonly InventorySettings _settings;
|
|
private readonly CombatSettings _profiles;
|
|
private CraftingPlan? _pending;
|
|
private CraftingPlan? _pendingSplit;
|
|
private long _observedCompletion;
|
|
private long _observedInventoryCompletion;
|
|
private double _untilScan;
|
|
private double _untilCriticalScan;
|
|
private double _untilIdleScan;
|
|
private double _splitElapsed;
|
|
private bool _splitAcknowledged;
|
|
|
|
public CraftingController(
|
|
IPluginHost host,
|
|
InventorySettings settings,
|
|
CombatSettings profiles)
|
|
{
|
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
|
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
|
_profiles = profiles ?? throw new ArgumentNullException(nameof(profiles));
|
|
}
|
|
|
|
public string Status { get; private set; } = "AutoCraft idle";
|
|
|
|
/// <summary>
|
|
/// Immediate VTank subsystem request, used by ammunition selection. This
|
|
/// bypasses the general AutoCraftItems toggle just as bv.cs does, while
|
|
/// still using the one canonical crafting transaction state machine.
|
|
/// </summary>
|
|
public bool Request(string resultName, int desiredCount = 1)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(resultName)
|
|
|| _pending is not null
|
|
|| _pendingSplit is not null
|
|
|| !_host.Automation.IsAvailable)
|
|
{
|
|
return false;
|
|
}
|
|
IItemAutomation items = _host.Automation.Items;
|
|
if (!items.IsAvailable || items.IsBusy)
|
|
return false;
|
|
CraftingPlan? plan = CraftingPlanner.Plan(
|
|
items.CaptureOwnedItems(),
|
|
[resultName],
|
|
_host.Automation.Character,
|
|
desiredCount,
|
|
_settings.ArrowheadFletchDifficultyExcess);
|
|
return plan is { } next && Start(items, next);
|
|
}
|
|
|
|
public bool CanRequest(string resultName, int desiredCount = 1)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(resultName)
|
|
|| !_host.Automation.IsAvailable
|
|
|| !_host.Automation.Items.IsAvailable)
|
|
{
|
|
return false;
|
|
}
|
|
return CraftingPlanner.Plan(
|
|
_host.Automation.Items.CaptureOwnedItems(),
|
|
[resultName],
|
|
_host.Automation.Character,
|
|
desiredCount,
|
|
_settings.ArrowheadFletchDifficultyExcess)
|
|
is not null;
|
|
}
|
|
|
|
public bool TickCritical(double elapsedSeconds, bool canAct)
|
|
{
|
|
IItemAutomation items = _host.Automation.Items;
|
|
ObserveCompletion(items);
|
|
if (ObserveSplitCompletion(items, elapsedSeconds))
|
|
return true;
|
|
if (_pending is not null)
|
|
{
|
|
if (items.IsBusy)
|
|
return true;
|
|
_pending = null;
|
|
}
|
|
if (!canAct
|
|
|| !_settings.AutoCraftItems
|
|
|| !_host.Automation.IsAvailable
|
|
|| !items.IsAvailable
|
|
|| items.IsBusy)
|
|
{
|
|
return false;
|
|
}
|
|
_untilCriticalScan -= Math.Max(0d, elapsedSeconds);
|
|
if (_untilCriticalScan > 0d)
|
|
return false;
|
|
_untilCriticalScan = Math.Max(0.1d, _settings.ScanIntervalSeconds);
|
|
IReadOnlyList<PluginInventoryItem> inventory = items.CaptureOwnedItems();
|
|
CraftingPlan? plan = _settings.SplitPeas
|
|
? CraftingPlanner.PlanPeaSplit(
|
|
inventory,
|
|
_profiles.ConsumableNames,
|
|
_settings.CriticalComponentMinimum)
|
|
: null;
|
|
plan ??= PlanCategoryCraft(
|
|
inventory,
|
|
idleCounts: false);
|
|
return plan is { } next && Start(items, next);
|
|
}
|
|
|
|
public bool Tick(double elapsedSeconds, bool canAct)
|
|
{
|
|
IItemAutomation items = _host.Automation.Items;
|
|
ObserveCompletion(items);
|
|
if (ObserveSplitCompletion(items, elapsedSeconds))
|
|
return true;
|
|
if (_pending is not null)
|
|
{
|
|
if (items.IsBusy)
|
|
return true;
|
|
_pending = null;
|
|
}
|
|
if (!canAct
|
|
|| !_settings.AutoCraftItems
|
|
|| !_host.Automation.IsAvailable
|
|
|| !items.IsAvailable
|
|
|| items.IsBusy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_untilScan -= Math.Max(0d, elapsedSeconds);
|
|
if (_untilScan > 0d)
|
|
return false;
|
|
_untilScan = Math.Max(0.1d, _settings.ScanIntervalSeconds);
|
|
IReadOnlyList<PluginInventoryItem> inventory = items.CaptureOwnedItems();
|
|
CraftingPlan? plan = _settings.SplitPeas
|
|
? CraftingPlanner.PlanPeaSplit(
|
|
inventory,
|
|
_profiles.ConsumableNames,
|
|
_settings.NormalComponentMinimum)
|
|
: null;
|
|
plan ??= CraftingPlanner.Plan(
|
|
inventory,
|
|
_profiles.ConsumableNames
|
|
.Concat(_profiles.CombatItemNames)
|
|
.Where(static name =>
|
|
!name.Equals(CraftingPlanner.AllPeas, StringComparison.Ordinal)
|
|
&& !name.EndsWith(" Pea", StringComparison.Ordinal)),
|
|
_host.Automation.Character,
|
|
arrowheadFletchDifficultyExcess:
|
|
_settings.ArrowheadFletchDifficultyExcess);
|
|
if (plan is not { } next)
|
|
{
|
|
Status = "AutoCraft idle";
|
|
return false;
|
|
}
|
|
|
|
return Start(items, next);
|
|
}
|
|
|
|
public bool TickIdle(double elapsedSeconds, bool canAct)
|
|
{
|
|
IItemAutomation items = _host.Automation.Items;
|
|
ObserveCompletion(items);
|
|
if (ObserveSplitCompletion(items, elapsedSeconds))
|
|
return true;
|
|
if (_pending is not null)
|
|
{
|
|
if (items.IsBusy)
|
|
return true;
|
|
_pending = null;
|
|
}
|
|
if (!canAct
|
|
|| !_settings.AutoCraftItems
|
|
|| !_host.Automation.IsAvailable
|
|
|| !items.IsAvailable
|
|
|| items.IsBusy)
|
|
{
|
|
return false;
|
|
}
|
|
_untilIdleScan -= Math.Max(0d, elapsedSeconds);
|
|
if (_untilIdleScan > 0d)
|
|
return false;
|
|
_untilIdleScan = Math.Max(0.1d, _settings.ScanIntervalSeconds);
|
|
IReadOnlyList<PluginInventoryItem> inventory = items.CaptureOwnedItems();
|
|
CraftingPlan? plan = _settings.SplitPeas
|
|
? CraftingPlanner.PlanPeaSplit(
|
|
inventory,
|
|
_profiles.ConsumableNames,
|
|
_settings.IdleComponentMinimum)
|
|
: null;
|
|
plan ??= PlanCategoryCraft(inventory, idleCounts: true);
|
|
return plan is { } next && Start(items, next);
|
|
}
|
|
|
|
private CraftingPlan? PlanCategoryCraft(
|
|
IReadOnlyList<PluginInventoryItem> inventory,
|
|
bool idleCounts)
|
|
{
|
|
foreach (string name in _profiles.ConsumableNames
|
|
.OrderBy(static name => name, StringComparer.Ordinal))
|
|
{
|
|
ConsumableCategory category = _profiles.ConsumableCategories
|
|
.TryGetValue(name, out ConsumableCategory stored)
|
|
? stored
|
|
: ConsumableClassifier.ClassifyName(name);
|
|
int desired = idleCounts ? IdleCount(category) : category switch
|
|
{
|
|
ConsumableCategory.HealthKit
|
|
or ConsumableCategory.HealthFood
|
|
or ConsumableCategory.StaminaKit
|
|
or ConsumableCategory.StaminaFood
|
|
or ConsumableCategory.ManaKit
|
|
or ConsumableCategory.ManaFood => 1,
|
|
_ => 0,
|
|
};
|
|
if (desired <= 0)
|
|
continue;
|
|
CraftingPlan? plan = CraftingPlanner.Plan(
|
|
inventory,
|
|
[name],
|
|
_host.Automation.Character,
|
|
desired,
|
|
_settings.ArrowheadFletchDifficultyExcess);
|
|
if (plan is not null)
|
|
return plan;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private int IdleCount(ConsumableCategory category) => category switch
|
|
{
|
|
ConsumableCategory.HealthKit => _settings.IdleHealthKitCount,
|
|
ConsumableCategory.StaminaKit => _settings.IdleStaminaKitCount,
|
|
ConsumableCategory.ManaKit => _settings.IdleManaKitCount,
|
|
ConsumableCategory.HealthFood => _settings.IdleHealthFoodCount,
|
|
ConsumableCategory.StaminaFood => _settings.IdleStaminaFoodCount,
|
|
ConsumableCategory.ManaFood => _settings.IdleManaFoodCount,
|
|
_ => 0,
|
|
};
|
|
|
|
private bool Start(IItemAutomation items, CraftingPlan next)
|
|
{
|
|
if (next.RequiresSplitFirstStack)
|
|
{
|
|
long completionBefore = items.LastInventoryCompletion.Revision;
|
|
PluginItemCommandResult split = items.MoveToContainer(
|
|
next.FirstObjectId,
|
|
next.SplitContainerObjectId,
|
|
amount: 1u);
|
|
if (!split.Accepted)
|
|
{
|
|
Status = $"AutoCraft split waiting: {split.Status}";
|
|
return split.Status == PluginItemCommandStatus.Busy;
|
|
}
|
|
_pendingSplit = next;
|
|
_observedInventoryCompletion = completionBefore;
|
|
_splitElapsed = 0d;
|
|
_splitAcknowledged = false;
|
|
Status = $"Splitting {next.Recipe.FirstItem} for crafting";
|
|
return true;
|
|
}
|
|
PluginItemCommandResult result = items.Apply(
|
|
next.FirstObjectId,
|
|
next.SecondObjectId);
|
|
if (!result.Accepted)
|
|
{
|
|
Status = $"AutoCraft waiting: {result.Status}";
|
|
return result.Status == PluginItemCommandStatus.Busy;
|
|
}
|
|
_pending = next;
|
|
Status = $"Crafting {next.Recipe.ResultItem}";
|
|
return true;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_pending = null;
|
|
_pendingSplit = null;
|
|
_untilScan = 0d;
|
|
_untilCriticalScan = 0d;
|
|
_untilIdleScan = 0d;
|
|
_splitElapsed = 0d;
|
|
_splitAcknowledged = false;
|
|
Status = "AutoCraft idle";
|
|
}
|
|
|
|
private void ObserveCompletion(IItemAutomation items)
|
|
{
|
|
PluginItemUseCompletion completion = items.LastCompletion;
|
|
if (completion.Revision == 0 || completion.Revision == _observedCompletion)
|
|
return;
|
|
_observedCompletion = completion.Revision;
|
|
if (_pending is not { } pending
|
|
|| completion.SourceObjectId != pending.FirstObjectId)
|
|
{
|
|
return;
|
|
}
|
|
Status = completion.IsSuccess
|
|
? $"Crafted {pending.Recipe.ResultItem}"
|
|
: $"Craft failed (0x{completion.WeenieError:X})";
|
|
_pending = null;
|
|
_untilScan = 0d;
|
|
_untilCriticalScan = 0d;
|
|
_untilIdleScan = 0d;
|
|
}
|
|
|
|
private bool ObserveSplitCompletion(
|
|
IItemAutomation items,
|
|
double elapsedSeconds)
|
|
{
|
|
if (_pendingSplit is not { } splitPlan)
|
|
return false;
|
|
|
|
_splitElapsed += Math.Max(0d, elapsedSeconds);
|
|
PluginInventoryCompletion completion = items.LastInventoryCompletion;
|
|
if (completion.Revision != 0
|
|
&& completion.Revision != _observedInventoryCompletion)
|
|
{
|
|
_observedInventoryCompletion = completion.Revision;
|
|
if (completion.SourceObjectId == splitPlan.FirstObjectId)
|
|
{
|
|
if (!completion.IsSuccess)
|
|
{
|
|
Status = $"AutoCraft split failed (0x{completion.WeenieError:X})";
|
|
ClearPendingSplit();
|
|
return true;
|
|
}
|
|
_splitAcknowledged = true;
|
|
}
|
|
}
|
|
|
|
if (_splitAcknowledged && TryStartAfterSplit(items, splitPlan))
|
|
return true;
|
|
if (_splitElapsed < SplitTimeoutSeconds)
|
|
{
|
|
Status = _splitAcknowledged
|
|
? "AutoCraft waiting for split inventory"
|
|
: $"Splitting {splitPlan.Recipe.FirstItem} for crafting";
|
|
return true;
|
|
}
|
|
|
|
Status = "AutoCraft split timed out";
|
|
ClearPendingSplit();
|
|
return true;
|
|
}
|
|
|
|
private bool TryStartAfterSplit(
|
|
IItemAutomation items,
|
|
CraftingPlan splitPlan)
|
|
{
|
|
PluginInventoryItem[] inputs = items.CaptureOwnedItems()
|
|
.Where(item => item.Name.Equals(
|
|
splitPlan.Recipe.FirstItem,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
.OrderBy(static item => item.ObjectId)
|
|
.ToArray();
|
|
if (inputs.Length < 2)
|
|
return false;
|
|
CraftingPlan ready = splitPlan with
|
|
{
|
|
FirstObjectId = inputs[0].ObjectId,
|
|
SecondObjectId = inputs[1].ObjectId,
|
|
RequiresSplitFirstStack = false,
|
|
};
|
|
ClearPendingSplit();
|
|
return Start(items, ready);
|
|
}
|
|
|
|
private void ClearPendingSplit()
|
|
{
|
|
_pendingSplit = null;
|
|
_splitElapsed = 0d;
|
|
_splitAcknowledged = false;
|
|
_untilScan = Math.Max(0.1d, _settings.ScanIntervalSeconds);
|
|
_untilCriticalScan = Math.Max(0.1d, _settings.ScanIntervalSeconds);
|
|
_untilIdleScan = Math.Max(0.1d, _settings.ScanIntervalSeconds);
|
|
}
|
|
}
|