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; }