#include "AnimQueue.h" FlxAnimationStep::FlxAnimationStep(uint64 hash, std::string_view body) { Hash = hash; Body.SetNum(body.size()); memcpy(Body.GetData(), body.data(), body.size()); } static bool ClearProperties(const FString& prefix, UObject* obj) { UClass* uclass = obj->GetClass(); if (prefix.IsEmpty()) { return false; } FName prefixlo(prefix); FName prefixhi(prefix + TEXT("\xFFFF")); for (TFieldIterator It(uclass); It; ++It) { FProperty* fprop = *It; 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); } } return true; } static bool SetProperty(const FString& name, UObject* obj, const FlxAnimationField& field) { UClass* uclass = obj->GetClass(); FName nname(name); switch (field.Type) { case ElxAnimValueType::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 ElxAnimValueType::NUMBER: { FDoubleProperty* fprop = FindFProperty(uclass, nname); if (fprop == nullptr) return false; double* pptr = fprop->ContainerPtrToValuePtr(obj); *pptr = field.X; return true; } case ElxAnimValueType::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 ElxAnimValueType::XYZ: { 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; } bool FlxAnimationStep::Unpack(const FString& prefix, UObject* into, bool preclear) const { UClass* uclass = into->GetClass(); std::string_view body((const char*)(Body.GetData()), Body.Num()); FlxAnimationStepDecoder decoder(body); bool ok = true; if (preclear) { ok &= ClearProperties(prefix, into); } while (!decoder.AtEOF()) { FlxAnimationField field = decoder.ReadField(); FString sname(field.Name.size(), (const UTF8CHAR*)field.Name.data()); ok &= SetProperty(prefix + sname, into, field); } return ok; } FString UlxAnimationStepLibrary::AnimationStepDebugString(const FlxAnimationStep& step) { std::string_view body((const char*)(step.Body.GetData()), step.Body.Num()); return FlxAnimationStepDecoder::DebugString(step.Hash, body); } void UlxAnimationStepLibrary::UnpackAnimationStep(const FlxAnimationStep& step, const FString& prefix, UObject* into) { step.Unpack(prefix, into, true); }; static FlxAnimationField FindAnimationField(const FlxAnimationStep& step, const FString& name) { std::string_view body((const char*)(step.Body.GetData()), step.Body.Num()); FTCHARToUTF8 utf8name(name); std::string_view uname(utf8name.Get(), utf8name.Length()); FlxAnimationStepDecoder decoder(body); FlxAnimationField result; while (!decoder.AtEOF()) { result = decoder.ReadField(); if (result.Name == uname) { return result; } } result.Type = ElxAnimValueType::INVALID; return result; } FVector UlxAnimationStepLibrary::AnimationStepGetVector(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != ElxAnimValueType::XYZ) 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 != ElxAnimValueType::NUMBER) return 0.0; return field.X; } #pragma optimize("", off) FString UlxAnimationStepLibrary::AnimationStepGetString(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != ElxAnimValueType::STRING) return TEXT(""); return FString(field.S.size(), (const UTF8CHAR*)(field.S.data())); } bool UlxAnimationStepLibrary::AnimationStepGetBool(const FlxAnimationStep& step, const FString& name) { FlxAnimationField field = FindAnimationField(step, name); if (field.Type != ElxAnimValueType::BOOLEAN) return false; return field.X == 1.0; } FlxAnimationStepView FlxAnimQueueDecoder::ReadStep() { FlxAnimationStepView result; result.Hash = Decoder.read_uint64(); result.Body = Decoder.read_string_view(); return result; } FlxAnimationField FlxAnimationStepDecoder::ReadField() { FlxAnimationField result; result.Name = Decoder.read_string_view(); result.Persistent = Decoder.read_bool(); result.Type = (ElxAnimValueType)Decoder.read_uint8(); switch (result.Type) { case ElxAnimValueType::STRING: { result.S = Decoder.read_string_view(); break; } case ElxAnimValueType::NUMBER: { result.X = Decoder.read_double(); break; } case ElxAnimValueType::BOOLEAN: { result.X = Decoder.read_bool() ? 1.0 : 0.0; break; } case ElxAnimValueType::XYZ: { result.X = Decoder.read_double(); result.Y = Decoder.read_double(); result.Z = Decoder.read_double(); break; } default: { Decoder.set_at_eof(); result.Type = ElxAnimValueType::BOOLEAN; result.X = 0; break; } } return result; } FString FlxAnimationStepDecoder::DebugString(uint64 hash, std::string_view body) { FString result; FlxAnimationStepDecoder decoder(body); result.Appendf(TEXT("Hash=%016llx"), hash); 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 ElxAnimValueType::STRING: result.Append(FString(field.S.size(), (const UTF8CHAR*)field.S.data())); break; case ElxAnimValueType::NUMBER: result.Appendf(TEXT("%lf"), field.X); break; case ElxAnimValueType::BOOLEAN: result.Append((field.X) == 1.0 ? TEXT("true") : TEXT("false")); break; case ElxAnimValueType::XYZ: result.Appendf(TEXT("%lf,%lf,%lf"), field.X, field.Y, field.Z); break; } } return result; } FString FlxAnimQueueDecoder::DebugString(std::string_view queue) { FString result; FlxAnimQueueDecoder decoder(queue); while (!decoder.AtEOF()) { FlxAnimationStepView step = decoder.ReadStep(); FString stepdebug = FlxAnimationStepDecoder::DebugString(step.Hash, step.Body); result.Appendf(TEXT("%s\n"), *stepdebug); } return result; } FlxAnimTracker::FlxAnimTracker() { Clear(); } void FlxAnimTracker::Clear() { AQ.Empty(); FirstSeqno = 0; HashToSeqno.Empty(); UnstartedSeqno = 0; PlaybackMode = ElxAnimationMode::INVALID; AbortedHashes.Empty(); } void FlxAnimTracker::Update(std::string_view encqueue) { // Check for an exact match. If the most recent entry // in encqueue has the same hash as the most recent // entry in AQ, then that's an exact match. Or, // if both are empty, that's also an exact match. // In case of exact match, abort early, there's no // further work needed. // if (encqueue.empty()) { if (AQ.IsEmpty()) { return; } } else { if (!AQ.IsEmpty()) { FlxStringDecoder qdecoder(encqueue); uint64 hash = qdecoder.read_uint64(); if (hash == AQ.Last().Hash) { return; } } } // 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. // FlxAnimQueueDecoder decoder(encqueue); TArray newsteps; int32 matchingseqno = -1; while (!decoder.AtEOF()) { FlxAnimationStepView step = decoder.ReadStep(); int32* stepseq = HashToSeqno.Find(step.Hash); if (stepseq == nullptr) { newsteps.Emplace(step); } else { matchingseqno = *stepseq; break; } } // Remove all animations after the most recent matching // record. If we remove a 'started' animation, add that // animation to the list of aborted animations. // int32 nremove = (FirstSeqno + AQ.Num()) - (matchingseqno + 1); check((nremove >= 0) && (nremove <= AQ.Num())); for (int32 i = 0; i < nremove; i++) { uint64 lasthash = AQ.Last().Hash; int32 seqno = FirstSeqno + AQ.Num() - 1; HashToSeqno.Remove(lasthash); if (seqno < UnstartedSeqno) { AbortedHashes.Emplace(lasthash); } AQ.PopLast(); } // If we aborted a 'started' animation, we have to fix // up the Unstarted animation pointer. // // Note: this could leave the Unstarted pointer at // seqno less than FirstSeqno. We will fix that state // of affairs up later. // if (UnstartedSeqno > (FirstSeqno + AQ.Num())) { UnstartedSeqno = matchingseqno; PlaybackMode = ElxAnimationMode::BlendToFinal; } // Transfer the new animations onto the queue. // while (!newsteps.IsEmpty()) { FlxAnimationStepView step = newsteps.Pop(); int32 seqno = FirstSeqno + AQ.Num(); AQ.EmplaceLast(step.Hash, step.Body); HashToSeqno.Emplace(step.Hash, seqno); } // If there are too many animations in AQ, discard // any very old ones. // if (AQ.Num() > 10) { int ndiscard = AQ.Num() - 10; for (int i = 0; i < ndiscard; i++) { uint64 hash = AQ.First().Hash; HashToSeqno.Remove(hash); AQ.PopFirst(); FirstSeqno += 1; } } // If UnstartedSeqno is before the live range, // then the whole queue has to be replayed. Don't // do that: just fast skip to the end. // if (UnstartedSeqno <= FirstSeqno) { if (AQ.Num() == 0) { UnstartedSeqno = FirstSeqno; } else { UnstartedSeqno = FirstSeqno + AQ.Num() - 1; } PlaybackMode = ElxAnimationMode::WarpToFinal; } } TArray FlxAnimTracker::GetAborted() { TArray result; result = std::move(AbortedHashes); AbortedHashes.Empty(); return result; } ElxAnimationMode FlxAnimTracker::GetNextStep(FlxAnimationStep &step) { int offset = UnstartedSeqno - FirstSeqno; if (offset < AQ.Num()) { step = AQ[offset]; return PlaybackMode; } else { step.Hash = 0; step.Body.Empty(); return ElxAnimationMode::INVALID; } } void FlxAnimTracker::StartedStep(uint64 hash) { int offset = UnstartedSeqno - FirstSeqno; check(offset < AQ.Num()); check(AQ[offset].Hash == hash); UnstartedSeqno += 1; PlaybackMode = ElxAnimationMode::StartAnimation; }