using System;
using System.Collections.Generic;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Wb;
///
/// Routes server-spawned (CreateObject) entities through the
/// per-instance rendering path. Server entities always carry per-instance
/// customizations (palette overrides, texture changes, part swaps) that
/// don't fit WB's atlas key, so they bypass the atlas and use the existing
///
/// path which already hash-keys overrides for caching.
///
///
/// Companion to : that adapter handles
/// atlas-tier (procedural) entities; this one handles per-instance-tier
/// (server-spawned). The boundary is ServerGuid != 0 on
/// .
///
///
///
/// Per-entity texture decode: when entity.PaletteOverride is
/// non-null, the adapter calls
///
/// once per surface id that is known at spawn time (those on
/// ). Surfaces whose ids are only
/// discoverable by opening the GfxObj dat are decoded lazily by the draw
/// dispatcher (Task 22) on first use — that matches the existing
/// StaticMeshRenderer behavior.
///
///
///
/// Sequencer factory: the adapter is constructed with a
/// Func<WorldEntity, AnimationSequencer> factory so tests can
/// inject a stub without needing a live DatCollection or MotionTable.
/// Production callers supply a factory that fetches MotionTable from dats.
///
///
///
/// Adjustment 6 (resolved Adjustment 4): now
/// carries and
/// . applies
/// both to the created .
///
///
public sealed class EntitySpawnAdapter
{
private readonly ITextureCachePerInstance _textureCache;
private readonly Func _sequencerFactory;
private readonly IWbMeshAdapter? _meshAdapter;
// Per-server-guid state. Written on OnCreate, released on OnRemove.
// Single-threaded: called only from the render thread (same as GpuWorldState).
private readonly Dictionary _stateByGuid = new();
// Per-server-guid set of GfxObj ids registered with the mesh adapter,
// so OnRemove can decrement each. Per-instance entities don't go through
// LandblockSpawnAdapter, so without this their meshes would never load
// (WB doesn't know they exist).
private readonly Dictionary> _meshIdsByGuid = new();
///
/// Per-instance texture decode path. In production this is the
/// instance (which implements
/// ); in tests it is a capturing mock.
///
///
/// Factory that builds an for a given
/// entity. Receives the full so it can look up
/// the Setup + MotionTable from the entity's SourceGfxObjOrSetupId
/// and server-supplied motion table override. Tests pass a lambda that
/// returns a stub sequencer.
///
///
/// Optional WB mesh adapter. When non-null,
/// registers each unique MeshRef.GfxObjId with the adapter so WB
/// background-loads the mesh data; decrements the
/// matching ref counts. When null, the adapter only tracks per-instance
/// state without driving WB lifecycle (test mode + flag-off mode).
///
public EntitySpawnAdapter(
ITextureCachePerInstance textureCache,
Func sequencerFactory,
IWbMeshAdapter? meshAdapter = null)
{
ArgumentNullException.ThrowIfNull(textureCache);
ArgumentNullException.ThrowIfNull(sequencerFactory);
_textureCache = textureCache;
_sequencerFactory = sequencerFactory;
_meshAdapter = meshAdapter;
}
///
/// Process a server-spawned entity. Returns the created
/// for the entity, or null if
/// is atlas-tier (ServerGuid == 0).
///
public AnimatedEntityState? OnCreate(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
// Atlas-tier entities (procedural / dat-hydrated, ServerGuid == 0)
// are handled by LandblockSpawnAdapter, not here.
if (entity.ServerGuid == 0) return null;
// Pre-warm the per-instance texture cache for surfaces whose ids are
// already known at spawn time (those appearing as keys in
// MeshRef.SurfaceOverrides). GfxObj sub-mesh surface ids that aren't
// covered by SurfaceOverrides are decoded lazily by the draw
// dispatcher on first use — consistent with StaticMeshRenderer.
if (entity.PaletteOverride is { } paletteOverride)
{
foreach (var meshRef in entity.MeshRefs)
{
if (meshRef.SurfaceOverrides is null) continue;
// SurfaceOverrides maps surfaceId → origTextureOverride (may be 0
// meaning "no texture swap, just the palette override applies").
foreach (var (surfaceId, origTexOverride) in meshRef.SurfaceOverrides)
{
_textureCache.GetOrUploadWithPaletteOverride(
surfaceId,
origTexOverride == 0 ? null : origTexOverride,
paletteOverride);
}
}
}
// Build the per-entity AnimatedEntityState. The sequencer factory
// may return a stub (in tests) or a fully-constructed sequencer from
// the MotionTable (in production). Factory must not return null —
// if the entity has no motion table the factory should construct a
// no-op sequencer (Setup + empty MotionTable + NullAnimationLoader).
var sequencer = _sequencerFactory(entity);
var state = new AnimatedEntityState(sequencer);
// Adjustment 6: WorldEntity now carries PartOverrides + HiddenPartsMask.
state.HideParts(entity.HiddenPartsMask);
foreach (var po in entity.PartOverrides)
state.SetPartOverride(po.PartIndex, po.GfxObjId);
_stateByGuid[entity.ServerGuid] = state;
// Register each unique GfxObj id with WB so the meshes background-load.
// Includes both the entity's natural MeshRefs AND any server-sent
// PartOverride GfxObjs (weapons, clothing, helmets) — those replace the
// Setup default and need their own mesh data uploaded.
if (_meshAdapter is not null)
{
var unique = new HashSet();
foreach (var meshRef in entity.MeshRefs)
unique.Add((ulong)meshRef.GfxObjId);
foreach (var po in entity.PartOverrides)
unique.Add((ulong)po.GfxObjId);
_meshIdsByGuid[entity.ServerGuid] = unique;
foreach (var id in unique) _meshAdapter.IncrementRefCount(id);
}
return state;
}
///
/// Release the per-entity state for . Called
/// on RemoveObject. Unknown guids (never spawned, or already
/// removed) are silently ignored.
///
public void OnRemove(uint serverGuid)
{
_stateByGuid.Remove(serverGuid);
if (_meshAdapter is not null && _meshIdsByGuid.TryGetValue(serverGuid, out var ids))
{
foreach (var id in ids) _meshAdapter.DecrementRefCount(id);
_meshIdsByGuid.Remove(serverGuid);
}
}
///
/// Look up the for a server guid.
/// Returns null if the entity was never spawned or has already
/// been removed.
///
public AnimatedEntityState? GetState(uint serverGuid)
=> _stateByGuid.TryGetValue(serverGuid, out var s) ? s : null;
}