#pragma once #include "CoreMinimal.h" /** * The player's body as data: everything that belongs to ONE character model, and the table of animations every model plays. * Both come from Config/DescentBody.json, the file the retarget script (Tools/ue/assets_14_body_anims.py) reads too. * The body code (Player/Body) refers to roles (a bone role, an animation role), never to a model's own names. * Another model: add a profile to the file, run the script for it, set "model". Guide: Docs/PLAYER_MODEL_SWAP.md. */ /** A row of the animation table: a situation role and the clip on the UE5 mannequin it is baked from. */ struct FDescentBodyAnimRow { FString Role, Source; bool bLoop = true; // repeats; otherwise it stops on its last frame bool bRootLock = true; // in place: the root stays at the reference pose, the capsule moves him bool bReversed = false; // played backward bool bUpperBody = false; // used on the spine and arms only }; /** What the script measured of a baked clip (/BodyAnims.measured.json). Centimetres of the UNSCALED mesh; zero = not measured. */ struct FDescentBodyAnimMeasure { double Length = 0.0; double SpeedCmS = 0.0; // locomotion loops: the planted foot against the body = the speed the legs walk at double RootSpeedCmS = 0.0; // the source's root travel per second (not what the retargeted legs do: 27-33 % more on Twinblast) double SlideCmS = 0.0; // how unevenly the planted foot moves: the skating left at a matched play rate int32 BobSteps = 0; // footfalls in the clip double BobPhase = 0.0; // pelvis height ~ cos(2 pi * BobSteps * t / Length - BobPhase) }; /** Bone names by the role the code uses them in. */ struct FDescentBodyBones { FName Pelvis, Spine, Chest, Neck, Head; // Spine: the first spine bone, it and everything above is the "upper body"; Neck: what is anchored under the camera FName UpperArmR, LowerArmR, HandR, UpperArmL, LowerArmL, HandL; FName KneeL, KneeR, FootL, FootR; }; struct THEDESCENT_API FDescentBodyProfile { FString Name; FString MeshPath; // object path of the skeletal mesh double YawDeg = -90.0; // the mesh component's yaw so the model faces the character's +X (a model facing +Y: -90) double Scale = 1.0; // puts the eyes of the standing pose at the camera's height (measured by scenario player_body) FString AnimFolder; // the baked sequences live here as A_Body_ FName EyeSocket; TArray OwnViewHiddenBones; // not drawn in the player's own view (each with everything below it) FDescentBodyBones Bones; TArray> MaterialOverrides; // material slot name -> object path of our material TArray GunSlots; // material slots not drawn unless descent.Body.Guns is 1 // descent.Body.AnimSet 0: the model's own sequences, if it has any FString LegacyAnimFolder; double LegacyScale = 1.0; TMap LegacyNames; // role -> asset name in LegacyAnimFolder TArray Rows; // the animation table (the same for every model) TMap Measured; // by role bool bValid = false; FString Error; const FDescentBodyAnimRow* FindRow(const FString& Role) const; const FDescentBodyAnimMeasure* FindMeasure(const FString& Role) const { return Measured.Find(Role); } /** Object path of a role's baked sequence, /A_Body_.A_Body_. */ FString BakedPath(const FString& Role) const; /** Object path of a role's sequence in the model's own set; empty when it has none. */ FString LegacyPath(const FString& Role) const; bool HasLegacy() const { return !LegacyAnimFolder.IsEmpty() && LegacyNames.Num() > 0; } /** The profile of the active model (cvar descent.Body.Model, else "model" in the file). Read once per model; game thread. */ static const FDescentBodyProfile& Get(); static FString ActiveModel(); /** Reads a profile from Config/DescentBody.json and its measured file. bValid false and Error set when something is missing. */ static FDescentBodyProfile Load(const FString& Model); /** Forgets what was read (after the file or the measured numbers changed). */ static void Reload(); };