feat(rendering): journal live scene projections
Mirror exact live-root and equipped-child lifetimes behind the non-drawing render-scene boundary. Ready, visibility, final-pose, rebucket, removal, and resource teardown edges retain canonical LiveEntityRuntime identity while active-only synchronization avoids resident-static scans. Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
parent
5d19c56d15
commit
58e7c2eb99
10 changed files with 949 additions and 148 deletions
|
|
@ -20,8 +20,9 @@ retail-faithful gameplay
|
||||||
| F0b — survivor/dispatcher/selection referee | complete | Exact PView routes, dispatcher candidates and final group payloads, material/alpha/clip/light/selection fields, and accepted picking parts. Release gate: 3,690 App tests / 3 skips and 8,174 complete-solution tests / 5 skips. |
|
| F0b — survivor/dispatcher/selection referee | complete | Exact PView routes, dispatcher candidates and final group payloads, material/alpha/clip/light/selection fields, and accepted picking parts. Release gate: 3,690 App tests / 3 skips and 8,174 complete-solution tests / 5 skips. |
|
||||||
| F1 — scene types and contained adapter | complete | `Arch 2.1.0` is pinned in App only behind acdream contracts. Five narrow archetypes, generation/incarnation/sequence gates, borrowed-query invalidation, memory accounting, deterministic digesting, and update-thread enforcement pass 11 focused tests. Release gate: 3,701 App tests / 3 skips and 8,185 complete-solution tests / 5 skips. |
|
| F1 — scene types and contained adapter | complete | `Arch 2.1.0` is pinned in App only behind acdream contracts. Five narrow archetypes, generation/incarnation/sequence gates, borrowed-query invalidation, memory accounting, deterministic digesting, and update-thread enforcement pass 11 focused tests. Release gate: 3,701 App tests / 3 skips and 8,185 complete-solution tests / 5 skips. |
|
||||||
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
||||||
| F3 — live/equipped projection | active | Live/equipped accepted-state publication and final-frame synchronization have not landed. |
|
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
||||||
| F4–F5 | pending | Dynamic indices and continuous shadow comparison have not started. |
|
| F4 — dynamic indices | active | Packed spatial/class/visibility query indices have not landed. |
|
||||||
|
| F5 | pending | Continuous shadow comparison has not started. |
|
||||||
| G0–G5 | pending | No production consumer has switched. |
|
| G0–G5 | pending | No production consumer has switched. |
|
||||||
|
|
||||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||||
() => source.ProjectionPoseReady -= handler));
|
() => source.ProjectionPoseReady -= handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BindProjectionRemoved(
|
||||||
|
EquippedChildRenderController source,
|
||||||
|
Action<uint> handler)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(handler);
|
||||||
|
source.ProjectionRemoved += handler;
|
||||||
|
Adopt("equipped-child projection removal", new DelegateBinding(
|
||||||
|
() => source.ProjectionRemoved -= handler));
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_deactivationStarted && _bindings.Count == 0)
|
if (_deactivationStarted && _bindings.Count == 0)
|
||||||
|
|
|
||||||
266
src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs
Normal file
266
src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
internal interface ILiveRenderProjectionSink
|
||||||
|
{
|
||||||
|
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
||||||
|
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
||||||
|
void OnProjectionPoseReady(uint serverGuid);
|
||||||
|
void OnProjectionRemoved(uint localEntityId);
|
||||||
|
void OnResourceUnregister(WorldEntity entity);
|
||||||
|
void SynchronizeActiveSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change-driven render projection for canonical live records. It retains
|
||||||
|
/// exact record references and local presentation IDs only; server GUID lookup
|
||||||
|
/// remains exclusively in <see cref="LiveEntityRuntime"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
|
{
|
||||||
|
private const byte LiveProjectionDomain = 3;
|
||||||
|
|
||||||
|
private readonly LiveEntityRuntime _runtime;
|
||||||
|
private readonly RenderProjectionJournal _journal;
|
||||||
|
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
||||||
|
private readonly List<TrackedProjection> _activeScratch = [];
|
||||||
|
|
||||||
|
public LiveRenderProjectionJournal(
|
||||||
|
LiveEntityRuntime runtime,
|
||||||
|
RenderProjectionJournal journal)
|
||||||
|
{
|
||||||
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ProjectionCount => _byLocalId.Count;
|
||||||
|
|
||||||
|
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
|
||||||
|
{
|
||||||
|
if (!candidate.IsCurrent(_runtime)
|
||||||
|
|| candidate.WorldEntity is not { } entity
|
||||||
|
|| !candidate.Record.ResourcesRegistered)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Upsert(candidate.Record, entity, candidate.Record.IsSpatiallyVisible);
|
||||||
|
return candidate.IsCurrent(_runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnProjectionVisibilityChanged(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
bool visible)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
if (!_runtime.IsCurrentRecord(record)
|
||||||
|
|| record.WorldEntity is not { } entity
|
||||||
|
|| !_byLocalId.ContainsKey(entity.Id))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Upsert(record, entity, visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnProjectionPoseReady(uint serverGuid)
|
||||||
|
{
|
||||||
|
if (!_runtime.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||||
|
|| record.WorldEntity is not { } entity
|
||||||
|
|| !_byLocalId.ContainsKey(entity.Id)
|
||||||
|
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Upsert(record, entity, record.IsSpatiallyVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnProjectionRemoved(uint localEntityId)
|
||||||
|
{
|
||||||
|
if (_byLocalId.TryGetValue(
|
||||||
|
localEntityId,
|
||||||
|
out TrackedProjection? tracked)
|
||||||
|
&& tracked.Projection.ProjectionClass is
|
||||||
|
RenderProjectionClass.EquippedChild)
|
||||||
|
{
|
||||||
|
Remove(tracked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResourceUnregister(WorldEntity entity)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
if (_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked)
|
||||||
|
&& ReferenceEquals(tracked.Entity, entity))
|
||||||
|
{
|
||||||
|
Remove(tracked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors only registered live/attached sources after simulation,
|
||||||
|
/// animation, and attachment pose composition have committed. Resident
|
||||||
|
/// statics are never scanned.
|
||||||
|
/// </summary>
|
||||||
|
public void SynchronizeActiveSources()
|
||||||
|
{
|
||||||
|
_activeScratch.Clear();
|
||||||
|
foreach (TrackedProjection tracked in _byLocalId.Values)
|
||||||
|
{
|
||||||
|
if (tracked.Record.IsSpatiallyProjected)
|
||||||
|
_activeScratch.Add(tracked);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _activeScratch.Count; i++)
|
||||||
|
{
|
||||||
|
TrackedProjection tracked = _activeScratch[i];
|
||||||
|
if (!_runtime.IsCurrentRecord(tracked.Record)
|
||||||
|
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|
||||||
|
|| !_byLocalId.TryGetValue(
|
||||||
|
tracked.Entity.Id,
|
||||||
|
out TrackedProjection? current)
|
||||||
|
|| !ReferenceEquals(current, tracked))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Upsert(
|
||||||
|
tracked.Record,
|
||||||
|
tracked.Entity,
|
||||||
|
tracked.Record.IsSpatiallyVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetTracking()
|
||||||
|
{
|
||||||
|
_byLocalId.Clear();
|
||||||
|
_activeScratch.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryGet(
|
||||||
|
uint localEntityId,
|
||||||
|
out RenderProjectionRecord record)
|
||||||
|
{
|
||||||
|
if (_byLocalId.TryGetValue(
|
||||||
|
localEntityId,
|
||||||
|
out TrackedProjection? tracked))
|
||||||
|
{
|
||||||
|
record = tracked.Projection;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
record = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static RenderProjectionId ProjectionId(uint localEntityId) =>
|
||||||
|
RenderProjectionId.FromRaw(
|
||||||
|
((ulong)LiveProjectionDomain << 56) | localEntityId);
|
||||||
|
|
||||||
|
private void Upsert(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
WorldEntity entity,
|
||||||
|
bool spatiallyVisible)
|
||||||
|
{
|
||||||
|
RenderProjectionRecord projected = Project(
|
||||||
|
record,
|
||||||
|
entity,
|
||||||
|
spatiallyVisible);
|
||||||
|
if (!_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked))
|
||||||
|
{
|
||||||
|
_journal.Register(in projected);
|
||||||
|
_byLocalId.Add(
|
||||||
|
entity.Id,
|
||||||
|
new TrackedProjection(record, entity, projected));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReferenceEquals(tracked.Record, record)
|
||||||
|
|| !ReferenceEquals(tracked.Entity, entity)
|
||||||
|
|| tracked.Projection.OwnerIncarnation
|
||||||
|
!= projected.OwnerIncarnation
|
||||||
|
|| tracked.Projection.ProjectionClass
|
||||||
|
!= projected.ProjectionClass)
|
||||||
|
{
|
||||||
|
_journal.Register(in projected);
|
||||||
|
_byLocalId[entity.Id] =
|
||||||
|
new TrackedProjection(record, entity, projected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projected = projected with
|
||||||
|
{
|
||||||
|
PreviousTransform = new PreviousRenderTransform(
|
||||||
|
tracked.Projection.Transform.LocalToWorld),
|
||||||
|
};
|
||||||
|
_journal.AppendDifference(tracked.Projection, projected);
|
||||||
|
tracked.Projection = projected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderProjectionRecord Project(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
WorldEntity entity,
|
||||||
|
bool spatiallyVisible)
|
||||||
|
{
|
||||||
|
uint fullCellId = record.FullCellId != 0
|
||||||
|
? record.FullCellId
|
||||||
|
: entity.ParentCellId ?? 0;
|
||||||
|
uint ownerLandblockId = record.CanonicalLandblockId != 0
|
||||||
|
? record.CanonicalLandblockId
|
||||||
|
: fullCellId == 0
|
||||||
|
? 0
|
||||||
|
: Canonicalize(fullCellId);
|
||||||
|
return RenderProjectionRecordFactory.ProjectEntity(
|
||||||
|
ProjectionId(entity.Id),
|
||||||
|
record.ProjectionKind is LiveEntityProjectionKind.Attached
|
||||||
|
? RenderProjectionClass.EquippedChild
|
||||||
|
: RenderProjectionClass.LiveDynamicRoot,
|
||||||
|
RenderOwnerIncarnation.FromRaw(
|
||||||
|
((ulong)record.Generation << 32) | entity.Id),
|
||||||
|
ownerLandblockId,
|
||||||
|
fullCellId,
|
||||||
|
entity,
|
||||||
|
spatiallyVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Remove(TrackedProjection tracked)
|
||||||
|
{
|
||||||
|
if (!_byLocalId.Remove(tracked.Entity.Id))
|
||||||
|
return;
|
||||||
|
_journal.Unregister(
|
||||||
|
tracked.Projection.Id,
|
||||||
|
tracked.Projection.OwnerIncarnation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint Canonicalize(uint cellOrLandblockId) =>
|
||||||
|
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
|
|
||||||
|
private sealed class TrackedProjection(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
WorldEntity entity,
|
||||||
|
RenderProjectionRecord projection)
|
||||||
|
{
|
||||||
|
public LiveEntityRecord Record { get; } = record;
|
||||||
|
public WorldEntity Entity { get; } = entity;
|
||||||
|
public RenderProjectionRecord Projection { get; set; } = projection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class LiveRenderProjectionResourceLifecycle(
|
||||||
|
ILiveRenderProjectionSink sink) : ILiveEntityResourceLifecycle
|
||||||
|
{
|
||||||
|
private readonly ILiveRenderProjectionSink _sink =
|
||||||
|
sink ?? throw new ArgumentNullException(nameof(sink));
|
||||||
|
|
||||||
|
public void Register(WorldEntity entity)
|
||||||
|
{
|
||||||
|
// The exact EntityReady edge follows all resource registration and is
|
||||||
|
// the first point at which the projection may enter the journal.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister(WorldEntity entity) =>
|
||||||
|
_sink.OnResourceUnregister(entity);
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,45 @@ internal sealed class RenderProjectionJournal
|
||||||
id,
|
id,
|
||||||
incarnation));
|
incarnation));
|
||||||
|
|
||||||
|
public void AppendDifference(
|
||||||
|
in RenderProjectionRecord prior,
|
||||||
|
in RenderProjectionRecord current)
|
||||||
|
{
|
||||||
|
if (prior.Id != current.Id
|
||||||
|
|| prior.OwnerIncarnation != current.OwnerIncarnation
|
||||||
|
|| prior.ProjectionClass != current.ProjectionClass)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"A channel update cannot change projection identity, incarnation, or class.",
|
||||||
|
nameof(current));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prior.Transform != current.Transform
|
||||||
|
|| prior.Bounds != current.Bounds
|
||||||
|
|| prior.SortKey != current.SortKey
|
||||||
|
|| prior.Source.TransformFingerprint
|
||||||
|
!= current.Source.TransformFingerprint)
|
||||||
|
{
|
||||||
|
Update(RenderProjectionDeltaKind.UpdateTransform, in current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prior.MeshSet != current.MeshSet
|
||||||
|
|| prior.Material != current.Material
|
||||||
|
|| prior.DegradeState != current.DegradeState
|
||||||
|
|| prior.Source.GeometryFingerprint
|
||||||
|
!= current.Source.GeometryFingerprint
|
||||||
|
|| prior.Source.AppearanceFingerprint
|
||||||
|
!= current.Source.AppearanceFingerprint)
|
||||||
|
{
|
||||||
|
Update(RenderProjectionDeltaKind.UpdateAppearance, in current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prior.Residency != current.Residency)
|
||||||
|
Update(RenderProjectionDeltaKind.Rebucket, in current);
|
||||||
|
if (prior.Flags != current.Flags)
|
||||||
|
Update(RenderProjectionDeltaKind.UpdateFlags, in current);
|
||||||
|
}
|
||||||
|
|
||||||
public RenderDeltaApplyResult DrainTo(IRenderScene scene)
|
public RenderDeltaApplyResult DrainTo(IRenderScene scene)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(scene);
|
ArgumentNullException.ThrowIfNull(scene);
|
||||||
|
|
|
||||||
130
src/AcDream.App/Rendering/Scene/RenderProjectionRecordFactory.cs
Normal file
130
src/AcDream.App/Rendering/Scene/RenderProjectionRecordFactory.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure conversion from an accepted <see cref="WorldEntity"/> presentation
|
||||||
|
/// snapshot into the packed render-scene record. This method never mutates
|
||||||
|
/// source AABB state or acquires gameplay identity.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RenderProjectionRecordFactory
|
||||||
|
{
|
||||||
|
private const float DefaultAabbRadius = 5.0f;
|
||||||
|
|
||||||
|
public static RenderProjectionRecord ProjectEntity(
|
||||||
|
RenderProjectionId id,
|
||||||
|
RenderProjectionClass projectionClass,
|
||||||
|
RenderOwnerIncarnation incarnation,
|
||||||
|
uint ownerLandblockId,
|
||||||
|
uint fullCellId,
|
||||||
|
WorldEntity entity,
|
||||||
|
bool spatiallyVisible)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
CurrentRenderProjectionFingerprint fingerprint =
|
||||||
|
CurrentRenderSceneOracle.CreateProjectionFingerprint(
|
||||||
|
ownerLandblockId,
|
||||||
|
entity);
|
||||||
|
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
||||||
|
if (spatiallyVisible
|
||||||
|
&& entity.IsDrawVisible
|
||||||
|
&& entity.IsAncestorDrawVisible)
|
||||||
|
{
|
||||||
|
flags |= RenderProjectionFlags.Draw;
|
||||||
|
}
|
||||||
|
if (!entity.IsDrawVisible)
|
||||||
|
flags |= RenderProjectionFlags.Hidden;
|
||||||
|
if (!entity.IsAncestorDrawVisible)
|
||||||
|
flags |= RenderProjectionFlags.AncestorHidden;
|
||||||
|
|
||||||
|
RenderTransform transform = RenderTransform.FromRoot(
|
||||||
|
entity.Position,
|
||||||
|
entity.Rotation,
|
||||||
|
entity.Scale);
|
||||||
|
(Vector3 minimum, Vector3 maximum) = CalculateBounds(entity);
|
||||||
|
return new RenderProjectionRecord(
|
||||||
|
id,
|
||||||
|
projectionClass,
|
||||||
|
incarnation,
|
||||||
|
transform,
|
||||||
|
new PreviousRenderTransform(transform.LocalToWorld),
|
||||||
|
new RenderMeshSet(
|
||||||
|
RenderAssetHandle.FromRaw(
|
||||||
|
fingerprint.Geometry.Low ^ fingerprint.Geometry.High),
|
||||||
|
entity.MeshRefs.Count,
|
||||||
|
0),
|
||||||
|
new RenderMaterialVariant(
|
||||||
|
fingerprint.Appearance.Low,
|
||||||
|
fingerprint.Appearance.High,
|
||||||
|
1.0f),
|
||||||
|
new RenderSpatialResidency(
|
||||||
|
RenderSpatialBucket.FromRaw(fullCellId),
|
||||||
|
ownerLandblockId,
|
||||||
|
fullCellId),
|
||||||
|
new RenderWorldBounds(minimum, maximum),
|
||||||
|
flags,
|
||||||
|
default,
|
||||||
|
id.ToSortKey(),
|
||||||
|
RenderDirtyMask.All,
|
||||||
|
new RenderSourceMetadata(
|
||||||
|
entity.Id,
|
||||||
|
entity.ServerGuid,
|
||||||
|
entity.SourceGfxObjOrSetupId,
|
||||||
|
entity.ParentCellId ?? 0,
|
||||||
|
entity.EffectCellId ?? 0,
|
||||||
|
entity.BuildingShellAnchorCellId ?? 0,
|
||||||
|
fingerprint.Transform,
|
||||||
|
fingerprint.Geometry,
|
||||||
|
fingerprint.Appearance));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||||
|
WorldEntity entity)
|
||||||
|
{
|
||||||
|
Vector3 position = entity.Position;
|
||||||
|
if (entity.HasLocalBounds)
|
||||||
|
{
|
||||||
|
Vector3 localMin = entity.LocalBoundMin;
|
||||||
|
Vector3 localMax = entity.LocalBoundMax;
|
||||||
|
Vector3 minimum = default;
|
||||||
|
Vector3 maximum = default;
|
||||||
|
for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++)
|
||||||
|
{
|
||||||
|
Vector3 corner = new(
|
||||||
|
(cornerIndex & 1) == 0 ? localMin.X : localMax.X,
|
||||||
|
(cornerIndex & 2) == 0 ? localMin.Y : localMax.Y,
|
||||||
|
(cornerIndex & 4) == 0 ? localMin.Z : localMax.Z);
|
||||||
|
Vector3 transformed =
|
||||||
|
Vector3.Transform(corner, entity.Rotation);
|
||||||
|
if (cornerIndex == 0)
|
||||||
|
{
|
||||||
|
minimum = transformed;
|
||||||
|
maximum = transformed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
minimum = Vector3.Min(minimum, transformed);
|
||||||
|
maximum = Vector3.Max(maximum, transformed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 margin = new(DefaultAabbRadius);
|
||||||
|
return (
|
||||||
|
position + minimum - margin,
|
||||||
|
position + maximum + margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
float radius = DefaultAabbRadius;
|
||||||
|
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||||
|
{
|
||||||
|
radius = MathF.Max(
|
||||||
|
radius,
|
||||||
|
DefaultAabbRadius
|
||||||
|
+ entity.MeshRefs[i].PartTransform.Translation.Length());
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 extent = new(radius);
|
||||||
|
return (position - extent, position + extent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Numerics;
|
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
@ -25,7 +24,6 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
{
|
{
|
||||||
private const byte StaticEntityDomain = 1;
|
private const byte StaticEntityDomain = 1;
|
||||||
private const byte EnvCellShellDomain = 2;
|
private const byte EnvCellShellDomain = 2;
|
||||||
private const float DefaultAabbRadius = 5.0f;
|
|
||||||
|
|
||||||
private readonly RenderProjectionJournal _journal;
|
private readonly RenderProjectionJournal _journal;
|
||||||
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
|
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
|
||||||
|
|
@ -115,7 +113,7 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
|
|
||||||
if (accepted != retained.Record)
|
if (accepted != retained.Record)
|
||||||
{
|
{
|
||||||
AppendUpdates(retained.Record, accepted);
|
_journal.AppendDifference(retained.Record, accepted);
|
||||||
current[candidate.Id] = new TrackedProjection(accepted);
|
current[candidate.Id] = new TrackedProjection(accepted);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -217,61 +215,18 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
WorldEntity entity)
|
WorldEntity entity)
|
||||||
{
|
{
|
||||||
CurrentRenderProjectionFingerprint fingerprint =
|
|
||||||
CurrentRenderSceneOracle.CreateProjectionFingerprint(
|
|
||||||
landblockId,
|
|
||||||
entity);
|
|
||||||
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
|
||||||
if (entity.IsDrawVisible && entity.IsAncestorDrawVisible)
|
|
||||||
flags |= RenderProjectionFlags.Draw;
|
|
||||||
if (!entity.IsDrawVisible)
|
|
||||||
flags |= RenderProjectionFlags.Hidden;
|
|
||||||
if (!entity.IsAncestorDrawVisible)
|
|
||||||
flags |= RenderProjectionFlags.AncestorHidden;
|
|
||||||
|
|
||||||
RenderTransform transform = RenderTransform.FromRoot(
|
|
||||||
entity.Position,
|
|
||||||
entity.Rotation,
|
|
||||||
entity.Scale);
|
|
||||||
(Vector3 minimum, Vector3 maximum) = CalculateBounds(entity);
|
|
||||||
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
||||||
uint fullCellId = entity.ParentCellId ?? landblockId;
|
uint fullCellId = entity.ParentCellId ?? landblockId;
|
||||||
return new RenderProjectionRecord(
|
return RenderProjectionRecordFactory.ProjectEntity(
|
||||||
id,
|
id,
|
||||||
InteriorEntityPartition.IsIndoorCellId(entity.ParentCellId)
|
InteriorEntityPartition.IsIndoorCellId(entity.ParentCellId)
|
||||||
? RenderProjectionClass.IndoorCellStatic
|
? RenderProjectionClass.IndoorCellStatic
|
||||||
: RenderProjectionClass.OutdoorStatic,
|
: RenderProjectionClass.OutdoorStatic,
|
||||||
default,
|
default,
|
||||||
transform,
|
|
||||||
new PreviousRenderTransform(transform.LocalToWorld),
|
|
||||||
new RenderMeshSet(
|
|
||||||
RenderAssetHandle.FromRaw(
|
|
||||||
fingerprint.Geometry.Low ^ fingerprint.Geometry.High),
|
|
||||||
entity.MeshRefs.Count,
|
|
||||||
0),
|
|
||||||
new RenderMaterialVariant(
|
|
||||||
fingerprint.Appearance.Low,
|
|
||||||
fingerprint.Appearance.High,
|
|
||||||
1.0f),
|
|
||||||
new RenderSpatialResidency(
|
|
||||||
RenderSpatialBucket.FromRaw(fullCellId),
|
|
||||||
landblockId,
|
landblockId,
|
||||||
fullCellId),
|
fullCellId,
|
||||||
new RenderWorldBounds(minimum, maximum),
|
entity,
|
||||||
flags,
|
spatiallyVisible: true);
|
||||||
default,
|
|
||||||
id.ToSortKey(),
|
|
||||||
RenderDirtyMask.All,
|
|
||||||
new RenderSourceMetadata(
|
|
||||||
entity.Id,
|
|
||||||
entity.ServerGuid,
|
|
||||||
entity.SourceGfxObjOrSetupId,
|
|
||||||
entity.ParentCellId ?? 0,
|
|
||||||
entity.EffectCellId ?? 0,
|
|
||||||
entity.BuildingShellAnchorCellId ?? 0,
|
|
||||||
fingerprint.Transform,
|
|
||||||
fingerprint.Geometry,
|
|
||||||
fingerprint.Appearance));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RenderProjectionRecord ProjectEnvCellShell(
|
private static RenderProjectionRecord ProjectEnvCellShell(
|
||||||
|
|
@ -332,95 +287,6 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
RenderSceneHash128.Empty));
|
RenderSceneHash128.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendUpdates(
|
|
||||||
in RenderProjectionRecord prior,
|
|
||||||
in RenderProjectionRecord current)
|
|
||||||
{
|
|
||||||
if (prior.Transform != current.Transform
|
|
||||||
|| prior.Bounds != current.Bounds
|
|
||||||
|| prior.SortKey != current.SortKey
|
|
||||||
|| prior.Source.TransformFingerprint
|
|
||||||
!= current.Source.TransformFingerprint)
|
|
||||||
{
|
|
||||||
_journal.Update(
|
|
||||||
RenderProjectionDeltaKind.UpdateTransform,
|
|
||||||
in current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prior.MeshSet != current.MeshSet
|
|
||||||
|| prior.Material != current.Material
|
|
||||||
|| prior.DegradeState != current.DegradeState
|
|
||||||
|| prior.Source.GeometryFingerprint != current.Source.GeometryFingerprint
|
|
||||||
|| prior.Source.AppearanceFingerprint != current.Source.AppearanceFingerprint)
|
|
||||||
{
|
|
||||||
_journal.Update(
|
|
||||||
RenderProjectionDeltaKind.UpdateAppearance,
|
|
||||||
in current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prior.Residency != current.Residency)
|
|
||||||
{
|
|
||||||
_journal.Update(
|
|
||||||
RenderProjectionDeltaKind.Rebucket,
|
|
||||||
in current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prior.Flags != current.Flags)
|
|
||||||
{
|
|
||||||
_journal.Update(
|
|
||||||
RenderProjectionDeltaKind.UpdateFlags,
|
|
||||||
in current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
|
||||||
WorldEntity entity)
|
|
||||||
{
|
|
||||||
Vector3 position = entity.Position;
|
|
||||||
if (entity.HasLocalBounds)
|
|
||||||
{
|
|
||||||
Vector3 localMin = entity.LocalBoundMin;
|
|
||||||
Vector3 localMax = entity.LocalBoundMax;
|
|
||||||
Vector3 minimum = default;
|
|
||||||
Vector3 maximum = default;
|
|
||||||
for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++)
|
|
||||||
{
|
|
||||||
Vector3 corner = new(
|
|
||||||
(cornerIndex & 1) == 0 ? localMin.X : localMax.X,
|
|
||||||
(cornerIndex & 2) == 0 ? localMin.Y : localMax.Y,
|
|
||||||
(cornerIndex & 4) == 0 ? localMin.Z : localMax.Z);
|
|
||||||
Vector3 transformed = Vector3.Transform(corner, entity.Rotation);
|
|
||||||
if (cornerIndex == 0)
|
|
||||||
{
|
|
||||||
minimum = transformed;
|
|
||||||
maximum = transformed;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
minimum = Vector3.Min(minimum, transformed);
|
|
||||||
maximum = Vector3.Max(maximum, transformed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 margin = new(DefaultAabbRadius);
|
|
||||||
return (
|
|
||||||
position + minimum - margin,
|
|
||||||
position + maximum + margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
float radius = DefaultAabbRadius;
|
|
||||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
|
||||||
{
|
|
||||||
radius = MathF.Max(
|
|
||||||
radius,
|
|
||||||
DefaultAabbRadius
|
|
||||||
+ entity.MeshRefs[i].PartTransform.Translation.Length());
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 extent = new(radius);
|
|
||||||
return (position - extent, position + extent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderOwnerIncarnation NextIncarnation()
|
private RenderOwnerIncarnation NextIncarnation()
|
||||||
{
|
{
|
||||||
if (_nextIncarnation == ulong.MaxValue)
|
if (_nextIncarnation == ulong.MaxValue)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -136,6 +137,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
_animatedEntities;
|
_animatedEntities;
|
||||||
private readonly EquippedChildRenderController _equippedChildren;
|
private readonly EquippedChildRenderController _equippedChildren;
|
||||||
private readonly LiveEffectFrameController _effects;
|
private readonly LiveEffectFrameController _effects;
|
||||||
|
private readonly ILiveRenderProjectionSink? _renderProjections;
|
||||||
|
|
||||||
public LiveObjectFrameController(
|
public LiveObjectFrameController(
|
||||||
RetailInboundEventDispatcher inboundEvents,
|
RetailInboundEventDispatcher inboundEvents,
|
||||||
|
|
@ -149,7 +151,8 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
LiveEntityAnimationPresenter animationPresenter,
|
LiveEntityAnimationPresenter animationPresenter,
|
||||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
|
||||||
EquippedChildRenderController equippedChildren,
|
EquippedChildRenderController equippedChildren,
|
||||||
LiveEffectFrameController effects)
|
LiveEffectFrameController effects,
|
||||||
|
ILiveRenderProjectionSink? renderProjections = null)
|
||||||
{
|
{
|
||||||
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
|
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
|
||||||
_localPlayerFrame = localPlayerFrame
|
_localPlayerFrame = localPlayerFrame
|
||||||
|
|
@ -168,6 +171,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
_equippedChildren = equippedChildren
|
_equippedChildren = equippedChildren
|
||||||
?? throw new ArgumentNullException(nameof(equippedChildren));
|
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||||
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
||||||
|
_renderProjections = renderProjections;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float deltaSeconds) =>
|
public void Tick(float deltaSeconds) =>
|
||||||
|
|
@ -202,6 +206,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
if (_animatedEntities.Count > 0)
|
if (_animatedEntities.Count > 0)
|
||||||
_animationPresenter.Present(schedules);
|
_animationPresenter.Present(schedules);
|
||||||
_equippedChildren.Tick();
|
_equippedChildren.Tick();
|
||||||
|
_renderProjections?.SynchronizeActiveSources();
|
||||||
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
|
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
|
||||||
// Script -> Particle -> process_hooks. acdream intentionally retains
|
// Script -> Particle -> process_hooks. acdream intentionally retains
|
||||||
// the TS-51 static-order difference here: ProcessHooks precedes the
|
// the TS-51 static-order difference here: ProcessHooks precedes the
|
||||||
|
|
@ -221,24 +226,28 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
||||||
private readonly EquippedChildRenderController _equippedChildren;
|
private readonly EquippedChildRenderController _equippedChildren;
|
||||||
private readonly ParticleHookSink _particleSink;
|
private readonly ParticleHookSink _particleSink;
|
||||||
private readonly LiveEntityLightController _lights;
|
private readonly LiveEntityLightController _lights;
|
||||||
|
private readonly ILiveRenderProjectionSink? _renderProjections;
|
||||||
|
|
||||||
public LiveSpatialPresentationReconciler(
|
public LiveSpatialPresentationReconciler(
|
||||||
EntityEffectController entityEffects,
|
EntityEffectController entityEffects,
|
||||||
EquippedChildRenderController equippedChildren,
|
EquippedChildRenderController equippedChildren,
|
||||||
ParticleHookSink particleSink,
|
ParticleHookSink particleSink,
|
||||||
LiveEntityLightController lights)
|
LiveEntityLightController lights,
|
||||||
|
ILiveRenderProjectionSink? renderProjections = null)
|
||||||
{
|
{
|
||||||
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
||||||
_equippedChildren = equippedChildren
|
_equippedChildren = equippedChildren
|
||||||
?? throw new ArgumentNullException(nameof(equippedChildren));
|
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||||
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
||||||
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||||
|
_renderProjections = renderProjections;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reconcile()
|
public void Reconcile()
|
||||||
{
|
{
|
||||||
_entityEffects.RefreshLiveOwnerPoses();
|
_entityEffects.RefreshLiveOwnerPoses();
|
||||||
_equippedChildren.Tick();
|
_equippedChildren.Tick();
|
||||||
|
_renderProjections?.SynchronizeActiveSources();
|
||||||
_particleSink.RefreshAttachedEmitters();
|
_particleSink.RefreshAttachedEmitters();
|
||||||
_lights.Refresh();
|
_lights.Refresh();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
|
|
@ -80,16 +81,19 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
||||||
private readonly Func<LiveEntityRecord, bool> _prepareEffects;
|
private readonly Func<LiveEntityRecord, bool> _prepareEffects;
|
||||||
private readonly Func<LiveEntityRecord, bool> _presentState;
|
private readonly Func<LiveEntityRecord, bool> _presentState;
|
||||||
private readonly Func<LiveEntityRecord, bool> _replayEffects;
|
private readonly Func<LiveEntityRecord, bool> _replayEffects;
|
||||||
|
private readonly ILiveRenderProjectionSink? _renderProjection;
|
||||||
|
|
||||||
public LiveEntityReadyPublisher(
|
public LiveEntityReadyPublisher(
|
||||||
LiveEntityRuntime runtime,
|
LiveEntityRuntime runtime,
|
||||||
EntityEffectController effects,
|
EntityEffectController effects,
|
||||||
LiveEntityPresentationController presentation)
|
LiveEntityPresentationController presentation,
|
||||||
|
ILiveRenderProjectionSink? renderProjection = null)
|
||||||
: this(
|
: this(
|
||||||
runtime,
|
runtime,
|
||||||
record => effects.PrepareLiveEntityOwner(record.ServerGuid),
|
record => effects.PrepareLiveEntityOwner(record.ServerGuid),
|
||||||
record => presentation.OnLiveEntityReady(record.ServerGuid),
|
record => presentation.OnLiveEntityReady(record.ServerGuid),
|
||||||
record => effects.ReplayPendingForLiveEntity(record.ServerGuid))
|
record => effects.ReplayPendingForLiveEntity(record.ServerGuid),
|
||||||
|
renderProjection)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(effects);
|
ArgumentNullException.ThrowIfNull(effects);
|
||||||
ArgumentNullException.ThrowIfNull(presentation);
|
ArgumentNullException.ThrowIfNull(presentation);
|
||||||
|
|
@ -99,7 +103,8 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
||||||
LiveEntityRuntime runtime,
|
LiveEntityRuntime runtime,
|
||||||
Func<LiveEntityRecord, bool> prepareEffects,
|
Func<LiveEntityRecord, bool> prepareEffects,
|
||||||
Func<LiveEntityRecord, bool> presentState,
|
Func<LiveEntityRecord, bool> presentState,
|
||||||
Func<LiveEntityRecord, bool> replayEffects)
|
Func<LiveEntityRecord, bool> replayEffects,
|
||||||
|
ILiveRenderProjectionSink? renderProjection = null)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
_prepareEffects = prepareEffects
|
_prepareEffects = prepareEffects
|
||||||
|
|
@ -108,6 +113,7 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
||||||
?? throw new ArgumentNullException(nameof(presentState));
|
?? throw new ArgumentNullException(nameof(presentState));
|
||||||
_replayEffects = replayEffects
|
_replayEffects = replayEffects
|
||||||
?? throw new ArgumentNullException(nameof(replayEffects));
|
?? throw new ArgumentNullException(nameof(replayEffects));
|
||||||
|
_renderProjection = renderProjection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Publish(LiveEntityReadyCandidate candidate)
|
public bool Publish(LiveEntityReadyCandidate candidate)
|
||||||
|
|
@ -125,7 +131,13 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _replayEffects(expectedRecord)
|
if (!_replayEffects(expectedRecord)
|
||||||
|
|| !candidate.IsCurrent(_runtime))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (_renderProjection?.OnEntityReady(candidate) ?? true)
|
||||||
&& candidate.IsCurrent(_runtime);
|
&& candidate.IsCurrent(_runtime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,379 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Scene.Arch;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class LiveRenderProjectionJournalTests
|
||||||
|
{
|
||||||
|
private const uint LandblockId = 0x1234FFFFu;
|
||||||
|
private const uint CellId = 0x12340100u;
|
||||||
|
private const uint Guid = 0x80000001u;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EntityReady_RegistersExactRootOnceAfterResourcesCommit()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 7);
|
||||||
|
LiveEntityReadyCandidate candidate =
|
||||||
|
LiveEntityReadyCandidate.Capture(record);
|
||||||
|
|
||||||
|
Assert.True(harness.Projections.OnEntityReady(candidate));
|
||||||
|
Assert.True(harness.Projections.OnEntityReady(candidate));
|
||||||
|
|
||||||
|
Assert.Equal(1, harness.Journal.Count);
|
||||||
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
|
RenderProjectionDelta registered = harness.Journal.Pending[0];
|
||||||
|
Assert.Equal(RenderProjectionDeltaKind.Register, registered.Kind);
|
||||||
|
Assert.Equal(RenderProjectionClass.LiveDynamicRoot,
|
||||||
|
registered.Record.ProjectionClass);
|
||||||
|
Assert.Equal(record.LocalEntityId, registered.Record.Source.LocalEntityId);
|
||||||
|
Assert.Equal(Guid, registered.Record.Source.ServerGuid);
|
||||||
|
Assert.Equal(LandblockId, registered.Record.Residency.OwnerLandblockId);
|
||||||
|
Assert.True((registered.Record.Flags & RenderProjectionFlags.Draw) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PendingToLoadedAndLoadedToLoaded_RebucketWithoutLogicalRecreate()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: false);
|
||||||
|
BindVisibility(harness);
|
||||||
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 1);
|
||||||
|
Assert.False(record.IsSpatiallyVisible);
|
||||||
|
harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
RenderOwnerIncarnation incarnation =
|
||||||
|
harness.Journal.Pending[0].Record.OwnerIncarnation;
|
||||||
|
|
||||||
|
Load(harness.State, LandblockId);
|
||||||
|
|
||||||
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
|
Assert.Equal(2, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.UpdateFlags,
|
||||||
|
harness.Journal.Pending[1].Kind);
|
||||||
|
|
||||||
|
const uint secondLandblock = 0x1235FFFFu;
|
||||||
|
const uint secondCell = 0x12350100u;
|
||||||
|
Load(harness.State, secondLandblock);
|
||||||
|
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, secondCell));
|
||||||
|
harness.Projections.SynchronizeActiveSources();
|
||||||
|
|
||||||
|
Assert.Equal(3, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.Rebucket,
|
||||||
|
harness.Journal.Pending[2].Kind);
|
||||||
|
Assert.Equal(
|
||||||
|
incarnation,
|
||||||
|
harness.Journal.Pending[2].Record.OwnerIncarnation);
|
||||||
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SynchronizeActiveSources_MirrorsFinalTransformAppearanceAndHiddenFlags()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 2);
|
||||||
|
harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
WorldEntity entity = record.WorldEntity!;
|
||||||
|
|
||||||
|
entity.SetPosition(new Vector3(20, 30, 40));
|
||||||
|
entity.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
|
||||||
|
entity.ApplyAppearance(
|
||||||
|
[new MeshRef(0x01010077u, Matrix4x4.Identity)],
|
||||||
|
new PaletteOverride(0x04000001u, []),
|
||||||
|
[new PartOverride(0, 0x01010077u)]);
|
||||||
|
entity.IsDrawVisible = false;
|
||||||
|
|
||||||
|
harness.Projections.SynchronizeActiveSources();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
RenderProjectionDeltaKind.UpdateAppearance,
|
||||||
|
RenderProjectionDeltaKind.UpdateFlags,
|
||||||
|
],
|
||||||
|
harness.Journal.Pending.ToArray()
|
||||||
|
.Select(delta => delta.Kind)
|
||||||
|
.ToArray());
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
Assert.True(harness.Scene.OpenQuery().TryGet(
|
||||||
|
LiveRenderProjectionJournal.ProjectionId(entity.Id),
|
||||||
|
out var projected));
|
||||||
|
Assert.Equal(entity.Position, projected.Transform.Position);
|
||||||
|
Assert.True((projected.Flags & RenderProjectionFlags.Hidden) != 0);
|
||||||
|
Assert.True((projected.Flags & RenderProjectionFlags.Draw) == 0);
|
||||||
|
Assert.Equal(1, projected.MeshSet.MeshCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EquippedPoseAndRemoval_UseSameIncarnationWithoutGuidOwnershipMap()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord record = Materialize(
|
||||||
|
harness,
|
||||||
|
Guid,
|
||||||
|
instance: 3,
|
||||||
|
projectionKind: LiveEntityProjectionKind.Attached);
|
||||||
|
harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
RenderOwnerIncarnation incarnation =
|
||||||
|
harness.Journal.Pending[0].Record.OwnerIncarnation;
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
|
record.WorldEntity!.SetPosition(new Vector3(9, 8, 7));
|
||||||
|
harness.Projections.OnProjectionPoseReady(Guid);
|
||||||
|
|
||||||
|
Assert.Single(harness.Journal.Pending.ToArray());
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
harness.Journal.Pending[0].Kind);
|
||||||
|
Assert.Equal(
|
||||||
|
incarnation,
|
||||||
|
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
||||||
|
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
||||||
|
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
||||||
|
Assert.Equal(2, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.Unregister,
|
||||||
|
harness.Journal.Pending[1].Kind);
|
||||||
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerationReplacement_UnregistersOldAndRejectsStaleReadyCandidate()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord first = Materialize(harness, Guid, instance: 4);
|
||||||
|
LiveEntityReadyCandidate stale = LiveEntityReadyCandidate.Capture(first);
|
||||||
|
harness.Projections.OnEntityReady(stale);
|
||||||
|
uint firstLocalId = first.WorldEntity!.Id;
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
|
LiveEntityRegistrationResult replacement =
|
||||||
|
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
||||||
|
LiveEntityRecord second = replacement.Record!;
|
||||||
|
WorldEntity secondEntity = harness.Runtime.MaterializeLiveEntity(
|
||||||
|
Guid,
|
||||||
|
CellId,
|
||||||
|
localId => Entity(localId, Guid))!;
|
||||||
|
second.InitialHydrationCompleted = true;
|
||||||
|
LiveEntityReadyCandidate current = LiveEntityReadyCandidate.Capture(second);
|
||||||
|
Assert.True(harness.Projections.OnEntityReady(current));
|
||||||
|
Assert.False(harness.Projections.OnEntityReady(stale));
|
||||||
|
|
||||||
|
Assert.Equal(2, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RenderProjectionDeltaKind.Unregister,
|
||||||
|
RenderProjectionDeltaKind.Register,
|
||||||
|
],
|
||||||
|
harness.Journal.Pending.ToArray()
|
||||||
|
.Select(delta => delta.Kind)
|
||||||
|
.ToArray());
|
||||||
|
Assert.NotEqual(firstLocalId, secondEntity.Id);
|
||||||
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
Assert.False(harness.Scene.OpenQuery().TryGet(
|
||||||
|
LiveRenderProjectionJournal.ProjectionId(firstLocalId),
|
||||||
|
out _));
|
||||||
|
Assert.True(harness.Scene.OpenQuery().TryGet(
|
||||||
|
LiveRenderProjectionJournal.ProjectionId(secondEntity.Id),
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DuplicateCreateAndReady_DoNotRegisterProjectionTwice()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 5);
|
||||||
|
harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
|
LiveEntityRegistrationResult duplicate =
|
||||||
|
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
||||||
|
Assert.Same(record, duplicate.Record);
|
||||||
|
Assert.True(harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
|
|
||||||
|
Assert.Equal(0, harness.Journal.Count);
|
||||||
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
|
Assert.Equal(1, harness.Scene.Counts.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SessionClear_UnregistersEveryTrackedProjectionExactlyOnce()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 6);
|
||||||
|
harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
|
||||||
|
harness.Runtime.Clear();
|
||||||
|
|
||||||
|
Assert.Equal(2, harness.Journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.Unregister,
|
||||||
|
harness.Journal.Pending[1].Kind);
|
||||||
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
||||||
|
Assert.Equal(0, harness.Runtime.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetTracking_DropsDerivedSourcesWithoutTouchingGameplayRuntime()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 7);
|
||||||
|
harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
|
||||||
|
harness.Projections.ResetTracking();
|
||||||
|
|
||||||
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
||||||
|
Assert.Equal(1, harness.Runtime.Count);
|
||||||
|
Assert.Same(record, harness.Runtime.Records.Single());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Harness CreateHarness(bool loadLandblock)
|
||||||
|
{
|
||||||
|
var state = new GpuWorldState();
|
||||||
|
if (loadLandblock)
|
||||||
|
Load(state, LandblockId);
|
||||||
|
|
||||||
|
ILiveRenderProjectionSink? sink = null;
|
||||||
|
var resources = new DelegateLiveEntityResourceLifecycle(
|
||||||
|
static _ => { },
|
||||||
|
entity => sink?.OnResourceUnregister(entity));
|
||||||
|
var runtime = new LiveEntityRuntime(state, resources);
|
||||||
|
var journal = new RenderProjectionJournal(
|
||||||
|
RenderSceneGeneration.FromRaw(1));
|
||||||
|
var projections = new LiveRenderProjectionJournal(runtime, journal);
|
||||||
|
sink = projections;
|
||||||
|
var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1));
|
||||||
|
return new Harness(state, runtime, journal, projections, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BindVisibility(Harness harness) =>
|
||||||
|
harness.Runtime.ProjectionVisibilityChanged +=
|
||||||
|
harness.Projections.OnProjectionVisibilityChanged;
|
||||||
|
|
||||||
|
private static LiveEntityRecord Materialize(
|
||||||
|
Harness harness,
|
||||||
|
uint guid,
|
||||||
|
ushort instance,
|
||||||
|
LiveEntityProjectionKind projectionKind =
|
||||||
|
LiveEntityProjectionKind.World)
|
||||||
|
{
|
||||||
|
LiveEntityRecord record =
|
||||||
|
harness.Runtime.RegisterLiveEntity(Spawn(guid, instance)).Record!;
|
||||||
|
WorldEntity? entity = harness.Runtime.MaterializeLiveEntity(
|
||||||
|
guid,
|
||||||
|
CellId,
|
||||||
|
localId => Entity(localId, guid),
|
||||||
|
projectionKind);
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
record.InitialHydrationCompleted = true;
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Load(GpuWorldState state, uint landblockId) =>
|
||||||
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
landblockId,
|
||||||
|
new LandBlock(),
|
||||||
|
Array.Empty<WorldEntity>(),
|
||||||
|
PhysicsDatBundle.Empty));
|
||||||
|
|
||||||
|
private static WorldEntity Entity(uint localId, uint guid) => new()
|
||||||
|
{
|
||||||
|
Id = localId,
|
||||||
|
ServerGuid = guid,
|
||||||
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
|
Position = new Vector3(1, 2, 3),
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(0x01010001u, Matrix4x4.Identity),
|
||||||
|
],
|
||||||
|
ParentCellId = CellId,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
|
||||||
|
{
|
||||||
|
var position = new CreateObject.ServerPosition(
|
||||||
|
CellId,
|
||||||
|
10f,
|
||||||
|
10f,
|
||||||
|
5f,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
var timestamps = new PhysicsTimestamps(
|
||||||
|
Position: 1,
|
||||||
|
Movement: 1,
|
||||||
|
State: 1,
|
||||||
|
Vector: 1,
|
||||||
|
Teleport: 0,
|
||||||
|
ServerControlledMove: 1,
|
||||||
|
ForcePosition: 0,
|
||||||
|
ObjDesc: 1,
|
||||||
|
Instance: instance);
|
||||||
|
var physics = new PhysicsSpawnData(
|
||||||
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||||
|
Position: position,
|
||||||
|
Movement: null,
|
||||||
|
AnimationFrame: null,
|
||||||
|
SetupTableId: 0x02000001u,
|
||||||
|
MotionTableId: 0x09000001u,
|
||||||
|
SoundTableId: null,
|
||||||
|
PhysicsScriptTableId: null,
|
||||||
|
Parent: null,
|
||||||
|
Children: null,
|
||||||
|
Scale: null,
|
||||||
|
Friction: null,
|
||||||
|
Elasticity: null,
|
||||||
|
Translucency: null,
|
||||||
|
Velocity: null,
|
||||||
|
Acceleration: null,
|
||||||
|
AngularVelocity: null,
|
||||||
|
DefaultScriptType: null,
|
||||||
|
DefaultScriptIntensity: null,
|
||||||
|
Timestamps: timestamps);
|
||||||
|
return new WorldSession.EntitySpawn(
|
||||||
|
guid,
|
||||||
|
position,
|
||||||
|
0x02000001u,
|
||||||
|
Array.Empty<CreateObject.AnimPartChange>(),
|
||||||
|
Array.Empty<CreateObject.TextureChange>(),
|
||||||
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"fixture",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x09000001u,
|
||||||
|
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||||
|
InstanceSequence: instance,
|
||||||
|
MovementSequence: 1,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
Physics: physics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record Harness(
|
||||||
|
GpuWorldState State,
|
||||||
|
LiveEntityRuntime Runtime,
|
||||||
|
RenderProjectionJournal Journal,
|
||||||
|
LiveRenderProjectionJournal Projections,
|
||||||
|
ArchRenderScene Scene);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
|
@ -1496,6 +1497,63 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
Assert.False(expected.IsSpatiallyProjected);
|
Assert.False(expected.IsSpatiallyProjected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void ReadyPublisher_PublishesRenderProjectionAfterExistingReadyStages(
|
||||||
|
bool projectionAccepted)
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(originKnown: true);
|
||||||
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||||
|
LiveEntityRecord record = fixture.Record;
|
||||||
|
var operations = new List<string>();
|
||||||
|
var projection = new RecordingLiveProjectionSink(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
operations.Add("projection");
|
||||||
|
return projectionAccepted;
|
||||||
|
});
|
||||||
|
var publisher = new LiveEntityReadyPublisher(
|
||||||
|
fixture.Runtime,
|
||||||
|
_ => { operations.Add("effects-owner"); return true; },
|
||||||
|
_ => { operations.Add("state"); return true; },
|
||||||
|
_ => { operations.Add("effects-replay"); return true; },
|
||||||
|
projection);
|
||||||
|
|
||||||
|
bool result = publisher.Publish(
|
||||||
|
LiveEntityReadyCandidate.Capture(record));
|
||||||
|
|
||||||
|
Assert.Equal(projectionAccepted, result);
|
||||||
|
Assert.Equal(
|
||||||
|
["effects-owner", "state", "effects-replay", "projection"],
|
||||||
|
operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadyPublisher_RevalidatesAfterReentrantRenderProjectionCallback()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(originKnown: true);
|
||||||
|
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||||
|
LiveEntityReadyCandidate candidate =
|
||||||
|
LiveEntityReadyCandidate.Capture(fixture.Record);
|
||||||
|
var projection = new RecordingLiveProjectionSink(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
fixture.Runtime.RegisterLiveEntity(
|
||||||
|
Spawn(Generation: 1, PositionSequence: 2));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var publisher = new LiveEntityReadyPublisher(
|
||||||
|
fixture.Runtime,
|
||||||
|
static _ => true,
|
||||||
|
static _ => true,
|
||||||
|
static _ => true,
|
||||||
|
projection);
|
||||||
|
|
||||||
|
Assert.False(publisher.Publish(candidate));
|
||||||
|
Assert.Equal(2UL, fixture.Record.CreateIntegrationVersion);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EquippedChildReadyCandidate_RejectsVersionAdvancedByPoseCallback()
|
public void EquippedChildReadyCandidate_RejectsVersionAdvancedByPoseCallback()
|
||||||
{
|
{
|
||||||
|
|
@ -1937,6 +1995,36 @@ public sealed class LiveEntityHydrationControllerTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingLiveProjectionSink(
|
||||||
|
Func<LiveEntityReadyCandidate, bool> ready)
|
||||||
|
: ILiveRenderProjectionSink
|
||||||
|
{
|
||||||
|
public bool OnEntityReady(LiveEntityReadyCandidate candidate) =>
|
||||||
|
ready(candidate);
|
||||||
|
|
||||||
|
public void OnProjectionVisibilityChanged(
|
||||||
|
LiveEntityRecord record,
|
||||||
|
bool visible)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnProjectionPoseReady(uint serverGuid)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnProjectionRemoved(uint localEntityId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResourceUnregister(WorldEntity entity)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SynchronizeActiveSources()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingOrigin(bool isKnown) : ILiveEntityWorldOriginCoordinator
|
private sealed class RecordingOrigin(bool isKnown) : ILiveEntityWorldOriginCoordinator
|
||||||
{
|
{
|
||||||
public bool IsKnownValue { get; set; } = isKnown;
|
public bool IsKnownValue { get; set; } = isKnown;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue