using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.MossTank;
///
/// UtilityBelt-compatible named-profile item giver. It classifies a stable
/// inventory snapshot up front and then submits exactly one canonical give at
/// a time, advancing only after the server completion or the owned-object view
/// confirms that the item left inventory.
///
internal sealed class ProfileGiveController
{
private const double GiveTimeoutSeconds = 10d;
private const int MaximumAttemptsPerItem = 5;
private static readonly PluginItemProperties EmptyProperties = new(
new Dictionary(),
new Dictionary(),
new Dictionary(),
new Dictionary(),
new Dictionary(),
new Dictionary(),
new Dictionary());
private readonly IPluginHost _host;
private readonly MossTankLootProfileStore _profiles;
private readonly Queue _pending = new();
private uint _targetObjectId;
private uint _waitingObjectId;
private long _completionRevision;
private double _waitingSeconds;
private int _attempts;
private int _given;
private string _profileName = string.Empty;
private string _targetName = string.Empty;
public ProfileGiveController(
IPluginHost host,
MossTankLootProfileStore profiles)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
_profiles = profiles ?? throw new ArgumentNullException(nameof(profiles));
}
public bool IsRunning { get; private set; }
public string Status { get; private set; } = "Item giver idle.";
public bool TryStart(string? profileName, string? targetName)
{
if (IsRunning || !_host.Automation.IsAvailable)
return false;
string requestedProfile = profileName?.Trim() ?? string.Empty;
string requestedTarget = targetName?.Trim() ?? string.Empty;
PluginWorldObject target = _host.Automation.Objects.CaptureObjects()
.Where(obj => obj.ObjectClass is PluginObjectClass.Player
or PluginObjectClass.Npc)
.Where(obj => obj.Name.Equals(
requestedTarget,
StringComparison.OrdinalIgnoreCase))
.Where(obj => obj.ObjectId != _host.Automation.Character.ObjectId)
.OrderBy(obj => DistanceFromPlayer(obj))
.ThenBy(static obj => obj.ObjectId)
.FirstOrDefault();
if (target.ObjectId == 0u)
{
Status = $"Item giver target not found: {requestedTarget}.";
return false;
}
var rules = new List();
if (!_profiles.TryLoadNamed(requestedProfile, rules))
{
Status = $"Item giver profile not found: {requestedProfile}.";
return false;
}
IReadOnlyList owned =
_host.Automation.Items.CaptureOwnedItems();
_pending.Clear();
foreach (PluginInventoryItem item in owned
.Where(static item => !item.IsEquipped && item.WielderObjectId == 0u)
.OrderBy(static item => item.ObjectId))
{
PluginItemProperties properties = _host.Automation.Items
.TryCaptureProperties(item.ObjectId, out PluginItemProperties value)
? value
: EmptyProperties;
if (MatchesGiveProfile(item, properties, rules))
_pending.Enqueue(item.ObjectId);
}
_targetObjectId = target.ObjectId;
_profileName = requestedProfile;
_targetName = target.Name;
_waitingObjectId = 0u;
_attempts = 0;
_given = 0;
_waitingSeconds = 0d;
IsRunning = true;
Status = _pending.Count == 0
? $"No items match {_profileName}."
: $"Giving {_pending.Count} item(s) to {_targetName}.";
return true;
}
public bool Tick(double elapsedSeconds, bool canAct)
{
if (!IsRunning)
return false;
if (!_host.Automation.IsAvailable
|| !_host.Automation.Objects.TryGet(
_targetObjectId,
out PluginWorldObject target)
|| target.ObjectClass is not (PluginObjectClass.Player
or PluginObjectClass.Npc))
{
Stop("Item giver stopped: target vanished.");
return false;
}
if (_waitingObjectId != 0u)
{
_waitingSeconds += Math.Max(0d, elapsedSeconds);
PluginInventoryCompletion completion =
_host.Automation.Items.LastInventoryCompletion;
bool itemStillOwned = _host.Automation.Items.CaptureOwnedItems()
.Any(item => item.ObjectId == _waitingObjectId);
if (!itemStillOwned
|| (completion.Revision > _completionRevision
&& completion.Kind == PluginInventoryCommandKind.Give
&& completion.SourceObjectId == _waitingObjectId))
{
if (!itemStillOwned || completion.IsSuccess)
_given++;
_pending.Dequeue();
_waitingObjectId = 0u;
_attempts = 0;
_waitingSeconds = 0d;
}
else if (_waitingSeconds >= GiveTimeoutSeconds)
{
if (_attempts >= MaximumAttemptsPerItem)
{
_pending.Dequeue();
_waitingObjectId = 0u;
_attempts = 0;
_waitingSeconds = 0d;
}
else
{
_waitingObjectId = 0u;
_waitingSeconds = 0d;
}
}
return true;
}
if (_pending.Count == 0)
{
Stop($"Item giver finished: {_given} item(s) given to {_targetName}.");
return false;
}
if (!canAct || _host.Automation.Items.IsBusy)
return true;
uint objectId = _pending.Peek();
if (!_host.Automation.Items.CaptureOwnedItems()
.Any(item => item.ObjectId == objectId))
{
_pending.Dequeue();
return true;
}
long baselineRevision =
_host.Automation.Items.LastInventoryCompletion.Revision;
PluginItemCommandResult result = _host.Automation.Items.Give(
objectId,
_targetObjectId);
if (result.Accepted)
{
_waitingObjectId = objectId;
_completionRevision = baselineRevision;
_waitingSeconds = 0d;
_attempts++;
Status = $"Giving item {_given + 1} to {_targetName}…";
}
else if (result.Status is PluginItemCommandStatus.InvalidItem
or PluginItemCommandStatus.InvalidTarget
or PluginItemCommandStatus.Refused
or PluginItemCommandStatus.Unavailable)
{
_pending.Dequeue();
_attempts = 0;
}
return true;
}
public void Reset()
{
_pending.Clear();
_targetObjectId = 0u;
_waitingObjectId = 0u;
_completionRevision = 0;
_waitingSeconds = 0d;
_attempts = 0;
_given = 0;
IsRunning = false;
Status = "Item giver idle.";
}
private bool MatchesGiveProfile(
in PluginInventoryItem item,
in PluginItemProperties properties,
IReadOnlyList rules)
{
foreach (LootRule rule in rules)
{
if (!rule.IsMatch(item, properties, _host, out _))
continue;
return rule.Action is LootAction.Keep or LootAction.KeepUpTo;
}
return false;
}
private double DistanceFromPlayer(in PluginWorldObject target)
{
PluginNavigationSnapshot player = _host.Automation.Navigation.Snapshot;
if (!player.IsAvailable || !target.HasPosition)
return double.MaxValue;
double dx = target.Position.NorthSouth - player.Position.NorthSouth;
double dy = target.Position.EastWest - player.Position.EastWest;
return Math.Sqrt((dx * dx) + (dy * dy));
}
private void Stop(string status)
{
_pending.Clear();
_targetObjectId = 0u;
_waitingObjectId = 0u;
_completionRevision = 0;
_waitingSeconds = 0d;
_attempts = 0;
IsRunning = false;
Status = status;
}
}