acdream/src/AcDream.Core.Net/ObjectTableWiring.cs

188 lines
8.2 KiB
C#

using AcDream.Core.Items;
using AcDream.Core.Player;
namespace AcDream.Core.Net;
/// <summary>
/// Wires WorldSession quality/property events into the client object table:
/// PublicUpdatePropertyInt (0x02CE) = live int apply, PrivateUpdatePropertyInt
/// (0x02CD) = player int (burden), PrivateUpdatePropertyInt64 (0x02CF) = player
/// 64-bit qualities (experience), SetStackSize (0x0197) = stack count,
/// InventoryRemoveObject (0x0024) = inventory-view removal.
/// CreateObject/DeleteObject application remains exposed as pure helpers so
/// the App live-entity owner can freshness-gate the shared projections first.
/// Retail: ACCObjectMaint::CreateObject / DeleteObject (the weenie_object_table side).
/// </summary>
public static class ObjectTableWiring
{
/// <summary>
/// Subscribe <paramref name="table"/> to quality, inventory, and property
/// updates whose freshness is independent of the physics timestamp pack.
/// </summary>
public static IDisposable Wire(
WorldSession session,
ClientObjectTable table,
Func<uint>? playerGuid = null,
LocalPlayerState? localPlayer = null,
Func<bool>? accepting = null)
{
ArgumentNullException.ThrowIfNull(session);
ArgumentNullException.ThrowIfNull(table);
var subscriptions = new SubscriptionSet();
// Create/Delete/Pickup are deliberately not subscribed here. Their
// shared live-object timestamps must be accepted before either the
// retained weenie projection or the render projection mutates.
// 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.
Action<WorldSession.ObjectIntPropertyUpdate> objectIntUpdated = u =>
{
if (accepting?.Invoke() == false) return;
table.UpdateIntProperty(u.Guid, u.Property, u.Value);
};
session.ObjectIntPropertyUpdated += objectIntUpdated;
subscriptions.Add(() => session.ObjectIntPropertyUpdated -= objectIntUpdated);
// 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.
Action<WorldSession.PlayerIntPropertyUpdate> playerIntUpdated = u =>
{
if (accepting?.Invoke() == false) return;
if (playerGuid is not null)
table.UpdateIntProperty(playerGuid(), u.Property, u.Value);
};
session.PlayerIntPropertyUpdated += playerIntUpdated;
subscriptions.Add(() => session.PlayerIntPropertyUpdated -= playerIntUpdated);
// Retail's qualities system owns one local-player value and notifies every
// registered panel. acdream currently exposes that value through two projections:
// ClientObjectTable (retained UI) and LocalPlayerState (Core consumers/fallback).
// Apply the authoritative 0x02CF update to both in this one wiring owner so they
// cannot drift after the login PlayerDescription snapshot.
Action<WorldSession.PlayerInt64PropertyUpdate> playerInt64Updated = u =>
{
if (accepting?.Invoke() == false) return;
ApplyPlayerInt64PropertyUpdate(
table, localPlayer, playerGuid?.Invoke() ?? 0u, u);
};
session.PlayerInt64PropertyUpdated += playerInt64Updated;
subscriptions.Add(() => session.PlayerInt64PropertyUpdated -= playerInt64Updated);
// B-Wire: SetStackSize (0x0197) — update the object's stack count + value.
Action<WorldSession.StackSizeUpdate> stackSizeUpdated = u =>
{
if (accepting?.Invoke() == false) return;
table.UpdateStackSize(u.Guid, u.StackSize, u.Value);
};
session.StackSizeUpdated += stackSizeUpdated;
subscriptions.Add(() => session.StackSizeUpdated -= stackSizeUpdated);
// B-Wire: InventoryRemoveObject (0x0024) — the object left the player's inventory
// view; drop it from the table (retail ClientUISystem removes it from maintenance).
Action<uint> inventoryObjectRemoved = guid =>
{
if (accepting?.Invoke() == false) return;
table.Remove(guid);
};
session.InventoryObjectRemoved += inventoryObjectRemoved;
subscriptions.Add(() => session.InventoryObjectRemoved -= inventoryObjectRemoved);
return subscriptions;
}
/// <summary>
/// Applies one CreateObject to the retained object projection after the
/// owning runtime accepts its physics generation. Internal for a
/// socket-free conformance test of the subscription body.
/// </summary>
public static bool ApplyEntitySpawn(
ClientObjectTable table,
WorldSession.EntitySpawn spawn,
bool replaceGeneration = false,
Func<bool>? accepting = null)
{
ArgumentNullException.ThrowIfNull(table);
if (accepting?.Invoke() == false)
return false;
WeenieData data = ToWeenieData(spawn);
if (replaceGeneration)
{
return table.ReplaceGeneration(
data,
spawn.InstanceSequence,
accepting) is not null;
}
else
table.Ingest(data);
// Ingest publishes ObjectAdded/ObjectUpdated synchronously. A nested
// replacement wins; report the invalidated outer application so its
// remaining CreateObject tail cannot run.
return accepting?.Invoke() != false;
}
/// <summary>
/// Applies a true DeleteObject only after the owning runtime accepts the
/// exact object incarnation. Pickup still removes only the 3-D projection.
/// </summary>
public static void ApplyEntityDelete(
ClientObjectTable table,
Messages.DeleteObject.Parsed delete)
{
table.RemoveLogicalGeneration(delete.Guid, delete.InstanceSequence);
}
/// <summary>Shared application step kept internal for byte-to-state conformance tests.</summary>
internal static void ApplyPlayerInt64PropertyUpdate(
ClientObjectTable table,
LocalPlayerState? localPlayer,
uint playerGuid,
WorldSession.PlayerInt64PropertyUpdate update)
{
if (playerGuid != 0u)
table.UpdateInt64Property(playerGuid, update.Property, update.Value);
localPlayer?.OnInt64PropertyUpdate(update.Property, update.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,
Useability: s.Useability,
TargetType: s.TargetType,
RadarBlipColor: s.RadarBlipColor,
RadarBehavior: s.RadarBehavior,
PublicWeenieBitfield: s.ObjectDescriptionFlags,
CombatUse: s.CombatUse,
PluralName: s.PluralName,
PetOwnerId: s.PetOwnerId,
AmmoType: s.AmmoType,
SpellId: s.SpellId);
}