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
|
|
@ -75,6 +75,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
// dispatched-vs-silent-no-op shape as _sendBuy — see TryBuyAll/TrySell.
|
||||
private readonly Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, uint, bool>? _sendBuyAll;
|
||||
private readonly Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, bool>? _sendSell;
|
||||
private readonly Func<uint, IReadOnlyList<uint>, bool>? _sendSalvage;
|
||||
private readonly RuntimeInteractionTransactionState _runtimeTransactions;
|
||||
private readonly InventoryTransactionState _transactions;
|
||||
|
||||
|
|
@ -120,7 +121,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, uint, bool>? sendBuyAll = null,
|
||||
Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, bool>? sendSell = null,
|
||||
Action<string, RetailLogTextType>? interfaceText = null,
|
||||
Action<uint, uint, uint>? sendStackableMerge = null)
|
||||
Action<uint, uint, uint>? sendStackableMerge = null,
|
||||
Func<uint, IReadOnlyList<uint>, bool>? sendSalvage = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -155,6 +157,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_sendBuy = sendBuy;
|
||||
_sendBuyAll = sendBuyAll;
|
||||
_sendSell = sendSell;
|
||||
_sendSalvage = sendSalvage;
|
||||
_interactionState = interactionState
|
||||
?? throw new ArgumentNullException(nameof(interactionState));
|
||||
_runtimeTransactions = runtimeTransactions
|
||||
|
|
@ -500,6 +503,197 @@ public sealed class ItemInteractionController : IDisposable
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-facing form of retail's put/split-to-container attempts. It
|
||||
/// borrows this controller's exact transaction gate and wire delegates;
|
||||
/// plugins supply policy, never a second optimistic inventory model.
|
||||
/// </summary>
|
||||
public bool TryMoveItemForAutomation(
|
||||
uint itemId,
|
||||
uint containerId,
|
||||
uint amount = 0u,
|
||||
int placement = 0)
|
||||
{
|
||||
if (itemId == 0u
|
||||
|| containerId == 0u
|
||||
|| _sendPutItemInContainer is null
|
||||
|| _objects.Get(itemId) is not { } item
|
||||
|| !IsOwnedByPlayer(itemId)
|
||||
|| (containerId != _playerGuid() && !IsOwnedByPlayer(containerId)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint fullStack = (uint)Math.Max(1, item.StackSize);
|
||||
uint requested = amount == 0u ? fullStack : amount;
|
||||
if (requested == 0u || requested > fullStack)
|
||||
return false;
|
||||
if (requested < fullStack)
|
||||
{
|
||||
return TrySplitToContainer(
|
||||
itemId,
|
||||
containerId,
|
||||
(uint)Math.Max(0, placement),
|
||||
requested);
|
||||
}
|
||||
|
||||
return TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
itemId,
|
||||
() =>
|
||||
{
|
||||
_sendPutItemInContainer(itemId, containerId, placement);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-facing retail stack merge. The shared planner performs the same
|
||||
/// WCID, maximum-size, staged-trade, and transfer-size checks as a drag.
|
||||
/// </summary>
|
||||
public bool TryMergeItemsForAutomation(
|
||||
uint sourceItemId,
|
||||
uint targetItemId,
|
||||
uint amount = 0u)
|
||||
{
|
||||
if (_sendStackableMerge is null
|
||||
|| !IsOwnedByPlayer(sourceItemId)
|
||||
|| !IsOwnedByPlayer(targetItemId)
|
||||
|| _objects.Get(sourceItemId) is not { } source
|
||||
|| _objects.Get(targetItemId) is not { } target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int requested = amount > int.MaxValue ? int.MaxValue : (int)amount;
|
||||
StackMergePlan? plan = StackMergePlanner.Plan(
|
||||
ToStackMergeItem(source),
|
||||
ToStackMergeItem(target),
|
||||
CanMakeInventoryRequest,
|
||||
requested);
|
||||
if (plan is not { } merge)
|
||||
return false;
|
||||
|
||||
return TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Merge,
|
||||
sourceItemId,
|
||||
() =>
|
||||
{
|
||||
_sendStackableMerge(
|
||||
merge.SourceObjectId,
|
||||
merge.TargetObjectId,
|
||||
merge.Amount);
|
||||
MergeAttempted?.Invoke(
|
||||
merge.SourceObjectId,
|
||||
merge.TargetObjectId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Plugin-facing retail full-stack drop or split-to-world.</summary>
|
||||
public bool TryDropItemForAutomation(uint itemId, uint amount = 0u)
|
||||
{
|
||||
if (!IsOwnedByPlayer(itemId) || _objects.Get(itemId) is not { } item)
|
||||
return false;
|
||||
|
||||
uint fullStack = (uint)Math.Max(1, item.StackSize);
|
||||
uint requested = amount == 0u ? fullStack : amount;
|
||||
if (requested == 0u || requested > fullStack)
|
||||
return false;
|
||||
InventoryRequestKind kind = requested < fullStack
|
||||
? InventoryRequestKind.SplitToWorld
|
||||
: InventoryRequestKind.DropToWorld;
|
||||
return TryDispatchInventoryRequest(
|
||||
kind,
|
||||
itemId,
|
||||
() =>
|
||||
{
|
||||
if (requested < fullStack)
|
||||
{
|
||||
if (_sendSplitToWorld is null)
|
||||
return false;
|
||||
_sendSplitToWorld(itemId, requested);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_sendDrop is null)
|
||||
return false;
|
||||
_sendDrop(itemId);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Plugin-facing retail Give attempt with an exact stack amount.</summary>
|
||||
public bool TryGiveItemForAutomation(
|
||||
uint itemId,
|
||||
uint targetId,
|
||||
uint amount = 0u)
|
||||
{
|
||||
if (_sendGive is null
|
||||
|| targetId == 0u
|
||||
|| targetId == _playerGuid()
|
||||
|| _objects.Get(targetId) is null
|
||||
|| !IsOwnedByPlayer(itemId)
|
||||
|| _objects.Get(itemId) is not { } item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint fullStack = (uint)Math.Max(1, item.StackSize);
|
||||
uint requested = amount == 0u ? fullStack : amount;
|
||||
if (requested == 0u || requested > fullStack)
|
||||
return false;
|
||||
return TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Give,
|
||||
itemId,
|
||||
() =>
|
||||
{
|
||||
_sendGive(targetId, itemId, requested);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-facing form of gmSalvageUI::Salvage. Retail validates an owned
|
||||
/// tinkering tool and a non-empty ordered list of suitable owned source
|
||||
/// items, then sends 0x027D without entering the ordinary one-item move
|
||||
/// transaction. The server owns the final material and option checks.
|
||||
/// </summary>
|
||||
public bool TrySalvageItemsForAutomation(
|
||||
uint toolId,
|
||||
IReadOnlyList<uint> itemIds)
|
||||
{
|
||||
if (_sendSalvage is null
|
||||
|| toolId == 0u
|
||||
|| itemIds is null
|
||||
|| itemIds.Count == 0
|
||||
|| !CanMakeInventoryRequest
|
||||
|| !IsOwnedByPlayer(toolId)
|
||||
|| _objects.Get(toolId) is not { } tool
|
||||
|| (tool.Type & ItemType.TinkeringTool) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var distinct = new HashSet<uint>();
|
||||
foreach (uint itemId in itemIds)
|
||||
{
|
||||
if (itemId == 0u
|
||||
|| itemId == toolId
|
||||
|| !distinct.Add(itemId)
|
||||
|| !IsOwnedByPlayer(itemId)
|
||||
|| _objects.Get(itemId) is not { } item
|
||||
|| item.MaterialType is null or 0u
|
||||
|| item.Structure >= 100
|
||||
|| ((item.PublicWeenieBitfield ?? 0u) & 0xFF000000u) != 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return _sendSalvage(toolId, itemIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments retail's shared <c>ClientUISystem</c> busy reference after a
|
||||
/// request issued by another retained controller has been sent. The
|
||||
|
|
@ -655,6 +849,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_runtimeTransactions.TryRequestAppraisal(objectId, _sendExamine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin/automation appraisal through the one retail appraisal owner.
|
||||
/// It does not mutate selection or open/raise the examination window.
|
||||
/// </summary>
|
||||
public bool TryAppraiseForAutomation(uint objectId)
|
||||
{
|
||||
if (objectId == 0u
|
||||
|| _sendExamine is null
|
||||
|| _objects.Get(objectId) is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return _runtimeTransactions.TryRequestAppraisal(objectId, _sendExamine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts only the pending or current appraisal, matching
|
||||
/// <c>gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0</c>.
|
||||
|
|
@ -750,6 +959,76 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return ExecuteUseActions(decision.Actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin/automation entry for an ordinary item request. Unlike interactive
|
||||
/// activation, it never turns a use request into wielding, sorting, or a
|
||||
/// modal target cursor, and returns true only when a wire Use was issued.
|
||||
/// </summary>
|
||||
public bool TryUseItemForAutomation(uint itemGuid)
|
||||
{
|
||||
if (itemGuid == 0u || _objects.Get(itemGuid) is not { } item)
|
||||
return false;
|
||||
if (ItemUseability.IsTargeted(item.Useability ?? ItemUseability.Undef))
|
||||
return false;
|
||||
if (!ConsumeUseThrottle())
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
|
||||
var input = new ItemUsePolicyInput(
|
||||
Snapshot(item),
|
||||
_playerGuid(),
|
||||
_groundObjectId(),
|
||||
CanMakeInventoryRequest,
|
||||
_activeVendorId(),
|
||||
BypassClassification: true,
|
||||
UseCurrentSelection: false,
|
||||
SelectedTarget: null,
|
||||
ConfirmVolatileRareUses: true,
|
||||
InNonCombatMode: _inNonCombatMode());
|
||||
ItemUsePolicyDecision decision = ItemInteractionPolicy.DecideUse(input);
|
||||
bool sends = decision.Actions.Any(static action =>
|
||||
action.Kind == ItemPolicyActionKind.SendUse);
|
||||
return sends && ExecuteUseActions(decision.Actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin/automation entry for a targeted item action. It follows the same
|
||||
/// retail compatibility, throttle, busy-reference and UseDone ownership as
|
||||
/// choosing a target through the interactive target cursor, without
|
||||
/// installing a modal cursor state that automation cannot safely own.
|
||||
/// </summary>
|
||||
public bool TryApplyItem(uint itemGuid, uint targetGuid)
|
||||
{
|
||||
if (itemGuid == 0u || targetGuid == 0u)
|
||||
return false;
|
||||
if (_objects.Get(itemGuid) is not { } item
|
||||
|| _objects.Get(targetGuid) is not { } target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!ConsumeUseThrottle())
|
||||
return true;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
|
||||
var input = new ItemUsePolicyInput(
|
||||
Snapshot(item),
|
||||
_playerGuid(),
|
||||
_groundObjectId(),
|
||||
CanMakeInventoryRequest,
|
||||
_activeVendorId(),
|
||||
BypassClassification: true,
|
||||
UseCurrentSelection: true,
|
||||
SelectedTarget: Snapshot(target),
|
||||
ConfirmVolatileRareUses: true,
|
||||
InNonCombatMode: _inNonCombatMode());
|
||||
ItemUsePolicyDecision decision = ItemInteractionPolicy.DecideUse(input);
|
||||
bool sends = decision.Actions.Any(static action =>
|
||||
action.Kind == ItemPolicyActionKind.SendUseWithTarget);
|
||||
return sends && ExecuteUseActions(decision.Actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail keyboard pickup entry point. <c>CPlayerSystem::PlaceInBackpack</c>
|
||||
/// publishes the waiting destination slot before issuing the move request,
|
||||
|
|
@ -1095,6 +1374,24 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return _autoWield.TryWield(item, targetMask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin/automation entry into the exact same AutoWield transaction used
|
||||
/// by inventory activation and paperdoll drops.
|
||||
/// </summary>
|
||||
public bool TryWieldItem(uint itemGuid, EquipMask requestedMask = EquipMask.None)
|
||||
{
|
||||
if (itemGuid == 0u || _objects.Get(itemGuid) is not { } item)
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
return requestedMask == EquipMask.None
|
||||
? _autoWield.TryWield(item)
|
||||
: _autoWield.TryWield(item, requestedMask);
|
||||
}
|
||||
|
||||
public bool IsAutoWieldBusy =>
|
||||
_autoWield.IsBusy || !_transactions.CanBeginRequest;
|
||||
|
||||
/// <summary>User combat-mode input supersedes AutoWield's retained mode.</summary>
|
||||
public void NotifyExplicitCombatModeRequest()
|
||||
=> _autoWield.NotifyExplicitCombatModeRequest();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue