using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
///
/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player)
/// clone: the player's Setup id + current ObjDesc (base palette + subpalette overlays
/// + part overrides), posed at the scene origin facing the viewer.
///
///
/// The palette / part-override mapping mirrors the inline construction in
/// GameWindow.cs around lines 3390–3431. Extracted here so it is
/// unit-testable without dats and so the paperdoll renderer owns a clean
/// seam: it calls with fresh player state each time the
/// ObjDesc changes, and the renderer only has to swap the entity into its
/// dedicated mini-scene.
///
///
///
/// The is a reserved synthetic guid that is:
/// (a) non-zero (so EntitySpawnAdapter.OnCreate's ServerGuid != 0
/// guard accepts it), and (b) high enough that it never collides with a real
/// server-assigned guid.
///
///
public static class DollEntityBuilder
{
///
/// Reserved synthetic guid for the paperdoll clone. High, deliberately
/// outside the server's assignable range, and non-zero.
///
public const uint DollServerGuid = 0xDA11_D011u;
///
/// Reserved render-local entity id for the doll. FIXED (the doll is a singleton)
/// and high enough never to collide with _liveEntityIdCounter ids. The
/// renderer passes this in animatedEntityIds so the dispatcher treats the
/// doll as animated — which BYPASSES the Tier-1 classification cache
/// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW
/// WorldEntity with this SAME id; without the cache bypass the dispatcher would
/// serve the previous doll's cached batches and the new gear wouldn't appear.
///
public const uint DollRenderId = 0xDA11_D012u;
// retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h)
// (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC
// North), the heading increasing CLOCKWISE toward +X. We rotate the doll's default +Y-forward body about
// +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is
// NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer).
private const float _headingDegrees = 191.367905f;
private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f);
private static readonly Quaternion _dollRotation =
Quaternion.CreateFromAxisAngle(new Vector3(0f, 0f, 1f), _headingRad);
///
/// Builds a synthetic for the paperdoll mini-scene.
///
/// The player's Setup dat id (0x02xxxxxx).
/// Pre-resolved mesh refs (may be empty; caller fills them in).
///
/// ObjDesc base palette id (0x04xxxxxx). Passed only when subpalettes are
/// also present — mirrors GameWindow which only builds a PaletteOverride
/// when SubPalettes.Count > 0.
///
///
/// Subpalette overlays from the server ObjDesc. Each tuple carries the
/// subpalette dat id, byte offset into the base palette, and byte length.
/// Null or empty → PaletteOverride on the returned entity is null.
///
///
/// AnimPartChange swaps from the server ObjDesc. Each tuple is a
/// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array.
///
public static WorldEntity Build(
uint setupId,
IReadOnlyList meshRefs,
uint? basePaletteId = null,
IReadOnlyList<(uint SubPaletteId, byte Offset, byte Length)>? subPalettes = null,
IReadOnlyList<(byte PartIndex, uint GfxObjId)>? partOverrides = null)
{
// --- palette override (mirrors GameWindow:3395-3405) ---
// Only build when there are sub-palette overlays — same gate as GameWindow.
PaletteOverride? paletteOverride = null;
if (subPalettes is { Count: > 0 } spList)
{
var ranges = new PaletteOverride.SubPaletteRange[spList.Count];
for (int i = 0; i < spList.Count; i++)
ranges[i] = new PaletteOverride.SubPaletteRange(
spList[i].SubPaletteId,
spList[i].Offset,
spList[i].Length);
paletteOverride = new PaletteOverride(
BasePaletteId: basePaletteId ?? 0u,
SubPalettes: ranges);
}
// --- part overrides (mirrors GameWindow:3407-3418) ---
PartOverride[] entityPartOverrides;
if (partOverrides is null or { Count: 0 })
{
entityPartOverrides = Array.Empty();
}
else
{
entityPartOverrides = new PartOverride[partOverrides.Count];
for (int i = 0; i < partOverrides.Count; i++)
entityPartOverrides[i] = new PartOverride(
partOverrides[i].PartIndex,
partOverrides[i].GfxObjId);
}
// --- assemble entity (mirrors GameWindow:3420-3431) ---
// Id=0: the paperdoll renderer assigns its own render-local id when it
// registers this entity with the mini-scene GpuWorldState. We set zero
// here so the builder stays pure (no static counter). The caller may
// replace it before registration.
return new WorldEntity
{
Id = DollRenderId,
ServerGuid = DollServerGuid,
SourceGfxObjOrSetupId = setupId,
Position = Vector3.Zero,
Rotation = _dollRotation,
MeshRefs = meshRefs,
PaletteOverride = paletteOverride,
PartOverrides = entityPartOverrides,
ParentCellId = null, // paperdoll mini-scene has no parent cell
};
}
}