diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs index dba723c6..0413611d 100644 --- a/src/AcDream.Core.Net/GameEventWiring.cs +++ b/src/AcDream.Core.Net/GameEventWiring.cs @@ -65,7 +65,10 @@ public static class GameEventWiring // D.5.1 Task 4: persists Shortcuts from each PlayerDescription so the // toolbar can populate itself at login without keeping a parser reference. // Optional so all existing callers and tests compile unchanged. - Action>? onShortcuts = null) + Action>? onShortcuts = null, + // B-Wire: the local player's server guid. When provided, the PD handler upserts + // the player's own PropertyBundle (EncumbranceVal etc.) into the player ClientObject. + Func? playerGuid = null) { ArgumentNullException.ThrowIfNull(dispatcher); ArgumentNullException.ThrowIfNull(items); @@ -289,6 +292,13 @@ public static class GameEventWiring Console.WriteLine($"vitals: PlayerDescription body.len={e.Payload.Length} parsed={(p is null ? "NULL" : $"vec={p.Value.VectorFlags} attrs={p.Value.Attributes.Count} spells={p.Value.Spells.Count}")}"); if (p is null) return; + // B-Wire: deliver the player's OWN properties to the player ClientObject. + // (PD's "membership manifest" rule is about ITEMS, whose data comes from + // CreateObject; the player's own stats legitimately come from PD.) Upsert + // because PD can arrive before the player's CreateObject. Retires AP-48/AP-49. + if (playerGuid is not null) + items.UpsertProperties(playerGuid(), p.Value.Properties); + // K-fix13 (2026-04-26): build attrId → current map while // iterating attributes so the skill-formula resolver below // can apply (attr1.current * mult1 + attr2.current * mult2) diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index d0a92463..ca0a54db 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -496,4 +496,43 @@ public sealed class GameEventWiringTests Assert.Equal(0x5001u, got![0].ObjectGuid); } + [Fact] + public void WireAll_PlayerDescription_UpsertsPlayerPropertiesIntoClientObject() + { + // PD with a PropertyInt32 table carrying EncumbranceVal(5)=1500. The handler + // must land it in the player ClientObject so the burden bar reads the wire value. + const uint playerGuid = 0x50000001u; + var dispatcher = new GameEventDispatcher(); + var items = new ClientObjectTable(); + GameEventWiring.WireAll(dispatcher, items, new CombatState(), new Spellbook(), + new ChatLog(), playerGuid: () => playerGuid); + + var sb = new MemoryStream(); + using var w = new BinaryWriter(sb); + w.Write(0x00000001u); // propertyFlags = PropertyInt32 + w.Write(0x52u); // weenieType + // int table: u16 count, u16 buckets, then key/val pairs + w.Write((ushort)1); // count + w.Write((ushort)8); // buckets (ignored) + w.Write(5u); // key = EncumbranceVal + w.Write(1500u); // val + // vector + has_health (no vector blocks) + w.Write(0u); // vectorFlags = None + w.Write(0u); // has_health + // strict trailer + w.Write(0u); // option_flags = None + w.Write(0u); // options1 + w.Write(0u); // legacy hotbar count + w.Write(0u); // spellbook_filters + w.Write(0u); // inventory count + w.Write(0u); // equipped count + + var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.PlayerDescription, sb.ToArray())); + dispatcher.Dispatch(env!.Value); + + var player = items.Get(playerGuid); + Assert.NotNull(player); + Assert.Equal(1500, player!.Properties.Ints[5]); + } + }