phase(N.4) Tasks 16+18+19: AnimatedEntityState + AnimPartChange + HiddenParts
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>
This commit is contained in:
parent
36f7a601c4
commit
ce72c574e9
4 changed files with 230 additions and 0 deletions
67
src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs
Normal file
67
src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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;
|
||||
}
|
||||
62
tests/AcDream.Core.Tests/Rendering/Wb/AnimPartChangeTests.cs
Normal file
62
tests/AcDream.Core.Tests/Rendering/Wb/AnimPartChangeTests.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class AnimPartChangeTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetPartOverride_ResolvedAtLookup()
|
||||
{
|
||||
var state = MakeState();
|
||||
|
||||
state.SetPartOverride(partIdx: 5, gfxObjId: 0x01001234ul);
|
||||
|
||||
Assert.True(state.TryGetPartOverride(5, out var got));
|
||||
Assert.Equal(0x01001234ul, got);
|
||||
Assert.False(state.TryGetPartOverride(6, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPartOverride_TwiceForSamePart_TakesLatest()
|
||||
{
|
||||
var state = MakeState();
|
||||
|
||||
state.SetPartOverride(0, 0x01000001ul);
|
||||
state.SetPartOverride(0, 0x01999999ul);
|
||||
|
||||
Assert.True(state.TryGetPartOverride(0, out var got));
|
||||
Assert.Equal(0x01999999ul, got);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvePartGfxObj_WithoutOverride_ReturnsSetupDefault()
|
||||
{
|
||||
var state = MakeState();
|
||||
|
||||
Assert.Equal(0x01000001ul,
|
||||
state.ResolvePartGfxObj(partIdx: 0, setupDefault: 0x01000001ul));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvePartGfxObj_WithOverride_ReturnsOverride()
|
||||
{
|
||||
var state = MakeState();
|
||||
state.SetPartOverride(partIdx: 0, gfxObjId: 0x01999999ul);
|
||||
|
||||
Assert.Equal(0x01999999ul,
|
||||
state.ResolvePartGfxObj(partIdx: 0, setupDefault: 0x01000001ul));
|
||||
}
|
||||
|
||||
private static AnimatedEntityState MakeState() => new(MakeSequencer());
|
||||
|
||||
private static AnimationSequencer MakeSequencer()
|
||||
=> new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class AnimatedEntityStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultState_HasNoOverridesAndNoHiddenParts()
|
||||
{
|
||||
var state = MakeState();
|
||||
|
||||
Assert.False(state.IsPartHidden(0));
|
||||
Assert.False(state.IsPartHidden(63));
|
||||
Assert.False(state.TryGetPartOverride(0, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sequencer_AccessibleAsProperty()
|
||||
{
|
||||
var sequencer = MakeSequencer();
|
||||
var state = new AnimatedEntityState(sequencer);
|
||||
|
||||
Assert.Same(sequencer, state.Sequencer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Construct_WithNullSequencer_ThrowsArgumentNull()
|
||||
{
|
||||
Assert.Throws<System.ArgumentNullException>(
|
||||
() => new AnimatedEntityState(null!));
|
||||
}
|
||||
|
||||
private static AnimatedEntityState MakeState() => new(MakeSequencer());
|
||||
|
||||
private static AnimationSequencer MakeSequencer()
|
||||
=> new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
}
|
||||
56
tests/AcDream.Core.Tests/Rendering/Wb/HiddenPartsTests.cs
Normal file
56
tests/AcDream.Core.Tests/Rendering/Wb/HiddenPartsTests.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class HiddenPartsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0b0000_0000ul, 0, false)]
|
||||
[InlineData(0b0000_0001ul, 0, true)]
|
||||
[InlineData(0b1000_0000ul, 7, true)]
|
||||
[InlineData(0b1000_0000ul, 6, false)]
|
||||
[InlineData(0xFFFF_FFFF_FFFF_FFFFul, 63, true)]
|
||||
public void IsPartHidden_RespectsBitmaskBit(ulong mask, int partIdx, bool expected)
|
||||
{
|
||||
var state = MakeState();
|
||||
state.HideParts(mask);
|
||||
Assert.Equal(expected, state.IsPartHidden(partIdx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPartHidden_NegativeIdx_ReturnsFalse()
|
||||
{
|
||||
var state = MakeState();
|
||||
state.HideParts(0xFFFF_FFFF_FFFF_FFFFul);
|
||||
Assert.False(state.IsPartHidden(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPartHidden_PartIdxOver64_ReturnsFalse()
|
||||
{
|
||||
var state = MakeState();
|
||||
state.HideParts(0xFFFF_FFFF_FFFF_FFFFul);
|
||||
Assert.False(state.IsPartHidden(64));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HideParts_DefaultsToNoneHidden()
|
||||
{
|
||||
var state = MakeState();
|
||||
for (int i = 0; i < 64; i++)
|
||||
Assert.False(state.IsPartHidden(i));
|
||||
}
|
||||
|
||||
private static AnimatedEntityState MakeState() => new(MakeSequencer());
|
||||
|
||||
private static AnimationSequencer MakeSequencer()
|
||||
=> new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue