#include "Player/Body/DescentBodyProfile.h" #include "TheDescent.h" #include "Dom/JsonObject.h" #include "HAL/IConsoleManager.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" namespace { TAutoConsoleVariable CVarBodyModel(TEXT("descent.Body.Model"), TEXT(""), TEXT("The player's body model: a profile name of Config/DescentBody.json. Empty (default) = the file's \"model\". Read when a character is set up.")); TMap> GBodyProfiles; FString BodyTableFile() { return FPaths::ProjectConfigDir() / TEXT("DescentBody.json"); } TSharedPtr BodyReadJson(const FString& File) { FString Text; TSharedPtr Root; if (!FFileHelper::LoadFileToString(Text, *File)) { return nullptr; } const TSharedRef> Reader = TJsonReaderFactory<>::Create(Text); return FJsonSerializer::Deserialize(Reader, Root) ? Root : nullptr; } FName BodyBone(const TSharedPtr& Bones, const TCHAR* Role, FString& Missing) { FString Name; if (!Bones.IsValid() || !Bones->TryGetStringField(Role, Name) || Name.IsEmpty()) { Missing += FString(Missing.IsEmpty() ? TEXT("") : TEXT(", ")) + Role; return NAME_None; } return FName(*Name); } } const FDescentBodyAnimRow* FDescentBodyProfile::FindRow(const FString& Role) const { return Rows.FindByPredicate([&Role](const FDescentBodyAnimRow& R) { return R.Role == Role; }); } FString FDescentBodyProfile::BakedPath(const FString& Role) const { const FString Asset = TEXT("A_Body_") + Role; return AnimFolder / Asset + TEXT(".") + Asset; } FString FDescentBodyProfile::LegacyPath(const FString& Role) const { const FString* Asset = LegacyNames.Find(Role); return (Asset && !LegacyAnimFolder.IsEmpty()) ? LegacyAnimFolder / *Asset + TEXT(".") + *Asset : FString(); } FString FDescentBodyProfile::ActiveModel() { const FString Wanted = CVarBodyModel.GetValueOnGameThread(); if (!Wanted.IsEmpty()) { return Wanted; } FString Model; const TSharedPtr Root = BodyReadJson(BodyTableFile()); return (Root.IsValid() && Root->TryGetStringField(TEXT("model"), Model)) ? Model : FString(); } void FDescentBodyProfile::Reload() { GBodyProfiles.Reset(); } const FDescentBodyProfile& FDescentBodyProfile::Get() { static FString CachedModel; static TSharedPtr Cached; const FString Wanted = CVarBodyModel.GetValueOnGameThread(); // the file is read once; the cvar can change the model without a restart if (!Cached.IsValid() || GBodyProfiles.Num() == 0 || (!Wanted.IsEmpty() && Wanted != CachedModel)) { CachedModel = ActiveModel(); TSharedPtr& Slot = GBodyProfiles.FindOrAdd(CachedModel); if (!Slot.IsValid()) { Slot = MakeShared(Load(CachedModel)); if (!Slot->bValid) { UE_LOG(LogDescent, Error, TEXT("Body: profile '%s' of %s: %s"), *CachedModel, *BodyTableFile(), *Slot->Error); } } Cached = Slot; } return *Cached; } FDescentBodyProfile FDescentBodyProfile::Load(const FString& Model) { FDescentBodyProfile P; P.Name = Model; const TSharedPtr Root = BodyReadJson(BodyTableFile()); if (!Root.IsValid()) { P.Error = TEXT("the file is missing or is not JSON"); return P; } const TSharedPtr* Profiles = nullptr; const TSharedPtr* Json = nullptr; if (!Root->TryGetObjectField(TEXT("profiles"), Profiles) || !(*Profiles)->TryGetObjectField(Model, Json)) { P.Error = TEXT("no such profile"); return P; } const FJsonObject& J = **Json; FString Missing; if (!J.TryGetStringField(TEXT("mesh"), P.MeshPath)) { Missing = TEXT("mesh"); } else if (!P.MeshPath.Contains(TEXT("."))) { P.MeshPath += TEXT(".") + FPaths::GetBaseFilename(P.MeshPath); } if (!J.TryGetStringField(TEXT("anim_folder"), P.AnimFolder)) { Missing += TEXT(" anim_folder"); } J.TryGetNumberField(TEXT("yaw_deg"), P.YawDeg); J.TryGetNumberField(TEXT("scale"), P.Scale); FString Text; if (J.TryGetStringField(TEXT("eye_socket"), Text)) { P.EyeSocket = FName(*Text); } const TArray>* List = nullptr; if (J.TryGetArrayField(TEXT("hide_bones_own_view"), List)) { for (const TSharedPtr& V : *List) { P.OwnViewHiddenBones.Add(FName(*V->AsString())); } } if (J.TryGetArrayField(TEXT("gun_slots"), List)) { for (const TSharedPtr& V : *List) { P.GunSlots.Add(FName(*V->AsString())); } } const TSharedPtr* Obj = nullptr; if (J.TryGetObjectField(TEXT("material_overrides"), Obj)) { for (const auto& Pair : (*Obj)->Values) { FString Path = Pair.Value->AsString(); if (!Path.Contains(TEXT("."))) { Path += TEXT(".") + FPaths::GetBaseFilename(Path); } P.MaterialOverrides.Emplace(FName(*FString(Pair.Key)), Path); } } TSharedPtr Bones; if (J.TryGetObjectField(TEXT("bones"), Obj)) { Bones = *Obj; } FString NoBone; FDescentBodyBones& B = P.Bones; B.Pelvis = BodyBone(Bones, TEXT("pelvis"), NoBone); B.Spine = BodyBone(Bones, TEXT("spine"), NoBone); B.Chest = BodyBone(Bones, TEXT("chest"), NoBone); B.Neck = BodyBone(Bones, TEXT("neck"), NoBone); B.Head = BodyBone(Bones, TEXT("head"), NoBone); B.UpperArmR = BodyBone(Bones, TEXT("upperarm_r"), NoBone); B.LowerArmR = BodyBone(Bones, TEXT("lowerarm_r"), NoBone); B.HandR = BodyBone(Bones, TEXT("hand_r"), NoBone); B.UpperArmL = BodyBone(Bones, TEXT("upperarm_l"), NoBone); B.LowerArmL = BodyBone(Bones, TEXT("lowerarm_l"), NoBone); B.HandL = BodyBone(Bones, TEXT("hand_l"), NoBone); B.KneeL = BodyBone(Bones, TEXT("knee_l"), NoBone); B.KneeR = BodyBone(Bones, TEXT("knee_r"), NoBone); B.FootL = BodyBone(Bones, TEXT("foot_l"), NoBone); B.FootR = BodyBone(Bones, TEXT("foot_r"), NoBone); if (!NoBone.IsEmpty()) { Missing += TEXT(" bones: ") + NoBone; } if (J.TryGetObjectField(TEXT("legacy"), Obj)) { (*Obj)->TryGetStringField(TEXT("folder"), P.LegacyAnimFolder); (*Obj)->TryGetNumberField(TEXT("scale"), P.LegacyScale); const TSharedPtr* Names = nullptr; if ((*Obj)->TryGetObjectField(TEXT("names"), Names)) { for (const auto& Pair : (*Names)->Values) { P.LegacyNames.Add(FString(Pair.Key), Pair.Value->AsString()); } } } // the animation table, shared by every model if (Root->TryGetObjectField(TEXT("animations"), Obj) && (*Obj)->TryGetArrayField(TEXT("rows"), List)) { for (const TSharedPtr& V : *List) { const TSharedPtr R = V->AsObject(); FDescentBodyAnimRow Row; if (!R.IsValid() || !R->TryGetStringField(TEXT("role"), Row.Role)) { continue; } R->TryGetStringField(TEXT("source"), Row.Source); R->TryGetBoolField(TEXT("loop"), Row.bLoop); R->TryGetBoolField(TEXT("root_lock"), Row.bRootLock); R->TryGetBoolField(TEXT("reversed"), Row.bReversed); R->TryGetBoolField(TEXT("upper_body"), Row.bUpperBody); P.Rows.Add(MoveTemp(Row)); } } if (P.Rows.Num() == 0) { Missing += TEXT(" animations.rows"); } // what the script measured of the baked clips (absent until the script ran for this model: the code's own defaults stand in) FString Disk = P.AnimFolder; if (Disk.RemoveFromStart(TEXT("/Game/"))) { const TSharedPtr M = BodyReadJson(FPaths::ProjectContentDir() / Disk / TEXT("BodyAnims.measured.json")); const TSharedPtr* Roles = nullptr; if (M.IsValid() && M->TryGetObjectField(TEXT("roles"), Roles)) { for (const auto& Pair : (*Roles)->Values) { const TSharedPtr R = Pair.Value->AsObject(); if (!R.IsValid()) { continue; } FDescentBodyAnimMeasure Me; R->TryGetNumberField(TEXT("length"), Me.Length); R->TryGetNumberField(TEXT("speed_cm_s"), Me.SpeedCmS); R->TryGetNumberField(TEXT("root_speed_cm_s"), Me.RootSpeedCmS); R->TryGetNumberField(TEXT("slide_cm_s"), Me.SlideCmS); R->TryGetNumberField(TEXT("bob_steps"), Me.BobSteps); R->TryGetNumberField(TEXT("bob_phase"), Me.BobPhase); P.Measured.Add(FString(Pair.Key), Me); } } } P.Error = Missing.IsEmpty() ? FString() : TEXT("missing: ") + Missing; P.bValid = Missing.IsEmpty(); return P; }