feat(mosstank): add VTank-style automation PoC
This commit is contained in:
parent
f6fe0f2a4f
commit
4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions
247
src/AcDream.Plugins.MossTank/ProfileGiveController.cs
Normal file
247
src/AcDream.Plugins.MossTank/ProfileGiveController.cs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Plugins.MossTank;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class ProfileGiveController
|
||||
{
|
||||
private const double GiveTimeoutSeconds = 10d;
|
||||
private const int MaximumAttemptsPerItem = 5;
|
||||
|
||||
private static readonly PluginItemProperties EmptyProperties = new(
|
||||
new Dictionary<uint, int>(),
|
||||
new Dictionary<uint, long>(),
|
||||
new Dictionary<uint, bool>(),
|
||||
new Dictionary<uint, double>(),
|
||||
new Dictionary<uint, string>(),
|
||||
new Dictionary<uint, uint>(),
|
||||
new Dictionary<uint, uint>());
|
||||
|
||||
private readonly IPluginHost _host;
|
||||
private readonly MossTankLootProfileStore _profiles;
|
||||
private readonly Queue<uint> _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<LootRule>();
|
||||
if (!_profiles.TryLoadNamed(requestedProfile, rules))
|
||||
{
|
||||
Status = $"Item giver profile not found: {requestedProfile}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<PluginInventoryItem> 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<LootRule> 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue