SComposerBehaviorWidget.cpp
1 // Copyright (C) 2025 Radaway Software LLC. All Rights Reserved. 2 3 #include "Slate/SComposerBehaviorWidget.h" 4 #include "ISinglePropertyView.h" 5 #include "Editor.h" 6 #include "Widgets/SBoxPanel.h" 7 #include "SlateCore.h" 8 #include "UObject/Class.h" 9 #include "Core/RpaiPlannerBase.h" 10 #include "Core/RpaiReasonerBase.h" 11 #include "Core/RpaiGoalBase.h" 12 #include "Core/RpaiActionBase.h" 13 14 #define LOCTEXT_NAMESPACE "ReasonablePlanningAIEditor" 15 16 // structure for debug views of data tables 17 struct FRpaiDiagnosticsViewData 18 { 19 TArray<FText> ArbitraryRowData; 20 }; 21 22 BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION 23 void SComposerBehaviorWidget::Construct(const FArguments& InArgs) 24 { 25 ComposerBehavior = InArgs._ComposerBehavior; 26 bIsExperimenting = false; 27 LastPlanResult = ERpaiPlannerResult::Invalid; 28 29 if (!ComposerBehavior.IsSet()) 30 { 31 return; 32 } 33 34 FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor"); 35 36 FDetailsViewArgs DetailsViewArgs; 37 DetailsViewArgs.bAllowSearch = false; 38 DetailsViewArgs.bShowPropertyMatrixButton = false; 39 DetailsViewArgs.bShowOptions = false; 40 41 TSharedRef<IDetailsView> TestStartingStateDetailViewRef = PropertyEditorModule.CreateDetailView(DetailsViewArgs); 42 TestStartingStateDetailView = TestStartingStateDetailViewRef; 43 44 NotifyStateTypePropertyChanged(); 45 NotifyGoalsPropertyChanged(); 46 47 ChildSlot 48 [ 49 SNew(SVerticalBox) 50 + SVerticalBox::Slot() 51 [ 52 SNew(STextBlock) 53 .Visibility(this, &SComposerBehaviorWidget::GetSetStateMessageVisibility) 54 .Text(LOCTEXT("ComposerWidgetBehavior_AssignClass", "Assign a type to ConstructedStateType to begin.")) 55 ] 56 + SVerticalBox::Slot() 57 .AutoHeight() 58 [ 59 SNew(STextBlock) 60 .Visibility(this, &SComposerBehaviorWidget::GetSetStateMessageVisibility) 61 .Text(LOCTEXT("ComposerWidgetBehavior_Summary", "Modify the state details below to run experiments by using the available button actions.")) 62 ] 63 + SVerticalBox::Slot() 64 .AutoHeight() 65 [ 66 SNew(SHorizontalBox) 67 + SHorizontalBox::Slot() 68 .AutoWidth() 69 [ 70 SNew(SButton) 71 .ToolTipText(LOCTEXT("ComposerWidgetBehavior_FullEvaluateTip", "Using the given input state below, preview how actions will be evaluated. Additionally shows the goal weights and distances after the action is performed.")) 72 .Text(LOCTEXT("ComposerWidgetBehavior_ActionOnlyEvaluate", "Evaluate Actions")) 73 .OnClicked(this, &SComposerBehaviorWidget::OnEvaluateActions) 74 .IsEnabled(this, &SComposerBehaviorWidget::IsEvaluateButtonEnabled) 75 ] 76 + SHorizontalBox::Slot() 77 .AutoWidth() 78 [ 79 SNew(SButton) 80 .ToolTipText(LOCTEXT("ComposerWidgetBehavior_FullEvaluateTip", "Using the given input state below, preview how goals will be evaluated.")) 81 .Text(LOCTEXT("ComposerWidgetBehavior_GoalOnlyEvaluate", "Evaluate Goals")) 82 .OnClicked(this, &SComposerBehaviorWidget::OnEvaluateGoals) 83 .IsEnabled(this, &SComposerBehaviorWidget::IsEvaluateButtonEnabled) 84 ] 85 + SHorizontalBox::Slot() 86 .AutoWidth() 87 [ 88 SNew(SButton) 89 .ToolTipText(LOCTEXT("ComposerWidgetBehavior_FullEvaluateTip", "Using the given input state below, run a full heuristics run on a Reasoner to determine a goal, and use that goal to determine actions from the Planner.")) 90 .Text(LOCTEXT("ComposerWidgetBehavior_FullEvaluate", "Evaluate Goal & Plan")) 91 .OnClicked(this, &SComposerBehaviorWidget::OnEvaluateGoalAndPlan) 92 .IsEnabled(this, &SComposerBehaviorWidget::IsEvaluateButtonEnabled) 93 ] 94 + SHorizontalBox::Slot() 95 .FillWidth(1.f) 96 .HAlign(EHorizontalAlignment::HAlign_Right) 97 [ 98 SNew(SHorizontalBox) 99 + SHorizontalBox::Slot() 100 .AutoWidth() 101 [ 102 GoalSelectionContent() 103 ] 104 + SHorizontalBox::Slot() 105 .AutoWidth() 106 [ 107 SNew(SButton) 108 .ToolTipText(LOCTEXT("ComposerWidgetBehavior_ActionsEvaluateTip", "Using the given input state below and a selected goal from the state, determine actions from the Planner.")) 109 .Text(LOCTEXT("ComposerWidgetBehavior_SetGoalActionsEvaluate", "Evaluate Plan with Goal")) 110 .OnClicked(this, &SComposerBehaviorWidget::OnEvaluatePlanWithGoal) 111 .IsEnabled(this, &SComposerBehaviorWidget::IsEvaluateGoalButtonEnabled) 112 ] 113 ] 114 ] 115 + SVerticalBox::Slot() 116 [ 117 TestStartingStateDetailViewRef 118 ] 119 + SVerticalBox::Slot() 120 [ 121 SNew(SBorder) 122 .Content() 123 [ 124 SNew(SScrollBox) 125 + SScrollBox::Slot() 126 [ 127 SAssignNew(ExperimentOutputList, SListView<TSharedPtr<FRpaiDiagnosticsViewData>>) 128 .ItemHeight(24) 129 .SelectionMode(ESelectionMode::None) 130 .ListItemsSource(&ExperimentOutput) 131 .OnGenerateRow(this, &SComposerBehaviorWidget::OnGenerateExperimentOutputRow) 132 133 ] 134 ] 135 ] 136 ]; 137 } 138 END_SLATE_FUNCTION_BUILD_OPTIMIZATION 139 140 void SComposerBehaviorWidget::AddReferencedObjects(FReferenceCollector& Collector) 141 { 142 Collector.AddReferencedObject(TestStartingState); 143 } 144 145 void SComposerBehaviorWidget::NotifyStateTypePropertyChanged() 146 { 147 if (TestStartingState != nullptr && TestStartingState->IsValidLowLevel()) 148 { 149 TestStartingState->ConditionalBeginDestroy(); 150 } 151 152 TSubclassOf<URpaiState> ConstructedStateType = ComposerBehavior.Get()->GetConstructedStateType(); 153 if (ConstructedStateType.Get() != nullptr) 154 { 155 TestStartingState = NewObject<URpaiState>(GetTransientPackage(), ConstructedStateType); 156 TestStartingStateDetailView->SetObject(TestStartingState, true); 157 } 158 } 159 160 void SComposerBehaviorWidget::NotifyGoalsPropertyChanged() 161 { 162 Goals = ComposerBehavior.Get()->GetGoals(); 163 CurrentGoal = nullptr; 164 if (GoalComboBox) 165 { 166 GoalComboBox->RefreshOptions(); 167 } 168 } 169 170 EVisibility SComposerBehaviorWidget::GetSetStateMessageVisibility() const 171 { 172 TSubclassOf<URpaiState> ConstructedStateType = ComposerBehavior.Get()->GetConstructedStateType(); 173 return ConstructedStateType.Get() == nullptr ? EVisibility::Visible : EVisibility::Collapsed; 174 } 175 176 bool SComposerBehaviorWidget::IsEvaluateButtonEnabled() const 177 { 178 return !bIsExperimenting && GetSetStateMessageVisibility() == EVisibility::Collapsed; 179 } 180 181 EVisibility SComposerBehaviorWidget::GetSummaryVisibility() const 182 { 183 return GetSetStateMessageVisibility() == EVisibility::Collapsed ? EVisibility::Visible : EVisibility::Collapsed; 184 } 185 186 FReply SComposerBehaviorWidget::OnEvaluateGoalAndPlan() 187 { 188 bIsExperimenting = true; 189 return FReply::Handled(); 190 } 191 192 void SComposerBehaviorWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) 193 { 194 SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); 195 if (bIsExperimenting) 196 { 197 TArray<URpaiActionBase*> PlannedActions; 198 URpaiComposerBehavior* Behavior = ComposerBehavior.Get(); 199 const URpaiPlannerBase* Planner = Behavior->GetPlanner(); 200 const URpaiReasonerBase* Reasoner = Behavior->GetReasoner(); 201 switch (LastPlanResult) 202 { 203 case ERpaiPlannerResult::CompletedFailure: 204 case ERpaiPlannerResult::CompletedCancelled: 205 bIsExperimenting = false; 206 LastPlanResult = ERpaiPlannerResult::Invalid; 207 break; 208 case ERpaiPlannerResult::CompletedSuccess: 209 bIsExperimenting = false; 210 LastPlanResult = ERpaiPlannerResult::Invalid; 211 EmitPlanOutput(CurrentGoal, PlannedActions); 212 break; 213 case ERpaiPlannerResult::RequiresTick: 214 LastPlanResult = Planner->TickGoalPlanning(CurrentGoal, TestStartingState, Behavior->GetActions(), PlannedActions, CurrentPlannerMemory); 215 if (LastPlanResult == ERpaiPlannerResult::CompletedSuccess) 216 { 217 bIsExperimenting = false; 218 LastPlanResult = ERpaiPlannerResult::Invalid; 219 EmitPlanOutput(CurrentGoal, PlannedActions); 220 } 221 break; 222 case ERpaiPlannerResult::Invalid: 223 CurrentGoal = Reasoner->ReasonNextGoal(Behavior->GetGoals(), TestStartingState); 224 GoalComboBox->SetSelectedItem(CurrentGoal); 225 CurrentPlannerMemory = Planner->AllocateMemorySlice(ComponentActionMemory); 226 LastPlanResult = Planner->StartGoalPlanning(CurrentGoal, TestStartingState, Behavior->GetActions(), PlannedActions, CurrentPlannerMemory); 227 if (LastPlanResult == ERpaiPlannerResult::CompletedSuccess) 228 { 229 bIsExperimenting = false; 230 LastPlanResult = ERpaiPlannerResult::Invalid; 231 EmitPlanOutput(CurrentGoal, PlannedActions); 232 } 233 break; 234 default: 235 break; 236 } 237 } 238 } 239 240 bool SComposerBehaviorWidget::IsEvaluateGoalButtonEnabled() const 241 { 242 return IsEvaluateButtonEnabled() && CurrentGoal != nullptr && CurrentGoal->IsValidLowLevel(); 243 } 244 245 FReply SComposerBehaviorWidget::OnEvaluatePlanWithGoal() 246 { 247 TArray<URpaiActionBase*> PlannedActions; 248 URpaiComposerBehavior* Behavior = ComposerBehavior.Get(); 249 const URpaiPlannerBase* Planner = Behavior->GetPlanner(); 250 CurrentPlannerMemory = Planner->AllocateMemorySlice(ComponentActionMemory); 251 LastPlanResult = Planner->StartGoalPlanning(CurrentGoal, TestStartingState, Behavior->GetActions(), PlannedActions, CurrentPlannerMemory); 252 if (LastPlanResult == ERpaiPlannerResult::RequiresTick) 253 { 254 bIsExperimenting = true; 255 } 256 else 257 { 258 EmitPlanOutput(CurrentGoal, PlannedActions); 259 } 260 261 return FReply::Handled(); 262 } 263 264 FReply SComposerBehaviorWidget::OnEvaluateActions() 265 { 266 EmitCurrentStateActionWeightsAndDistances(); 267 return FReply::Handled(); 268 } 269 270 FReply SComposerBehaviorWidget::OnEvaluateGoals() 271 { 272 EmitCurrentStateGoalCategoryWeightDistance(); 273 return FReply::Handled(); 274 } 275 276 TSharedRef<SWidget> SComposerBehaviorWidget::GoalSelectionContent() 277 { 278 return SAssignNew(GoalComboBox, SComboBox<URpaiGoalBase*>) 279 .OptionsSource(&Goals) 280 .OnSelectionChanged(this, &SComposerBehaviorWidget::HandleGoalSelectionChanged) 281 .OnGenerateWidget(this, &SComposerBehaviorWidget::OnGenerateGoalRow) 282 .InitiallySelectedItem(CurrentGoal) 283 [ 284 SNew(STextBlock) 285 .Text(this, &SComposerBehaviorWidget::GetCurrentGoalSelectionText) 286 ]; 287 } 288 289 TSharedRef<SWidget> SComposerBehaviorWidget::OnGenerateGoalRow(URpaiGoalBase* Item) 290 { 291 if (Item != nullptr && Item->IsValidLowLevel()) 292 { 293 return SNew(STextBlock) 294 .Text(FText::FromString(Item->GetGoalName())); 295 } 296 return SNew(STextBlock) 297 .Text(LOCTEXT("ComposerWidgetBehavior_InvalidObject", "Invalid Object!")); 298 } 299 300 FText SComposerBehaviorWidget::GetCurrentGoalSelectionText() const 301 { 302 if (CurrentGoal != nullptr && CurrentGoal->IsValidLowLevel()) 303 { 304 return FText::FromString(CurrentGoal->GetGoalName()); 305 } 306 return LOCTEXT("ComposerWidgetBehavior_SelectGoalPrompt", "Select a Goal"); 307 } 308 309 void SComposerBehaviorWidget::HandleGoalSelectionChanged(URpaiGoalBase* Selection, ESelectInfo::Type SelectInfo) 310 { 311 if (SelectInfo != ESelectInfo::OnNavigation) 312 { 313 CurrentGoal = Selection; 314 GoalComboBox->SetSelectedItem(CurrentGoal); 315 } 316 } 317 318 void SComposerBehaviorWidget::EmitPlanOutput(const URpaiGoalBase* Goal, const TArray<URpaiActionBase*>& Actions) 319 { 320 ExperimentOutput.Empty(); 321 322 TSharedPtr<FRpaiDiagnosticsViewData> GoalNameRow = MakeShareable(new FRpaiDiagnosticsViewData()); 323 GoalNameRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalNameRow_Title", "Goal Name")); 324 GoalNameRow->ArbitraryRowData.Add(FText::FromString(Goal->GetGoalName())); 325 ExperimentOutput.Add(GoalNameRow); 326 327 TSharedPtr<FRpaiDiagnosticsViewData> GoalCategoryRow = MakeShareable(new FRpaiDiagnosticsViewData()); 328 GoalCategoryRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalCategoryRow_Title", "Goal Category")); 329 GoalCategoryRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_GoalOutput_Category", "{0}"), Goal->GetCategory())); 330 ExperimentOutput.Add(GoalCategoryRow); 331 332 TSharedPtr<FRpaiDiagnosticsViewData> GoalWeightRow = MakeShareable(new FRpaiDiagnosticsViewData()); 333 GoalWeightRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalWeightRow_Title", "Goal Weight")); 334 GoalWeightRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_GoalOutput_Weight", "{0}"), Goal->GetWeight(TestStartingState))); 335 ExperimentOutput.Add(GoalWeightRow); 336 337 for (const auto& Action : Actions) 338 { 339 TSharedPtr<FRpaiDiagnosticsViewData> ActionRow = MakeShareable(new FRpaiDiagnosticsViewData()); 340 ActionRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalWeightRow_Title", "Action Name")); 341 ActionRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionOutput", "{0}"), FText::FromString(Action->GetActionName()))); 342 ExperimentOutput.Add(ActionRow); 343 } 344 ExperimentOutputList->RequestListRefresh(); 345 } 346 347 void SComposerBehaviorWidget::EmitCurrentStateActionWeightsAndDistances() 348 { 349 if (ComposerBehavior.IsSet()) 350 { 351 URpaiComposerBehavior* Behavior = ComposerBehavior.Get(); 352 if (Behavior == nullptr) 353 { 354 return; 355 } 356 357 ExperimentOutput.Empty(); 358 TArray<URpaiActionBase*> BehaviorActions = Behavior->GetActions(); 359 TArray<URpaiGoalBase*> BehaviorGoals = Behavior->GetGoals(); 360 361 TSharedPtr<FRpaiDiagnosticsViewData> DynamicHeaderRow = MakeShareable(new FRpaiDiagnosticsViewData()); 362 DynamicHeaderRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_ActionNameHeader", "Action Name")); 363 DynamicHeaderRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_ActionWeightHeader", "Action Weight")); 364 DynamicHeaderRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_ActionApplicableHeader", "Action Is Applicable?")); 365 for (const auto& Goal : BehaviorGoals) 366 { 367 FText GoalNameText = FText::FromString(Goal->GetGoalName()); 368 DynamicHeaderRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_GoalCategoryHeader_Fmt", "{0}.Category"), GoalNameText)); 369 DynamicHeaderRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_GoalWeightHeader_Fmt", "{0}.Weight"), GoalNameText)); 370 DynamicHeaderRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_GoalDistanceHeader_Fmt", "{0}.Distance"), GoalNameText)); 371 } 372 ExperimentOutput.Add(DynamicHeaderRow); 373 374 for (const auto& Action : BehaviorActions) 375 { 376 URpaiState* FutureState = NewObject<URpaiState>(GetTransientPackage(), TestStartingState->GetClass()); 377 Action->ApplyToState(FutureState); 378 379 TSharedPtr<FRpaiDiagnosticsViewData> ActionRow = MakeShareable(new FRpaiDiagnosticsViewData()); 380 381 ActionRow->ArbitraryRowData.Add(FText::FromString(Action->GetActionName())); 382 ActionRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_ActionWeightValue_Fmt", "{0}"), Action->ExecutionWeight(TestStartingState))); 383 ActionRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_ActionApplicableValue_Fmt", "{0}"), Action->IsApplicable(TestStartingState))); 384 385 for (const auto& Goal : BehaviorGoals) 386 { 387 ActionRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_GoalCategoryValue_Fmt", "{0}"), Goal->GetCategory())); 388 ActionRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_GoalWeightValue_Fmt", "{0}"), Goal->GetWeight(FutureState))); 389 ActionRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_ActionPreviewOutput_GoalDistanceValue_Fmt", "{0}"), Goal->GetDistanceToDesiredState(FutureState))); 390 } 391 ExperimentOutput.Add(ActionRow); 392 } 393 ExperimentOutputList->RequestListRefresh(); 394 } 395 } 396 397 void SComposerBehaviorWidget::EmitCurrentStateGoalCategoryWeightDistance() 398 { 399 if (ComposerBehavior.IsSet()) 400 { 401 URpaiComposerBehavior* Behavior = ComposerBehavior.Get(); 402 if (Behavior == nullptr) 403 { 404 return; 405 } 406 TArray<URpaiGoalBase*> BehaviorGoals = Behavior->GetGoals(); 407 408 ExperimentOutput.Empty(); 409 410 TSharedPtr<FRpaiDiagnosticsViewData> GoalHeaderRow = MakeShareable(new FRpaiDiagnosticsViewData()); 411 GoalHeaderRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalNameRow_Title", "Goal Name")); 412 GoalHeaderRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalCategoryRow_Title", "Goal Category")); 413 GoalHeaderRow->ArbitraryRowData.Add(LOCTEXT("ComposerWidgetBehavior_GoalOutput_GoalWeightRow_Title", "Goal Weight")); 414 ExperimentOutput.Add(GoalHeaderRow); 415 416 for (const auto& Goal : BehaviorGoals) 417 { 418 TSharedPtr<FRpaiDiagnosticsViewData> GoalRow = MakeShareable(new FRpaiDiagnosticsViewData()); 419 GoalRow->ArbitraryRowData.Add(FText::FromString(Goal->GetGoalName())); 420 GoalRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_GoalOutput_Category", "{0}"), Goal->GetCategory())); 421 GoalRow->ArbitraryRowData.Add(FText::Format(LOCTEXT("ComposerWidgetBehavior_GoalOutput_Weight", "{0}"), Goal->GetWeight(TestStartingState))); 422 ExperimentOutput.Add(GoalRow); 423 } 424 ExperimentOutputList->RequestListRefresh(); 425 } 426 } 427 428 TSharedRef<ITableRow> SComposerBehaviorWidget::OnGenerateExperimentOutputRow(TSharedPtr<FRpaiDiagnosticsViewData> RowData, const TSharedRef<STableViewBase>& OwnerTable) 429 { 430 TSharedRef<SHorizontalBox> Cells = SNew(SHorizontalBox); 431 for (const auto& Text : RowData->ArbitraryRowData) 432 { 433 Cells->AddSlot() 434 [ 435 SNew(SBorder) 436 .Padding(0.5f, 0.5f) 437 [ 438 SNew(STextBlock) 439 .Margin(FMargin(3.f, 3.f)) 440 .Text(Text) 441 ] 442 ]; 443 } 444 return SNew(STableRow<TSharedPtr<FRpaiDiagnosticsViewData>>, OwnerTable) 445 [ 446 Cells 447 ]; 448 }