I’m trying to parse JSON with the following general structure:
{
"dialogue": [
{
"name": "Test 1",
"image": "Qyvnily_WildFlower",
"lines": [
"Howie doin'?"
]
},
{
"name": "Test 2",
"image": "Maya_Confused",
"lines": [
"Hi! Um. I'm Maya. Very happy to meet you!"
],
"choices": [
{
"text": "Test Question 1 Prompt?...",
"dialogue": [
{
"name": "Test Question 1",
"image": "Maya_Eldritch",
"lines": [
"So... testing 1?"
]
},
{
"name": "Test Answer 1",
"image": "Qyvnily_WildFlower",
"lines": [
"Answering 1, certainly."
]
},
{
"name": "Test Answer 1 Response",
"image": "Maya_Embarrassed",
"lines": [
"Cool, cool. ANyway..."
],
"thoughts": [
"Sweet crumpets"
]
}
]
},
{
"text": "Test Question 2 Prompt?...",
"dialogue": [
{
"name": "Test Question 2",
"image": "Maya_Happy",
"lines": [
"Hey how come test question 2?"
]
},
{
"name": "Test Answer 2",
"image": "Qyvnily_WildFlower",
"lines": [
"You know the answer is test answer 2."
]
}
]
}
]
}
]
}
The idea is that characters are having a conversation and the conversation script supports conversation subtree processing in response to the player picking an option from the choices array (which is currently rendered as a list of UButtons in the UI).
The first pass works fine, rendering the initial conversation and the choice set, but when I try to render a dialogue subtree I run into a serious problem – the memory hosting the inflated JSON from FJsonSerializer::Deserialize() appears to have been freed if I try to access it from the context of a lambda function bound to the button OnClicked delegate! I get variably a SIGBUS or SIGSEGV crash citing non-zero memory address if I try to access references to the nested JSON objects. If I create a new shared pointer to the nested dialogue array I can get the correct number of entries reported when the lambda function executes, but trying to dereference any of them results in a segmentation fault crash.
My call flow looks like:
1. GenerateConversationUI() creates widgets to host the conversation content ->
2. ParseConversationScript() deserializes the JSON string and inflates it into JSON objects, storing a shared pointer to the result in a class member field CurrentScriptJsonObject ->
3. ParseDialogue() extracts the dialogue array of JSON objects from CurrentDialogueJsonObject and steps through each to generate UI elements representing them, adding these to the parent container widget. For the choices array, it generates UButtons whose OnClicked delegate gets bound to a lambda function; this lambda moves the current subdialogue array to a separate shared pointer class member, CurrentDialogueJsonObject, and calls ParseDialogue() again. Upon entering ParseDialogue() from the lambda exec context the extracted dialogue array has the correct number of elements, but dereferencing any causes a crash.
UUserWidget* UConversationStarter::GenerateConversationUI(const FString& Script)
{
UUserWidget* ConvoWidget = CreateWidget<UUserWidget>(GetWorld(), ConvoBaseWidgetClass);
UScrollBox* ScrollBox = Cast<UScrollBox>(ConvoWidget->WidgetTree->FindWidget(TEXT("DialogueScrollBox")));
ParseConversationScript(Script);
if (CurrentScriptJsonObject)
{
auto DialogueElementsArray = CurrentScriptJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; dialogue array has %d elements"), DialogueElementsArray.Num());
// todo: set current dialogue json object in a desperate bid to keep the ■■■■ nested crap alive through stack pops
CurrentDialogueJsonObject = MakeShareable(new FJsonObject());
CurrentDialogueJsonObject->SetArrayField(KEY_ARRAY_DIALOGUE, std::move(DialogueElementsArray));
ParseDialogue(ConvoWidget, ScrollBox);
}
else
{
UE_LOG(LogTemp, Error, TEXT("GenerateConvoUI; failed to extract top level script JSON object"));
}
return ConvoWidget;
}
void UConversationStarter::ParseConversationScript(const FString& Script)
{
TSharedPtr<FJsonObject> ScriptJsonObject = MakeShareable(new FJsonObject());;
auto Reader = TJsonReaderFactory<>::Create(Script);
if (FJsonSerializer::Deserialize(Reader, ScriptJsonObject))
{
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; deserialized json"));
CurrentScriptJsonObject = ScriptJsonObject;
}
else
{
UE_LOG(LogTemp, Error, TEXT("ParseConvoScript; deserializing json failed"));
}
}
void UConversationStarter::ParseDialogue(UUserWidget* ConvoWidget, UPanelWidget* Container)
{
auto DialogueElementsArrayExperiment = CurrentDialogueJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
UE_LOG(LogTemp, Warning, TEXT("ParseDialogue; current dialogue object has dialogue array of %d elements"), DialogueElementsArrayExperiment.Num());
for (auto DialogueElement : DialogueElementsArrayExperiment)
{
const TSharedPtr<FJsonObject>* DialogueObject;
if (DialogueElement->TryGetObject(DialogueObject))
{
...
const TArray<TSharedPtr<FJsonValue>>* ChoicesArray;
if ((*DialogueObject)->TryGetArrayField(KEY_ARRAY_CHOICES, ChoicesArray))
{
...
// populate choiceswidget with buttons hosting the choices array text
for (auto Choice : *ChoicesArray)
{
const TSharedPtr<FJsonObject>* ChoiceObject;
if (Choice->TryGetObject(ChoiceObject))
{
auto* ChoiceButton = ChoicesWidget->WidgetTree->ConstructWidget<ULambdaButton>();
...
const TArray<TSharedPtr<FJsonValue>>* SubDialogueElements;
...
if (Choice->AsObject()->TryGetArrayField(KEY_ARRAY_DIALOGUE, SubDialogueElements))
{
// todo: set current dialogue json object in a desperate bid to keep the ■■■■ nested crap alive through stack pops
CurrentDialogueJsonObject = MakeShareable(new FJsonObject());
CurrentDialogueJsonObject->SetArrayField(KEY_ARRAY_DIALOGUE, std::move(DialogueElementsArray));
// install subdialogue elements to OnClick lambda event
ChoiceButton->LambdaEvent.BindLambda([&]()
{
ParseDialogue(ConvoWidget, Container);
});
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("ParseConvoScript; choice jsonval could not be converted to object"));
}
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; no choices found"));
}
...
}
}
}
At first I assumed the problem was that the shared pointers created by FJsonSerializer::Deserialize() must be dying off when the scope in which FJsonSerializer::Deserialize() was called is popped off the call stack, but I tried manually placing the Deserialize() call in its own scope and analyzing right after and it came up with the correct dialogue elements count, and allows me to dereference them:
void UConversationStarter::ParseConversationScript(const FString& Script)
{
{
TSharedPtr<FJsonObject> ScriptJsonObject = MakeShareable(new FJsonObject());;
auto Reader = TJsonReaderFactory<>::Create(Script);
if (FJsonSerializer::Deserialize(Reader, ScriptJsonObject))
{
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; deserialized json"));
CurrentScriptJsonObject = ScriptJsonObject;
}
else
{
UE_LOG(LogTemp, Error, TEXT("ParseConvoScript; deserializing json failed"));
}
}
// NOTE: dialogue elements are still alive at this point, so it isn't simply leaving scope that causes the problem.
auto DialogueElementsArrayExperiment = CurrentScriptJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current script object has dialogue array of %d elements"), DialogueElementsArrayExperiment.Num());
const TSharedPtr<FJsonObject>* DialogueObject;
if (DialogueElementsArrayExperiment[0]->TryGetObject(DialogueObject))
{
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current script object's dialogue array first element has image named %s"), *(*DialogueObject)->GetStringField(KEY_STRING_IMAGE));
}
CurrentDialogueJsonObject = MakeShareable(new FJsonObject());
CurrentDialogueJsonObject->SetArrayField(KEY_ARRAY_DIALOGUE, DialogueElementsArrayExperiment);
auto DialogueElementsArrayStat = CurrentDialogueJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current dialogue object has dialogue array of %d elements"), DialogueElementsArrayStat.Num());
const TSharedPtr<FJsonObject>* DialogueObject2;
if (DialogueElementsArrayStat[0]->TryGetObject(DialogueObject2))
{
UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current dialogue object's dialogue array first element has image named %s"), *(*DialogueObject2)->GetStringField(KEY_STRING_IMAGE));
}
}
So I’m not really sure where that leaves me… how can I keep a handle to the entire inflated JSON DOM, including nested objects, regardless of scope and call stack?