diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs
index d263f0e7..db21014d 100644
--- a/src/AcDream.Core.Net/WorldSession.cs
+++ b/src/AcDream.Core.Net/WorldSession.cs
@@ -88,7 +88,10 @@ public sealed class WorldSession : IDisposable
// (0x40000000) and WeenieHeaderFlag2.IconUnderlay (0x01) respectively.
// Zero when the server did not send the field (common for most entities).
uint IconOverlayId = 0,
- uint IconUnderlayId = 0);
+ uint IconUnderlayId = 0,
+ // D.5.2 (2026-06-17): UiEffects bitfield (weenieFlags 0x80) — drives the icon's
+ // effect recolor. CreateObject-only; 0 = no effect.
+ uint UiEffects = 0);
/// Fires when the session finishes parsing a CreateObject.
public event Action? EntitySpawned;
@@ -161,6 +164,20 @@ public sealed class WorldSession : IDisposable
///
public event Action? StateUpdated;
+ ///
+ /// Payload for : a single PropertyInt change on
+ /// a visible object (from PublicUpdatePropertyInt 0x02CE). Subscribers map the
+ /// property to typed state (e.g. UiEffects → the item's icon effect).
+ ///
+ public readonly record struct ObjectIntPropertyUpdate(uint Guid, uint Property, int Value);
+
+ ///
+ /// Fires when the session parses a PublicUpdatePropertyInt (0x02CE) — one
+ /// PropertyInt updated on a visible object. D.5.2 routes UiEffects (18) to the
+ /// item repository so the icon re-composites live.
+ ///
+ public event Action? ObjectIntPropertyUpdated;
+
///
/// Fires when the server sends a PlayerTeleport (0xF751) game message,
/// signalling that the player is entering portal space. The uint payload
@@ -727,7 +744,8 @@ public sealed class WorldSession : IDisposable
parsed.Value.UseRadius,
parsed.Value.IconId,
parsed.Value.IconOverlayId,
- parsed.Value.IconUnderlayId));
+ parsed.Value.IconUnderlayId,
+ parsed.Value.UiEffects));
}
}
else if (op == DeleteObject.Opcode)
@@ -901,6 +919,13 @@ public sealed class WorldSession : IDisposable
if (parsed is not null)
VitalCurrentUpdated?.Invoke(parsed.Value);
}
+ else if (op == PublicUpdatePropertyInt.Opcode)
+ {
+ var p = PublicUpdatePropertyInt.TryParse(body);
+ if (p is not null)
+ ObjectIntPropertyUpdated?.Invoke(
+ new ObjectIntPropertyUpdate(p.Value.Guid, p.Value.Property, p.Value.Value));
+ }
else if (op == GameEventEnvelope.Opcode)
{
// Phase F.1: 0xF7B0 is the GameEvent envelope. Parse the