From 8fa66c23d5e0a116030cb690892b220c8d190069 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 25 Jun 2026 13:16:27 +0200 Subject: [PATCH] =?UTF-8?q?fix(D.2b):=20Slice=202=20=E2=80=94=20retail-exa?= =?UTF-8?q?ct=20doll=20pose,=20camera=20+=20heading=20(visual=20gate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paperdoll doll now matches retail: correct held pose, framing, and facing. Three decomp-sourced fixes closed the visual gate. Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played once + HELD (set_sequence_animation framerate=0, RedressCreature 0x004a3c22). Dumping the dat showed the 29-frame anim has only two distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28 (byte-identical: the settled stance, arms down + leg back) — so ApplyPaperdollPose applies the LAST frame statically (no looping). Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp 0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the camera at mid-body, adding a ~2deg yaw that turned the doll's face away — full-body framing comes from eye-height + FOV, not aiming. Heading: retail Frame::set_heading(h) (0x00535e40) builds facing (sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED (~22deg), the real cause of the turned-away face. Negate the angle to land on retail's facing. Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2 diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script. Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../retail-divergence-register.md | 4 +- src/AcDream.App/Rendering/DollCamera.cs | 47 +++++++----- .../Rendering/DollEntityBuilder.cs | 10 ++- src/AcDream.App/Rendering/GameWindow.cs | 71 ++++++++++++++++++- .../Rendering/PaperdollViewportRenderer.cs | 2 +- .../UI/Layout/PaperdollController.cs | 8 --- .../Rendering/DollCameraTests.cs | 22 ++++-- tools/cdb/paperdoll-pose.cdb | 9 +++ 8 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 tools/cdb/paperdoll-pose.cdb diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 33912121..772856a0 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -164,7 +164,9 @@ accepted-divergence entries (#96, #49, #50). | AP-63 | **Dual-wield-into-shield-slot special not implemented** — retail `OnItemListDragOver` (decomp 174302) lets a melee-capable item also drop on the Shield slot; acdream's `wieldMask = ValidLocations & slotMask` rejects it. | `src/AcDream.App/UI/Layout/PaperdollController.cs` (`HandleDropRelease`) | A dual-wielder cannot off-hand a melee weapon via the doll. | Dual-wield via drag-onto-shield-slot is blocked — functional gap for dual-wield characters. | `gmPaperDollUI::OnItemListDragOver` decomp 174302 | | AP-64 | **Wield-reject rollback assumes `InventoryServerSaveFailed 0x00A0`** — an optimistic wield rolls back only if ACE emits `0x00A0` for a `GetAndWieldItem` rejection; otherwise corrected by the next authoritative message. Gate-verify via WireMCP. | `src/AcDream.Core.Net/GameEventWiring.cs` (0x00A0 handler) | If ACE uses a different reject opcode for wield, the optimistic state is left dangling until the next full update. | Rejected wield briefly shows the item as equipped in the doll — cosmetic flicker or stuck state if `0x00A0` is not the rejection path. | `InventoryServerSaveFailed 0x00A0`; WireMCP gate-verify | | AP-65 | **PickupEvent (0xF74A) no longer evicts the weenie from `ClientObjectTable`** — only `DeleteObject (0xF747)` evicts, matching the retail `object_table`-vs-`weenie_object_table` split. An item another player picks up near you (only ever gets `PickupEvent`, never `DeleteObject`) lingers as a data-only entry (`ContainerId 0`, not in any view) until teleport/relog `Clear()`. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (EntityDeleted handler); `src/AcDream.Core.Net/WorldSession.cs` (PickupEvent branch) | The weenie entry for nearby pickups is a harmless data ghost — no UI shows it (no container, not wielded). Retail evicts it from `weenie_object_table` when the object fully leaves interest range via `DeleteObject`; acdream defers to teleport/relog clear. | Slight memory growth in long sessions if many world items are picked up by other players near you; item data ghosts cannot cause functional issues because no UI queries `ContainerId 0`. | `CACObjectMaint::DeleteObject` / `SmartBox::HandleDeleteObject`; ACE `Player_Inventory.cs TryDequipObjectWithNetworking` | -| AP-66 | **Empty equip slots show a generic frame (Slice 1) instead of the retail doll-backed transparent look** — retail's slot-view leaves empty armor slots transparent with the live 3-D doll body behind them; Slice 1 has no doll yet, so empty slots render a visible frame (`0x06004D20`, the inventory grid's empty square) so every position is seen + usable. The "figure" in the paperdoll is the doll, NOT a per-slot silhouette. | `src/AcDream.App/UI/Layout/PaperdollController.cs` (`emptySlotSprite`); `src/AcDream.App/Rendering/GameWindow.cs` (PaperdollController.Bind) | Retired in Slice 2 when the doll `UiViewport` (`0x100001D5`) renders behind the slots and the empty slots flip to transparent (`EmptySprite=0`). | Empty equip slots look like plain framed squares instead of revealing the character body — cosmetic, pending Slice 2's doll viewport. | `gmPaperDollUI` init `SetVisible(0)` per slot; user retail screenshots (slot-view) | +| AP-66 | **Empty equip slots paint a frame sprite (`EmptySprite` 0x06004D20), faithfulness vs retail's empty-slot art unverified** — Slice 2 shipped the doll `UiViewport` (0x100001D5) + the Slots toggle (doll-view ↔ slot-view, decomp 175674-175706), so empty ARMOR slots are now hidden in doll-view (the 3-D body shows instead). The always-visible non-armor equip slots (and armor slots in slot-view) still paint a frame when empty. | `src/AcDream.App/UI/Layout/PaperdollController.cs:99` | The doll + toggle are faithful (decomp-cited); painting a slot frame on empty positions is plausibly retail (the outlines guide drops), but is not pixel-verified against retail's doll-view. | Empty equip slots may show a framed square where retail shows a different empty treatment — cosmetic. | `gmPaperDollUI::ListenToElementMessage` 175674-175706; user retail screenshots | +| AP-67 | **Paperdoll doll is rendered to an off-screen FBO and blitted as a 2-D sprite (RTT), not retail's in-cell `CreatureMode::Render`** — retail adds the doll to `m_pPaperDoll->creature_mode_objects` and draws it into the UI's creature cell during the frame; acdream renders the re-dressed clone into a private framebuffer (`PaperdollViewportRenderer`, sealed in a `GLStateScope`), then the `UiViewport` widget blits the color texture. | `src/AcDream.App/Rendering/PaperdollViewportRenderer.cs`; `src/AcDream.App/UI/UiViewport.cs` | Architectural adaptation: acdream's modern bindless/MDI world pipeline can't cheaply render one creature into a UI cell mid-pass; the RTT seam reuses `WbDrawDispatcher` with the retail camera/light/pose/heading so the pixels match. No visual difference intended. | An FBO-format / lighting-UBO-overwrite / blit-V-flip bug shows as a doll that's mis-lit, clipped, or upside-down vs retail — cosmetic, confined to the viewport. | retail `CreatureMode::Render` 0x004529d0; `RedressCreature` 0x004a3bc0 | +| AP-68 | **Per-race paperdoll pose (`gmPaperDollUI::UpdateForRace`) not implemented** — the ctor-default pose animation `0x030003C0` is applied for ALL body types; retail's `UpdateForRace` (decomp 174129) can override `m_didAnimation` per race. | `src/AcDream.App/Rendering/GameWindow.cs` (`ResolvePaperdollPoseDid`) | cdb-confirmed `UpdateForRace` does NOT fire for Horan, so the ctor default 0x030003C0 is authoritative for the gate character; other races/genders may pose differently. | A non-default-race character could hold the wrong stance in the doll — cosmetic. | `gmPaperDollUI::UpdateForRace` decomp 174129; cdb trace 2026-06-25 | --- diff --git a/src/AcDream.App/Rendering/DollCamera.cs b/src/AcDream.App/Rendering/DollCamera.cs index c1616bfe..53a164ea 100644 --- a/src/AcDream.App/Rendering/DollCamera.cs +++ b/src/AcDream.App/Rendering/DollCamera.cs @@ -1,35 +1,46 @@ +using System; using System.Numerics; namespace AcDream.App.Rendering; /// -/// Fixed camera for the paperdoll mini-scene. Derived from gmPaperDollUI::PostInit -/// (decomp lines 175521–175527: eye offset ~(0.12, -2.4, 0.88) about the origin), but with the -/// look-at raised to mid-body and the camera pulled back after the first visual gate: the doll's -/// model origin is at the FEET (z=0), so aiming at the origin framed only the lower legs. We aim -/// at ~0.95 m (a human's vertical centre) and stand back ~3.7 m so the whole ~1.9 m figure fits. -/// AC up-axis = +Z. +/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport +/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by +/// UIElement_Viewport::SetCamera(position, direction) with: +/// +/// position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae] +/// direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to +/// IDENTITY (euler_set_rotate(0,0,0) then rotate(0,0,0)), so the camera looks +/// straight down +Y with +Z up — NO yaw, NO pitch. +/// +/// The doll is framed purely by camera POSITION + FOV, NOT by aiming at the body: at distance 2.4 with +/// a π/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m +/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is CreatureMode's default +/// m_fFOVRadians (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the +/// paperdoll uses UseSharpMode (not SmartboxFOV) so Render::SetFOVRad(m_fFOVRadians) applies. /// -/// Uses the same / -/// convention as -/// so the doll's triangle winding and back-face -/// culling match the world render pass. +/// An earlier hand-tune aimed the camera at mid-body to "fit the figure", which introduced a +/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the +/// viewer. This restores retail's zero-yaw frame, where full-body framing comes from the eye height + +/// FOV, not from aiming. /// -/// Exact per-race camera framing (UpdateForRace) is deferred polish. +/// Uses the same / +/// convention as so the doll's triangle winding + back-face culling match the +/// world render pass. AC up-axis = +Z. /// public sealed class DollCamera : ICamera { - // Body centre height (metres) the camera aims at — model origin is at the feet (z=0). - private const float BodyCentreZ = 0.95f; - private static readonly Vector3 Eye = new(0.12f, -3.7f, BodyCentreZ); - private static readonly Vector3 Target = new(0f, 0f, BodyCentreZ); + // Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61). + private static readonly Vector3 Eye = new(0.12f, -2.4f, 0.88f); + // Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0). + private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f); private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera - /// Vertical field of view in radians. ~34° default; tune at the visual gate. - public float FovRadians { get; set; } = 0.6f; + /// Vertical field of view — retail CreatureMode default m_fFOVRadians = π/4 (45°). + public float FovRadians { get; set; } = MathF.PI / 4f; public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear - public float Far { get; set; } = 50f; // doll scene is small; 50 m is ample + public float Far { get; set; } = 50f; // doll scene is small; 50 m is ample public float Aspect { get; set; } = 1f; /// diff --git a/src/AcDream.App/Rendering/DollEntityBuilder.cs b/src/AcDream.App/Rendering/DollEntityBuilder.cs index 3dee4351..2b344120 100644 --- a/src/AcDream.App/Rendering/DollEntityBuilder.cs +++ b/src/AcDream.App/Rendering/DollEntityBuilder.cs @@ -45,9 +45,13 @@ public static class DollEntityBuilder /// public const uint DollRenderId = 0xDA11_D012u; - // retail set_heading: 191.367905° about +Z, expressed as a Quaternion - // (axis-angle, axis = (0,0,1), θ = 191.367905° in radians). - private static readonly float _headingRad = 191.367905f * (MathF.PI / 180f); + // 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); diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index c36f3d1b..c95bace1 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -3816,13 +3816,82 @@ public sealed class GameWindow : IDisposable foreach (var p in pe.PartOverrides) parts.Add((p.PartIndex, p.GfxObjId)); } - var meshRefsCopy = new List(pe.MeshRefs); // frozen-pose snapshot + var meshRefsCopy = new List(pe.MeshRefs); // dressed parts (player's live frame) var doll = AcDream.App.Rendering.DollEntityBuilder.Build( pe.SourceGfxObjOrSetupId, meshRefsCopy, basePal, subs, parts); + ApplyPaperdollPose(doll, pe.SourceGfxObjOrSetupId); // re-pose into the paperdoll "posing" stance _paperdollViewportRenderer.SetDoll(doll); return true; } + /// + /// Resolve the paperdoll "posing" stance DID (the model pose — left leg back) exactly as retail: + /// gmPaperDollUI ctor (decomp 174243) sets m_didAnimation = DBObj::GetDIDByEnum(0x10000005, 7), + /// which is DBCache::GetDIDFromEnumStatic (decomp 20380) = master[MasterMapId][0x10000005] + /// → submap 0x25000009 → key 7. (Icon effects use keys 1-6 of the same submap; key 7 is the + /// paperdoll pose.) Per-race UpdateForRace override deferred — the ctor default applies to all + /// body-types for now. Returns 0 if the chain can't resolve. + /// + private uint ResolvePaperdollPoseDid() + { + var dats = _dats; + if (dats is null) return 0u; + uint masterDid = (uint)dats.Portal.Header.MasterMapId; + if (masterDid == 0) return 0u; + if (!dats.Portal.TryGet(masterDid, out var master)) return 0u; + // DBCache::GetDIDFromEnum (decomp 20380) resolves master[arg4] → submap, then submap[arg3]. + // GetDIDFromEnumStatic(0x10000005, 7) ⇒ arg3=0x10000005, arg4=7 ⇒ master[7] → submap, submap[0x10000005]. + if (!master.ClientEnumToID.TryGetValue(7u, out var subDid)) return 0u; // master-level key = 7 + if (!dats.Portal.TryGet(subDid, out var sub)) return 0u; + return sub.ClientEnumToID.TryGetValue(0x10000005u, out var did) ? did : 0u; // submap index = 0x10000005 + } + + /// + /// Re-pose the doll's already-dressed parts into the paperdoll "posing" stance — the C# analog of + /// retail set_sequence_animation(doll, m_didAnimation, …). Keeps each cloned part's GfxObjId + + /// surface overrides (the dressed appearance), but replaces its transform with the pose animation's + /// frame-0 per-part transform, using the same Scale·Rotate·Translate composition as the per-frame + /// animation tick (, GameWindow.cs ~9999). Static (the pose is fixed). If the + /// DID does not resolve to an Animation, the doll keeps its cloned pose (no regression) — the log + /// line surfaces what resolved so the pose is verified, not guessed. + /// + private void ApplyPaperdollPose(AcDream.Core.World.WorldEntity doll, uint setupId) + { + var dats = _dats; + if (dats is null) return; + // Retail's paperdoll doll is STATIC — it holds the pose animation's frame, it does NOT loop. + _animatedEntities.Remove(doll.Id); + // poseDID is cdb-CONFIRMED = 0x030003C0 (== retail gmPaperDollUI m_didAnimation for Horan; UpdateForRace + // did not override). Crash guard: only load a 0x03xxxxxx (Animation) DID (a non-animation id parses a + // garbage frame count → OOM). + uint poseDid = ResolvePaperdollPoseDid(); + if ((poseDid >> 24) != 0x03u) return; + var anim = dats.Get(poseDid); + var setup = dats.Get(setupId); + if (anim is null || setup is null || anim.PartFrames.Count == 0) return; + + // Retail plays the pose animation once and HOLDS the last frame (set_sequence_animation framerate=0, + // RedressCreature decomp 0x004a3c22). For 0x030003C0 the animation has only two distinct keyframes — + // frame 0 (a transitional, raised/bent arm) and frames 1..N (all byte-identical: the settled stance, + // arms down + one leg back) — verified by dumping the dat. The held pose is therefore the last frame. + int frameIdx = anim.PartFrames.Count - 1; + var frame = anim.PartFrames[frameIdx]; + var src = doll.MeshRefs; + var reposed = new List(src.Count); + for (int i = 0; i < src.Count; i++) + { + var scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : System.Numerics.Vector3.One; + System.Numerics.Vector3 origin = System.Numerics.Vector3.Zero; + System.Numerics.Quaternion orient = System.Numerics.Quaternion.Identity; + if (i < frame.Frames.Count) { origin = frame.Frames[i].Origin; orient = frame.Frames[i].Orientation; } + var transform = System.Numerics.Matrix4x4.CreateScale(scale) + * System.Numerics.Matrix4x4.CreateFromQuaternion(orient) + * System.Numerics.Matrix4x4.CreateTranslation(origin); + reposed.Add(new AcDream.Core.World.MeshRef(src[i].GfxObjId, transform) { SurfaceOverrides = src[i].SurfaceOverrides }); + } + doll.MeshRefs = reposed; + } + /// /// Commit B 2026-04-29 — register a live (server-spawned) entity into /// the as a single collision body. diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs index d57a8200..0a3f83bd 100644 --- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs @@ -132,7 +132,7 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis ColorAndIntensity = new Vector4(1f, 1f, 1f, 2.0f), // white @ retail intensity 2.0 ConeAngleEtc = Vector4.Zero, }, - CellAmbient = new Vector4(0.40f, 0.40f, 0.40f, 1f), // 1 active light; ambient tunable at the gate + CellAmbient = new Vector4(0.30f, 0.30f, 0.30f, 1f), // retail CreatureMode default ambient (ctor 0x004543cf = 0.3,0.3,0.3) FogParams = new Vector4(1e9f, 1e9f, 0f, 0f), // fog pushed to infinity = no fog on the doll FogColor = Vector4.Zero, CameraAndTime = new Vector4(0.12f, -2.4f, 0.88f, 0f), // camera world pos (matches DollCamera eye) diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index 4d454270..28841b0d 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -132,14 +132,6 @@ public sealed class PaperdollController : IItemListDragHandler } } - // Slice 2 diagnostic (temporary): confirm the importer surfaced the toggle button + doll - // viewport, and WHERE the button sits. Stripped once the toggle is visually confirmed. - Console.WriteLine($"[Slice2-paperdoll] armorSlots={_armorSlots.Count}/9 " + - $"viewport={_dollViewport?.GetType().Name ?? "MISSING"} " + - $"slotsButton={slotsBtnEl?.GetType().Name ?? "MISSING"} " + - $"btnRect=({slotsBtnEl?.Left ?? -1},{slotsBtnEl?.Top ?? -1},{slotsBtnEl?.Width ?? -1},{slotsBtnEl?.Height ?? -1}) " + - $"btnVisible={slotsBtnEl?.Visible.ToString() ?? "n/a"}"); - ApplyView(); // initial state = doll-view (armor slots hidden) Populate(); diff --git a/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs b/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs index 6a82af78..134a4c5e 100644 --- a/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs +++ b/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs @@ -7,16 +7,28 @@ namespace AcDream.App.Tests.Rendering; public class DollCameraTests { [Fact] - public void Eye_position_frames_the_doll_from_the_front() + public void Eye_position_is_retail_verbatim() { var cam = new DollCamera { Aspect = 100f / 214f }; Assert.True(Matrix4x4.Invert(cam.View, out var inv)); var eye = inv.Translation; - // Tuned at the visual gate: aim at mid-body (z≈0.95) and stand back ~3.7 m so the whole - // figure fits (model origin is at the feet, so aiming at the origin framed only the legs). + // Retail UIElement_Viewport::SetCamera position (decomp 0x004a5a51-0x004a5a61). Assert.Equal(0.12f, eye.X, 3); - Assert.Equal(-3.7f, eye.Y, 3); - Assert.Equal(0.95f, eye.Z, 3); + Assert.Equal(-2.4f, eye.Y, 3); + Assert.Equal(0.88f, eye.Z, 3); + } + + [Fact] + public void Look_axis_is_pure_plus_y_zero_yaw() + { + // retail SetCameraDirection(0,0,0) ⇒ IDENTITY view frame ⇒ camera looks straight down +Y. + // System.Numerics CreateLookAt: forward = -(M13, M23, M33). Any yaw (Target.x≠Eye.x) would put a + // non-zero X here and turn the doll's face away — the bug this guards against. + var cam = new DollCamera { Aspect = 100f / 214f }; + var forward = -new Vector3(cam.View.M13, cam.View.M23, cam.View.M33); + Assert.Equal(0f, forward.X, 4); + Assert.Equal(1f, forward.Y, 4); + Assert.Equal(0f, forward.Z, 4); } [Fact] diff --git a/tools/cdb/paperdoll-pose.cdb b/tools/cdb/paperdoll-pose.cdb new file mode 100644 index 00000000..a5a14b13 --- /dev/null +++ b/tools/cdb/paperdoll-pose.cdb @@ -0,0 +1,9 @@ +.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\hopeful-maxwell-214a12\paperdoll-pose-v2.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe +.echo ==== PAPERDOLL POSE TRACE v2 (reads m_didAnimation VALUE at +0x66c) ARMED ==== +r $t0 = 0 +bp acclient!gmPaperDollUI::RedressCreature ".echo === RedressCreature: poseDID value ===; ? poi(@ecx+0x66c); dt acclient!gmPaperDollUI @ecx m_didAnimation.id; r $t0 = @$t0 + 1; .if (@$t0 >= 3) { .echo ==== DETACHING (qd) ====; qd } .else { gc }" +bp acclient!gmPaperDollUI::UpdateForRace ".echo === UpdateForRace: bodyType then poseDID value ===; ? poi(@esp+4); ? poi(@ecx+0x66c); r $t0 = @$t0 + 1; .if (@$t0 >= 3) { .echo ==== DETACHING (qd) ====; qd } .else { gc }" +g