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