// Copyright Epic Games, Inc. All Rights Reserved. #include "EnvironmentQuery/EnvQueryTest.h" #include "EnvironmentQuery/Contexts/EnvQueryContext_Item.h" #include "EnvironmentQuery/Items/EnvQueryItemType_ActorBase.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(EnvQueryTest) #define LOCTEXT_NAMESPACE "EnvQueryGenerator" UEnvQueryTest::UEnvQueryTest(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { TestPurpose = EEnvTestPurpose::FilterAndScore; Cost = EEnvTestCost::Low; FilterType = EEnvTestFilterType::Range; ScoringEquation = EEnvTestScoreEquation::Linear; ClampMinType = EEnvQueryTestClamping::None; ClampMaxType = EEnvQueryTestClamping::None; BoolValue.DefaultValue = true; ScoringFactor.DefaultValue = 1.0f; NormalizationType = EEQSNormalizationType::Absolute; bWorkOnFloatValues = true; } void UEnvQueryTest::NormalizeItemScores(FEnvQueryInstance& QueryInstance) { UObject* QueryOwner = QueryInstance.Owner.Get(); if (QueryOwner == nullptr || !IsScoring()) { return; } ScoringFactor.BindData(QueryOwner, QueryInstance.QueryID); const float ScoringFactorValue = ScoringFactor.GetValue(); float MinScore = (NormalizationType == EEQSNormalizationType::Absolute) ? 0 : BIG_NUMBER; float MaxScore = -BIG_NUMBER; if (ClampMinType == EEnvQueryTestClamping::FilterThreshold) { FloatValueMin.BindData(QueryOwner, QueryInstance.QueryID); MinScore = FloatValueMin.GetValue(); } else if (ClampMinType == EEnvQueryTestClamping::SpecifiedValue) { ScoreClampMin.BindData(QueryOwner, QueryInstance.QueryID); MinScore = ScoreClampMin.GetValue(); } if (ClampMaxType == EEnvQueryTestClamping::FilterThreshold) { FloatValueMax.BindData(QueryOwner, QueryInstance.QueryID); MaxScore = FloatValueMax.GetValue(); } else if (ClampMaxType == EEnvQueryTestClamping::SpecifiedValue) { ScoreClampMax.BindData(QueryOwner, QueryInstance.QueryID); MaxScore = ScoreClampMax.GetValue(); } FEnvQueryItemDetails* DetailInfo = QueryInstance.ItemDetails.GetData(); if ((ClampMinType == EEnvQueryTestClamping::None) || (ClampMaxType == EEnvQueryTestClamping::None) ) { for (int32 ItemIndex = 0; ItemIndex < QueryInstance.Items.Num(); ItemIndex++, DetailInfo++) { if (!QueryInstance.Items[ItemIndex].IsValid()) { continue; } float TestValue = DetailInfo->TestResults[QueryInstance.CurrentTest]; if (TestValue != UEnvQueryTypes::SkippedItemValue) { if (ClampMinType == EEnvQueryTestClamping::None) { MinScore = FMath::Min(MinScore, TestValue); } if (ClampMaxType == EEnvQueryTestClamping::None) { MaxScore = FMath::Max(MaxScore, TestValue); } } } } DetailInfo = QueryInstance.ItemDetails.GetData(); if (MinScore != MaxScore) { if (bDefineReferenceValue) { ReferenceValue.BindData(QueryOwner, QueryInstance.QueryID); } const float TargetScore = bDefineReferenceValue ? ReferenceValue.GetValue() : MaxScore; const float ValueSpan = FMath::Max(FMath::Abs(TargetScore - MinScore), FMath::Abs(TargetScore - MaxScore)); const float AbsoluteWeight = FMath::Abs(ScoringFactorValue); for (int32 ItemIndex = 0; ItemIndex < QueryInstance.ItemDetails.Num(); ItemIndex++, DetailInfo++) { if (QueryInstance.Items[ItemIndex].IsValid() == false) { continue; } float WeightedScore = 0.0f; float& TestValue = DetailInfo->TestResults[QueryInstance.CurrentTest]; if (TestValue != UEnvQueryTypes::SkippedItemValue) { const float ClampedScore = FMath::Clamp(TestValue, MinScore, MaxScore); const float NormalizedScore = (ScoringFactorValue >= 0) ? (1.f - (FMath::Abs(TargetScore - ClampedScore) / ValueSpan)) : (FMath::Abs(TargetScore - ClampedScore) / ValueSpan); switch (ScoringEquation) { case EEnvTestScoreEquation::Linear: WeightedScore = AbsoluteWeight * NormalizedScore; break; case EEnvTestScoreEquation::InverseLinear: { // For now, we're avoiding having a separate flag for flipping the direction of the curve // because we don't have usage cases yet and want to avoid too complex UI. If we decide // to add that flag later, we'll need to remove this option, since it should just be "mirror // curve" plus "Linear". float InverseNormalizedScore = (1.0f - NormalizedScore); WeightedScore = AbsoluteWeight * InverseNormalizedScore; break; } case EEnvTestScoreEquation::Square: WeightedScore = AbsoluteWeight * (NormalizedScore * NormalizedScore); break; case EEnvTestScoreEquation::SquareRoot: WeightedScore = AbsoluteWeight * FMath::Sqrt(NormalizedScore); break; case EEnvTestScoreEquation::Constant: // I know, it's not "constant". It's "Constant, or zero". The tooltip should explain that. WeightedScore = (NormalizedScore > 0) ? AbsoluteWeight : 0.0f; break; default: break; } } else { // Do NOT clear TestValue to 0, because the SkippedItemValue is used to display "SKIP" when debugging. // TestValue = 0.0f; WeightedScore = 0.0f; } #if USE_EQS_DEBUGGER DetailInfo->TestWeightedScores[QueryInstance.CurrentTest] = WeightedScore; #endif QueryInstance.Items[ItemIndex].Score += WeightedScore; } } } bool UEnvQueryTest::IsContextPerItem(TSubclassOf CheckContext) const { return CheckContext == UEnvQueryContext_Item::StaticClass(); } FVector UEnvQueryTest::GetItemLocation(FEnvQueryInstance& QueryInstance, int32 ItemIndex) const { return QueryInstance.ItemTypeVectorCDO ? QueryInstance.ItemTypeVectorCDO->GetItemLocation(QueryInstance.RawData.GetData() + QueryInstance.Items[ItemIndex].DataOffset) : FVector::ZeroVector; } FRotator UEnvQueryTest::GetItemRotation(FEnvQueryInstance& QueryInstance, int32 ItemIndex) const { return QueryInstance.ItemTypeVectorCDO ? QueryInstance.ItemTypeVectorCDO->GetItemRotation(QueryInstance.RawData.GetData() + QueryInstance.Items[ItemIndex].DataOffset) : FRotator::ZeroRotator; } AActor* UEnvQueryTest::GetItemActor(FEnvQueryInstance& QueryInstance, int32 ItemIndex) const { return QueryInstance.ItemTypeActorCDO ? QueryInstance.ItemTypeActorCDO->GetActor(QueryInstance.RawData.GetData() + QueryInstance.Items[ItemIndex].DataOffset) : NULL; } void UEnvQueryTest::PostLoad() { Super::PostLoad(); if (VerNum < EnvQueryTestVersion::ReferenceValueFix && bDefineReferenceValue) { // if someone was using the ReferenceValue the fix will reverse their score. We can fix it by flipping the // ScoringFactor. Unfortunately we won't be able to to that if the ScoringFactor is actually bound to some // provider - in that case all we can do is to spit out a warning. UWorld* World = GetWorld(); if (World == nullptr) { // we want this logic to run only for assets, not the instantiated queries that should pull the following change in automatically if (ScoringFactor.IsDynamic() == false) { ScoringFactor.DefaultValue *= -1; UE_LOG(LogEQS, Verbose, TEXT("%s using a ScoringFactor. Due to a bug-fix the effect will be flipped so the\ value\'s sign has been flipped for this EQS Test asset instance.") , *GetPathName()); } else if (ScoringEquation == EEnvTestScoreEquation::Linear) { // we can still address this by flipping the ScoringEquation to ScoringEquation = EEnvTestScoreEquation::InverseLinear; UE_LOG(LogEQS, Verbose, TEXT("%s using a AIDataProvider for ScoringFactor. Due to a bug-fix the effect will be flipped so ScoringEquation has been automatically flipped from Linear to InverseLinear for this EQS Test instance.") , *GetPathName()); } else if (ScoringEquation == EEnvTestScoreEquation::InverseLinear) { // we can still address this by flipping the ScoringEquation to ScoringEquation = EEnvTestScoreEquation::Linear; UE_LOG(LogEQS, Verbose, TEXT("%s using a AIDataProvider for ScoringFactor. Due to a bug-fix the effect will be flipped so ScoringEquation has been automatically flipped from InverseLinear to Linear for this EQS Test instance.") , *GetPathName()); } else { UE_LOG(LogEQS, Warning, TEXT("%s using a AIDataProvider for ScoringFactor. Due to a bug-fix the effect will be flipped and an automated change could not be performed. Please address manually.") , *GetPathName()); } if (MarkPackageDirty() == false) { UE_LOG(LogEQS, Warning, TEXT("%s unable to mark package dirty - please resave manually or use a commandlet.") , *GetPathName()); } } } UpdateNodeVersion(); #if WITH_EDITOR UpdatePreviewData(); #endif // WITH_EDITOR } void UEnvQueryTest::UpdateNodeVersion() { VerNum = EnvQueryTestVersion::Latest; } FText UEnvQueryTest::DescribeFloatTestParams() const { FText FilterDesc; if (IsFiltering()) { switch (FilterType) { case EEnvTestFilterType::Minimum: FilterDesc = FText::Format(LOCTEXT("FilterAtLeast", "at least {0}"), FText::FromString(FloatValueMin.ToString())); break; case EEnvTestFilterType::Maximum: FilterDesc = FText::Format(LOCTEXT("FilterUpTo", "up to {0}"), FText::FromString(FloatValueMax.ToString())); break; case EEnvTestFilterType::Range: FilterDesc = FText::Format(LOCTEXT("FilterBetween", "between {0} and {1}"), FText::FromString(FloatValueMin.ToString()), FText::FromString(FloatValueMax.ToString())); break; default: break; } } FNumberFormattingOptions NumberFormattingOptions; NumberFormattingOptions.MaximumFractionalDigits = 2; FText ScoreDesc; if (!IsScoring()) { ScoreDesc = LOCTEXT("DontScore", "don't score"); } else if (ScoringEquation == EEnvTestScoreEquation::Constant) { FText FactorDesc = ScoringFactor.IsDynamic() ? FText::FromString(ScoringFactor.ToString()) : FText::Format(FText::FromString("x{0}"), FText::AsNumber(FMath::Abs(ScoringFactor.DefaultValue), &NumberFormattingOptions)); ScoreDesc = FText::Format(FText::FromString("{0} [{1}]"), LOCTEXT("ScoreConstant", "constant score"), FactorDesc); } else if (ScoringFactor.IsDynamic()) { ScoreDesc = FText::Format(FText::FromString("{0}: {1}"), LOCTEXT("ScoreFactor", "score factor"), FText::FromString(ScoringFactor.ToString())); } else { const bool bScalesUp = (ScoringEquation == EEnvTestScoreEquation::InverseLinear) ? (ScoringFactor.DefaultValue < 0) : (ScoringFactor.DefaultValue > 0); const FText ScoreSignDesc = bScalesUp ? LOCTEXT("Greater", "greater") : LOCTEXT("Lesser", "lesser"); const FText ScoreValueDesc = FText::AsNumber(FMath::Abs(ScoringFactor.DefaultValue), &NumberFormattingOptions); ScoreDesc = FText::Format(FText::FromString("{0} {1} [x{2}]"), LOCTEXT("ScorePrefer", "prefer"), ScoreSignDesc, ScoreValueDesc); } return FilterDesc.IsEmpty() ? ScoreDesc : FText::Format(FText::FromString("{0}, {1}"), FilterDesc, ScoreDesc); } FText UEnvQueryTest::DescribeBoolTestParams(const FString& ConditionDesc) const { FText FilterDesc; if (IsFiltering() && FilterType == EEnvTestFilterType::Match) { FilterDesc = BoolValue.IsDynamic() ? FText::Format(FText::FromString("{0} {1}: {2}"), LOCTEXT("FilterRequire", "require"), FText::FromString(ConditionDesc), FText::FromString(BoolValue.ToString())) : FText::Format(FText::FromString("{0} {1}{2}"), LOCTEXT("FilterRequire", "require"), BoolValue.DefaultValue ? FText::GetEmpty() : LOCTEXT("NotWithSpace", "not "), FText::FromString(ConditionDesc)); } FNumberFormattingOptions NumberFormattingOptions; NumberFormattingOptions.MaximumFractionalDigits = 2; FText ScoreDesc; if (!IsScoring()) { ScoreDesc = LOCTEXT("DontScore", "don't score"); } else if (ScoringEquation == EEnvTestScoreEquation::Constant) { FText FactorDesc = ScoringFactor.IsDynamic() ? FText::FromString(ScoringFactor.ToString()) : FText::Format(FText::FromString("x{0}"), FText::AsNumber(FMath::Abs(ScoringFactor.DefaultValue), &NumberFormattingOptions)); ScoreDesc = FText::Format(FText::FromString("{0} [{1}]"), LOCTEXT("ScoreConstant", "constant score"), FactorDesc); } else if (ScoringFactor.IsDynamic()) { ScoreDesc = FText::Format(FText::FromString("{0}: {1}"), LOCTEXT("ScoreFactor", "score factor"), FText::FromString(ScoringFactor.ToString())); } else { FText ScoreSignDesc = (ScoringFactor.DefaultValue > 0) ? FText::GetEmpty() : LOCTEXT("NotWithSpace", "not "); FText ScoreValueDesc = FText::AsNumber(FMath::Abs(ScoringFactor.DefaultValue), &NumberFormattingOptions); ScoreDesc = FText::Format(FText::FromString("{0} {1}{2} [x{3}]"), LOCTEXT("ScorePrefer", "prefer"), ScoreSignDesc, FText::FromString(ConditionDesc), ScoreValueDesc); } return FilterDesc.IsEmpty() ? ScoreDesc : FText::Format(FText::FromString("{0}, {1}"), FilterDesc, ScoreDesc); } void UEnvQueryTest::SetWorkOnFloatValues(bool bWorkOnFloats) { bWorkOnFloatValues = bWorkOnFloats; // Make sure FilterType is set to a valid value. if (bWorkOnFloats) { if (FilterType == EEnvTestFilterType::Match) { FilterType = EEnvTestFilterType::Range; } } else { if (FilterType != EEnvTestFilterType::Match) { FilterType = EEnvTestFilterType::Match; } // Scoring MUST be Constant for boolean tests. ScoringEquation = EEnvTestScoreEquation::Constant; } UpdatePreviewData(); } #if WITH_EDITOR void UEnvQueryTest::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); if (PropertyChangedEvent.MemberProperty) { const FName MemberPropName = PropertyChangedEvent.MemberProperty->GetFName(); if (MemberPropName == GET_MEMBER_NAME_CHECKED(UEnvQueryTest, TestPurpose) || MemberPropName == GET_MEMBER_NAME_CHECKED(UEnvQueryTest, FilterType) || MemberPropName == GET_MEMBER_NAME_CHECKED(UEnvQueryTest, ClampMaxType) || MemberPropName == GET_MEMBER_NAME_CHECKED(UEnvQueryTest, ClampMinType) || MemberPropName == GET_MEMBER_NAME_CHECKED(UEnvQueryTest, ScoringEquation) || MemberPropName == GET_MEMBER_NAME_CHECKED(UEnvQueryTest, ScoringFactor)) { UpdatePreviewData(); } } } #endif void UEnvQueryTest::UpdatePreviewData() { #if WITH_EDITORONLY_DATA && WITH_EDITOR PreviewData.Samples.Reset(FEnvQueryTestScoringPreview::DefaultSamplesCount); switch (ScoringEquation) { case EEnvTestScoreEquation::Linear: { for (int32 SampleIndex = 0; SampleIndex < FEnvQueryTestScoringPreview::DefaultSamplesCount; ++SampleIndex) { PreviewData.Samples.Add(static_cast(SampleIndex) / (FEnvQueryTestScoringPreview::DefaultSamplesCount - 1)); } } break; case EEnvTestScoreEquation::Square: { for (int32 SampleIndex = 0; SampleIndex < FEnvQueryTestScoringPreview::DefaultSamplesCount; ++SampleIndex) { PreviewData.Samples.Add(FMath::Square(static_cast(SampleIndex) / (FEnvQueryTestScoringPreview::DefaultSamplesCount - 1))); } } break; case EEnvTestScoreEquation::InverseLinear: { for (int32 SampleIndex = 0; SampleIndex < FEnvQueryTestScoringPreview::DefaultSamplesCount; ++SampleIndex) { PreviewData.Samples.Add(1.f - static_cast(SampleIndex) / (FEnvQueryTestScoringPreview::DefaultSamplesCount - 1)); } } break; case EEnvTestScoreEquation::SquareRoot: { for (int32 SampleIndex = 0; SampleIndex < FEnvQueryTestScoringPreview::DefaultSamplesCount; ++SampleIndex) { PreviewData.Samples.Add(FMath::Sqrt(static_cast(SampleIndex) / (FEnvQueryTestScoringPreview::DefaultSamplesCount - 1))); } } break; case EEnvTestScoreEquation::Constant: { for (int32 SampleIndex = 0; SampleIndex < FEnvQueryTestScoringPreview::DefaultSamplesCount; ++SampleIndex) { PreviewData.Samples.Add(0.5f); } } break; default: checkNoEntry(); break; } const bool bInversed = ScoringFactor.GetValue() < 0.0f; if (bInversed) { for (float& Sample : PreviewData.Samples) { Sample = 1.f - Sample; } } PreviewData.bShowClampMin = (ClampMinType != EEnvQueryTestClamping::None); PreviewData.bShowClampMax = (ClampMaxType != EEnvQueryTestClamping::None); const bool bCanFilter = (TestPurpose != EEnvTestPurpose::Score); PreviewData.bShowFilterHigh = ((FilterType == EEnvTestFilterType::Maximum) || (FilterType == EEnvTestFilterType::Range)) && bCanFilter; PreviewData.bShowFilterLow = ((FilterType == EEnvTestFilterType::Minimum) || (FilterType == EEnvTestFilterType::Range)) && bCanFilter; PreviewData.FilterLow = 0.2f; PreviewData.FilterHigh = 0.8f; PreviewData.ClampMin = (ClampMinType == EEnvQueryTestClamping::FilterThreshold) ? PreviewData.FilterLow : 0.3f; PreviewData.ClampMax = (ClampMaxType == EEnvQueryTestClamping::FilterThreshold) ? PreviewData.FilterHigh : 0.7f; if (PreviewData.bShowClampMin) { const int32 FixedIdx = FMath::RoundToInt(PreviewData.ClampMin * (PreviewData.Samples.Num() - 1)); for (int32 Idx = 0; Idx < FixedIdx; Idx++) { PreviewData.Samples[Idx] = PreviewData.Samples[FixedIdx]; } } if (PreviewData.bShowClampMax) { const int32 FixedIdx = FMath::RoundToInt(PreviewData.ClampMax * (PreviewData.Samples.Num() - 1)); for (int32 Idx = FixedIdx + 1; Idx < PreviewData.Samples.Num(); Idx++) { PreviewData.Samples[Idx] = PreviewData.Samples[FixedIdx]; } } #endif } #undef LOCTEXT_NAMESPACE