using System.Numerics;
namespace AcDream.Core.World;
public sealed class WorldEntity
{
public required uint Id { get; init; }
///
/// Server-assigned GUID (from CreateObject). Zero for dat-hydrated
/// scenery/static entities that don't come from the server.
/// Used by GpuWorldState for persistent-entity rescue on landblock unload.
///
public uint ServerGuid { get; init; }
public required uint SourceGfxObjOrSetupId { get; init; }
///
/// World-space position. Settable so Phase 6.7 position-update events
/// can reseat an existing entity without rebuilding its meshes.
///
public required Vector3 Position { get; set; }
/// Settable for the same reason as .
public required Quaternion Rotation { get; set; }
///
/// Per-part mesh references with their root-relative transforms.
/// Mutable so the animation tick can replace it each frame for
/// entities that play a cycle (Phase 6.4); static entities set it
/// once at hydration and never touch it again.
///
public required IReadOnlyList MeshRefs { get; set; }
///
/// Optional per-entity palette override (server-specified base +
/// subpalette overlays). When non-null, applies to every palette-
/// indexed texture on this entity. Used for character skin/hair
/// colors, creature recolors (e.g. stone-colored drudge statue),
/// and team colors. Non-palette-indexed textures ignore this field.
///
public PaletteOverride? PaletteOverride { get; init; }
///
/// EnvCell or outdoor cell ID that owns this entity (room geometry, static
/// object, or live object inside/outside a cell).
/// the cell). Used by portal visibility to filter interior entities — only
/// entities whose ParentCellId appears in the visible set are rendered.
/// Null for outdoor dat scenery/building stabs or unresolved live entities.
///
public uint? ParentCellId { get; set; }
///
/// True when this entity originates from LandBlockInfo.Buildings[]
/// (the dat array that carries building shells: cottage walls, smithy walls,
/// inn walls — every solid building enclosure). False for entities from
/// LandBlockInfo.Objects[] (rocks, fences, lampposts, tree clusters —
/// outdoor scenery placeholders). The two arrays are conflated through
/// hydration today but the dat itself carries the distinction; retail
/// (CLandBlock::init_buildings) and WorldBuilder
/// (SceneryInstance.IsBuilding) both preserve it.
///
///
/// Read at draw time by WbDrawDispatcher's IndoorPass
/// partition so building shells can render when the camera is inside their
/// own building (they ARE the indoor walls), not stencil-gated as outdoor
/// scenery would be.
///
///
public bool IsBuildingShell { get; init; }
///
/// Dat-derived EnvCell anchor for a building shell. Building shells are
/// top-level landblock stabs, so they do not have a real ParentCellId, but
/// the LandBlockInfo.Buildings[] portal list names cells owned by the same
/// building. The indoor renderer uses this anchor only for draw scoping:
/// a shell renders in IndoorPass when its anchor belongs to the camera
/// building's EnvCell set. Collision still treats the shell as an outdoor
/// stab unless ParentCellId is explicitly set.
///
public uint? BuildingShellAnchorCellId { get; init; }
///
/// Uniform scale applied to this entity's mesh by the scenery pipeline.
/// For scenery objects this is spawn.Scale (typically 0.8–1.3). For stabs
/// and interior static objects this is 1.0 (no scaling).
///
/// Used by the collision registration path to scale CylSphere / Sphere /
/// Setup.Radius shapes so they match the visually-scaled mesh. Without
/// this, scaled scenery has a collision cylinder that's smaller than the
/// visible trunk, producing "partial passthrough" bugs.
///
public float Scale { get; init; } = 1.0f;
///
/// Server-sent part-swap overrides from AnimPartChange. Each entry
/// replaces a Setup part's GfxObj with an alternate model (clothing, weapons,
/// helmets). Carried on the entity so EntitySpawnAdapter can populate
/// AnimatedEntityState's override map at spawn time. Empty for atlas-
/// tier entities.
///
public IReadOnlyList PartOverrides { get; init; } = Array.Empty();
///
/// Bitmask of hidden Setup parts. Bit i set hides part i at
/// draw time. Sourced from the server's CreateObject record when
/// present. Zero (no parts hidden) is the default.
///
public ulong HiddenPartsMask { get; init; }
// Per Phase A.5 spec §4.6 Change #2 — cache per-entity AABB so the
// dispatcher's frustum cull is a memory read, not a per-frame recompute.
// AabbDirty starts true so the dispatcher calls RefreshAabb on first read
// (AabbMin/AabbMax are Vector3.Zero until refreshed).
public Vector3 AabbMin { get; private set; }
public Vector3 AabbMax { get; private set; }
public bool AabbDirty { get; private set; } = true;
///
/// Root-local geometry bounds: the union over MeshRefs of each part's dat
/// vertex AABB transformed by its part transform (see
/// Meshing.LocalBoundsAccumulator). Set at hydration from the same
/// vertex data that gets drawn — the every-case fix for the "#119 bounds
/// must cover the mesh" class. When absent (HasLocalBounds false), the
/// part-offset heuristic below is the fallback.
///
public Vector3 LocalBoundMin { get; private set; }
public Vector3 LocalBoundMax { get; private set; }
public bool HasLocalBounds { get; private set; }
public void SetLocalBounds(Vector3 min, Vector3 max)
{
LocalBoundMin = min;
LocalBoundMax = max;
HasLocalBounds = true;
AabbDirty = true;
}
private const float DefaultAabbRadius = 5.0f;
public void RefreshAabb()
{
var p = Position;
// #119 follow-up (2026-06-11): the box must cover the MESH, not just the
// anchor. BOTH visibility gates derive from this box: the dispatcher's
// per-entity frustum cull (WbDrawDispatcher.WalkEntitiesInto) and the
// viewcone sphere (RetailPViewRenderer.EntitySphere = this box's
// bounding sphere). The original fixed ±5 m anchor box dropped the AAB3
// tower staircase (parts spiralling 15 m above the anchor) whenever the
// gaze left the anchor's neighborhood — stairs visible looking down,
// gone looking up.
//
// Preferred path: dat-vertex-derived root-local bounds (SetLocalBounds
// at hydration), rotated into world axes — re-boxing the 8 rotated
// corners contains the rotated contents, so this is correct for EVERY
// shape including a single tall part at identity transform (which the
// offset heuristic below cannot see). DefaultAabbRadius stays as a
// margin: it absorbs animated-pose drift (MeshRefs are swapped per
// frame for animated entities while local bounds are rest-pose) and
// keeps small objects at their historical box size.
if (HasLocalBounds)
{
Vector3 lo = LocalBoundMin, hi = LocalBoundMax;
var rot = Rotation;
Vector3 min = default, max = default;
for (int c = 0; c < 8; c++)
{
var corner = new Vector3(
(c & 1) == 0 ? lo.X : hi.X,
(c & 2) == 0 ? lo.Y : hi.Y,
(c & 4) == 0 ? lo.Z : hi.Z);
var t = Vector3.Transform(corner, rot);
if (c == 0) { min = max = t; }
else { min = Vector3.Min(min, t); max = Vector3.Max(max, t); }
}
AabbMin = p + min - new Vector3(DefaultAabbRadius);
AabbMax = p + max + new Vector3(DefaultAabbRadius);
AabbDirty = false;
return;
}
// Fallback (no hydration bounds — e.g. tests, minimal fixtures): anchor
// box expanded by the largest part-translation magnitude. Rotation-
// invariant; covers multi-part spreads but NOT a single part whose mesh
// extends >5 m from its own origin — which is why hydrated entities use
// the vertex-derived path above.
float radius = DefaultAabbRadius;
var refs = MeshRefs;
if (refs is not null)
{
float maxOffset = 0f;
for (int i = 0; i < refs.Count; i++)
{
float len = refs[i].PartTransform.Translation.Length();
if (len > maxOffset) maxOffset = len;
}
radius += maxOffset;
}
AabbMin = new Vector3(p.X - radius, p.Y - radius, p.Z - radius);
AabbMax = new Vector3(p.X + radius, p.Y + radius, p.Z + radius);
AabbDirty = false;
}
public void SetPosition(Vector3 pos)
{
Position = pos;
AabbDirty = true;
}
}
///
/// Lightweight value type for a server-sent AnimPartChange (part index
/// → replacement GfxObj id). Decouples WorldEntity (Core) from the
/// network-layer CreateObject.AnimPartChange type.
///
public readonly record struct PartOverride(byte PartIndex, uint GfxObjId);