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
|
|
@ -1136,7 +1136,13 @@ public sealed class GameWindow : IDisposable
|
|||
// rotating to face the target). Only set for close-range Use/PickUp;
|
||||
// far-range sends fire the wire packet immediately at SendUse/SendPickUp
|
||||
// time. Cleared before the deferred send fires — single-fire, no retry.
|
||||
private (uint Guid, bool IsPickup)? _pendingPostArrivalAction;
|
||||
private PendingPostArrivalAction? _pendingPostArrivalAction;
|
||||
|
||||
private readonly record struct PendingPostArrivalAction(
|
||||
uint Guid,
|
||||
bool IsPickup,
|
||||
uint DestinationContainerId = 0u,
|
||||
int Placement = 0);
|
||||
|
||||
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
|
||||
// TargetManager voyeur system, registered in _physicsHosts so remote
|
||||
|
|
@ -2127,7 +2133,13 @@ public sealed class GameWindow : IDisposable
|
|||
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
|
||||
Objects,
|
||||
playerGuid: () => _playerServerGuid,
|
||||
sendUse: g => _liveSession?.SendUse(g),
|
||||
// ItemHolder::UseObject is the common policy owner, but world
|
||||
// activation still has to pass through the application-level
|
||||
// use adapter. It owns speculative turn/move, the close-range
|
||||
// deferred send, and ACE's far-range MoveTo callback. Calling
|
||||
// WorldSession directly here made keyboard R approach a corpse
|
||||
// without completing the same open transaction as double-click.
|
||||
sendUse: SendUse,
|
||||
sendExamine: g => _liveSession?.SendAppraise(g),
|
||||
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
|
||||
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||
|
|
@ -2144,8 +2156,19 @@ public sealed class GameWindow : IDisposable
|
|||
playerOnGround: GetDebugPlayerOnGround,
|
||||
inNonCombatMode: () => Combat.CurrentMode
|
||||
== AcDream.Core.Combat.CombatMode.NonCombat,
|
||||
combatState: Combat,
|
||||
sendChangeCombatMode: mode =>
|
||||
_liveSession?.SendChangeCombatMode(mode),
|
||||
isComponentPack: magicCatalog.IsComponentPack,
|
||||
placeInBackpack: SendPickUp,
|
||||
backpackContainerId: () =>
|
||||
_retailUiRuntime?.InventoryPanelController?.CurrentOpenContainerId
|
||||
?? _playerServerGuid,
|
||||
// Retail ItemHolder::DetermineUseResult treats children of the
|
||||
// current ground object as loot. Keep this late-bound because
|
||||
// ViewContents can replace/close the external container while
|
||||
// the retained controller remains alive.
|
||||
groundObjectId: () => _externalContainers.CurrentContainerId,
|
||||
sendSplitToWorld: (item, amount) =>
|
||||
_liveSession?.SendStackableSplitTo3D(item, amount),
|
||||
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
||||
|
|
@ -13258,9 +13281,6 @@ public sealed class GameWindow : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
if (IsExternalContainerTarget(guid))
|
||||
_externalContainers.RequestOpen(guid);
|
||||
|
||||
// B.6/R4-V5: install a speculative local TurnToObject/MoveToObject
|
||||
// through the player's MoveToManager so close-range Use rotates the
|
||||
// body to face before the action fires. For FAR targets, ACE's
|
||||
|
|
@ -13278,7 +13298,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
// Defer the wire packet — OnAutoWalkArrivedSendDeferredAction
|
||||
// will fire it after rotation completes.
|
||||
_pendingPostArrivalAction = (guid, false);
|
||||
_pendingPostArrivalAction = new PendingPostArrivalAction(guid, IsPickup: false);
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
|
||||
return;
|
||||
|
|
@ -13318,6 +13338,9 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
private void SendPickUp(uint itemGuid)
|
||||
=> SendPickUp(itemGuid, _playerServerGuid, placement: 0);
|
||||
|
||||
private void SendPickUp(uint itemGuid, uint destinationContainerId, int placement)
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
|
|
@ -13362,7 +13385,11 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
if (closeRange)
|
||||
{
|
||||
_pendingPostArrivalAction = (itemGuid, true);
|
||||
_pendingPostArrivalAction = new PendingPostArrivalAction(
|
||||
itemGuid,
|
||||
IsPickup: true,
|
||||
destinationContainerId,
|
||||
placement);
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}");
|
||||
return;
|
||||
|
|
@ -13375,9 +13402,9 @@ public sealed class GameWindow : IDisposable
|
|||
// ACE's chain alive.
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
||||
seq, itemGuid, _playerServerGuid, placement: 0);
|
||||
seq, itemGuid, destinationContainerId, placement);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
|
||||
Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{destinationContainerId:X8} placement={placement} seq={seq}");
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
{
|
||||
string label = DescribeLiveEntity(itemGuid);
|
||||
|
|
@ -13398,7 +13425,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private void OnAutoWalkArrivedSendDeferredAction()
|
||||
{
|
||||
if (_pendingPostArrivalAction is not (uint guid, bool isPickup) pending)
|
||||
if (_pendingPostArrivalAction is not { } pending)
|
||||
return;
|
||||
_pendingPostArrivalAction = null;
|
||||
|
||||
|
|
@ -13407,18 +13434,21 @@ public sealed class GameWindow : IDisposable
|
|||
return;
|
||||
|
||||
var seq = _liveSession.NextGameActionSequence();
|
||||
if (isPickup)
|
||||
if (pending.IsPickup)
|
||||
{
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
||||
seq, guid, _playerServerGuid, placement: 0);
|
||||
seq,
|
||||
pending.Guid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.5] pickup-deferred item=0x{guid:X8} container=0x{_playerServerGuid:X8} seq={seq}");
|
||||
Console.WriteLine($"[B.5] pickup-deferred item=0x{pending.Guid:X8} container=0x{pending.DestinationContainerId:X8} placement={pending.Placement} seq={seq}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
|
||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, pending.Guid);
|
||||
_liveSession.SendGameAction(body);
|
||||
Console.WriteLine($"[B.4b] use-deferred guid=0x{guid:X8} seq={seq}");
|
||||
Console.WriteLine($"[B.4b] use-deferred guid=0x{pending.Guid:X8} seq={seq}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -13497,20 +13527,6 @@ public sealed class GameWindow : IDisposable
|
|||
return dx * dx + dy * dy <= useRadius * useRadius;
|
||||
}
|
||||
|
||||
private bool IsExternalContainerTarget(uint guid)
|
||||
{
|
||||
if (Objects.Get(guid) is { } item
|
||||
&& (item.ContainerTypeHint != 0u
|
||||
|| item.Type.HasFlag(AcDream.Core.Items.ItemType.Container)
|
||||
|| item.ItemsCapacity > 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return LastSpawns.TryGetValue(guid, out var spawn)
|
||||
&& (spawn.ObjectDescriptionFlags.GetValueOrDefault() & 0x2000u) != 0u;
|
||||
}
|
||||
|
||||
private void InstallSpeculativeTurnToTarget(uint targetGuid)
|
||||
{
|
||||
if (_playerController is not { } pc || pc.MoveTo is null) return;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -40,7 +41,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly Func<bool> _playerOnGround;
|
||||
private readonly Func<bool> _inNonCombatMode;
|
||||
private readonly Func<uint, bool> _isComponentPack;
|
||||
private readonly Action<uint>? _placeInBackpack;
|
||||
private readonly Action<uint, uint, int>? _placeInBackpack;
|
||||
private readonly Func<uint> _backpackContainerId;
|
||||
private readonly Action<uint>? _requestExternalContainer;
|
||||
private readonly Action<ItemPolicyAction>? _auxiliaryAction;
|
||||
private readonly InteractionState _interactionState;
|
||||
|
|
@ -73,7 +75,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Func<bool>? playerOnGround = null,
|
||||
Func<bool>? inNonCombatMode = null,
|
||||
Func<uint, bool>? isComponentPack = null,
|
||||
Action<uint>? placeInBackpack = null,
|
||||
Action<uint, uint, int>? placeInBackpack = null,
|
||||
Func<uint>? backpackContainerId = null,
|
||||
Action<ItemPolicyAction>? auxiliaryAction = null,
|
||||
Action<uint, uint>? sendSplitToWorld = null,
|
||||
Func<uint>? selectedObjectId = null,
|
||||
|
|
@ -83,7 +86,9 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Func<bool>? dragOnPlayerOpensSecureTrade = null,
|
||||
Action<string>? systemMessage = null,
|
||||
Action<uint, uint, uint, uint>? sendSplitToContainer = null,
|
||||
Action<uint>? requestExternalContainer = null)
|
||||
Action<uint>? requestExternalContainer = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -105,6 +110,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_inNonCombatMode = inNonCombatMode ?? (() => false);
|
||||
_isComponentPack = isComponentPack ?? (_ => false);
|
||||
_placeInBackpack = placeInBackpack;
|
||||
_backpackContainerId = backpackContainerId ?? _playerGuid;
|
||||
_requestExternalContainer = requestExternalContainer;
|
||||
_auxiliaryAction = auxiliaryAction;
|
||||
_selectedObjectId = selectedObjectId ?? (() => 0u);
|
||||
|
|
@ -119,11 +125,20 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_sendWield,
|
||||
sendPutItemInContainer,
|
||||
_toast,
|
||||
_systemMessage);
|
||||
_systemMessage,
|
||||
combatState,
|
||||
sendChangeCombatMode);
|
||||
}
|
||||
|
||||
public event Action? StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CM_Item::SendNotice_ShowPendingInPlayer</c>: the inventory
|
||||
/// panel inserts a waiting projection before the pickup request is sent.
|
||||
/// The destination and placement are the same values sent on the wire.
|
||||
/// </summary>
|
||||
public event Action<uint, uint, int>? PendingBackpackPlacementRequested;
|
||||
|
||||
public uint PlayerGuid => _playerGuid();
|
||||
|
||||
public InteractionState InteractionState => _interactionState;
|
||||
|
|
@ -399,8 +414,18 @@ public sealed class ItemInteractionController : IDisposable
|
|||
switch (action.Kind)
|
||||
{
|
||||
case ItemPolicyActionKind.PlaceInBackpack:
|
||||
_placeInBackpack?.Invoke(action.ObjectId);
|
||||
acted |= _placeInBackpack is not null;
|
||||
if (_placeInBackpack is null)
|
||||
break;
|
||||
uint containerId = _backpackContainerId();
|
||||
if (containerId == 0u)
|
||||
containerId = _playerGuid();
|
||||
const int placement = 0;
|
||||
PendingBackpackPlacementRequested?.Invoke(
|
||||
action.ObjectId,
|
||||
containerId,
|
||||
placement);
|
||||
_placeInBackpack(action.ObjectId, containerId, placement);
|
||||
acted = true;
|
||||
break;
|
||||
case ItemPolicyActionKind.WieldRight:
|
||||
case ItemPolicyActionKind.WieldLeft:
|
||||
|
|
|
|||
|
|
@ -246,6 +246,18 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
|
||||
private void OnExternalContainerChanged(ExternalContainerTransition transition)
|
||||
{
|
||||
if (transition.Kind == ExternalContainerTransitionKind.ReplacementRequested)
|
||||
{
|
||||
// Retail SetGroundObject(new) first publishes SetGroundObject(0)
|
||||
// to the panel and only repopulates it when the new root's
|
||||
// ViewContents is accepted.
|
||||
_openContainer = 0u;
|
||||
_closeRequested = false;
|
||||
_window.Hide();
|
||||
ClearLists();
|
||||
return;
|
||||
}
|
||||
|
||||
if (transition.ContainerId == 0u)
|
||||
{
|
||||
_openContainer = 0u;
|
||||
|
|
|
|||
|
|
@ -180,7 +180,10 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.Cleared += OnObjectsCleared;
|
||||
_selection.Changed += OnSelectionChanged;
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
||||
_itemInteraction.PendingBackpackPlacementRequested += OnPendingBackpackPlacementRequested;
|
||||
}
|
||||
|
||||
Populate();
|
||||
}
|
||||
|
|
@ -269,6 +272,26 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Populate();
|
||||
}
|
||||
private void OnInteractionStateChanged() => ApplyIndicators();
|
||||
private void OnPendingBackpackPlacementRequested(
|
||||
uint itemId,
|
||||
uint containerId,
|
||||
int placement)
|
||||
{
|
||||
if (_pendingListPlacement is not null
|
||||
|| itemId == 0u
|
||||
|| containerId != EffectiveOpen()
|
||||
|| _objects.Get(itemId) is not { } item
|
||||
|| IsBag(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingListPlacement = new PendingListPlacement(
|
||||
itemId,
|
||||
containerId,
|
||||
placement);
|
||||
Populate();
|
||||
}
|
||||
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
|
||||
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
||||
{
|
||||
|
|
@ -411,6 +434,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
private uint EffectiveOpen() => _openContainer != 0 ? _openContainer : _playerGuid();
|
||||
|
||||
/// <summary>The owned destination retail PlaceInBackpack currently uses.</summary>
|
||||
public uint CurrentOpenContainerId => EffectiveOpen();
|
||||
|
||||
/// <summary>Add a populated cell wired to its click role: container cell → open+select,
|
||||
/// item cell → select-only.</summary>
|
||||
private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false)
|
||||
|
|
@ -785,6 +811,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.Cleared -= OnObjectsCleared;
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||
_itemInteraction.PendingBackpackPlacementRequested -= OnPendingBackpackPlacementRequested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public SelectedObjectController? SelectedObjectController { get; private set; }
|
||||
public UiViewport? PaperdollViewportWidget { get; private set; }
|
||||
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
||||
public InventoryController? InventoryPanelController { get; private set; }
|
||||
public RetailDialogFactory? DialogFactory { get; private set; }
|
||||
public ExternalContainerController? ExternalContainerController { get; private set; }
|
||||
|
||||
|
|
@ -1603,6 +1604,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
() => CloseWindow(WindowNames.Inventory),
|
||||
StackSplitQuantity,
|
||||
b.ResolveDragIcon);
|
||||
InventoryPanelController = inventory;
|
||||
PaperdollController paperdoll = PaperdollController.Bind(
|
||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.ItemInteraction,
|
||||
contents, _bindings.Assets.DefaultFont, paperdollClickMap,
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@ public sealed class ExternalContainerLifecycleController : IDisposable
|
|||
if (transition.PreviousContainerId != 0u)
|
||||
_objects.StopViewingContentsTree(transition.PreviousContainerId);
|
||||
|
||||
// Only replacement emits Event_NoLongerViewingContents. The authored
|
||||
// close/range path instead sends Use(root) and waits for event 0x0052.
|
||||
if (transition.Kind == ExternalContainerTransitionKind.Replaced
|
||||
// SetGroundObject(new, notifyServer:true) emits this while the new
|
||||
// request is still pending, not after ViewContents arrives. The
|
||||
// authored close/range path instead sends Use(root) and waits for
|
||||
// event 0x0052.
|
||||
if (transition.Kind == ExternalContainerTransitionKind.ReplacementRequested
|
||||
&& transition.PreviousContainerId != 0u)
|
||||
{
|
||||
_sendNoLongerViewingContents(transition.PreviousContainerId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue