From 87ba5c9a987782f8ae1b70f7498aa7967ac35aac Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 16:54:17 +0200 Subject: [PATCH] feat(B.5): pickup feedback chat line + toast ("You pick up the X.") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After B.5 shipped, the actual pickup was invisible feedback-wise: the item left the ground, ACE despawned it via PickupEvent (0xF74A), and the ItemRepository got updated — but the player had no visual acknowledgement that anything happened. The M1 demo's "pick up an item" target visually felt like the item just vanished into the void. Add a new EntityPickedUp event to WorldSession that fires from the PickupEvent (0xF74A) dispatch branch BEFORE EntityDeleted, so the subscriber can still read the entity's display name from _entitiesByServerGuid before the despawn handler clears it. GameWindow subscribes during the live-session wiring block and emits a retail-style system chat line plus a debug toast on every successful pickup, mirroring retail behavior (retail synthesized this line client-side; ACE doesn't echo it). Closes the M1 demo "pick up" target's visible-payoff gap. --- src/AcDream.App/Rendering/GameWindow.cs | 11 +++++++++++ src/AcDream.Core.Net/WorldSession.cs | 23 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index c6fc8e8..9b7e247 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1921,6 +1921,17 @@ public sealed class GameWindow : IDisposable _liveSession.PlayerKilledReceived += pk => Chat.OnPlayerKilled(pk.DeathMessage, pk.VictimGuid, pk.KillerGuid); + // B.5 polish (2026-05-14): surface successful pickups as a + // retail-style "You pick up the X." system chat line plus a + // toast. PickupEvent fires BEFORE the EntityDeleted despawn + // chain so the entity-name lookup still hits. + _liveSession.EntityPickedUp += parsed => + { + string name = DescribeLiveEntity(parsed.Guid); + Chat.OnSystemMessage($"You pick up the {name}.", chatType: 0); + _debugVm?.AddToast($"Picked up: {name}"); + }; + _liveSession.TurbineChatReceived += parsed => { if (parsed.Body is AcDream.Core.Net.Messages.TurbineChat.Payload.EventSendToRoom ev) diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 2e644c6..2e09315 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -84,6 +84,17 @@ public sealed class WorldSession : IDisposable /// public event Action? EntityDeleted; + /// + /// Fires when the session parses a 0xF74A PickupEvent game message — + /// distinguishes a despawn caused by a player pickup from a generic + /// DeleteObject despawn. Fires BEFORE + /// for the same guid so subscribers can read entity state (e.g. name) + /// before the despawn handler removes the entity. Useful for "You pick + /// up the X" chat/toast feedback that needs the entity's display + /// name at the moment of pickup. + /// + public event Action? EntityPickedUp; + /// /// Payload for : the server guid of the entity /// whose motion changed and its new server-side stance + forward command. @@ -717,13 +728,21 @@ public sealed class WorldSession : IDisposable // ACE sends PickupEvent (0xF74A) instead of DeleteObject // when a player picks up a world item (Player_Tracking // .RemoveTrackedObject with fromPickup=true). Downstream - // view-removal semantics are identical, so we adapt to - // DeleteObject.Parsed and reuse the existing handler. + // view-removal semantics are identical to DeleteObject, so + // we adapt to DeleteObject.Parsed and reuse the existing + // EntityDeleted handler. We also fire a distinct + // EntityPickedUp event BEFORE the despawn so a subscriber + // can read entity-side state (e.g. its display name) for + // pickup-feedback chat / toast lines while the entity is + // still resident. var parsed = PickupEvent.TryParse(body); if (parsed is not null) + { + EntityPickedUp?.Invoke(parsed.Value); EntityDeleted?.Invoke( new DeleteObject.Parsed( parsed.Value.Guid, parsed.Value.InstanceSequence)); + } } else if (op == UpdateMotion.Opcode) {