feat(D.5.4): ObjectTableWiring (CreateObject=upsert, Delete=evict, 0x02CE) off GameWindow
CreateObject ingestion moves to Core.Net; GameWindow drops the EnrichItem call + inline 0x02CE handler. Fixes the Coldeve blank-icon root cause: items with no PD stub are now created, not dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e3f209707
commit
82f5968316
3 changed files with 158 additions and 15 deletions
|
|
@ -2357,6 +2357,10 @@ public sealed class GameWindow : IDisposable
|
||||||
private void WireLiveSessionEvents(AcDream.Core.Net.WorldSession session)
|
private void WireLiveSessionEvents(AcDream.Core.Net.WorldSession session)
|
||||||
{
|
{
|
||||||
_liveSession = session;
|
_liveSession = session;
|
||||||
|
// D.5.4: ingest CreateObject into the object table (upsert) and wire Delete +
|
||||||
|
// UiEffects live update. Wire BEFORE EntitySpawned += OnLiveEntitySpawned so
|
||||||
|
// the table is populated before the render handler runs.
|
||||||
|
AcDream.Core.Net.ObjectTableWiring.Wire(session, Objects);
|
||||||
_liveSession.EntitySpawned += OnLiveEntitySpawned;
|
_liveSession.EntitySpawned += OnLiveEntitySpawned;
|
||||||
_liveSession.EntityDeleted += OnLiveEntityDeleted;
|
_liveSession.EntityDeleted += OnLiveEntityDeleted;
|
||||||
_liveSession.MotionUpdated += OnLiveMotionUpdated;
|
_liveSession.MotionUpdated += OnLiveMotionUpdated;
|
||||||
|
|
@ -2632,13 +2636,6 @@ public sealed class GameWindow : IDisposable
|
||||||
_liveSession.VitalCurrentUpdated += v =>
|
_liveSession.VitalCurrentUpdated += v =>
|
||||||
LocalPlayer.OnVitalCurrent(v.VitalId, v.Current);
|
LocalPlayer.OnVitalCurrent(v.VitalId, v.Current);
|
||||||
|
|
||||||
// D.5.2: live PublicUpdatePropertyInt(0x02CE). Route UiEffects (18) to the item
|
|
||||||
// repository so a draining/charging item re-composites its icon in real time.
|
|
||||||
_liveSession.ObjectIntPropertyUpdated += u =>
|
|
||||||
{
|
|
||||||
if (u.Property == AcDream.Core.Items.ClientObjectTable.UiEffectsPropertyId)
|
|
||||||
Objects.UpdateIntProperty(u.Guid, u.Property, u.Value);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -2648,14 +2645,6 @@ public sealed class GameWindow : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnLiveEntitySpawned(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
|
private void OnLiveEntitySpawned(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
|
||||||
{
|
{
|
||||||
// D.5.1: enrich a known inventory/equipped item (stubbed from PlayerDescription)
|
|
||||||
// with the icon/name/type its CreateObject carries, so the toolbar can render it.
|
|
||||||
// D.5.1 (2026-06-17): also pass overlay/underlay ids from the extended
|
|
||||||
// WeenieHeader tail so IconComposer composites all icon layers.
|
|
||||||
Objects.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty,
|
|
||||||
(AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0),
|
|
||||||
spawn.IconOverlayId, spawn.IconUnderlayId, spawn.UiEffects);
|
|
||||||
|
|
||||||
// Phase A.1 hotfix: live CreateObject handler reads dats extensively
|
// Phase A.1 hotfix: live CreateObject handler reads dats extensively
|
||||||
// (Setup, GfxObj, Surface, SurfaceTexture) to hydrate the spawned
|
// (Setup, GfxObj, Surface, SurfaceTexture) to hydrate the spawned
|
||||||
// entity. All of it must run under the dat lock so it doesn't race
|
// entity. All of it must run under the dat lock so it doesn't race
|
||||||
|
|
|
||||||
57
src/AcDream.Core.Net/ObjectTableWiring.cs
Normal file
57
src/AcDream.Core.Net/ObjectTableWiring.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wires WorldSession GameMessage-level object events into the client object
|
||||||
|
/// table: CreateObject (0xF745) = canonical merge-upsert, DeleteObject (0xF747)
|
||||||
|
/// = evict, PublicUpdatePropertyInt (0x02CE) UiEffects = live icon re-composite.
|
||||||
|
/// Keeps object ingestion in Core.Net (pure data, no GL) and off GameWindow.
|
||||||
|
/// Retail: ACCObjectMaint::CreateObject / DeleteObject (the weenie_object_table side).
|
||||||
|
/// </summary>
|
||||||
|
public static class ObjectTableWiring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe <paramref name="table"/> to the three object-lifecycle events
|
||||||
|
/// on <paramref name="session"/>. Call this BEFORE the render handler subscribes
|
||||||
|
/// to EntitySpawned so the table is populated before the render path runs.
|
||||||
|
/// </summary>
|
||||||
|
public static void Wire(WorldSession session, ClientObjectTable table)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(session);
|
||||||
|
ArgumentNullException.ThrowIfNull(table);
|
||||||
|
|
||||||
|
session.EntitySpawned += s => table.Ingest(ToWeenieData(s));
|
||||||
|
session.EntityDeleted += d => table.Remove(d.Guid);
|
||||||
|
session.ObjectIntPropertyUpdated += u =>
|
||||||
|
{
|
||||||
|
if (u.Property == ClientObjectTable.UiEffectsPropertyId)
|
||||||
|
table.UpdateIntProperty(u.Guid, u.Property, u.Value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Translate the wire spawn into the table's merge patch.</summary>
|
||||||
|
public static WeenieData ToWeenieData(WorldSession.EntitySpawn s) => new(
|
||||||
|
Guid: s.Guid,
|
||||||
|
Name: s.Name,
|
||||||
|
Type: s.ItemType is { } it ? (ItemType)it : (ItemType?)null,
|
||||||
|
WeenieClassId: s.WeenieClassId,
|
||||||
|
IconId: s.IconId,
|
||||||
|
IconOverlayId: s.IconOverlayId,
|
||||||
|
IconUnderlayId: s.IconUnderlayId,
|
||||||
|
Effects: s.UiEffects,
|
||||||
|
Value: s.Value,
|
||||||
|
StackSize: s.StackSize,
|
||||||
|
StackSizeMax: s.StackSizeMax,
|
||||||
|
Burden: s.Burden,
|
||||||
|
ContainerId: s.ContainerId,
|
||||||
|
WielderId: s.WielderId,
|
||||||
|
ValidLocations: s.ValidLocations,
|
||||||
|
CurrentWieldedLocation: s.CurrentWieldedLocation,
|
||||||
|
Priority: s.Priority,
|
||||||
|
ItemsCapacity: s.ItemsCapacity,
|
||||||
|
ContainersCapacity: s.ContainersCapacity,
|
||||||
|
Structure: s.Structure,
|
||||||
|
MaxStructure: s.MaxStructure,
|
||||||
|
Workmanship: s.Workmanship);
|
||||||
|
}
|
||||||
97
tests/AcDream.Core.Net.Tests/ObjectTableWiringTests.cs
Normal file
97
tests/AcDream.Core.Net.Tests/ObjectTableWiringTests.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Net.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D.5.4 Task 7 — ObjectTableWiring.
|
||||||
|
///
|
||||||
|
/// Integration test is omitted: WorldSession.EntitySpawned has no internal
|
||||||
|
/// test seam to fire it without a real Tick + packet bytes, so subscription
|
||||||
|
/// correctness is covered by build (type-checks) + the live run. Only the
|
||||||
|
/// pure mapping (ToWeenieData) is unit-tested here.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ObjectTableWiringTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ToWeenieData_CopiesFieldsFromSpawn()
|
||||||
|
{
|
||||||
|
// Every EntitySpawn item field is set to a DISTINCT recognisable value so
|
||||||
|
// a positional transposition in ObjectTableWiring.ToWeenieData would trip
|
||||||
|
// at least one Assert. All 22 WeenieData fields are verified below.
|
||||||
|
var spawn = new WorldSession.EntitySpawn(
|
||||||
|
Guid: 0x00000600u,
|
||||||
|
Position: null,
|
||||||
|
SetupTableId: null,
|
||||||
|
AnimPartChanges: System.Array.Empty<CreateObject.AnimPartChange>(),
|
||||||
|
TextureChanges: System.Array.Empty<CreateObject.TextureChange>(),
|
||||||
|
SubPalettes: System.Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||||
|
BasePaletteId: null,
|
||||||
|
ObjScale: null,
|
||||||
|
Name: "Iron Sword",
|
||||||
|
ItemType: (uint)ItemType.MeleeWeapon,
|
||||||
|
MotionState: null,
|
||||||
|
MotionTableId: null)
|
||||||
|
with
|
||||||
|
{
|
||||||
|
WeenieClassId = 0x00001001u,
|
||||||
|
IconId = 0x06001111u,
|
||||||
|
IconOverlayId = 0x06002222u,
|
||||||
|
IconUnderlayId = 0x06003333u,
|
||||||
|
UiEffects = 0x00000004u,
|
||||||
|
Value = 7,
|
||||||
|
StackSize = 1,
|
||||||
|
StackSizeMax = 1,
|
||||||
|
Burden = 300,
|
||||||
|
ContainerId = 0x000000C9u,
|
||||||
|
WielderId = 0x000000DAu,
|
||||||
|
ValidLocations = 0x00000012u, // MeleeWeapon wield mask
|
||||||
|
CurrentWieldedLocation = 0x00000002u, // right-hand
|
||||||
|
Priority = 0x00000005u,
|
||||||
|
ItemsCapacity = 0,
|
||||||
|
ContainersCapacity = 0,
|
||||||
|
Structure = 80,
|
||||||
|
MaxStructure = 100,
|
||||||
|
Workmanship = 4.5f,
|
||||||
|
};
|
||||||
|
|
||||||
|
var d = ObjectTableWiring.ToWeenieData(spawn);
|
||||||
|
|
||||||
|
// --- identity ---
|
||||||
|
Assert.Equal(0x00000600u, d.Guid);
|
||||||
|
Assert.Equal("Iron Sword", d.Name);
|
||||||
|
Assert.Equal(ItemType.MeleeWeapon, d.Type);
|
||||||
|
|
||||||
|
// --- weenie / icon ---
|
||||||
|
Assert.Equal(0x00001001u, d.WeenieClassId);
|
||||||
|
Assert.Equal(0x06001111u, d.IconId);
|
||||||
|
Assert.Equal(0x06002222u, d.IconOverlayId);
|
||||||
|
Assert.Equal(0x06003333u, d.IconUnderlayId);
|
||||||
|
Assert.Equal(0x00000004u, d.Effects);
|
||||||
|
|
||||||
|
// --- quantity / economy ---
|
||||||
|
Assert.Equal(7, d.Value);
|
||||||
|
Assert.Equal(1, d.StackSize);
|
||||||
|
Assert.Equal(1, d.StackSizeMax);
|
||||||
|
Assert.Equal(300, d.Burden);
|
||||||
|
|
||||||
|
// --- container / wielder ---
|
||||||
|
Assert.Equal(0x000000C9u, d.ContainerId);
|
||||||
|
Assert.Equal(0x000000DAu, d.WielderId);
|
||||||
|
|
||||||
|
// --- equip masks ---
|
||||||
|
Assert.Equal(0x00000012u, d.ValidLocations);
|
||||||
|
Assert.Equal(0x00000002u, d.CurrentWieldedLocation);
|
||||||
|
Assert.Equal(0x00000005u, d.Priority);
|
||||||
|
|
||||||
|
// --- capacity ---
|
||||||
|
Assert.Equal(0, d.ItemsCapacity);
|
||||||
|
Assert.Equal(0, d.ContainersCapacity);
|
||||||
|
|
||||||
|
// --- durability ---
|
||||||
|
Assert.Equal(80, d.Structure);
|
||||||
|
Assert.Equal(100, d.MaxStructure);
|
||||||
|
Assert.Equal(4.5f, d.Workmanship);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue