Lands the codex-worktree D.2b stream plus the extraction the 2026-07-02 UI architecture review mandated before commit: - ItemInteractionController: single owner of double-click use/equip/ container-open, targeted-use mode (health kits), drag-out drop; toolbar shortcut drags don't drop the real item. ItemEquipRules for multi-slot (coat) coverage via equip masks. - Cursor phase: CursorFeedbackController (semantic priority chain: drag > resize > window-move > target-mode > text) + RetailCursorCatalog (enums 0x27/0x28/0x29, hotspot 14,14; ClientUISystem::UpdateCursorState 0x00564630) resolved through the portal EnumIDMap chain by RetailCursorResolver; RetailCursorManager applies dat cursor art to the OS cursor. Register row AP-72 covers the OS standard-cursor fallback. - Character window goes live: CharacterSheetProvider owns sheet assembly, XP-curve/raise-cost math and the raise flow — extracted out of GameWindow per Code Structure Rule 1 instead of committing the ~430-line feature body there. Optimistic XP/credit debits go through eventful store APIs (new ClientObjectTable.UpdateInt64Property + LocalPlayerState.DebitIntProperty/DebitInt64Property) instead of raw property-dictionary writes; register row AP-73 covers the still-missing raise ledger (#163). - RetailWindowFrame: the shared nine-slice window mount recipe; the character window uses it, remaining windows migrate via #164. - Status-bar buttons toggle inventory/character windows; retail row-major backpack ordering; WorldSession.SendUseWithTarget + raise/train sends. GameWindow shrinks 14,214 -> 13,877 lines despite the new features; the sheet/raise logic is unit-tested in CharacterSheetProviderTests instead of trapped in the god object. Build green; full suite 3,286 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
4.3 KiB
C#
87 lines
4.3 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));
|
|
// Retail two-table model:
|
|
// PickupEvent (0xF74A) → object left the 3-D world view; the weenie persists
|
|
// (it is moving into a container, e.g. unwield-to-pack). Remove the 3-D
|
|
// render entity only — do NOT evict from ClientObjectTable.
|
|
// DeleteObject (0xF747) → weenie is DESTROYED; evict from both tables.
|
|
// FromPickup = true guards the weenie eviction so it only fires for true destroys.
|
|
session.EntityDeleted += d => { if (!d.FromPickup) 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,
|
|
Useability: s.Useability,
|
|
TargetType: s.TargetType);
|
|
}
|