Final-review response. Adds the spec §5 / Task-16 burden end-to-end coverage the per-task plan missed: (1) burden reads wire EncumbranceVal over the carried sum (asserts 50% from wire 7500, not 20% from sum 3000 — retires AP-48's fallback as the primary), (2) a live player-int update repaints the bar (60%), which only happens via the C1d Concerns `o.ObjectId == p` branch. Both are discriminating (fail if the respective branch is reverted). Plus two clarifying comments from the review: the 0x02CD player route depends on the login PD upsert having created the player object (no-ops, no phantom, if not); and a TODO that the container-open phase must treat ViewContents as a full replace, not the additive merge used here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
3.7 KiB
C#
79 lines
3.7 KiB
C#
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) = live int apply, PrivateUpdatePropertyInt
|
|
/// (0x02CD) = player int (burden), SetStackSize (0x0197) = stack count, InventoryRemoveObject
|
|
/// (0x0024) = inventory-view removal.
|
|
/// 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, Func<uint>? playerGuid = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(session);
|
|
ArgumentNullException.ThrowIfNull(table);
|
|
|
|
session.EntitySpawned += s => table.Ingest(ToWeenieData(s));
|
|
session.EntityDeleted += d => table.Remove(d.Guid);
|
|
|
|
// B-Wire: apply EVERY PropertyInt update on a visible object (0x02CE), not just
|
|
// UiEffects — the server is the authority on object properties. UpdateIntProperty
|
|
// stores it in the bundle and still mirrors UiEffects → the typed Effects field.
|
|
session.ObjectIntPropertyUpdated += u =>
|
|
table.UpdateIntProperty(u.Guid, u.Property, u.Value);
|
|
|
|
// B-Wire: PrivateUpdatePropertyInt (0x02CD) carries no guid — it targets the
|
|
// local player. Route it to the player object so live EncumbranceVal updates the
|
|
// burden bar. The player ClientObject is created at login by the PD UpsertProperties
|
|
// call (which precedes any live 0x02CD), so UpdateIntProperty finds it. If it somehow
|
|
// hasn't yet, this no-ops (UpdateIntProperty returns false on an unknown guid) rather
|
|
// than creating a phantom — the next PD / CreateObject seeds it.
|
|
session.PlayerIntPropertyUpdated += u =>
|
|
{
|
|
if (playerGuid is not null)
|
|
table.UpdateIntProperty(playerGuid(), u.Property, u.Value);
|
|
};
|
|
|
|
// B-Wire: SetStackSize (0x0197) — update the object's stack count + value.
|
|
session.StackSizeUpdated += u => table.UpdateStackSize(u.Guid, u.StackSize, u.Value);
|
|
|
|
// B-Wire: InventoryRemoveObject (0x0024) — the object left the player's inventory
|
|
// view; drop it from the table (retail ClientUISystem removes it from maintenance).
|
|
session.InventoryObjectRemoved += guid => table.Remove(guid);
|
|
}
|
|
|
|
/// <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);
|
|
}
|