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) {