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
|
|
@ -516,6 +516,18 @@ optimistic pre-confirm projection instead of treating equipment as backpack
|
||||||
contents. The connected gate completed NonCombat → Missile → NonCombat →
|
contents. The connected gate completed NonCombat → Missile → NonCombat →
|
||||||
Missile, including a Jump press, without a server rejection.
|
Missile, including a Jump press, without a server rejection.
|
||||||
|
|
||||||
|
**2026-07-17 active-switch follow-up:** A connected Magic wand -> missile
|
||||||
|
weapon trace exposed ACE's queued `Magic -> Melee -> NonCombat -> Missile ->
|
||||||
|
NonCombat` tail. `AutoWieldController` now retains the desired ready mode
|
||||||
|
through every confirmed blocker move and, only for a transaction that began
|
||||||
|
in active combat, arms settlement on authoritative `WieldObject`. An already-
|
||||||
|
correct first mode notice clears it; ACE's observed `NonCombat -> Missile ->
|
||||||
|
NonCombat` sequence sends the normal mode request from its trailing notice,
|
||||||
|
after the queued callback that caused that notice.
|
||||||
|
This keeps the missile bar present while retaining
|
||||||
|
ACE ownership of every state and motion; peace-mode switching is unchanged.
|
||||||
|
The narrow ACE compatibility difference is registered as AP-118.
|
||||||
|
|
||||||
**Research:** `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`
|
**Research:** `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`
|
||||||
|
|
||||||
**Acceptance:** Relaunch with the crossbow already equipped. The combat button
|
**Acceptance:** Relaunch with the crossbow already equipped. The combat button
|
||||||
|
|
@ -2294,6 +2306,19 @@ R5/R6 touches the action list. **Where:**
|
||||||
|
|
||||||
**Research:** `docs/research/2026-07-17-retail-external-container-looting-pseudocode.md`.
|
**Research:** `docs/research/2026-07-17-retail-external-container-looting-pseudocode.md`.
|
||||||
|
|
||||||
|
**2026-07-17 interaction-routing follow-up:** The production binding routes
|
||||||
|
keyboard R through the same application Use adapter as retained double-click,
|
||||||
|
preserving approach-then-send completion. It also supplies the live external
|
||||||
|
root to `DetermineUseResult`, so double-clicking a corpse child requests
|
||||||
|
loot-to-pack rather than ordinary item use.
|
||||||
|
|
||||||
|
**2026-07-17 pending/replacement follow-up:** Double-click loot now follows
|
||||||
|
`ShowPendingInPlayer`: it inserts a waiting projection in slot 1 of the current
|
||||||
|
owned pack and sends that same destination/placement on the wire. A new corpse
|
||||||
|
request now retires the previous ground-object presentation immediately, as
|
||||||
|
retail `SetGroundObject` does. This removes the stale range watcher whose close
|
||||||
|
Use canceled ACE's active MoveTo chain before the new corpse could open.
|
||||||
|
|
||||||
**Acceptance:** replacement sends `NoLongerViewingContents` once; authored close/range sends `Use` once and waits for `0x0052`; owned main/side-pack navigation sends neither; root/nested projections retire without deleting objects; corpse Use never attempts to pick up the corpse; a full-stack loot drop immediately shows a ghosted pending copy at the chosen destination slot and confirmation/failure clears it; canonical transfers remain server-authoritative. Focused Core, Core.Net, and App tests pin each path; visual acceptance remains the normal user gate.
|
**Acceptance:** replacement sends `NoLongerViewingContents` once; authored close/range sends `Use` once and waits for `0x0052`; owned main/side-pack navigation sends neither; root/nested projections retire without deleting objects; corpse Use never attempts to pick up the corpse; a full-stack loot drop immediately shows a ghosted pending copy at the chosen destination slot and confirmation/failure clears it; canonical transfers remain server-authoritative. Focused Core, Core.Net, and App tests pin each path; visual acceptance remains the normal user gate.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
| AP-115 | The DAT-authored portal-space viewport and its animation `SoundTweakedHook` are live, but the separate `ClientUISystem` enter/exit sound enums and centered repeating `"In Portal Space - Please Wait..."` display string are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; teleport event switch in `GameWindow.cs` | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
|
| AP-115 | The DAT-authored portal-space viewport and its animation `SoundTweakedHook` are live, but the separate `ClientUISystem` enter/exit sound enums and centered repeating `"In Portal Space - Please Wait..."` display string are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; teleport event switch in `GameWindow.cs` | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
|
||||||
| AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
|
| AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
|
||||||
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
|
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
|
||||||
|
| AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, an immediately correct mode clears settlement; local ACE's observed `NonCombat -> ready mode -> NonCombat` tail causes one normal `ChangeCombatMode` request from the trailing notice. Retail's client does not need this extra request against the retail server. | `src/AcDream.App/UI/AutoWieldController.cs`; production binding in `GameWindow.cs` | Local ACE queues a trailing NonCombat callback during wand-to-bow replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later replacement/logout clears it; peace-mode equips send none | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`; ACE `Player_Inventory.TryShuffleStance` / `TryDequipObjectWithNetworking` |
|
||||||
|
|
||||||
## 4. Temporary stopgap (TS) — 33 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)
|
## 4. Temporary stopgap (TS) — 33 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,17 @@ on server-confirmed WieldObject(item):
|
||||||
and pending.blockingItem is none:
|
and pending.blockingItem is none:
|
||||||
clear pending
|
clear pending
|
||||||
|
|
||||||
|
if the transaction began in an active combat mode:
|
||||||
|
// ACE compatibility boundary; see below.
|
||||||
|
retain the ready mode implied by the confirmed equip location
|
||||||
|
|
||||||
|
on combat-mode notices after confirmed WieldObject:
|
||||||
|
if the first notice is the retained ready mode:
|
||||||
|
clear the retained mode; the server already settled correctly
|
||||||
|
else if notices form NonCombat -> retained ready mode -> NonCombat:
|
||||||
|
clear the retained mode
|
||||||
|
request that ready mode from ACE's trailing NonCombat notice
|
||||||
|
|
||||||
on PrivateUpdatePropertyInt(property = CombatMode, value):
|
on PrivateUpdatePropertyInt(property = CombatMode, value):
|
||||||
if value is exactly NonCombat, Melee, Missile, or Magic:
|
if value is exactly NonCombat, Melee, Missile, or Magic:
|
||||||
CombatState.SetCombatMode(value)
|
CombatState.SetCombatMode(value)
|
||||||
|
|
@ -119,11 +130,28 @@ on PrivateUpdatePropertyInt(property = CombatMode, value):
|
||||||
## Animation ownership
|
## Animation ownership
|
||||||
|
|
||||||
The client must not synthesize a local bow-to-sword animation. In peace mode,
|
The client must not synthesize a local bow-to-sword animation. In peace mode,
|
||||||
ACE leaves `CombatMode` as `NonCombat`. In combat mode, ACE selects the new
|
ACE leaves `CombatMode` as `NonCombat`. The inventory controller sequences the
|
||||||
weapon's stance and sends the authoritative motion stream plus PropertyInt 40.
|
two wire requests and waits for the old weapon's confirmed move; the existing
|
||||||
The inventory controller only sequences the two wire requests and waits for
|
motion and equipped-child render paths consume the server events.
|
||||||
the old weapon's confirmed move; the existing motion and equipped-child
|
|
||||||
render paths consume the server events.
|
### ACE active-combat compatibility boundary
|
||||||
|
|
||||||
|
A connected 2026-07-17 trace of Magic wand -> missile weapon showed ACE emit
|
||||||
|
`Magic -> Melee -> NonCombat -> Missile -> NonCombat`. This is a consequence of
|
||||||
|
ACE `Player_Inventory.TryShuffleStance` plus its queued dequip callbacks: the
|
||||||
|
server does select Missile for the new weapon, but a later queued callback
|
||||||
|
publishes NonCombat after the authoritative `WieldObject`. Retail's client does
|
||||||
|
not need a second request against the retail server.
|
||||||
|
|
||||||
|
acdream therefore preserves the user's desired ready mode only when AutoWield
|
||||||
|
begins in active combat. Authoritative `WieldObject` arms settlement. A server
|
||||||
|
whose first subsequent notice is already the desired mode needs no request.
|
||||||
|
Local ACE instead emits `NonCombat -> desired mode -> NonCombat`; the client
|
||||||
|
sends one normal `ChangeCombatMode` request from that trailing notice,
|
||||||
|
necessarily after the queued callback that caused it. All resulting state and animation remain server-
|
||||||
|
authoritative. No update is ignored, delayed, or locally fabricated, and a
|
||||||
|
transaction begun in peace sends no mode request. This narrow server-
|
||||||
|
compatibility adaptation is recorded as AP-118.
|
||||||
|
|
||||||
The request is not complete when `GetAndWieldItem` is merely sent. Retail
|
The request is not complete when `GetAndWieldItem` is merely sent. Retail
|
||||||
`UIAttemptWield` records `prevRequest = IR_WIELD` and leaves the item waiting
|
`UIAttemptWield` records `prevRequest = IR_WIELD` and leaves the item waiting
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ Named Sept-2013 retail symbols:
|
||||||
- `UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`
|
- `UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`
|
||||||
- `ClientUISystem::SetGroundObject @ 0x00564510`
|
- `ClientUISystem::SetGroundObject @ 0x00564510`
|
||||||
- `ClientUISystem::OnViewContents @ 0x00565870`
|
- `ClientUISystem::OnViewContents @ 0x00565870`
|
||||||
|
- `CPlayerSystem::PlaceInBackpack @ 0x0055D8C0`
|
||||||
|
- `gmInventoryUI::RecvNotice_ShowPendingInPlayer @ 0x004A6B50`
|
||||||
|
- `gmInventoryUI::RecvNotice_EndPendingInPlayer @ 0x004A68B0`
|
||||||
|
- `UIElement_ItemList::ItemList_InsertItem @ 0x004E3F40`
|
||||||
- `ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680`
|
- `ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680`
|
||||||
- `ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0`
|
- `ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0`
|
||||||
- `ItemHolder::AttemptToPlaceInContainer @ 0x00588140`
|
- `ItemHolder::AttemptToPlaceInContainer @ 0x00588140`
|
||||||
|
|
@ -69,9 +73,22 @@ thumb, and arrow sprites from LayoutDesc `0x2100003E`.
|
||||||
```text
|
```text
|
||||||
UseWorldObject(objectId):
|
UseWorldObject(objectId):
|
||||||
decision = ItemHolder policy
|
decision = ItemHolder policy
|
||||||
if decision is SetGroundObject:
|
|
||||||
expectedExternalRoot = objectId
|
|
||||||
send Use(objectId)
|
send Use(objectId)
|
||||||
|
if decision is SetGroundObject:
|
||||||
|
SetGroundObject(objectId, notifyServer = true)
|
||||||
|
|
||||||
|
SetGroundObject(newRoot, notifyServer):
|
||||||
|
if currentExternalRoot == newRoot:
|
||||||
|
return
|
||||||
|
oldRoot = currentExternalRoot
|
||||||
|
if oldRoot != 0:
|
||||||
|
retire oldRoot's temporary contents tree
|
||||||
|
publish SetGroundObject(0) to the external panel
|
||||||
|
if notifyServer:
|
||||||
|
send NoLongerViewingContents(oldRoot)
|
||||||
|
currentExternalRoot = newRoot
|
||||||
|
expectedExternalRoot = newRoot
|
||||||
|
// The panel remains empty until ViewContents for newRoot is accepted.
|
||||||
|
|
||||||
UseCorpse(corpseId):
|
UseCorpse(corpseId):
|
||||||
// A corpse is a TYPE_CONTAINER carrying BF_STUCK | BF_CORPSE.
|
// A corpse is a TYPE_CONTAINER carrying BF_STUCK | BF_CORPSE.
|
||||||
|
|
@ -86,11 +103,6 @@ OnViewContents(containerId, orderedEntries):
|
||||||
if containerId != expectedExternalRoot:
|
if containerId != expectedExternalRoot:
|
||||||
return
|
return
|
||||||
|
|
||||||
old = currentExternalRoot
|
|
||||||
if old != 0 and old != containerId:
|
|
||||||
objectTable.StopViewingContentsTree(old)
|
|
||||||
send NoLongerViewingContents(old) exactly once
|
|
||||||
|
|
||||||
currentExternalRoot = containerId
|
currentExternalRoot = containerId
|
||||||
expectedExternalRoot = containerId
|
expectedExternalRoot = containerId
|
||||||
externalPanel.SetGroundObject(containerId)
|
externalPanel.SetGroundObject(containerId)
|
||||||
|
|
@ -138,6 +150,14 @@ OnExternalItemDoubleClick(itemId):
|
||||||
ItemHolder.UseObject(itemId)
|
ItemHolder.UseObject(itemId)
|
||||||
// Because item.ContainerId is currentExternalRoot, retail policy emits
|
// Because item.ContainerId is currentExternalRoot, retail policy emits
|
||||||
// PlaceInBackpack; the server remains authoritative for the move.
|
// PlaceInBackpack; the server remains authoritative for the move.
|
||||||
|
destination = current open owned pack
|
||||||
|
destinationList.RemoveTrailingEmptyItem()
|
||||||
|
destinationList.AddEmptySlot(at index 0)
|
||||||
|
destinationList.AddItem(itemId)
|
||||||
|
destinationList.pendingItem.SetWaitingState(true)
|
||||||
|
send PutItemInContainer(itemId, destination, placement = 0)
|
||||||
|
// The pending item therefore reserves the FIRST slot and shifts the
|
||||||
|
// existing packed list to the right until the server confirms or rejects.
|
||||||
|
|
||||||
DragExternalItemToOwnedInventory(itemId, placement, splitAmount):
|
DragExternalItemToOwnedInventory(itemId, placement, splitAmount):
|
||||||
if splitAmount < full stack:
|
if splitAmount < full stack:
|
||||||
|
|
@ -171,8 +191,10 @@ DragOwnedItemToExternalContainer(itemId, placement, splitAmount):
|
||||||
## Architectural mapping
|
## Architectural mapping
|
||||||
|
|
||||||
- `ExternalContainerState` in Core owns only expected/current external-root
|
- `ExternalContainerState` in Core owns only expected/current external-root
|
||||||
identity and transition ordering. It filters ACE's nested `ViewContents`
|
identity and transition ordering. A replacement request retires current
|
||||||
packets so they cannot replace the visible root.
|
presentation immediately, matching `SetGroundObject`; accepted
|
||||||
|
`ViewContents` later opens the requested root. It filters ACE's nested
|
||||||
|
`ViewContents` packets so they cannot replace the visible root.
|
||||||
- `ExternalContainerLifecycleController` in App observes those transitions,
|
- `ExternalContainerLifecycleController` in App observes those transitions,
|
||||||
retires the replaced root's temporary preview tree, then owns the
|
retires the replaced root's temporary preview tree, then owns the
|
||||||
replacement-only `NoLongerViewingContents` network side effect. The wire
|
replacement-only `NoLongerViewingContents` network side effect. The wire
|
||||||
|
|
|
||||||
|
|
@ -1136,7 +1136,13 @@ public sealed class GameWindow : IDisposable
|
||||||
// rotating to face the target). Only set for close-range Use/PickUp;
|
// rotating to face the target). Only set for close-range Use/PickUp;
|
||||||
// far-range sends fire the wire packet immediately at SendUse/SendPickUp
|
// far-range sends fire the wire packet immediately at SendUse/SendPickUp
|
||||||
// time. Cleared before the deferred send fires — single-fire, no retry.
|
// 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
|
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
|
||||||
// TargetManager voyeur system, registered in _physicsHosts so remote
|
// TargetManager voyeur system, registered in _physicsHosts so remote
|
||||||
|
|
@ -2127,7 +2133,13 @@ public sealed class GameWindow : IDisposable
|
||||||
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
|
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
playerGuid: () => _playerServerGuid,
|
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),
|
sendExamine: g => _liveSession?.SendAppraise(g),
|
||||||
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
|
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
|
||||||
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||||
|
|
@ -2144,8 +2156,19 @@ public sealed class GameWindow : IDisposable
|
||||||
playerOnGround: GetDebugPlayerOnGround,
|
playerOnGround: GetDebugPlayerOnGround,
|
||||||
inNonCombatMode: () => Combat.CurrentMode
|
inNonCombatMode: () => Combat.CurrentMode
|
||||||
== AcDream.Core.Combat.CombatMode.NonCombat,
|
== AcDream.Core.Combat.CombatMode.NonCombat,
|
||||||
|
combatState: Combat,
|
||||||
|
sendChangeCombatMode: mode =>
|
||||||
|
_liveSession?.SendChangeCombatMode(mode),
|
||||||
isComponentPack: magicCatalog.IsComponentPack,
|
isComponentPack: magicCatalog.IsComponentPack,
|
||||||
placeInBackpack: SendPickUp,
|
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) =>
|
sendSplitToWorld: (item, amount) =>
|
||||||
_liveSession?.SendStackableSplitTo3D(item, amount),
|
_liveSession?.SendStackableSplitTo3D(item, amount),
|
||||||
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
||||||
|
|
@ -13258,9 +13281,6 @@ public sealed class GameWindow : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsExternalContainerTarget(guid))
|
|
||||||
_externalContainers.RequestOpen(guid);
|
|
||||||
|
|
||||||
// B.6/R4-V5: install a speculative local TurnToObject/MoveToObject
|
// B.6/R4-V5: install a speculative local TurnToObject/MoveToObject
|
||||||
// through the player's MoveToManager so close-range Use rotates the
|
// through the player's MoveToManager so close-range Use rotates the
|
||||||
// body to face before the action fires. For FAR targets, ACE's
|
// 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
|
// Defer the wire packet — OnAutoWalkArrivedSendDeferredAction
|
||||||
// will fire it after rotation completes.
|
// will fire it after rotation completes.
|
||||||
_pendingPostArrivalAction = (guid, false);
|
_pendingPostArrivalAction = new PendingPostArrivalAction(guid, IsPickup: false);
|
||||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||||
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
|
Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}");
|
||||||
return;
|
return;
|
||||||
|
|
@ -13318,6 +13338,9 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SendPickUp(uint itemGuid)
|
private void SendPickUp(uint itemGuid)
|
||||||
|
=> SendPickUp(itemGuid, _playerServerGuid, placement: 0);
|
||||||
|
|
||||||
|
private void SendPickUp(uint itemGuid, uint destinationContainerId, int placement)
|
||||||
{
|
{
|
||||||
if (_liveSession is null
|
if (_liveSession is null
|
||||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||||
|
|
@ -13362,7 +13385,11 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
if (closeRange)
|
if (closeRange)
|
||||||
{
|
{
|
||||||
_pendingPostArrivalAction = (itemGuid, true);
|
_pendingPostArrivalAction = new PendingPostArrivalAction(
|
||||||
|
itemGuid,
|
||||||
|
IsPickup: true,
|
||||||
|
destinationContainerId,
|
||||||
|
placement);
|
||||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||||
Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}");
|
Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}");
|
||||||
return;
|
return;
|
||||||
|
|
@ -13375,9 +13402,9 @@ public sealed class GameWindow : IDisposable
|
||||||
// ACE's chain alive.
|
// ACE's chain alive.
|
||||||
var seq = _liveSession.NextGameActionSequence();
|
var seq = _liveSession.NextGameActionSequence();
|
||||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
||||||
seq, itemGuid, _playerServerGuid, placement: 0);
|
seq, itemGuid, destinationContainerId, placement);
|
||||||
_liveSession.SendGameAction(body);
|
_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)
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||||
{
|
{
|
||||||
string label = DescribeLiveEntity(itemGuid);
|
string label = DescribeLiveEntity(itemGuid);
|
||||||
|
|
@ -13398,7 +13425,7 @@ public sealed class GameWindow : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnAutoWalkArrivedSendDeferredAction()
|
private void OnAutoWalkArrivedSendDeferredAction()
|
||||||
{
|
{
|
||||||
if (_pendingPostArrivalAction is not (uint guid, bool isPickup) pending)
|
if (_pendingPostArrivalAction is not { } pending)
|
||||||
return;
|
return;
|
||||||
_pendingPostArrivalAction = null;
|
_pendingPostArrivalAction = null;
|
||||||
|
|
||||||
|
|
@ -13407,18 +13434,21 @@ public sealed class GameWindow : IDisposable
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var seq = _liveSession.NextGameActionSequence();
|
var seq = _liveSession.NextGameActionSequence();
|
||||||
if (isPickup)
|
if (pending.IsPickup)
|
||||||
{
|
{
|
||||||
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp(
|
||||||
seq, guid, _playerServerGuid, placement: 0);
|
seq,
|
||||||
|
pending.Guid,
|
||||||
|
pending.DestinationContainerId,
|
||||||
|
pending.Placement);
|
||||||
_liveSession.SendGameAction(body);
|
_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
|
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);
|
_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;
|
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)
|
private void InstallSpeculativeTurnToTarget(uint targetGuid)
|
||||||
{
|
{
|
||||||
if (_playerController is not { } pc || pc.MoveTo is null) return;
|
if (_playerController is not { } pc || pc.MoveTo is null) return;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
|
||||||
namespace AcDream.App.UI;
|
namespace AcDream.App.UI;
|
||||||
|
|
@ -63,8 +64,11 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
|
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
|
||||||
private readonly Action<string>? _toast;
|
private readonly Action<string>? _toast;
|
||||||
private readonly Action<string>? _systemMessage;
|
private readonly Action<string>? _systemMessage;
|
||||||
|
private readonly CombatState? _combatState;
|
||||||
|
private readonly Action<CombatMode>? _sendChangeCombatMode;
|
||||||
|
|
||||||
private PendingSwitch? _pendingSwitch;
|
private PendingSwitch? _pendingSwitch;
|
||||||
|
private PendingCombatSettlement? _pendingCombatSettlement;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public bool IsBusy => _pendingSwitch is not null;
|
public bool IsBusy => _pendingSwitch is not null;
|
||||||
|
|
@ -75,7 +79,9 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
Action<uint, uint>? sendWield,
|
Action<uint, uint>? sendWield,
|
||||||
Action<uint, uint, int>? sendPutItemInContainer,
|
Action<uint, uint, int>? sendPutItemInContainer,
|
||||||
Action<string>? toast,
|
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));
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||||
|
|
@ -83,12 +89,16 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
_sendPutItemInContainer = sendPutItemInContainer;
|
_sendPutItemInContainer = sendPutItemInContainer;
|
||||||
_toast = toast;
|
_toast = toast;
|
||||||
_systemMessage = systemMessage;
|
_systemMessage = systemMessage;
|
||||||
|
_combatState = combatState;
|
||||||
|
_sendChangeCombatMode = sendChangeCombatMode;
|
||||||
|
|
||||||
_objects.ObjectMoved += OnObjectMoved;
|
_objects.ObjectMoved += OnObjectMoved;
|
||||||
_objects.ObjectRemoved += OnObjectRemoved;
|
_objects.ObjectRemoved += OnObjectRemoved;
|
||||||
_objects.MoveRequestFailed += OnMoveRequestFailed;
|
_objects.MoveRequestFailed += OnMoveRequestFailed;
|
||||||
_objects.WieldConfirmed += OnWieldConfirmed;
|
_objects.WieldConfirmed += OnWieldConfirmed;
|
||||||
_objects.Cleared += OnObjectsCleared;
|
_objects.Cleared += OnObjectsCleared;
|
||||||
|
if (_combatState is not null)
|
||||||
|
_combatState.CombatModeChanged += OnCombatModeChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -97,7 +107,10 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
/// as retail's inventory-ready gate prevents a second concurrent request.
|
/// as retail's inventory-ready gate prevents a second concurrent request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryWield(ClientObject item)
|
public bool TryWield(ClientObject item)
|
||||||
=> TryWield(item, EquipMask.None);
|
{
|
||||||
|
_pendingCombatSettlement = null;
|
||||||
|
return TryWield(item, EquipMask.None, combatModeAfterWield: null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Execute AutoWield while retaining the paperdoll's resolved target side.
|
/// 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.
|
/// bypassing the transaction with a direct wire request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryWield(ClientObject item, EquipMask requestedMask)
|
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);
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
|
||||||
|
|
@ -130,9 +152,13 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
if (GetEquippedObjectAtLocation(
|
if (GetEquippedObjectAtLocation(
|
||||||
requestedMask, priority: 0, item.ObjectId) is { } blocker)
|
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
|
EquipMask weaponLocation = requestedMask == EquipMask.None
|
||||||
|
|
@ -140,10 +166,24 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
: requestedMask & WeaponReadyMask;
|
: requestedMask & WeaponReadyMask;
|
||||||
if (weaponLocation != EquipMask.None)
|
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(
|
ClientObject? blocker = GetEquippedObjectAtLocation(
|
||||||
WeaponReadyMask, priority: 0, item.ObjectId);
|
WeaponReadyMask, priority: 0, item.ObjectId);
|
||||||
if (blocker is not null)
|
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
|
// Retail checks secondary blockers only after the primary weapon
|
||||||
// slot is vacant, then re-enters AutoWield after each confirmed
|
// slot is vacant, then re-enters AutoWield after each confirmed
|
||||||
|
|
@ -151,15 +191,23 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
if (BlocksUseOfShield(item)
|
if (BlocksUseOfShield(item)
|
||||||
&& GetEquippedObjectAtLocation(
|
&& GetEquippedObjectAtLocation(
|
||||||
EquipMask.Shield, priority: 0, item.ObjectId) is { } shield)
|
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
|
if (item.AmmoType is > 0
|
||||||
&& GetEquippedObjectAtLocation(
|
&& GetEquippedObjectAtLocation(
|
||||||
EquipMask.MissileAmmo, priority: 0, item.ObjectId) is { } ammo
|
EquipMask.MissileAmmo, priority: 0, item.ObjectId) is { } ammo
|
||||||
&& ammo.AmmoType != item.AmmoType)
|
&& 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
|
EquipMask mask = requestedMask != EquipMask.None
|
||||||
|
|
@ -171,13 +219,14 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendWield(item, mask);
|
return SendWield(item, mask, combatModeAfterWield: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BeginWeaponReplacement(
|
private bool BeginWeaponReplacement(
|
||||||
uint requestedItemId,
|
uint requestedItemId,
|
||||||
ClientObject blockingItem,
|
ClientObject blockingItem,
|
||||||
EquipMask requestedMask)
|
EquipMask requestedMask,
|
||||||
|
CombatMode? combatModeAfterWield)
|
||||||
{
|
{
|
||||||
if (_sendPutItemInContainer is null)
|
if (_sendPutItemInContainer is null)
|
||||||
{
|
{
|
||||||
|
|
@ -192,7 +241,8 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
_pendingSwitch = new PendingSwitch(
|
_pendingSwitch = new PendingSwitch(
|
||||||
requestedItemId,
|
requestedItemId,
|
||||||
blockingItem.ObjectId,
|
blockingItem.ObjectId,
|
||||||
requestedMask);
|
requestedMask,
|
||||||
|
combatModeAfterWield);
|
||||||
|
|
||||||
// Retail AttemptToPlaceInContainer(blockingID, playerID, 0, 1, 0).
|
// Retail AttemptToPlaceInContainer(blockingID, playerID, 0, 1, 0).
|
||||||
// Do not change the local equip projection yet: the server's move event
|
// Do not change the local equip projection yet: the server's move event
|
||||||
|
|
@ -203,14 +253,18 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool SendWield(ClientObject item, EquipMask mask)
|
private bool SendWield(
|
||||||
|
ClientObject item,
|
||||||
|
EquipMask mask,
|
||||||
|
CombatMode? combatModeAfterWield)
|
||||||
{
|
{
|
||||||
if (_sendWield is null)
|
if (_sendWield is null)
|
||||||
return false;
|
return false;
|
||||||
_pendingSwitch = new PendingSwitch(
|
_pendingSwitch = new PendingSwitch(
|
||||||
item.ObjectId,
|
item.ObjectId,
|
||||||
BlockingItemId: 0,
|
BlockingItemId: 0,
|
||||||
RequestedMask: mask);
|
RequestedMask: mask,
|
||||||
|
CombatModeAfterWield: combatModeAfterWield);
|
||||||
if (!_objects.WieldItemOptimistic(item.ObjectId, _playerGuid(), mask))
|
if (!_objects.WieldItemOptimistic(item.ObjectId, _playerGuid(), mask))
|
||||||
{
|
{
|
||||||
_pendingSwitch = null;
|
_pendingSwitch = null;
|
||||||
|
|
@ -235,14 +289,75 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
// vacant ready slot and send GetAndWieldItem for the requested weapon.
|
// vacant ready slot and send GetAndWieldItem for the requested weapon.
|
||||||
_pendingSwitch = null;
|
_pendingSwitch = null;
|
||||||
if (_objects.Get(pending.RequestedItemId) is { } requested)
|
if (_objects.Get(pending.RequestedItemId) is { } requested)
|
||||||
TryWield(requested, pending.RequestedMask);
|
TryWield(
|
||||||
|
requested,
|
||||||
|
pending.RequestedMask,
|
||||||
|
pending.CombatModeAfterWield);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWieldConfirmed(uint itemId)
|
private void OnWieldConfirmed(uint itemId)
|
||||||
{
|
{
|
||||||
if (_pendingSwitch is { BlockingItemId: 0 } pending
|
if (_pendingSwitch is { BlockingItemId: 0 } pending
|
||||||
&& itemId == pending.RequestedItemId)
|
&& itemId == pending.RequestedItemId)
|
||||||
|
{
|
||||||
_pendingSwitch = null;
|
_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)
|
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
||||||
|
|
@ -259,10 +374,16 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
&& (item.ObjectId == pending.BlockingItemId
|
&& (item.ObjectId == pending.BlockingItemId
|
||||||
|| item.ObjectId == pending.RequestedItemId))
|
|| item.ObjectId == pending.RequestedItemId))
|
||||||
_pendingSwitch = null;
|
_pendingSwitch = null;
|
||||||
|
if (_pendingCombatSettlement is { } settlement
|
||||||
|
&& item.ObjectId == settlement.ItemId)
|
||||||
|
_pendingCombatSettlement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnObjectsCleared()
|
private void OnObjectsCleared()
|
||||||
=> _pendingSwitch = null;
|
{
|
||||||
|
_pendingSwitch = null;
|
||||||
|
_pendingCombatSettlement = null;
|
||||||
|
}
|
||||||
|
|
||||||
private EquipMask BestAvailableEquipMask(ClientObject item)
|
private EquipMask BestAvailableEquipMask(ClientObject item)
|
||||||
{
|
{
|
||||||
|
|
@ -357,21 +478,48 @@ internal sealed class AutoWieldController : IDisposable
|
||||||
|| item.Type.HasFlag(ItemType.Caster);
|
|| 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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_pendingSwitch = null;
|
_pendingSwitch = null;
|
||||||
|
_pendingCombatSettlement = null;
|
||||||
_objects.ObjectMoved -= OnObjectMoved;
|
_objects.ObjectMoved -= OnObjectMoved;
|
||||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||||
_objects.MoveRequestFailed -= OnMoveRequestFailed;
|
_objects.MoveRequestFailed -= OnMoveRequestFailed;
|
||||||
_objects.WieldConfirmed -= OnWieldConfirmed;
|
_objects.WieldConfirmed -= OnWieldConfirmed;
|
||||||
_objects.Cleared -= OnObjectsCleared;
|
_objects.Cleared -= OnObjectsCleared;
|
||||||
|
if (_combatState is not null)
|
||||||
|
_combatState.CombatModeChanged -= OnCombatModeChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct PendingSwitch(
|
private readonly record struct PendingSwitch(
|
||||||
uint RequestedItemId,
|
uint RequestedItemId,
|
||||||
uint BlockingItemId,
|
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 System;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
|
||||||
namespace AcDream.App.UI;
|
namespace AcDream.App.UI;
|
||||||
|
|
@ -40,7 +41,8 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
private readonly Func<bool> _playerOnGround;
|
private readonly Func<bool> _playerOnGround;
|
||||||
private readonly Func<bool> _inNonCombatMode;
|
private readonly Func<bool> _inNonCombatMode;
|
||||||
private readonly Func<uint, bool> _isComponentPack;
|
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<uint>? _requestExternalContainer;
|
||||||
private readonly Action<ItemPolicyAction>? _auxiliaryAction;
|
private readonly Action<ItemPolicyAction>? _auxiliaryAction;
|
||||||
private readonly InteractionState _interactionState;
|
private readonly InteractionState _interactionState;
|
||||||
|
|
@ -73,7 +75,8 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
Func<bool>? playerOnGround = null,
|
Func<bool>? playerOnGround = null,
|
||||||
Func<bool>? inNonCombatMode = null,
|
Func<bool>? inNonCombatMode = null,
|
||||||
Func<uint, bool>? isComponentPack = 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<ItemPolicyAction>? auxiliaryAction = null,
|
||||||
Action<uint, uint>? sendSplitToWorld = null,
|
Action<uint, uint>? sendSplitToWorld = null,
|
||||||
Func<uint>? selectedObjectId = null,
|
Func<uint>? selectedObjectId = null,
|
||||||
|
|
@ -83,7 +86,9 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
Func<bool>? dragOnPlayerOpensSecureTrade = null,
|
Func<bool>? dragOnPlayerOpensSecureTrade = null,
|
||||||
Action<string>? systemMessage = null,
|
Action<string>? systemMessage = null,
|
||||||
Action<uint, uint, uint, uint>? sendSplitToContainer = 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));
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||||
|
|
@ -105,6 +110,7 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
_inNonCombatMode = inNonCombatMode ?? (() => false);
|
_inNonCombatMode = inNonCombatMode ?? (() => false);
|
||||||
_isComponentPack = isComponentPack ?? (_ => false);
|
_isComponentPack = isComponentPack ?? (_ => false);
|
||||||
_placeInBackpack = placeInBackpack;
|
_placeInBackpack = placeInBackpack;
|
||||||
|
_backpackContainerId = backpackContainerId ?? _playerGuid;
|
||||||
_requestExternalContainer = requestExternalContainer;
|
_requestExternalContainer = requestExternalContainer;
|
||||||
_auxiliaryAction = auxiliaryAction;
|
_auxiliaryAction = auxiliaryAction;
|
||||||
_selectedObjectId = selectedObjectId ?? (() => 0u);
|
_selectedObjectId = selectedObjectId ?? (() => 0u);
|
||||||
|
|
@ -119,11 +125,20 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
_sendWield,
|
_sendWield,
|
||||||
sendPutItemInContainer,
|
sendPutItemInContainer,
|
||||||
_toast,
|
_toast,
|
||||||
_systemMessage);
|
_systemMessage,
|
||||||
|
combatState,
|
||||||
|
sendChangeCombatMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action? StateChanged;
|
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 uint PlayerGuid => _playerGuid();
|
||||||
|
|
||||||
public InteractionState InteractionState => _interactionState;
|
public InteractionState InteractionState => _interactionState;
|
||||||
|
|
@ -399,8 +414,18 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
switch (action.Kind)
|
switch (action.Kind)
|
||||||
{
|
{
|
||||||
case ItemPolicyActionKind.PlaceInBackpack:
|
case ItemPolicyActionKind.PlaceInBackpack:
|
||||||
_placeInBackpack?.Invoke(action.ObjectId);
|
if (_placeInBackpack is null)
|
||||||
acted |= _placeInBackpack is not 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;
|
break;
|
||||||
case ItemPolicyActionKind.WieldRight:
|
case ItemPolicyActionKind.WieldRight:
|
||||||
case ItemPolicyActionKind.WieldLeft:
|
case ItemPolicyActionKind.WieldLeft:
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,18 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
||||||
|
|
||||||
private void OnExternalContainerChanged(ExternalContainerTransition transition)
|
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)
|
if (transition.ContainerId == 0u)
|
||||||
{
|
{
|
||||||
_openContainer = 0u;
|
_openContainer = 0u;
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,10 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
_objects.Cleared += OnObjectsCleared;
|
_objects.Cleared += OnObjectsCleared;
|
||||||
_selection.Changed += OnSelectionChanged;
|
_selection.Changed += OnSelectionChanged;
|
||||||
if (_itemInteraction is not null)
|
if (_itemInteraction is not null)
|
||||||
|
{
|
||||||
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
||||||
|
_itemInteraction.PendingBackpackPlacementRequested += OnPendingBackpackPlacementRequested;
|
||||||
|
}
|
||||||
|
|
||||||
Populate();
|
Populate();
|
||||||
}
|
}
|
||||||
|
|
@ -269,6 +272,26 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
Populate();
|
Populate();
|
||||||
}
|
}
|
||||||
private void OnInteractionStateChanged() => ApplyIndicators();
|
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 OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
|
||||||
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
||||||
{
|
{
|
||||||
|
|
@ -411,6 +434,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
|
|
||||||
private uint EffectiveOpen() => _openContainer != 0 ? _openContainer : _playerGuid();
|
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,
|
/// <summary>Add a populated cell wired to its click role: container cell → open+select,
|
||||||
/// item cell → select-only.</summary>
|
/// item cell → select-only.</summary>
|
||||||
private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false)
|
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;
|
_objects.Cleared -= OnObjectsCleared;
|
||||||
_selection.Changed -= OnSelectionChanged;
|
_selection.Changed -= OnSelectionChanged;
|
||||||
if (_itemInteraction is not null)
|
if (_itemInteraction is not null)
|
||||||
|
{
|
||||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||||
|
_itemInteraction.PendingBackpackPlacementRequested -= OnPendingBackpackPlacementRequested;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
public SelectedObjectController? SelectedObjectController { get; private set; }
|
public SelectedObjectController? SelectedObjectController { get; private set; }
|
||||||
public UiViewport? PaperdollViewportWidget { get; private set; }
|
public UiViewport? PaperdollViewportWidget { get; private set; }
|
||||||
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
||||||
|
public InventoryController? InventoryPanelController { get; private set; }
|
||||||
public RetailDialogFactory? DialogFactory { get; private set; }
|
public RetailDialogFactory? DialogFactory { get; private set; }
|
||||||
public ExternalContainerController? ExternalContainerController { get; private set; }
|
public ExternalContainerController? ExternalContainerController { get; private set; }
|
||||||
|
|
||||||
|
|
@ -1603,6 +1604,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
() => CloseWindow(WindowNames.Inventory),
|
() => CloseWindow(WindowNames.Inventory),
|
||||||
StackSplitQuantity,
|
StackSplitQuantity,
|
||||||
b.ResolveDragIcon);
|
b.ResolveDragIcon);
|
||||||
|
InventoryPanelController = inventory;
|
||||||
PaperdollController paperdoll = PaperdollController.Bind(
|
PaperdollController paperdoll = PaperdollController.Bind(
|
||||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.ItemInteraction,
|
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.ItemInteraction,
|
||||||
contents, _bindings.Assets.DefaultFont, paperdollClickMap,
|
contents, _bindings.Assets.DefaultFont, paperdollClickMap,
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,11 @@ public sealed class ExternalContainerLifecycleController : IDisposable
|
||||||
if (transition.PreviousContainerId != 0u)
|
if (transition.PreviousContainerId != 0u)
|
||||||
_objects.StopViewingContentsTree(transition.PreviousContainerId);
|
_objects.StopViewingContentsTree(transition.PreviousContainerId);
|
||||||
|
|
||||||
// Only replacement emits Event_NoLongerViewingContents. The authored
|
// SetGroundObject(new, notifyServer:true) emits this while the new
|
||||||
// close/range path instead sends Use(root) and waits for event 0x0052.
|
// request is still pending, not after ViewContents arrives. The
|
||||||
if (transition.Kind == ExternalContainerTransitionKind.Replaced
|
// authored close/range path instead sends Use(root) and waits for
|
||||||
|
// event 0x0052.
|
||||||
|
if (transition.Kind == ExternalContainerTransitionKind.ReplacementRequested
|
||||||
&& transition.PreviousContainerId != 0u)
|
&& transition.PreviousContainerId != 0u)
|
||||||
{
|
{
|
||||||
_sendNoLongerViewingContents(transition.PreviousContainerId);
|
_sendNoLongerViewingContents(transition.PreviousContainerId);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace AcDream.Core.Items;
|
||||||
public enum ExternalContainerTransitionKind
|
public enum ExternalContainerTransitionKind
|
||||||
{
|
{
|
||||||
Opened,
|
Opened,
|
||||||
Replaced,
|
ReplacementRequested,
|
||||||
Closed,
|
Closed,
|
||||||
Reset,
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +33,24 @@ public sealed class ExternalContainerState
|
||||||
{
|
{
|
||||||
if (containerId == 0u || RequestedContainerId == containerId)
|
if (containerId == 0u || RequestedContainerId == containerId)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
uint previous = CurrentContainerId;
|
||||||
RequestedContainerId = containerId;
|
RequestedContainerId = containerId;
|
||||||
|
|
||||||
|
// ClientUISystem::SetGroundObject @ 0x00564510 retires the old
|
||||||
|
// ground-object presentation synchronously when a different root is
|
||||||
|
// requested. ViewContents for the new root arrives later. Keeping the
|
||||||
|
// old root current during that interval lets its range watcher issue a
|
||||||
|
// close Use, which ACE interprets as cancellation of the new root's
|
||||||
|
// MoveTo chain.
|
||||||
|
if (previous != 0u && previous != containerId)
|
||||||
|
{
|
||||||
|
CurrentContainerId = 0u;
|
||||||
|
Changed?.Invoke(new ExternalContainerTransition(
|
||||||
|
ExternalContainerTransitionKind.ReplacementRequested,
|
||||||
|
previous,
|
||||||
|
containerId));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,9 +66,7 @@ public sealed class ExternalContainerState
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Changed?.Invoke(new ExternalContainerTransition(
|
Changed?.Invoke(new ExternalContainerTransition(
|
||||||
previous == 0u
|
ExternalContainerTransitionKind.Opened,
|
||||||
? ExternalContainerTransitionKind.Opened
|
|
||||||
: ExternalContainerTransitionKind.Replaced,
|
|
||||||
previous,
|
previous,
|
||||||
containerId));
|
containerId));
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI;
|
namespace AcDream.App.Tests.UI;
|
||||||
|
|
@ -21,11 +22,14 @@ public sealed class ItemInteractionControllerTests
|
||||||
public readonly List<(uint Item, uint Container, int Placement)> Puts = new();
|
public readonly List<(uint Item, uint Container, int Placement)> Puts = new();
|
||||||
public readonly List<(uint Item, uint Container, uint Placement, uint Amount)> SplitPuts = new();
|
public readonly List<(uint Item, uint Container, uint Placement, uint Amount)> SplitPuts = new();
|
||||||
public readonly List<uint> ExternalRequests = new();
|
public readonly List<uint> ExternalRequests = new();
|
||||||
|
public readonly List<(uint Item, uint Container, int Placement)> BackpackPlacements = new();
|
||||||
public readonly List<uint> Drops = new();
|
public readonly List<uint> Drops = new();
|
||||||
public readonly List<(uint Item, uint Amount)> SplitDrops = new();
|
public readonly List<(uint Item, uint Amount)> SplitDrops = new();
|
||||||
public readonly List<(uint Target, uint Item, uint Amount)> Gives = new();
|
public readonly List<(uint Target, uint Item, uint Amount)> Gives = new();
|
||||||
public readonly List<string> Toasts = new();
|
public readonly List<string> Toasts = new();
|
||||||
public readonly List<string> SystemMessages = new();
|
public readonly List<string> SystemMessages = new();
|
||||||
|
public readonly List<CombatMode> CombatModeRequests = new();
|
||||||
|
public readonly CombatState Combat = new();
|
||||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||||
public uint SelectedObject;
|
public uint SelectedObject;
|
||||||
public bool NonCombatMode;
|
public bool NonCombatMode;
|
||||||
|
|
@ -65,6 +69,8 @@ public sealed class ItemInteractionControllerTests
|
||||||
stackSplitQuantity: SplitQuantity,
|
stackSplitQuantity: SplitQuantity,
|
||||||
inNonCombatMode: () => NonCombatMode,
|
inNonCombatMode: () => NonCombatMode,
|
||||||
groundObjectId: () => GroundObject,
|
groundObjectId: () => GroundObject,
|
||||||
|
placeInBackpack: (item, container, placement) =>
|
||||||
|
BackpackPlacements.Add((item, container, placement)),
|
||||||
sendPutItemInContainer: (item, container, placement) =>
|
sendPutItemInContainer: (item, container, placement) =>
|
||||||
Puts.Add((item, container, placement)),
|
Puts.Add((item, container, placement)),
|
||||||
sendGive: (target, item, amount) => Gives.Add((target, item, amount)),
|
sendGive: (target, item, amount) => Gives.Add((target, item, amount)),
|
||||||
|
|
@ -76,7 +82,9 @@ public sealed class ItemInteractionControllerTests
|
||||||
{
|
{
|
||||||
GroundObject = id;
|
GroundObject = id;
|
||||||
ExternalRequests.Add(id);
|
ExternalRequests.Add(id);
|
||||||
});
|
},
|
||||||
|
combatState: Combat,
|
||||||
|
sendChangeCombatMode: CombatModeRequests.Add);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemInteractionController Controller { get; }
|
public ItemInteractionController Controller { get; }
|
||||||
|
|
@ -158,6 +166,38 @@ public sealed class ItemInteractionControllerTests
|
||||||
Assert.Empty(h.Puts);
|
Assert.Empty(h.Puts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubleClickItemInOpenCorpse_placesThatItemInBackpack()
|
||||||
|
{
|
||||||
|
var h = new Harness();
|
||||||
|
const uint corpse = 0x70000012u;
|
||||||
|
const uint loot = 0x70000013u;
|
||||||
|
h.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = corpse,
|
||||||
|
Type = ItemType.Container,
|
||||||
|
ItemsCapacity = 24,
|
||||||
|
PublicWeenieBitfield = (uint)(
|
||||||
|
PublicWeenieFlags.Openable
|
||||||
|
| PublicWeenieFlags.Stuck
|
||||||
|
| PublicWeenieFlags.Corpse),
|
||||||
|
});
|
||||||
|
h.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = loot,
|
||||||
|
Name = "Loot",
|
||||||
|
Type = ItemType.Misc,
|
||||||
|
});
|
||||||
|
h.Objects.MoveItem(loot, corpse, 0);
|
||||||
|
h.GroundObject = corpse;
|
||||||
|
|
||||||
|
Assert.True(h.Controller.ActivateItem(loot));
|
||||||
|
|
||||||
|
Assert.Equal(new[] { (loot, Player, 0) }, h.BackpackPlacements);
|
||||||
|
Assert.Empty(h.Uses);
|
||||||
|
Assert.Empty(h.Wields);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DropOwnedItemOnOpenExternalContainer_SendsPutWithoutOptimisticMove()
|
public void DropOwnedItemOnOpenExternalContainer_SendsPutWithoutOptimisticMove()
|
||||||
{
|
{
|
||||||
|
|
@ -548,6 +588,107 @@ public sealed class ItemInteractionControllerTests
|
||||||
Assert.Equal(Player, h.Objects.Get(bow)!.ContainerId);
|
Assert.Equal(Player, h.Objects.Get(bow)!.ContainerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(CombatMode.NonCombat, false)]
|
||||||
|
[InlineData(CombatMode.Magic, true)]
|
||||||
|
public void WandToBow_reassertsMissileModeOnlyForActiveCombatTransaction(
|
||||||
|
CombatMode startingMode,
|
||||||
|
bool expectsMissileMode)
|
||||||
|
{
|
||||||
|
var h = new Harness();
|
||||||
|
h.Combat.SetCombatMode(startingMode);
|
||||||
|
const uint wand = 0x50000B91u;
|
||||||
|
const uint bow = 0x50000B92u;
|
||||||
|
h.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = wand,
|
||||||
|
Name = "Training Wand",
|
||||||
|
Type = ItemType.Caster,
|
||||||
|
CombatUse = 2,
|
||||||
|
ValidLocations = EquipMask.Held,
|
||||||
|
});
|
||||||
|
h.Objects.MoveItem(wand, Player, -1, EquipMask.Held);
|
||||||
|
h.AddContained(bow, item =>
|
||||||
|
{
|
||||||
|
item.Name = "Shortbow";
|
||||||
|
item.Type = ItemType.MissileWeapon;
|
||||||
|
item.CombatUse = 2;
|
||||||
|
item.ValidLocations = EquipMask.MissileWeapon;
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(h.Controller.ActivateItem(bow));
|
||||||
|
Assert.Equal(new[] { (wand, Player, 0) }, h.Puts);
|
||||||
|
|
||||||
|
// ACE reports several transitional combat modes while putting the
|
||||||
|
// wand away. The transaction retains the initial active-combat intent
|
||||||
|
// rather than consulting this intermediate state on its second pass.
|
||||||
|
h.Combat.SetCombatMode(CombatMode.Melee);
|
||||||
|
h.Objects.MoveItem(wand, Player, 0, EquipMask.None);
|
||||||
|
Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields);
|
||||||
|
Assert.Empty(h.CombatModeRequests);
|
||||||
|
|
||||||
|
Assert.True(h.Objects.ApplyConfirmedServerWield(
|
||||||
|
bow,
|
||||||
|
Player,
|
||||||
|
EquipMask.MissileWeapon));
|
||||||
|
Assert.Empty(h.CombatModeRequests);
|
||||||
|
|
||||||
|
// The first authoritative post-wield mode is the transaction's server
|
||||||
|
// settlement stream. Local ACE publishes Peace -> Missile -> Peace;
|
||||||
|
// only the trailing Peace is late enough to accept the reassertion.
|
||||||
|
if (expectsMissileMode)
|
||||||
|
{
|
||||||
|
h.Combat.SetCombatMode(CombatMode.NonCombat);
|
||||||
|
Assert.Empty(h.CombatModeRequests);
|
||||||
|
h.Combat.SetCombatMode(CombatMode.Missile);
|
||||||
|
Assert.Empty(h.CombatModeRequests);
|
||||||
|
h.Combat.SetCombatMode(CombatMode.NonCombat);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
h.Combat.SetCombatMode(CombatMode.Missile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectsMissileMode)
|
||||||
|
Assert.Equal(new[] { CombatMode.Missile }, h.CombatModeRequests);
|
||||||
|
else
|
||||||
|
Assert.Empty(h.CombatModeRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveWeaponReplacement_serverSettlesDirectly_doesNotReassertLaterPeace()
|
||||||
|
{
|
||||||
|
var h = new Harness();
|
||||||
|
h.Combat.SetCombatMode(CombatMode.Magic);
|
||||||
|
const uint wand = 0x50000BA1u;
|
||||||
|
const uint bow = 0x50000BA2u;
|
||||||
|
h.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = wand,
|
||||||
|
Type = ItemType.Caster,
|
||||||
|
CombatUse = 2,
|
||||||
|
ValidLocations = EquipMask.Held,
|
||||||
|
});
|
||||||
|
h.Objects.MoveItem(wand, Player, -1, EquipMask.Held);
|
||||||
|
h.AddContained(bow, item =>
|
||||||
|
{
|
||||||
|
item.Type = ItemType.MissileWeapon;
|
||||||
|
item.CombatUse = 2;
|
||||||
|
item.ValidLocations = EquipMask.MissileWeapon;
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(h.Controller.ActivateItem(bow));
|
||||||
|
h.Combat.SetCombatMode(CombatMode.Melee);
|
||||||
|
h.Objects.MoveItem(wand, Player, 0, EquipMask.None);
|
||||||
|
Assert.True(h.Objects.ApplyConfirmedServerWield(
|
||||||
|
bow, Player, EquipMask.MissileWeapon));
|
||||||
|
|
||||||
|
h.Combat.SetCombatMode(CombatMode.Missile);
|
||||||
|
h.Combat.SetCombatMode(CombatMode.NonCombat);
|
||||||
|
|
||||||
|
Assert.Empty(h.CombatModeRequests);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PreviouslyWieldedBow_AfterAuthoritativeUnwield_DoubleClickWieldsAgain()
|
public void PreviouslyWieldedBow_AfterAuthoritativeUnwield_DoubleClickWieldsAgain()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ public sealed class ExternalContainerControllerTests
|
||||||
sendWield: null,
|
sendWield: null,
|
||||||
sendDrop: null,
|
sendDrop: null,
|
||||||
groundObjectId: () => State.CurrentContainerId,
|
groundObjectId: () => State.CurrentContainerId,
|
||||||
placeInBackpack: Pickups.Add,
|
placeInBackpack: (item, _, _) => Pickups.Add(item),
|
||||||
sendPutItemInContainer: (item, container, placement) =>
|
sendPutItemInContainer: (item, container, placement) =>
|
||||||
Puts.Add((item, container, placement)),
|
Puts.Add((item, container, placement)),
|
||||||
sendSplitToContainer: (item, container, placement, amount) =>
|
sendSplitToContainer: (item, container, placement, amount) =>
|
||||||
|
|
@ -213,6 +213,35 @@ public sealed class ExternalContainerControllerTests
|
||||||
Assert.True(h.Window.IsVisible);
|
Assert.True(h.Window.IsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplacementRequest_HidesOldViewWithoutSendingRangeCloseUse()
|
||||||
|
{
|
||||||
|
using var h = new Harness();
|
||||||
|
const uint replacement = 0x70000002u;
|
||||||
|
h.Open(Chest);
|
||||||
|
h.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = replacement,
|
||||||
|
Type = ItemType.Container,
|
||||||
|
ItemsCapacity = 24,
|
||||||
|
Useability = ItemUseability.Remote,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.State.RequestOpen(replacement);
|
||||||
|
h.InRange = false;
|
||||||
|
h.Controller.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(0u, h.State.CurrentContainerId);
|
||||||
|
Assert.Equal(replacement, h.State.RequestedContainerId);
|
||||||
|
Assert.False(h.Window.IsVisible);
|
||||||
|
Assert.Empty(h.Uses);
|
||||||
|
Assert.Equal(new uint[] { Chest }, h.NoLonger);
|
||||||
|
|
||||||
|
h.State.ApplyViewContents(replacement);
|
||||||
|
Assert.Equal(replacement, h.State.CurrentContainerId);
|
||||||
|
Assert.True(h.Window.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DropOwnedPartialStackIntoChest_UsesSplitRequestWithoutLocalMove()
|
public void DropOwnedPartialStackIntoChest_UsesSplitRequestWithoutLocalMove()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -663,6 +663,49 @@ public class InventoryControllerTests
|
||||||
Assert.Equal(Player, objects.Get(loot)!.ContainerId);
|
Assert.Equal(Player, objects.Get(loot)!.ContainerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubleClickLoot_ReservesFirstOpenPackSlotBeforePickupIsSent()
|
||||||
|
{
|
||||||
|
const uint chest = 0x70000011u;
|
||||||
|
const uint loot = 0x70000012u;
|
||||||
|
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
SeedContained(objects, 0xAu, Player, slot: 0);
|
||||||
|
SeedContained(objects, 0xBu, Player, slot: 1);
|
||||||
|
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
||||||
|
var pickups = new List<(uint item, uint container, int placement)>();
|
||||||
|
using var interaction = new ItemInteractionController(
|
||||||
|
objects,
|
||||||
|
playerGuid: () => Player,
|
||||||
|
sendUse: null,
|
||||||
|
sendUseWithTarget: null,
|
||||||
|
sendWield: null,
|
||||||
|
sendDrop: null,
|
||||||
|
nowMs: () => 1_000,
|
||||||
|
groundObjectId: () => chest,
|
||||||
|
backpackContainerId: () => Player,
|
||||||
|
placeInBackpack: (item, container, placement) =>
|
||||||
|
pickups.Add((item, container, placement)));
|
||||||
|
using var inventory = InventoryController.Bind(
|
||||||
|
layout,
|
||||||
|
objects,
|
||||||
|
() => Player,
|
||||||
|
iconIds: static (_, _, _, _, _) => 0u,
|
||||||
|
strength: () => 100,
|
||||||
|
selection: new SelectionState(),
|
||||||
|
datFont: null,
|
||||||
|
itemInteraction: interaction);
|
||||||
|
|
||||||
|
Assert.True(interaction.ActivateItem(loot));
|
||||||
|
|
||||||
|
Assert.Equal(new[] { (loot, Player, 0) }, pickups);
|
||||||
|
Assert.Equal(chest, objects.Get(loot)!.ContainerId);
|
||||||
|
Assert.Equal(loot, grid.GetItem(0)!.ItemId);
|
||||||
|
Assert.True(grid.GetItem(0)!.WaitingVisual);
|
||||||
|
Assert.Equal(0xAu, grid.GetItem(1)!.ItemId);
|
||||||
|
Assert.Equal(0xBu, grid.GetItem(2)!.ItemId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LootDrop_ServerFailureRemovesOnlyThePendingProjection()
|
public void LootDrop_ServerFailureRemovesOnlyThePendingProjection()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace AcDream.App.Tests.World;
|
||||||
public sealed class ExternalContainerLifecycleControllerTests
|
public sealed class ExternalContainerLifecycleControllerTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Replacement_RetiresPreviousRootExactlyOnce()
|
public void ReplacementRequest_RetiresPreviousRootBeforeViewContents()
|
||||||
{
|
{
|
||||||
var state = new ExternalContainerState();
|
var state = new ExternalContainerState();
|
||||||
var objects = new ClientObjectTable();
|
var objects = new ClientObjectTable();
|
||||||
|
|
@ -18,6 +18,10 @@ public sealed class ExternalContainerLifecycleControllerTests
|
||||||
state.RequestOpen(10u);
|
state.RequestOpen(10u);
|
||||||
state.ApplyViewContents(10u);
|
state.ApplyViewContents(10u);
|
||||||
state.RequestOpen(20u);
|
state.RequestOpen(20u);
|
||||||
|
|
||||||
|
Assert.Equal(new uint[] { 10u }, retired);
|
||||||
|
Assert.Empty(objects.GetContents(10u));
|
||||||
|
|
||||||
state.ApplyViewContents(20u);
|
state.ApplyViewContents(20u);
|
||||||
state.ApplyViewContents(20u);
|
state.ApplyViewContents(20u);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public sealed class ExternalContainerStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReplacementPublishesOldAndNewExactlyOnce()
|
public void ReplacementRetiresOldImmediatelyThenOpensNewOnViewContents()
|
||||||
{
|
{
|
||||||
var state = new ExternalContainerState();
|
var state = new ExternalContainerState();
|
||||||
var changes = new List<ExternalContainerTransition>();
|
var changes = new List<ExternalContainerTransition>();
|
||||||
|
|
@ -25,15 +25,23 @@ public sealed class ExternalContainerStateTests
|
||||||
state.RequestOpen(1u);
|
state.RequestOpen(1u);
|
||||||
state.ApplyViewContents(1u);
|
state.ApplyViewContents(1u);
|
||||||
state.RequestOpen(2u);
|
state.RequestOpen(2u);
|
||||||
|
|
||||||
|
Assert.Equal(0u, state.CurrentContainerId);
|
||||||
|
Assert.Equal(2u, state.RequestedContainerId);
|
||||||
|
|
||||||
state.ApplyViewContents(2u);
|
state.ApplyViewContents(2u);
|
||||||
state.ApplyViewContents(2u);
|
state.ApplyViewContents(2u);
|
||||||
|
|
||||||
Assert.Equal(2, changes.Count);
|
Assert.Equal(3, changes.Count);
|
||||||
Assert.Equal(ExternalContainerTransitionKind.Opened, changes[0].Kind);
|
Assert.Equal(ExternalContainerTransitionKind.Opened, changes[0].Kind);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new ExternalContainerTransition(
|
new ExternalContainerTransition(
|
||||||
ExternalContainerTransitionKind.Replaced, 1u, 2u),
|
ExternalContainerTransitionKind.ReplacementRequested, 1u, 2u),
|
||||||
changes[1]);
|
changes[1]);
|
||||||
|
Assert.Equal(
|
||||||
|
new ExternalContainerTransition(
|
||||||
|
ExternalContainerTransitionKind.Opened, 0u, 2u),
|
||||||
|
changes[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -49,14 +57,14 @@ public sealed class ExternalContainerStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RefusedReplacementRestoresCurrentAsExpectedRoot()
|
public void RefusedReplacementLeavesTheRetiredRootClosed()
|
||||||
{
|
{
|
||||||
var state = Open(1u);
|
var state = Open(1u);
|
||||||
state.RequestOpen(2u);
|
state.RequestOpen(2u);
|
||||||
|
|
||||||
Assert.True(state.ApplyUseDone(0x550u));
|
Assert.True(state.ApplyUseDone(0x550u));
|
||||||
Assert.Equal(1u, state.CurrentContainerId);
|
Assert.Equal(0u, state.CurrentContainerId);
|
||||||
Assert.Equal(1u, state.RequestedContainerId);
|
Assert.Equal(0u, state.RequestedContainerId);
|
||||||
Assert.False(state.ApplyViewContents(2u));
|
Assert.False(state.ApplyViewContents(2u));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue