#include "AnimQueue.h" #include "Common.h" #include "UtilityLibrary.h" #include "GameFramework/Actor.h" #include "Components/MeshComponent.h" #include "Components/StaticMeshComponent.h" #include "Components/SkeletalMeshComponent.h" #include "AssetLookup.h" #include "Materials/MaterialInstanceDynamic.h" #include FlxAnimationStep::FlxAnimationStep(int64 hash, std::string_view body) { Finished = false; Hash = hash; Body.SetNum(body.size()); memcpy(Body.GetData(), body.data(), body.size()); Blueprint = UlxAnimationStepLibrary::AnimationStepGetString(*this, "bp"); } static void ClearProperties(const FString& prefix, UObject* obj, FProperty *except) { UClass* uclass = obj->GetClass(); FName prefixlo(prefix); FName prefixhi(prefix + TEXT("\xFFFF")); for (TFieldIterator It(uclass); It; ++It) { FProperty* fprop = *It; if (fprop == except) continue; bool match1 = (fprop->GetFName().Compare(prefixlo) > 0); bool match2 = (fprop->GetFName().Compare(prefixhi) < 0); if (match1 && match2) { uint8* pptr = fprop->ContainerPtrToValuePtr(obj); fprop->ClearValue(pptr); } } } #pragma optimize("", off) static bool SetProperty(const FString& prefix, UObject* obj, const FlxAnimationField& field) { UClass* uclass = obj->GetClass(); FString sname(field.Name.size(), (const UTF8CHAR*)field.Name.data()); FName nname(prefix + sname); switch (field.Type) { case LuaValueType::STRING: { FStrProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return false; FString* pptr = fprop->ContainerPtrToValuePtr(obj); *pptr = FString(field.S.size(), (const UTF8CHAR*)field.S.data()); return true; } case LuaValueType::TOKEN: { FNameProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return false; FName* pptr = fprop->ContainerPtrToValuePtr(obj); *pptr = FName(field.S.size(), (const UTF8CHAR*)field.S.data(), FNAME_Add); return true; } case LuaValueType::NUMBER: { FDoubleProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return false; double* pptr = fprop->ContainerPtrToValuePtr(obj); *pptr = field.X; return true; } case LuaValueType::BOOLEAN: { FBoolProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return false; uint8* pptr = fprop->ContainerPtrToValuePtr(obj); fprop->SetPropertyValue(pptr, (field.X == 1.0)); return true; } case LuaValueType::VECTOR: { FStructProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return false; if (fprop->Struct != TBaseStructure::Get()) return false; FVector* pptr = fprop->ContainerPtrToValuePtr(obj); *pptr = FVector(field.X, field.Y, field.Z); return true; } } return false; } static FStructProperty *FindAnimationStepProperty(UClass *uclass, const FString &prefix) { FName nname(prefix + TEXT("Animation Step")); FStructProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return nullptr; if (fprop->Struct != TBaseStructure::Get()) return nullptr; return fprop; } static FInt64Property *FindAnimationIDProperty(UClass *uclass, const FString &prefix) { FName nname(prefix + TEXT("Animation ID")); FInt64Property* fprop = FindFProperty(uclass, nname); return fprop; } void UlxAnimationStepLibrary::UnpackAnimationStep(bool &bChanged, FString &Action, const FlxAnimationStep& step, UObject* target, const FString& prefix) { bChanged = false; Action = TEXT(""); if (prefix.IsEmpty()) { UE_LOG(LogBlueprint, Error, TEXT("UnpackAnimationStep: You may not pass an empty string for prefix")); return; } UClass* uclass = target->GetClass(); std::string_view body((const char*)(step.Body.GetData()), step.Body.Num()); FlxAnimationStepDecoder decoder(body); FStructProperty* stepproperty = FindAnimationStepProperty(uclass, prefix); if (stepproperty == nullptr) { UE_LOG(LogBlueprint, Error, TEXT("UnpackAnimationStep: Target object does not have a variable named: '%s Animation Step'"), *prefix); return; } FInt64Property* idproperty = FindAnimationIDProperty(uclass, prefix); if (idproperty == nullptr) { UE_LOG(LogBlueprint, Error, TEXT("UnpackAnimationStep: Target object does not have a variable named: '%s Animation ID'"), *prefix); return; } FlxAnimationStep* stepstorage = stepproperty->ContainerPtrToValuePtr(target); int64 oldhash = stepstorage->Hash; FlxAnimationField actionfield; actionfield.Name = "action"; actionfield.Persistent = false; actionfield.Type = LuaValueType::STRING; actionfield.S = "unknown"; // Decode everything. If an action field is found, save it for later. ClearProperties(prefix, target, stepproperty); while (!decoder.AtEOF()) { FlxAnimationField field = decoder.ReadField(); if ((field.Type == LuaValueType::STRING) && (field.Name == "action")) { actionfield.S = field.S; continue; } if (step.Finished && !field.Persistent) continue; SetProperty(prefix, target, field); } // Store the action field. if (step.Finished) actionfield.S = "idle"; SetProperty(prefix, target, actionfield); // Store the whole step. *stepstorage = step; // Store the ID int64 *idstorage = idproperty->ContainerPtrToValuePtr(target); *idstorage = step.Hash; // Return the correct values. Action = FString(actionfield.S.size(), (const UTF8CHAR*)actionfield.S.data()); bChanged = (step.Hash != oldhash); } FString UlxAnimationStepLibrary::AnimationStepDebugString(const FlxAnimationStep& step) { std::string_view body((const char*)(step.Body.GetData()), step.Body.Num()); return FlxAnimationStepDecoder::DebugString(step.Finished, step.Hash, body); } static FlxAnimationField FindAnimationFieldLL(const FlxAnimationStep& step, std::string_view name) { std::string_view body((const char*)(step.Body.GetData()), step.Body.Num()); FlxAnimationStepDecoder decoder(body); FlxAnimationField result; while (!decoder.AtEOF()) { result = decoder.ReadField(); if (result.Name == name) { return result; } } result.Type = LuaValueType::UNINITIALIZED; return result; } static FlxAnimationField FindAnimationField(const FlxAnimationStep& step, const FString& name) { FTCHARToUTF8 utf8name(name); std::string_view uname(utf8name.Get(), utf8name.Length()); return FindAnimationFieldLL(step, uname); } void FlxAnimationStep::AutoUpdateXYZ(AActor *actor) const { FlxAnimationField xyz = FindAnimationFieldLL(*this, "xyz"); if (xyz.Type == LuaValueType::VECTOR) { actor->SetActorLocation(FVector(xyz.X, xyz.Y, xyz.Z)); } } void FlxAnimationStep::AutoUpdateFacing(AActor *actor) const { FlxAnimationField facing = FindAnimationFieldLL(*this, "facing"); if (facing.Type == LuaValueType::NUMBER) { actor->SetActorRotation(FRotator(0, facing.X, 0)); } } void FlxAnimationStep::AutoUpdatePlane(FName *planep) const { FlxAnimationField plane = FindAnimationFieldLL(*this, "plane"); if (plane.Type == LuaValueType::STRING) { FString pname(plane.S.size(), (const UTF8CHAR*)(plane.S.data())); *planep = FName(pname); } } bool UlxAnimationStepLibrary::AnimationStepEqual(const FlxAnimationStep &stepa, const FlxAnimationStep &stepb) { return (stepa.Finished == stepb.Finished) && (stepa.Hash == stepb.Hash); } bool UlxAnimationStepLibrary::AnimationStepIsIdle(const FlxAnimationStep &step) { return step.Finished; } FVector UlxAnimationStepLibrary::AnimationStepGetVector(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != LuaValueType::VECTOR) return FVector(0, 0, 0); return FVector(field.X, field.Y, field.Z); } double UlxAnimationStepLibrary::AnimationStepGetFloat(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != LuaValueType::NUMBER) return 0.0; return field.X; } FString UlxAnimationStepLibrary::AnimationStepGetString(const FlxAnimationStep& step, const FString& name) { if (step.Finished && (name == TEXT("action"))) { return TEXT("idle"); } FlxAnimationField field = FindAnimationField(step, name); if (field.Type != LuaValueType::STRING) return TEXT(""); return FString(field.S.size(), (const UTF8CHAR*)(field.S.data())); } FName UlxAnimationStepLibrary::AnimationStepGetName(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != LuaValueType::TOKEN) return FName(); return FName(field.S.size(), (const UTF8CHAR*)(field.S.data()), FNAME_Add); } bool UlxAnimationStepLibrary::AnimationStepGetBool(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != LuaValueType::BOOLEAN) return false; return field.X == 1.0; } FlxAnimationField FlxAnimationStepDecoder::ReadField() { FlxAnimationField result; result.Name = Decoder.read_string_view(); result.Persistent = Decoder.read_bool(); result.Type = (LuaValueType)Decoder.read_uint8(); switch (result.Type) { case LuaValueType::STRING: { result.S = Decoder.read_string_view(); break; } case LuaValueType::TOKEN: { result.S = Decoder.read_string_view(); break; } case LuaValueType::NUMBER: { result.X = Decoder.read_double(); break; } case LuaValueType::BOOLEAN: { result.X = Decoder.read_bool() ? 1.0 : 0.0; break; } case LuaValueType::VECTOR: { result.X = Decoder.read_double(); result.Y = Decoder.read_double(); result.Z = Decoder.read_double(); break; } default: { Decoder.read_bytes(Decoder.fill()); result.Type = LuaValueType::UNINITIALIZED; break; } } return result; } FString FlxAnimationStepDecoder::DebugString(bool finished, int64 hash, std::string_view body) { FString result; FlxAnimationStepDecoder decoder(body); result.Appendf(TEXT("Hash=%016llx"), hash); if (finished) { result.Appendf(TEXT(" finished")); } while (!decoder.AtEOF()) { FlxAnimationField field = decoder.ReadField(); result.Append(TEXT(" ")); result.Append(FString(field.Name.size(), (const UTF8CHAR*)field.Name.data())); result.Append(field.Persistent ? TEXT("=") : TEXT(":")); switch (field.Type) { case LuaValueType::STRING: result.Append(FString(field.S.size(), (const UTF8CHAR*)field.S.data())); break; case LuaValueType::TOKEN: result.Appendf(TEXT("[%s]"), *FString(field.S.size(), (const UTF8CHAR*)field.S.data())); break; case LuaValueType::NUMBER: result.Appendf(TEXT("%lf"), field.X); break; case LuaValueType::BOOLEAN: result.Append((field.X) == 1.0 ? TEXT("true") : TEXT("false")); break; case LuaValueType::VECTOR: result.Appendf(TEXT("%lf,%lf,%lf"), field.X, field.Y, field.Z); break; } } return result; } FlxAnimationStepView FlxAnimQueueDecoder::ReadStep() { FlxAnimationStepView result; result.Hash = Decoder.read_int64(); result.Body = Decoder.read_string_view(); return result; } int64 FlxAnimQueueDecoder::PeekHash() { int64_t read_count = Decoder.total_reads(); int64 result = Decoder.read_int64(); Decoder.unread_to(read_count); return result; } FlxAnimQueueDecoder::FlxAnimQueueDecoder(std::string_view queue) : Decoder(queue) { SizeLimit = Decoder.read_uint8(); ActualSize = Decoder.read_uint8(); } // FString FlxAnimQueueDecoder::DebugString(std::string_view queue) { // FString result; // FlxAnimQueueDecoder decoder(queue); // while (!decoder.AtEOF()) { // FlxAnimationStepView step = decoder.ReadStep(); // FString stepdebug = FlxAnimationStepDecoder::DebugString(false, step.Hash, step.Body); // result.Appendf(TEXT("%s\n"), *stepdebug); // } // return result; // } FlxAnimTracker::FlxAnimTracker() { Clear(); } void FlxAnimTracker::Clear() { AQ.Empty(); Changed = true; } void FlxAnimTracker::FinishedAnimation(int64 hash) { for (int i = 0; i < AQ.Num(); i++) { if (AQ[i].Hash == hash) { AQ[i].Finished = true; Changed = true; } } } bool FlxAnimTracker::IsFinished(int64 hash) { for (int i = 0; i < AQ.Num(); i++) { if (AQ[i].Hash == hash) { return AQ[i].Finished; } } return true; } void FlxAnimTracker::SkipToEnd() { for (int i = 0; i < AQ.Num(); i++) { if (!AQ[i].Finished) { AQ[i].Finished = true; Changed = true; } } } TArray FlxAnimTracker::GetHashes() { TArray Result; Result.SetNum(AQ.Num()); for (int i = 0; i < AQ.Num(); i++) { Result[i] = AQ[i].Hash; } return Result; } const FlxAnimationStep *FlxAnimTracker::FirstUnfinished() const { for (int i = 0; i < AQ.Num(); i++) { if (!AQ[i].Finished) return &AQ[i]; } return nullptr; } const FlxAnimationStep *FlxAnimTracker::LastFinished() const { for (int i = AQ.Num() - 1; i >= 0; i--) { if (AQ[i].Finished) return &AQ[i]; } return nullptr; } const FlxAnimationStep *FlxAnimTracker::FindAnimation(int64 hash) const { for (int i = 0; i < AQ.Num(); i++) { if (AQ[i].Hash == hash) return &AQ[i]; } return nullptr; } void FlxAnimTracker::Update(std::string_view encqueue) { check(!encqueue.empty()); // If the first hash matches, we don't bother updating at all. // FlxAnimQueueDecoder decoder(encqueue); if (!AQ.IsEmpty()) { if (decoder.PeekHash() == AQ.Last().Hash) { return; } } // The Changed flag is set whenever there is any change to // the animation queue of any kind. // Changed = true; // Build a map indexing the steps in AQ. // TMap HashToIndex; for (int i = 0; i < AQ.Num(); i++) { HashToIndex.Emplace(AQ[i].Hash, i); } // Search for a Luprex animation record that matches // something that we already have. Yields the sequence // number of the most recent matching record. // // At the same time, push all non-matching records // after the matching record onto a stack of new records. // TArray newsteps; int32 matchingindex = -1; while (!decoder.AtEOF()) { FlxAnimationStepView step = decoder.ReadStep(); int32* indexp = HashToIndex.Find(step.Hash); if (indexp == nullptr) { newsteps.Emplace(step); } else { matchingindex = *indexp; break; } } // Remove all animations after the most recent matching // record. // int32 nremove = (AQ.Num() - (matchingindex + 1)); check((nremove >= 0) && (nremove <= AQ.Num())); for (int32 i = 0; i < nremove; i++) { AQ.Pop(); } // Transfer the new animations onto the queue. // while (!newsteps.IsEmpty()) { FlxAnimationStepView step = newsteps.Pop(); AQ.Emplace(step.Hash, step.Body); } // If there are too many animations in AQ, discard // any very old ones. // // TODO: this is hardwired to keep 10. Instead, we // should keep the number specified in the queue. // const int limit = 10; if (AQ.Num() > limit) { int offset = AQ.Num() - limit; for (int i = 0; i < limit; i++) { AQ[i] = AQ[i + offset]; } AQ.SetNum(limit); } } FString FlxAnimTracker::DebugString() const { FString Result; if (Changed) { Result += TEXT("changed=true "); } else { Result += TEXT("changed=false "); } Result += TEXT("\n"); for (int i = 0; i < AQ.Num(); i++) { Result += UlxAnimationStepLibrary::AnimationStepDebugString(AQ[i]); Result += TEXT("\n"); } return Result; } FString FlxAnimTracker::GetCurrentBlueprintName() { for (int i = 0; i < AQ.Num(); i++) { if (!AQ[i].Finished) { return AQ[i].Blueprint; } } return AQ.Last().Blueprint; } FlxAnimationStep FlxAnimTracker::GetCurrentAnimation() { FlxAnimationStep result; for (int i = 0; i < AQ.Num(); i++) { if (!AQ[i].Finished) { return AQ[i]; } } result = AQ.Last(); // This next line is a hack. We need the idle step to have a unique // hash. This is a passable way to get a unique hash value. result.Hash += 1; return result; } void UlxAnimationStepLibrary::AnimationStepApplyMaterials(const FlxAnimationStep& step, AActor* actor) { if (actor == nullptr) return; // Step 1: Build tables of mat_ parameters (vectors and scalars). // TMap VectorParams; TMap ScalarParams; { std::string_view body((const char*)(step.Body.GetData()), step.Body.Num()); FlxAnimationStepDecoder decoder(body); while (!decoder.AtEOF()) { FlxAnimationField field = decoder.ReadField(); if (field.Name.size() <= 4) continue; if (field.Name.substr(0, 4) != "mat_") continue; std::string_view suffix = field.Name.substr(4); FName paramName(suffix.size(), (const UTF8CHAR*)suffix.data()); if (field.Type == LuaValueType::VECTOR) { VectorParams.Add(paramName, FVector(field.X, field.Y, field.Z)); } else if (field.Type == LuaValueType::NUMBER) { ScalarParams.Add(paramName, (float)field.X); } } } // Step 2: Early exit if no mat_ parameters found. // if (VectorParams.IsEmpty() && ScalarParams.IsEmpty()) return; // Step 3: Loop over all mesh components and apply material parameters. // TInlineComponentArray MeshComponents; actor->GetComponents(MeshComponents); for (UMeshComponent* MeshComp : MeshComponents) { int32 NumMaterials = MeshComp->GetNumMaterials(); for (int32 SlotIndex = 0; SlotIndex < NumMaterials; SlotIndex++) { UMaterialInterface* CurrentMat = MeshComp->GetMaterial(SlotIndex); if (CurrentMat == nullptr) continue; // Check if the material has any parameter that doesn't // match what was specified. // bool AnyMismatch = false; for (auto& Pair : VectorParams) { FLinearColor Compare; if (CurrentMat->GetVectorParameterValue(Pair.Key, Compare)) { FLinearColor Desired(Pair.Value.X, Pair.Value.Y, Pair.Value.Z); if (Compare != Desired) { AnyMismatch = true; } } } for (auto& Pair : ScalarParams) { float Compare; if (CurrentMat->GetScalarParameterValue(Pair.Key, Compare)) { if (Compare != Pair.Value) { AnyMismatch = true; } } } if (!AnyMismatch) continue; // Strip away any existing dynamic material before creating a new one. // UMaterialInterface* BaseMat = CurrentMat; while (UMaterialInstanceDynamic* MID = Cast(BaseMat)) { BaseMat = MID->Parent; } if (BaseMat != CurrentMat) { MeshComp->SetMaterial(SlotIndex, BaseMat); } // Create the new dynamic material. // UMaterialInstanceDynamic* MID = MeshComp->CreateDynamicMaterialInstance(SlotIndex); for (auto& Pair : VectorParams) { FVector& Vec = Pair.Value; MID->SetVectorParameterValue(Pair.Key, FLinearColor(Vec.X, Vec.Y, Vec.Z)); } for (auto& Pair : ScalarParams) { MID->SetScalarParameterValue(Pair.Key, Pair.Value); } } } } void UlxAnimationStepLibrary::AnimationStepApplyMesh(const FlxAnimationStep& step, bool FallbackToBP, AActor* actor) { if (actor == nullptr) return; // Step 1: Look for a "mesh" or "bp" string field in the animation step. // FString MeshName = AnimationStepGetString(step, TEXT("mesh")); if (MeshName.IsEmpty() && FallbackToBP) { MeshName = AnimationStepGetString(step, TEXT("bp")); } if (MeshName.IsEmpty()) return; // Step 2: Find the actor's mesh component. There must be exactly one. // TInlineComponentArray MeshComponents; actor->GetComponents(MeshComponents); if (MeshComponents.Num() != 1) { UE_LOG(LogLuprexIntegration, Error, TEXT("AnimationStepApplyMesh: Actor %s has %d mesh components, expected exactly 1"), *actor->GetName(), MeshComponents.Num()); return; } UMeshComponent* MeshComp = MeshComponents[0]; // Step 3: Apply the mesh based on the component type. // if (UStaticMeshComponent* StaticComp = Cast(MeshComp)) { UStaticMesh* NewMesh = nullptr; UlxAssetLookup::LoadStaticMeshAsset(NewMesh, actor, MeshName, false); if (NewMesh == nullptr) return; if (StaticComp->GetStaticMesh() != NewMesh) { StaticComp->SetStaticMesh(NewMesh); } } else if (USkeletalMeshComponent* SkelComp = Cast(MeshComp)) { USkeletalMesh* NewMesh = nullptr; UlxAssetLookup::LoadSkeletalMeshAsset(NewMesh, actor, MeshName, true); if (NewMesh == nullptr) return; if (SkelComp->GetSkeletalMeshAsset() != NewMesh) { SkelComp->SetSkeletalMeshAsset(NewMesh); } } else { UE_LOG(LogLuprexIntegration, Error, TEXT("AnimationStepApplyMesh: Actor %s has unsupported mesh component type"), *actor->GetName()); } }