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; } }