feat(D.5.2): thread UiEffects through EntitySpawn + route 0x02CE PublicUpdatePropertyInt

- EntitySpawn record: add UiEffects = 0 field (after IconUnderlayId)
- EntitySpawn construction site: thread parsed.Value.UiEffects as the new tail arg
- WorldSession: declare ObjectIntPropertyUpdate payload record + ObjectIntPropertyUpdated event (after StateUpdated)
- Message loop: add else-if branch for PublicUpdatePropertyInt.Opcode (0x02CE), parses + fires ObjectIntPropertyUpdated

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-17 18:33:18 +02:00
parent 242bc9286d
commit e7b6e83cf8

View file

@ -88,7 +88,10 @@ public sealed class WorldSession : IDisposable
// (0x40000000) and WeenieHeaderFlag2.IconUnderlay (0x01) respectively. // (0x40000000) and WeenieHeaderFlag2.IconUnderlay (0x01) respectively.
// Zero when the server did not send the field (common for most entities). // Zero when the server did not send the field (common for most entities).
uint IconOverlayId = 0, 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);
/// <summary>Fires when the session finishes parsing a CreateObject.</summary> /// <summary>Fires when the session finishes parsing a CreateObject.</summary>
public event Action<EntitySpawn>? EntitySpawned; public event Action<EntitySpawn>? EntitySpawned;
@ -161,6 +164,20 @@ public sealed class WorldSession : IDisposable
/// </summary> /// </summary>
public event Action<SetState.Parsed>? StateUpdated; public event Action<SetState.Parsed>? StateUpdated;
/// <summary>
/// Payload for <see cref="ObjectIntPropertyUpdated"/>: 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).
/// </summary>
public readonly record struct ObjectIntPropertyUpdate(uint Guid, uint Property, int Value);
/// <summary>
/// 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.
/// </summary>
public event Action<ObjectIntPropertyUpdate>? ObjectIntPropertyUpdated;
/// <summary> /// <summary>
/// Fires when the server sends a PlayerTeleport (0xF751) game message, /// Fires when the server sends a PlayerTeleport (0xF751) game message,
/// signalling that the player is entering portal space. The uint payload /// 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.UseRadius,
parsed.Value.IconId, parsed.Value.IconId,
parsed.Value.IconOverlayId, parsed.Value.IconOverlayId,
parsed.Value.IconUnderlayId)); parsed.Value.IconUnderlayId,
parsed.Value.UiEffects));
} }
} }
else if (op == DeleteObject.Opcode) else if (op == DeleteObject.Opcode)
@ -901,6 +919,13 @@ public sealed class WorldSession : IDisposable
if (parsed is not null) if (parsed is not null)
VitalCurrentUpdated?.Invoke(parsed.Value); 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) else if (op == GameEventEnvelope.Opcode)
{ {
// Phase F.1: 0xF7B0 is the GameEvent envelope. Parse the // Phase F.1: 0xF7B0 is the GameEvent envelope. Parse the