Per-entity render state for the per-instance rendering tier (server-spawned characters / creatures / equipped items). Holds: - partGfxObjOverrides: Dictionary<int, ulong> — AnimPartChange swaps (e.g. wielding a weapon replaces a hand-part's GfxObj). - hiddenMask: ulong — HiddenParts bitmask. Bit i set hides part i. - AnimationSequencer reference — N.4 doesn't touch the sequencer; this just exposes it for the draw dispatcher. Public API: HideParts / IsPartHidden / SetPartOverride / TryGetPartOverride / ResolvePartGfxObj. Bounds-checked (partIdx < 0 or >= 64 → IsPartHidden returns false). Twelve tests covering the type, the AnimPartChange resolution helper, and the HiddenParts bitmask edge cases (theories for 0b0/0b1/MSB/all-ones, plus negative-index + out-of-range guards). Consumed by Task 17's EntitySpawnAdapter (creates one per CreateObject) and Task 22's WbDrawDispatcher (reads via per-part draw loop). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
67 lines
2.9 KiB
C#
67 lines
2.9 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.Core.Physics;
|
|
|
|
namespace AcDream.App.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// Per-entity render state for animated entities (characters, creatures,
|
|
/// equipped items). Holds AC-specific per-instance customizations the WB
|
|
/// atlas cache doesn't carry: <c>AnimPartChange</c> override map +
|
|
/// <c>HiddenParts</c> bitmask. Also holds a reference to acdream's existing
|
|
/// <see cref="AnimationSequencer"/> — Phase N.4 explicitly does not touch
|
|
/// the sequencer; we just route through it at draw time.
|
|
///
|
|
/// <para>
|
|
/// Lifecycle: created by <c>EntitySpawnAdapter.OnCreate</c> (Task 17) when
|
|
/// a server <c>CreateObject</c> is processed; destroyed by
|
|
/// <c>EntitySpawnAdapter.OnRemove</c> on <c>RemoveObject</c>. The mesh
|
|
/// data backing each part is cached in WB's <c>ObjectMeshManager</c>;
|
|
/// per-instance customizations don't go through the atlas — they overlay
|
|
/// at draw time.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class AnimatedEntityState
|
|
{
|
|
private readonly Dictionary<int, ulong> _partGfxObjOverrides = new();
|
|
private ulong _hiddenMask = 0;
|
|
|
|
/// <summary>Reference to acdream's existing animation sequencer.
|
|
/// Phase N.4 doesn't touch the sequencer; the draw dispatcher consumes
|
|
/// per-part transforms it produces per frame.</summary>
|
|
public AnimationSequencer Sequencer { get; }
|
|
|
|
public AnimatedEntityState(AnimationSequencer sequencer)
|
|
{
|
|
System.ArgumentNullException.ThrowIfNull(sequencer);
|
|
Sequencer = sequencer;
|
|
}
|
|
|
|
/// <summary>Set the <c>HiddenParts</c> bitmask for this entity. Bit
|
|
/// <c>i</c> set hides part <c>i</c> at draw time.</summary>
|
|
public void HideParts(ulong hiddenMask) => _hiddenMask = hiddenMask;
|
|
|
|
/// <summary>True if part <c>partIdx</c> should be skipped at draw
|
|
/// time. Returns false for part indices outside [0, 63].</summary>
|
|
public bool IsPartHidden(int partIdx)
|
|
{
|
|
if (partIdx < 0 || partIdx >= 64) return false;
|
|
return (_hiddenMask & (1ul << partIdx)) != 0;
|
|
}
|
|
|
|
/// <summary>Override the GfxObj id for a Setup part. Used for
|
|
/// AnimPartChange — e.g. wielding a weapon swaps the hand-part's
|
|
/// GfxObj.</summary>
|
|
public void SetPartOverride(int partIdx, ulong gfxObjId)
|
|
=> _partGfxObjOverrides[partIdx] = gfxObjId;
|
|
|
|
/// <summary>Look up the GfxObj override for a part. Returns false if
|
|
/// no override is set (caller should fall back to Setup default).</summary>
|
|
public bool TryGetPartOverride(int partIdx, out ulong gfxObjId)
|
|
=> _partGfxObjOverrides.TryGetValue(partIdx, out gfxObjId);
|
|
|
|
/// <summary>Resolve the GfxObj id for <paramref name="partIdx"/>:
|
|
/// override if set, else <paramref name="setupDefault"/>. Used by the
|
|
/// draw dispatcher to pick the right cached mesh data per part.</summary>
|
|
public ulong ResolvePartGfxObj(int partIdx, ulong setupDefault)
|
|
=> TryGetPartOverride(partIdx, out var ov) ? ov : setupDefault;
|
|
}
|