acdream/src/AcDream.Core/World/WorldEntity.cs
Erik ed72704f7b feat(world): Phase A8 R1 — tag WorldEntity.IsBuildingShell at LandblockLoader
Adds a bool flag at the WorldEntity data layer set by LandblockLoader from
the source dat array: LandBlockInfo.Buildings → true (cottage walls, inn
walls, smithy walls); LandBlockInfo.Objects → false (trees, lampposts,
rocks, hitching posts).

Retail anchor: CLandBlock::init_buildings reads a separate BuildInfo**
array from objects (acclient.h:31893 num_buildings / buildings field;
acclient_2013_pseudo_c.txt:313854 init_buildings entry). WorldBuilder
preserves the same distinction via SceneryInstance.IsBuilding
(StaticObjectRenderManager.cs:334). Today acdream's loader reads both
arrays into the same WorldEntity pool with no tag, destroying the
distinction (the comment at GameWindow.cs:5175 already acknowledges this
gap for scenery suppression). This commit closes the gap.

Render-time consumption arrives in R2 (EntitySet partition refactor).
Two new LandblockLoader tests lock the tagging behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:31:11 +02:00

124 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
namespace AcDream.Core.World;
public sealed class WorldEntity
{
public required uint Id { get; init; }
/// <summary>
/// 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.
/// </summary>
public uint ServerGuid { get; init; }
public required uint SourceGfxObjOrSetupId { get; init; }
/// <summary>
/// World-space position. Settable so Phase 6.7 position-update events
/// can reseat an existing entity without rebuilding its meshes.
/// </summary>
public required Vector3 Position { get; set; }
/// <summary>Settable for the same reason as <see cref="Position"/>.</summary>
public required Quaternion Rotation { get; set; }
/// <summary>
/// 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.
/// </summary>
public required IReadOnlyList<MeshRef> MeshRefs { get; set; }
/// <summary>
/// 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.
/// </summary>
public PaletteOverride? PaletteOverride { get; init; }
/// <summary>
/// EnvCell ID that owns this entity (room geometry or static object inside
/// the cell). Used by portal visibility to filter interior entities — only
/// entities whose ParentCellId appears in the visible set are rendered.
/// Null for outdoor entities (stabs, scenery, live server spawns).
/// </summary>
public uint? ParentCellId { get; init; }
/// <summary>
/// True when this entity originates from <c>LandBlockInfo.Buildings[]</c>
/// (the dat array that carries building shells: cottage walls, smithy walls,
/// inn walls — every solid building enclosure). False for entities from
/// <c>LandBlockInfo.Objects[]</c> (rocks, fences, lampposts, tree clusters —
/// outdoor scenery placeholders). The two arrays are conflated through
/// hydration today but the dat itself carries the distinction; retail
/// (<c>CLandBlock::init_buildings</c>) and WorldBuilder
/// (<c>SceneryInstance.IsBuilding</c>) both preserve it.
///
/// <para>
/// Read at draw time by <c>WbDrawDispatcher</c>'s <c>IndoorPass</c>
/// partition so building shells render unconditionally when the camera
/// is inside their building (they ARE the indoor walls), not stencil-gated
/// as outdoor scenery would be.
/// </para>
/// </summary>
public bool IsBuildingShell { get; init; }
/// <summary>
/// Uniform scale applied to this entity's mesh by the scenery pipeline.
/// For scenery objects this is spawn.Scale (typically 0.81.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.
/// </summary>
public float Scale { get; init; } = 1.0f;
/// <summary>
/// Server-sent part-swap overrides from <c>AnimPartChange</c>. Each entry
/// replaces a Setup part's GfxObj with an alternate model (clothing, weapons,
/// helmets). Carried on the entity so <c>EntitySpawnAdapter</c> can populate
/// <c>AnimatedEntityState</c>'s override map at spawn time. Empty for atlas-
/// tier entities.
/// </summary>
public IReadOnlyList<PartOverride> PartOverrides { get; init; } = Array.Empty<PartOverride>();
/// <summary>
/// Bitmask of hidden Setup parts. Bit <c>i</c> set hides part <c>i</c> at
/// draw time. Sourced from the server's <c>CreateObject</c> record when
/// present. Zero (no parts hidden) is the default.
/// </summary>
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;
private const float DefaultAabbRadius = 5.0f;
public void RefreshAabb()
{
var p = Position;
AabbMin = new Vector3(p.X - DefaultAabbRadius, p.Y - DefaultAabbRadius, p.Z - DefaultAabbRadius);
AabbMax = new Vector3(p.X + DefaultAabbRadius, p.Y + DefaultAabbRadius, p.Z + DefaultAabbRadius);
AabbDirty = false;
}
public void SetPosition(Vector3 pos)
{
Position = pos;
AabbDirty = true;
}
}
/// <summary>
/// Lightweight value type for a server-sent <c>AnimPartChange</c> (part index
/// → replacement GfxObj id). Decouples <c>WorldEntity</c> (Core) from the
/// network-layer <c>CreateObject.AnimPartChange</c> type.
/// </summary>
public readonly record struct PartOverride(byte PartIndex, uint GfxObjId);