using System.Numerics; using AcDream.Core.World; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.Core.Meshing; /// Final child root and indexed part poses in the parent-root frame. public readonly record struct EquippedChildPose( Matrix4x4 RootLocal, Matrix4x4[] PartLocal, MeshRef[] AttachedParts); /// /// Retail held-object transform composition. A weapon is a separate child /// physics object: the parent's selects /// a hand part and a frame relative to it, while the child's placement frame /// poses the weapon's own setup parts. /// /// Sources: CPhysicsObj::add_child @ 0x0050F870, /// CPhysicsObj::UpdateChild @ 0x00512D50, /// CPartArray::SetPlacementFrame @ 0x005193D0, and /// Frame::combine @ 0x005122E0. /// public static class EquippedChildAttachment { public static bool TryCompose( Setup parentSetup, IReadOnlyList currentParentPose, Setup childSetup, ParentLocation parentLocation, Placement placement, IReadOnlyList childPartTemplate, float childScale, out IReadOnlyList attachedParts) { bool composed = TryComposePose( parentSetup, currentParentPose, childSetup, parentLocation, placement, childPartTemplate, childScale, out EquippedChildPose pose); attachedParts = composed ? pose.AttachedParts : Array.Empty(); return composed; } public static bool TryComposePose( Setup parentSetup, IReadOnlyList currentParentPose, Setup childSetup, ParentLocation parentLocation, Placement placement, IReadOnlyList childPartTemplate, float childScale, out EquippedChildPose pose) { ArgumentNullException.ThrowIfNull(currentParentPose); var parentParts = new Matrix4x4[currentParentPose.Count]; for (int i = 0; i < parentParts.Length; i++) parentParts[i] = currentParentPose[i].PartTransform; return TryComposePose( parentSetup, parentParts, childSetup, parentLocation, placement, childPartTemplate, childScale, out pose); } /// /// Compose from the parent's indexed final animation poses. The renderer /// exposes these independently of drawable MeshRefs so a hidden or missing /// visual part cannot shift the retail holding-location index. /// public static bool TryComposePose( Setup parentSetup, IReadOnlyList currentParentPose, Setup childSetup, ParentLocation parentLocation, Placement placement, IReadOnlyList childPartTemplate, float childScale, out EquippedChildPose pose) { return TryComposePoseInto( parentSetup, currentParentPose, parentPartAvailability: null, childSetup, parentLocation, placement, childPartTemplate, childScale, partPoseBuffer: null, attachedPartBuffer: null, out pose); } /// /// Allocation-free update path for an already realized child. Buffers are /// reused by the App-layer attachment owner after their first composition. /// public static bool TryComposePoseInto( Setup parentSetup, IReadOnlyList currentParentPose, IReadOnlyList? parentPartAvailability, Setup childSetup, ParentLocation parentLocation, Placement placement, IReadOnlyList childPartTemplate, float childScale, Matrix4x4[]? partPoseBuffer, MeshRef[]? attachedPartBuffer, out EquippedChildPose pose) { ArgumentNullException.ThrowIfNull(parentSetup); ArgumentNullException.ThrowIfNull(currentParentPose); ArgumentNullException.ThrowIfNull(childSetup); ArgumentNullException.ThrowIfNull(childPartTemplate); if (!parentSetup.HoldingLocations.TryGetValue(parentLocation, out LocationType? holding)) { pose = new EquippedChildPose( Matrix4x4.Identity, Array.Empty(), Array.Empty()); return false; } Matrix4x4 parentPart; if (holding.PartId >= 0 && holding.PartId < currentParentPose.Count) { if (parentPartAvailability is not null && (holding.PartId >= parentPartAvailability.Count || !parentPartAvailability[(int)holding.PartId])) { pose = default; return false; } parentPart = currentParentPose[(int)holding.PartId]; } else { // Retail CPhysicsObj::UpdateChild (0x00512D50) uses the parent // root for every part number >= num_parts. This includes the // canonical 0xFFFFFFFF root sentinel and malformed positives. parentPart = Matrix4x4.Identity; } Matrix4x4 holdingFrame = ToMatrix(holding.Frame); Matrix4x4 childRoot = holdingFrame * parentPart; // Retail CPartArray::SetPlacementFrame falls back specifically to // placement 0 (Default), then installs null if Default is absent. AnimationFrame? placementFrame = null; if (!childSetup.PlacementFrames.TryGetValue(placement, out placementFrame)) childSetup.PlacementFrames.TryGetValue(Placement.Default, out placementFrame); int partCount = Math.Min(childSetup.Parts.Count, childPartTemplate.Count); MeshRef[] result = attachedPartBuffer is { Length: var attachedLength } && attachedLength == partCount ? attachedPartBuffer : new MeshRef[partCount]; Matrix4x4[] childPartPoses = partPoseBuffer is { Length: var poseLength } && poseLength == partCount ? partPoseBuffer : new Matrix4x4[partCount]; for (int i = 0; i < partCount; i++) { Frame partFrame = placementFrame is not null && i < placementFrame.Frames.Count ? placementFrame.Frames[i] : new Frame { Orientation = Quaternion.Identity }; Vector3 defaultScale = i < childSetup.DefaultScale.Count ? childSetup.DefaultScale[i] : Vector3.One; Matrix4x4 visualChildPart = Matrix4x4.CreateScale(defaultScale) * ToMatrix(partFrame); if (childScale != 1.0f) visualChildPart *= Matrix4x4.CreateScale(childScale); Matrix4x4 rigidChildPart = Matrix4x4.CreateFromQuaternion(partFrame.Orientation) * Matrix4x4.CreateTranslation(partFrame.Origin * childScale); childPartPoses[i] = rigidChildPart; MeshRef template = childPartTemplate[i]; result[i] = new MeshRef(template.GfxObjId, visualChildPart * childRoot) { SurfaceOverrides = template.SurfaceOverrides, }; } pose = new EquippedChildPose(childRoot, childPartPoses, result); return true; } private static Matrix4x4 ToMatrix(Frame frame) => Matrix4x4.CreateFromQuaternion(frame.Orientation) * Matrix4x4.CreateTranslation(frame.Origin); }