Timeline: pre-animation baseline restore on Stop + display cache
Stop now rewinds animated properties to the value they had before the timeline first acted on them — not "first keyframe's value" as before. - New per-track baseline stored runtime-only in a TMap keyed by (ActorId, PropertyPath), so it survives track reordering and isn't persisted to JSON (avoids stale data in legacy scenarios). - Captured once: at track creation (first "+" on a fresh property) or at SetData time for tracks loaded from a scenario. Never re-captured, so scrub-then-Play doesn't shift the baseline to whatever the cursor happened to be parked on. - Stop writes the baselines back via ImportText, fires OnEditorPropertyChanged + RepNotify per track, then defers the actual StopSimulation by 100ms so the BehaviorTree has a grace period to see the restored values via OnRep / OnEditorPropertyChanged and transition to a neutral state before AI tick is cut. - Play cancels any in-flight deferred StopSimulation (Restart = Stop + Play keeps the simulation running across the handoff). - SetData and Clear reset the baseline map so loading a new scenario doesn't inherit the previous one's baselines. Also cache GetCurrentDisplayForProperty calls in RefreshDynamicPropertyValues: the property panel was invoking the BP interface every tick per visible combo. Now each row stores LastExportedValue + LastDisplayToShow and short-circuits if the property's exported text is unchanged — BP only runs on actual value transitions (seek, timeline keyframe, manual edit). Duplicated across MainWidget and MainWidget_Legacy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,23 @@ void UPS_Editor_TimelineSubsystem::Play()
|
|||||||
{
|
{
|
||||||
CurrentTime = 0.f;
|
CurrentTime = 0.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel any pending deferred StopSimulation (user hit Play again during the BT grace
|
||||||
|
// period after a previous Stop — don't cut the AI now).
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
if (PendingStopSimulationHandle.IsValid())
|
||||||
|
{
|
||||||
|
World->GetTimerManager().ClearTimer(PendingStopSimulationHandle);
|
||||||
|
PendingStopSimulationHandle.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure every track has a captured baseline. New tracks typically get their baseline
|
||||||
|
// at creation time (in AddOrUpdateKeyAtCurrentTime), tracks loaded from a scenario get
|
||||||
|
// theirs at SetData — this call is a safety net for anything that slipped through.
|
||||||
|
CaptureBaselinesForAllMissingTracks();
|
||||||
|
|
||||||
bIsPlaying = true;
|
bIsPlaying = true;
|
||||||
|
|
||||||
// Timeline and simulation are inherently linked: start simulation if it isn't running,
|
// Timeline and simulation are inherently linked: start simulation if it isn't running,
|
||||||
@@ -135,20 +152,116 @@ void UPS_Editor_TimelineSubsystem::Stop()
|
|||||||
LastAppliedValues.Empty();
|
LastAppliedValues.Empty();
|
||||||
LastAppliedValues.SetNum(Data.Tracks.Num());
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
|
|
||||||
// Full reset: stop simulation (restores pre-play transforms) and apply t=0 values so
|
// Restore pre-play baseline values onto the actors BEFORE stopping simulation, so the
|
||||||
// the scene visually matches the timeline start.
|
// BT sees the restored values (via OnEditorPropertyChanged / OnRep) and has a chance
|
||||||
|
// to transition to a neutral state during the grace period below.
|
||||||
|
RestoreBaseline();
|
||||||
|
|
||||||
|
// Defer the actual AI cut by a few frames so the BT can react to the restored values.
|
||||||
|
// If Play() is called again during this window, the timer is cancelled there.
|
||||||
if (APS_Editor_PlayerController* EditorPC = FindEditorPC(GetWorld()))
|
if (APS_Editor_PlayerController* EditorPC = FindEditorPC(GetWorld()))
|
||||||
{
|
{
|
||||||
if (EditorPC->IsSimulating())
|
if (EditorPC->IsSimulating())
|
||||||
{
|
{
|
||||||
EditorPC->StopSimulation();
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
TWeakObjectPtr<APS_Editor_PlayerController> WeakPC(EditorPC);
|
||||||
|
World->GetTimerManager().SetTimer(PendingStopSimulationHandle,
|
||||||
|
[WeakPC]()
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* PC = WeakPC.Get())
|
||||||
|
{
|
||||||
|
if (PC->IsSimulating())
|
||||||
|
{
|
||||||
|
PC->StopSimulation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0.1f, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorPC->StopSimulation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyValuesAtTime(CurrentTime);
|
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_TimelineSubsystem::CaptureBaselineForTrack(int32 TrackIndex)
|
||||||
|
{
|
||||||
|
if (!Data.Tracks.IsValidIndex(TrackIndex)) return;
|
||||||
|
const FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
||||||
|
const TPair<FGuid, FName> Key(Track.ActorId, Track.PropertyPath);
|
||||||
|
if (PrePlayBaseline.Contains(Key)) return; // already captured
|
||||||
|
|
||||||
|
AActor* Actor = FindActorById(Track.ActorId);
|
||||||
|
if (!Actor) return;
|
||||||
|
UObject* Target = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
|
ResolveTarget(Actor, Track.PropertyPath, Target, Prop);
|
||||||
|
if (!Target || !Prop) return;
|
||||||
|
|
||||||
|
FString Value;
|
||||||
|
Prop->ExportText_InContainer(0, Value, Target, Target, nullptr, PPF_None);
|
||||||
|
PrePlayBaseline.Add(Key, MoveTemp(Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_TimelineSubsystem::CaptureBaselinesForAllMissingTracks()
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < Data.Tracks.Num(); ++i)
|
||||||
|
{
|
||||||
|
CaptureBaselineForTrack(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
||||||
|
{
|
||||||
|
if (PrePlayBaseline.Num() == 0) return;
|
||||||
|
|
||||||
|
// Server-only write path (same guard as ApplyValuesAtTime).
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
if (World->GetNetMode() == NM_Client) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FPS_Editor_TimelineTrack& Track : Data.Tracks)
|
||||||
|
{
|
||||||
|
const TPair<FGuid, FName> Key(Track.ActorId, Track.PropertyPath);
|
||||||
|
const FString* BaselineValue = PrePlayBaseline.Find(Key);
|
||||||
|
if (!BaselineValue) continue;
|
||||||
|
|
||||||
|
AActor* Actor = FindActorById(Track.ActorId);
|
||||||
|
if (!Actor) continue;
|
||||||
|
UObject* Target = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
|
ResolveTarget(Actor, Track.PropertyPath, Target, Prop);
|
||||||
|
if (!Target || !Prop) continue;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(**BaselineValue, Target, Target, PPF_None);
|
||||||
|
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
||||||
|
{
|
||||||
|
AsComp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
Actor->ForceNetUpdate();
|
||||||
|
if (!Prop->RepNotifyFunc.IsNone())
|
||||||
|
{
|
||||||
|
if (UFunction* RepFunc = Target->FindFunction(Prop->RepNotifyFunc))
|
||||||
|
{
|
||||||
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
IPS_Editor_EditableInterface::Execute_OnEditorPropertyChanged(Actor, Track.PropertyPath.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clear the map — a subsequent Stop (or Restart) must still restore the same baseline.
|
||||||
|
// SetData/Clear are the only resetters.
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_TimelineSubsystem::Restart()
|
void UPS_Editor_TimelineSubsystem::Restart()
|
||||||
{
|
{
|
||||||
// Rewind + replay from the start. Done in two steps so simulation transforms are restored
|
// Rewind + replay from the start. Done in two steps so simulation transforms are restored
|
||||||
@@ -194,6 +307,13 @@ void UPS_Editor_TimelineSubsystem::SetData(const FPS_Editor_TimelineData& NewDat
|
|||||||
bIsPlaying = false;
|
bIsPlaying = false;
|
||||||
LastAppliedValues.Empty();
|
LastAppliedValues.Empty();
|
||||||
LastAppliedValues.SetNum(Data.Tracks.Num());
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
|
|
||||||
|
// Drop the previous scenario's baselines and capture fresh ones from the actors that
|
||||||
|
// were just spawned by the scene loader. Their current values are by definition the
|
||||||
|
// pre-animation state for this scenario's tracks.
|
||||||
|
PrePlayBaseline.Reset();
|
||||||
|
CaptureBaselinesForAllMissingTracks();
|
||||||
|
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +323,7 @@ void UPS_Editor_TimelineSubsystem::Clear()
|
|||||||
CurrentTime = 0.f;
|
CurrentTime = 0.f;
|
||||||
bIsPlaying = false;
|
bIsPlaying = false;
|
||||||
LastAppliedValues.Empty();
|
LastAppliedValues.Empty();
|
||||||
|
PrePlayBaseline.Reset();
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +369,10 @@ bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, co
|
|||||||
NewTrack.PropertyPath = PropertyPath;
|
NewTrack.PropertyPath = PropertyPath;
|
||||||
TrackIndex = Data.Tracks.Add(NewTrack);
|
TrackIndex = Data.Tracks.Add(NewTrack);
|
||||||
LastAppliedValues.SetNum(Data.Tracks.Num());
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
|
|
||||||
|
// Capture the pre-animation baseline NOW (before any key is added): the current actor
|
||||||
|
// value is the canonical "before timeline existed" state for this property.
|
||||||
|
CaptureBaselineForTrack(TrackIndex);
|
||||||
}
|
}
|
||||||
FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,17 @@ private:
|
|||||||
/** Cache of last applied exported-text value per track, used to avoid re-firing OnEditorPropertyChanged every frame when the value hasn't actually changed. */
|
/** Cache of last applied exported-text value per track, used to avoid re-firing OnEditorPropertyChanged every frame when the value hasn't actually changed. */
|
||||||
TArray<FString> LastAppliedValues;
|
TArray<FString> LastAppliedValues;
|
||||||
|
|
||||||
|
/** Runtime-only pre-animation baseline per track, keyed by (ActorId, PropertyPath) so it
|
||||||
|
* survives track re-ordering and isn't persisted to JSON (avoids stale data in legacy
|
||||||
|
* scenarios). Captured exactly once per track — at track creation (first keyframe) or
|
||||||
|
* at SetData time for tracks loaded from a scenario. Stop() reads from here; SetData
|
||||||
|
* and Clear reset it. */
|
||||||
|
TMap<TPair<FGuid, FName>, FString> PrePlayBaseline;
|
||||||
|
|
||||||
|
/** Deferred StopSimulation timer — set by Stop() so the BT gets a few frames to react
|
||||||
|
* to the restored baseline before AI tick is cut. Cancelled if Play() is called again. */
|
||||||
|
FTimerHandle PendingStopSimulationHandle;
|
||||||
|
|
||||||
/** Map of stable IDs for every actor that participates in the timeline / scene registry. */
|
/** Map of stable IDs for every actor that participates in the timeline / scene registry. */
|
||||||
TMap<TWeakObjectPtr<AActor>, FGuid> ActorIds;
|
TMap<TWeakObjectPtr<AActor>, FGuid> ActorIds;
|
||||||
|
|
||||||
@@ -160,4 +171,16 @@ private:
|
|||||||
|
|
||||||
/** Resolve the UObject target (actor or component) and the FProperty pointer. Nullable. */
|
/** Resolve the UObject target (actor or component) and the FProperty pointer. Nullable. */
|
||||||
static void ResolveTarget(AActor* Actor, const FName& PropertyPath, UObject*& OutTarget, FProperty*& OutProperty);
|
static void ResolveTarget(AActor* Actor, const FName& PropertyPath, UObject*& OutTarget, FProperty*& OutProperty);
|
||||||
|
|
||||||
|
/** Capture the current actor value as the baseline for the given track, unless already
|
||||||
|
* captured. Called at track creation and at SetData (for tracks loaded from JSON). */
|
||||||
|
void CaptureBaselineForTrack(int32 TrackIndex);
|
||||||
|
|
||||||
|
/** Walk every track and call CaptureBaselineForTrack if it doesn't already have an entry. */
|
||||||
|
void CaptureBaselinesForAllMissingTracks();
|
||||||
|
|
||||||
|
/** Write baseline values back onto the actors + fire OnEditorPropertyChanged / RepNotify
|
||||||
|
* per track. No-op for tracks without a captured baseline. Does NOT clear the baseline
|
||||||
|
* map so subsequent Stops still work. */
|
||||||
|
void RestoreBaseline();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2663,39 +2663,51 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
|||||||
FString CurrentVal;
|
FString CurrentVal;
|
||||||
Row.CachedProperty->ExportText_InContainer(0, CurrentVal, Target, Target, nullptr, PPF_None);
|
Row.CachedProperty->ExportText_InContainer(0, CurrentVal, Target, Target, nullptr, PPF_None);
|
||||||
|
|
||||||
// Resolve the DisplayName to show in the combo. Priority order:
|
// Cache hit: the exported value hasn't changed since the last refresh, so the
|
||||||
// 1. Ask the actor directly via GetCurrentDisplayForProperty — the user knows
|
// resolved DisplayName is identical too — skip the BP call + match loop entirely.
|
||||||
// their storage format best, so this is authoritative when overridden.
|
FString DisplayToShow;
|
||||||
// 2. Otherwise, fall back to matching CurrentVal / normalized-path against
|
if (!Row.LastExportedValue.IsEmpty() && CurrentVal == Row.LastExportedValue)
|
||||||
// the options returned by GetCustomOptionsWithValuesForProperty.
|
|
||||||
FString DisplayToShow = CurrentVal;
|
|
||||||
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
|
||||||
{
|
{
|
||||||
const FString FromActor = IPS_Editor_EditableInterface::Execute_GetCurrentDisplayForProperty(Actor, Row.Key);
|
DisplayToShow = Row.LastDisplayToShow;
|
||||||
if (!FromActor.IsEmpty())
|
|
||||||
{
|
|
||||||
DisplayToShow = FromActor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (DisplayToShow == CurrentVal && Row.CustomOptionDisplayToValue.Num() > 0)
|
else
|
||||||
{
|
{
|
||||||
// Normalize class-ref format `TypeName'/Path.X_C'` → `/Path.X_C`.
|
// Resolve the DisplayName to show in the combo. Priority order:
|
||||||
FString NormalizedCurrent = CurrentVal;
|
// 1. Ask the actor directly via GetCurrentDisplayForProperty — the user knows
|
||||||
int32 FirstQuote = INDEX_NONE;
|
// their storage format best, so this is authoritative when overridden.
|
||||||
int32 LastQuote = INDEX_NONE;
|
// 2. Otherwise, fall back to matching CurrentVal / normalized-path against
|
||||||
if (CurrentVal.FindChar(TEXT('\''), FirstQuote) && CurrentVal.FindLastChar(TEXT('\''), LastQuote)
|
// the options returned by GetCustomOptionsWithValuesForProperty.
|
||||||
&& LastQuote > FirstQuote + 1)
|
DisplayToShow = CurrentVal;
|
||||||
|
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
{
|
{
|
||||||
NormalizedCurrent = CurrentVal.Mid(FirstQuote + 1, LastQuote - FirstQuote - 1);
|
const FString FromActor = IPS_Editor_EditableInterface::Execute_GetCurrentDisplayForProperty(Actor, Row.Key);
|
||||||
}
|
if (!FromActor.IsEmpty())
|
||||||
for (const TPair<FString, FString>& P : Row.CustomOptionDisplayToValue)
|
|
||||||
{
|
|
||||||
if (!P.Value.IsEmpty() && (P.Value == CurrentVal || P.Value == NormalizedCurrent))
|
|
||||||
{
|
{
|
||||||
DisplayToShow = P.Key;
|
DisplayToShow = FromActor;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (DisplayToShow == CurrentVal && Row.CustomOptionDisplayToValue.Num() > 0)
|
||||||
|
{
|
||||||
|
// Normalize class-ref format `TypeName'/Path.X_C'` → `/Path.X_C`.
|
||||||
|
FString NormalizedCurrent = CurrentVal;
|
||||||
|
int32 FirstQuote = INDEX_NONE;
|
||||||
|
int32 LastQuote = INDEX_NONE;
|
||||||
|
if (CurrentVal.FindChar(TEXT('\''), FirstQuote) && CurrentVal.FindLastChar(TEXT('\''), LastQuote)
|
||||||
|
&& LastQuote > FirstQuote + 1)
|
||||||
|
{
|
||||||
|
NormalizedCurrent = CurrentVal.Mid(FirstQuote + 1, LastQuote - FirstQuote - 1);
|
||||||
|
}
|
||||||
|
for (const TPair<FString, FString>& P : Row.CustomOptionDisplayToValue)
|
||||||
|
{
|
||||||
|
if (!P.Value.IsEmpty() && (P.Value == CurrentVal || P.Value == NormalizedCurrent))
|
||||||
|
{
|
||||||
|
DisplayToShow = P.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row.LastExportedValue = CurrentVal;
|
||||||
|
Row.LastDisplayToShow = DisplayToShow;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != DisplayToShow)
|
if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != DisplayToShow)
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ struct FPS_Editor_PropertyRowWidgets
|
|||||||
* (display == value) or a native enum combo.
|
* (display == value) or a native enum combo.
|
||||||
*/
|
*/
|
||||||
TMap<FString, FString> CustomOptionDisplayToValue;
|
TMap<FString, FString> CustomOptionDisplayToValue;
|
||||||
|
|
||||||
|
/** Last exported text value applied to this row's combo; used to avoid recomputing
|
||||||
|
* the display name (and calling GetCurrentDisplayForProperty on the actor) each
|
||||||
|
* frame when the property hasn't actually changed. */
|
||||||
|
FString LastExportedValue;
|
||||||
|
FString LastDisplayToShow;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2322,39 +2322,54 @@ void UPS_Editor_MainWidget_Legacy::RefreshDynamicPropertyValues()
|
|||||||
FString CurrentVal;
|
FString CurrentVal;
|
||||||
Row.CachedProperty->ExportText_InContainer(0, CurrentVal, Target, Target, nullptr, PPF_None);
|
Row.CachedProperty->ExportText_InContainer(0, CurrentVal, Target, Target, nullptr, PPF_None);
|
||||||
|
|
||||||
// Resolve the DisplayName to show in the combo. Priority order:
|
// Cache hit: the exported value hasn't changed since the last refresh, so the
|
||||||
// 1. Ask the actor directly via GetCurrentDisplayForProperty — the user knows
|
// DisplayName we'd resolve is identical too. Skip the BP call + match loop
|
||||||
// their storage format best, so this is authoritative when overridden.
|
// entirely. This is what keeps GetCurrentDisplayForProperty off the hot tick
|
||||||
// 2. Otherwise, fall back to matching CurrentVal / normalized-path against
|
// path for props that don't move.
|
||||||
// the options returned by GetCustomOptionsWithValuesForProperty.
|
FString DisplayToShow;
|
||||||
FString DisplayToShow = CurrentVal;
|
if (!Row.LastExportedValue.IsEmpty() && CurrentVal == Row.LastExportedValue)
|
||||||
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
|
||||||
{
|
{
|
||||||
const FString FromActor = IPS_Editor_EditableInterface::Execute_GetCurrentDisplayForProperty(Actor, Row.Key);
|
DisplayToShow = Row.LastDisplayToShow;
|
||||||
if (!FromActor.IsEmpty())
|
|
||||||
{
|
|
||||||
DisplayToShow = FromActor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (DisplayToShow == CurrentVal && Row.CustomOptionDisplayToValue.Num() > 0)
|
else
|
||||||
{
|
{
|
||||||
// Normalize class-ref format `TypeName'/Path.X_C'` → `/Path.X_C`.
|
// Resolve the DisplayName to show in the combo. Priority order:
|
||||||
FString NormalizedCurrent = CurrentVal;
|
// 1. Ask the actor directly via GetCurrentDisplayForProperty — the user knows
|
||||||
int32 FirstQuote = INDEX_NONE;
|
// their storage format best, so this is authoritative when overridden.
|
||||||
int32 LastQuote = INDEX_NONE;
|
// 2. Otherwise, fall back to matching CurrentVal / normalized-path against
|
||||||
if (CurrentVal.FindChar(TEXT('\''), FirstQuote) && CurrentVal.FindLastChar(TEXT('\''), LastQuote)
|
// the options returned by GetCustomOptionsWithValuesForProperty.
|
||||||
&& LastQuote > FirstQuote + 1)
|
DisplayToShow = CurrentVal;
|
||||||
|
if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
{
|
{
|
||||||
NormalizedCurrent = CurrentVal.Mid(FirstQuote + 1, LastQuote - FirstQuote - 1);
|
const FString FromActor = IPS_Editor_EditableInterface::Execute_GetCurrentDisplayForProperty(Actor, Row.Key);
|
||||||
}
|
if (!FromActor.IsEmpty())
|
||||||
for (const TPair<FString, FString>& P : Row.CustomOptionDisplayToValue)
|
|
||||||
{
|
|
||||||
if (!P.Value.IsEmpty() && (P.Value == CurrentVal || P.Value == NormalizedCurrent))
|
|
||||||
{
|
{
|
||||||
DisplayToShow = P.Key;
|
DisplayToShow = FromActor;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (DisplayToShow == CurrentVal && Row.CustomOptionDisplayToValue.Num() > 0)
|
||||||
|
{
|
||||||
|
// Normalize class-ref format `TypeName'/Path.X_C'` → `/Path.X_C`.
|
||||||
|
FString NormalizedCurrent = CurrentVal;
|
||||||
|
int32 FirstQuote = INDEX_NONE;
|
||||||
|
int32 LastQuote = INDEX_NONE;
|
||||||
|
if (CurrentVal.FindChar(TEXT('\''), FirstQuote) && CurrentVal.FindLastChar(TEXT('\''), LastQuote)
|
||||||
|
&& LastQuote > FirstQuote + 1)
|
||||||
|
{
|
||||||
|
NormalizedCurrent = CurrentVal.Mid(FirstQuote + 1, LastQuote - FirstQuote - 1);
|
||||||
|
}
|
||||||
|
for (const TPair<FString, FString>& P : Row.CustomOptionDisplayToValue)
|
||||||
|
{
|
||||||
|
if (!P.Value.IsEmpty() && (P.Value == CurrentVal || P.Value == NormalizedCurrent))
|
||||||
|
{
|
||||||
|
DisplayToShow = P.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remember what we resolved so the next frame can short-circuit.
|
||||||
|
Row.LastExportedValue = CurrentVal;
|
||||||
|
Row.LastDisplayToShow = DisplayToShow;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != DisplayToShow)
|
if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != DisplayToShow)
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ struct FPS_Editor_PropertyRowWidgetsLegacy
|
|||||||
|
|
||||||
/** DisplayName -> stored Value mapping from GetCustomOptionsWithValuesForProperty. */
|
/** DisplayName -> stored Value mapping from GetCustomOptionsWithValuesForProperty. */
|
||||||
TMap<FString, FString> CustomOptionDisplayToValue;
|
TMap<FString, FString> CustomOptionDisplayToValue;
|
||||||
|
|
||||||
|
/** Cache to avoid calling GetCurrentDisplayForProperty + the match loop every frame
|
||||||
|
* when the property hasn't changed. */
|
||||||
|
FString LastExportedValue;
|
||||||
|
FString LastDisplayToShow;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user