/ Source / ReasonablePlanningAIEditor / Private / Slate / SComposerBehaviorWidget.cpp
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  }