fix(items): finish corpse pickup and combat switching
Port retail's first-slot ShowPendingInPlayer path for double-click loot and carry the current owned-container destination through deferred pickup. Retire the previous ground-container view as soon as a replacement is requested so its range close cannot cancel ACE's active MoveTo chain. Preserve active-combat weapon intent across ACE's authoritative wand-to-missile stance tail while leaving peace-mode switches unchanged. Release build succeeds and all 5,885 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
d51a0fc825
commit
0392c6d721
17 changed files with 631 additions and 81 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -63,8 +64,11 @@ internal sealed class AutoWieldController : IDisposable
|
|||
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
|
||||
private readonly Action<string>? _toast;
|
||||
private readonly Action<string>? _systemMessage;
|
||||
private readonly CombatState? _combatState;
|
||||
private readonly Action<CombatMode>? _sendChangeCombatMode;
|
||||
|
||||
private PendingSwitch? _pendingSwitch;
|
||||
private PendingCombatSettlement? _pendingCombatSettlement;
|
||||
private bool _disposed;
|
||||
|
||||
public bool IsBusy => _pendingSwitch is not null;
|
||||
|
|
@ -75,7 +79,9 @@ internal sealed class AutoWieldController : IDisposable
|
|||
Action<uint, uint>? sendWield,
|
||||
Action<uint, uint, int>? sendPutItemInContainer,
|
||||
Action<string>? toast,
|
||||
Action<string>? systemMessage = null)
|
||||
Action<string>? systemMessage = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -83,12 +89,16 @@ internal sealed class AutoWieldController : IDisposable
|
|||
_sendPutItemInContainer = sendPutItemInContainer;
|
||||
_toast = toast;
|
||||
_systemMessage = systemMessage;
|
||||
_combatState = combatState;
|
||||
_sendChangeCombatMode = sendChangeCombatMode;
|
||||
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.ObjectRemoved += OnObjectRemoved;
|
||||
_objects.MoveRequestFailed += OnMoveRequestFailed;
|
||||
_objects.WieldConfirmed += OnWieldConfirmed;
|
||||
_objects.Cleared += OnObjectsCleared;
|
||||
if (_combatState is not null)
|
||||
_combatState.CombatModeChanged += OnCombatModeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -97,7 +107,10 @@ internal sealed class AutoWieldController : IDisposable
|
|||
/// as retail's inventory-ready gate prevents a second concurrent request.
|
||||
/// </summary>
|
||||
public bool TryWield(ClientObject item)
|
||||
=> TryWield(item, EquipMask.None);
|
||||
{
|
||||
_pendingCombatSettlement = null;
|
||||
return TryWield(item, EquipMask.None, combatModeAfterWield: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute AutoWield while retaining the paperdoll's resolved target side.
|
||||
|
|
@ -106,6 +119,15 @@ internal sealed class AutoWieldController : IDisposable
|
|||
/// bypassing the transaction with a direct wire request.
|
||||
/// </summary>
|
||||
public bool TryWield(ClientObject item, EquipMask requestedMask)
|
||||
{
|
||||
_pendingCombatSettlement = null;
|
||||
return TryWield(item, requestedMask, combatModeAfterWield: null);
|
||||
}
|
||||
|
||||
private bool TryWield(
|
||||
ClientObject item,
|
||||
EquipMask requestedMask,
|
||||
CombatMode? combatModeAfterWield)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
|
|
@ -130,9 +152,13 @@ internal sealed class AutoWieldController : IDisposable
|
|||
if (GetEquippedObjectAtLocation(
|
||||
requestedMask, priority: 0, item.ObjectId) is { } blocker)
|
||||
{
|
||||
return BeginWeaponReplacement(item.ObjectId, blocker, requestedMask);
|
||||
return BeginWeaponReplacement(
|
||||
item.ObjectId,
|
||||
blocker,
|
||||
requestedMask,
|
||||
combatModeAfterWield);
|
||||
}
|
||||
return SendWield(item, requestedMask);
|
||||
return SendWield(item, requestedMask, combatModeAfterWield);
|
||||
}
|
||||
|
||||
EquipMask weaponLocation = requestedMask == EquipMask.None
|
||||
|
|
@ -140,10 +166,24 @@ internal sealed class AutoWieldController : IDisposable
|
|||
: requestedMask & WeaponReadyMask;
|
||||
if (weaponLocation != EquipMask.None)
|
||||
{
|
||||
// Preserve the ready mode only when the transaction starts in
|
||||
// combat. During a replacement ACE legitimately reports the
|
||||
// intermediate stances used to put the old weapon away; those
|
||||
// must not erase the user's original intent before the new wield
|
||||
// is authoritatively confirmed.
|
||||
combatModeAfterWield ??= (_combatState?.CurrentMode ?? CombatMode.NonCombat)
|
||||
== CombatMode.NonCombat
|
||||
? null
|
||||
: CombatModeForWeaponLocation(weaponLocation);
|
||||
|
||||
ClientObject? blocker = GetEquippedObjectAtLocation(
|
||||
WeaponReadyMask, priority: 0, item.ObjectId);
|
||||
if (blocker is not null)
|
||||
return BeginWeaponReplacement(item.ObjectId, blocker, weaponLocation);
|
||||
return BeginWeaponReplacement(
|
||||
item.ObjectId,
|
||||
blocker,
|
||||
weaponLocation,
|
||||
combatModeAfterWield);
|
||||
|
||||
// Retail checks secondary blockers only after the primary weapon
|
||||
// slot is vacant, then re-enters AutoWield after each confirmed
|
||||
|
|
@ -151,15 +191,23 @@ internal sealed class AutoWieldController : IDisposable
|
|||
if (BlocksUseOfShield(item)
|
||||
&& GetEquippedObjectAtLocation(
|
||||
EquipMask.Shield, priority: 0, item.ObjectId) is { } shield)
|
||||
return BeginWeaponReplacement(item.ObjectId, shield, weaponLocation);
|
||||
return BeginWeaponReplacement(
|
||||
item.ObjectId,
|
||||
shield,
|
||||
weaponLocation,
|
||||
combatModeAfterWield);
|
||||
|
||||
if (item.AmmoType is > 0
|
||||
&& GetEquippedObjectAtLocation(
|
||||
EquipMask.MissileAmmo, priority: 0, item.ObjectId) is { } ammo
|
||||
&& ammo.AmmoType != item.AmmoType)
|
||||
return BeginWeaponReplacement(item.ObjectId, ammo, weaponLocation);
|
||||
return BeginWeaponReplacement(
|
||||
item.ObjectId,
|
||||
ammo,
|
||||
weaponLocation,
|
||||
combatModeAfterWield);
|
||||
|
||||
return SendWield(item, weaponLocation);
|
||||
return SendWield(item, weaponLocation, combatModeAfterWield);
|
||||
}
|
||||
|
||||
EquipMask mask = requestedMask != EquipMask.None
|
||||
|
|
@ -171,13 +219,14 @@ internal sealed class AutoWieldController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
return SendWield(item, mask);
|
||||
return SendWield(item, mask, combatModeAfterWield: null);
|
||||
}
|
||||
|
||||
private bool BeginWeaponReplacement(
|
||||
uint requestedItemId,
|
||||
ClientObject blockingItem,
|
||||
EquipMask requestedMask)
|
||||
EquipMask requestedMask,
|
||||
CombatMode? combatModeAfterWield)
|
||||
{
|
||||
if (_sendPutItemInContainer is null)
|
||||
{
|
||||
|
|
@ -192,7 +241,8 @@ internal sealed class AutoWieldController : IDisposable
|
|||
_pendingSwitch = new PendingSwitch(
|
||||
requestedItemId,
|
||||
blockingItem.ObjectId,
|
||||
requestedMask);
|
||||
requestedMask,
|
||||
combatModeAfterWield);
|
||||
|
||||
// Retail AttemptToPlaceInContainer(blockingID, playerID, 0, 1, 0).
|
||||
// Do not change the local equip projection yet: the server's move event
|
||||
|
|
@ -203,14 +253,18 @@ internal sealed class AutoWieldController : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool SendWield(ClientObject item, EquipMask mask)
|
||||
private bool SendWield(
|
||||
ClientObject item,
|
||||
EquipMask mask,
|
||||
CombatMode? combatModeAfterWield)
|
||||
{
|
||||
if (_sendWield is null)
|
||||
return false;
|
||||
_pendingSwitch = new PendingSwitch(
|
||||
item.ObjectId,
|
||||
BlockingItemId: 0,
|
||||
RequestedMask: mask);
|
||||
RequestedMask: mask,
|
||||
CombatModeAfterWield: combatModeAfterWield);
|
||||
if (!_objects.WieldItemOptimistic(item.ObjectId, _playerGuid(), mask))
|
||||
{
|
||||
_pendingSwitch = null;
|
||||
|
|
@ -235,14 +289,75 @@ internal sealed class AutoWieldController : IDisposable
|
|||
// vacant ready slot and send GetAndWieldItem for the requested weapon.
|
||||
_pendingSwitch = null;
|
||||
if (_objects.Get(pending.RequestedItemId) is { } requested)
|
||||
TryWield(requested, pending.RequestedMask);
|
||||
TryWield(
|
||||
requested,
|
||||
pending.RequestedMask,
|
||||
pending.CombatModeAfterWield);
|
||||
}
|
||||
|
||||
private void OnWieldConfirmed(uint itemId)
|
||||
{
|
||||
if (_pendingSwitch is { BlockingItemId: 0 } pending
|
||||
&& itemId == pending.RequestedItemId)
|
||||
{
|
||||
_pendingSwitch = null;
|
||||
if (pending.CombatModeAfterWield is { } readyMode)
|
||||
{
|
||||
// ACE publishes its queued dequip tail after WieldObject. Wait
|
||||
// for that first authoritative post-wield mode notice before
|
||||
// responding; sending inside WieldConfirmed races the tail.
|
||||
_pendingCombatSettlement = new(
|
||||
itemId,
|
||||
readyMode,
|
||||
CombatSettlementPhase.AwaitingPostWieldMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCombatModeChanged(CombatMode mode)
|
||||
{
|
||||
if (_pendingCombatSettlement is not { } settlement)
|
||||
return;
|
||||
if (mode == settlement.ReadyMode)
|
||||
{
|
||||
if (settlement.Phase == CombatSettlementPhase.SawTransitionalPeace)
|
||||
{
|
||||
// ACE selected the correct weapon stance, but its queued
|
||||
// dequip callback still has one trailing Peace notice.
|
||||
_pendingCombatSettlement = settlement with
|
||||
{
|
||||
Phase = CombatSettlementPhase.SawReadyAfterPeace,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// A server that immediately settled on the intended ready
|
||||
// stance needs no compatibility request.
|
||||
_pendingCombatSettlement = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode != CombatMode.NonCombat)
|
||||
return;
|
||||
|
||||
if (settlement.Phase == CombatSettlementPhase.SawReadyAfterPeace)
|
||||
{
|
||||
// This is ACE's trailing Peace after it already selected the new
|
||||
// weapon stance. The server's stance shuffle is now settled, so a
|
||||
// normal request from this notice is accepted and ordered after it.
|
||||
_pendingCombatSettlement = null;
|
||||
_sendChangeCombatMode?.Invoke(settlement.ReadyMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
// First transitional Peace. ACE is still shuffling the old weapon;
|
||||
// requesting here races the later queued callback.
|
||||
_pendingCombatSettlement = settlement with
|
||||
{
|
||||
Phase = CombatSettlementPhase.SawTransitionalPeace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
||||
|
|
@ -259,10 +374,16 @@ internal sealed class AutoWieldController : IDisposable
|
|||
&& (item.ObjectId == pending.BlockingItemId
|
||||
|| item.ObjectId == pending.RequestedItemId))
|
||||
_pendingSwitch = null;
|
||||
if (_pendingCombatSettlement is { } settlement
|
||||
&& item.ObjectId == settlement.ItemId)
|
||||
_pendingCombatSettlement = null;
|
||||
}
|
||||
|
||||
private void OnObjectsCleared()
|
||||
=> _pendingSwitch = null;
|
||||
{
|
||||
_pendingSwitch = null;
|
||||
_pendingCombatSettlement = null;
|
||||
}
|
||||
|
||||
private EquipMask BestAvailableEquipMask(ClientObject item)
|
||||
{
|
||||
|
|
@ -357,21 +478,48 @@ internal sealed class AutoWieldController : IDisposable
|
|||
|| item.Type.HasFlag(ItemType.Caster);
|
||||
}
|
||||
|
||||
private static CombatMode? CombatModeForWeaponLocation(EquipMask location)
|
||||
{
|
||||
if ((location & EquipMask.MissileWeapon) != 0)
|
||||
return CombatMode.Missile;
|
||||
if ((location & EquipMask.Held) != 0)
|
||||
return CombatMode.Magic;
|
||||
if ((location & (EquipMask.MeleeWeapon | EquipMask.TwoHanded)) != 0)
|
||||
return CombatMode.Melee;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_pendingSwitch = null;
|
||||
_pendingCombatSettlement = null;
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||
_objects.MoveRequestFailed -= OnMoveRequestFailed;
|
||||
_objects.WieldConfirmed -= OnWieldConfirmed;
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
if (_combatState is not null)
|
||||
_combatState.CombatModeChanged -= OnCombatModeChanged;
|
||||
}
|
||||
|
||||
private readonly record struct PendingSwitch(
|
||||
uint RequestedItemId,
|
||||
uint BlockingItemId,
|
||||
EquipMask RequestedMask);
|
||||
EquipMask RequestedMask,
|
||||
CombatMode? CombatModeAfterWield);
|
||||
|
||||
private readonly record struct PendingCombatSettlement(
|
||||
uint ItemId,
|
||||
CombatMode ReadyMode,
|
||||
CombatSettlementPhase Phase);
|
||||
|
||||
private enum CombatSettlementPhase
|
||||
{
|
||||
AwaitingPostWieldMode,
|
||||
SawTransitionalPeace,
|
||||
SawReadyAfterPeace,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue