// Copyright Epic Games, Inc. All Rights Reserved. #include "JsonConfig.h" #include "Containers/StringView.h" #include "Dom/JsonObject.h" #include "Dom/JsonValue.h" #include "EditorConfigModule.h" #include "HAL/PlatformCrt.h" #include "Internationalization/Text.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Misc/AssertionMacros.h" #include "Misc/CString.h" #include "Misc/FileHelper.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonTypes.h" #include "Serialization/JsonWriter.h" #include "Templates/EnableIf.h" #include "Templates/UnrealTemplate.h" #include "Trace/Detail/Channel.h" #include "UObject/NameTypes.h" DEFINE_LOG_CATEGORY_STATIC(LogJsonConfig, Log, All); namespace UE { FJsonPath::FJsonPath() { } FJsonPath::FJsonPath(const TCHAR* InPath) { ParsePath(InPath); } FJsonPath::FJsonPath(FStringView InPath) { ParsePath(FString(InPath)); } FJsonPath::FJsonPath(const FJsonPath& Other) : PathParts(Other.PathParts) { } FJsonPath::FJsonPath(FJsonPath&& Other) : PathParts(MoveTemp(Other.PathParts)) { } void FJsonPath::ParsePath(const FString& InPath) { TArray PathStrings; InPath.ParseIntoArray(PathStrings, TEXT("."), false); // we don't really care about the document root, remove it if it exists if (PathStrings.Num() > 0 && PathStrings[0] == TEXT("$")) { PathStrings.RemoveAt(0); } for (const FString& Part : PathStrings) { if (Part.IsEmpty()) { PathParts.Empty(); UE_LOG(LogJsonConfig, Warning, TEXT("Path part was empty in JSON path \"%s\""), *InPath); return; } // check for index int32 OpenBracket, CloseBracket; if (Part.FindChar('[', OpenBracket)) { if (Part.FindChar(']', CloseBracket) && CloseBracket > OpenBracket && CloseBracket == Part.Len() - 1) { FString Index = Part.Mid(OpenBracket + 1, (CloseBracket - OpenBracket) - 1); if (Index.IsNumeric()) { FPart NewPart; NewPart.Name = *Part.Left(OpenBracket); NewPart.Index = FCString::Atoi(*Index); PathParts.Add(NewPart); } else { PathParts.Empty(); UE_LOG(LogJsonConfig, Warning, TEXT("Path part \"%s\" looked to be index into array but was malformed in JSON path \"%s\""), *Part, *InPath); return; } } else { PathParts.Empty(); UE_LOG(LogJsonConfig, Warning, TEXT("Path part \"%s\" looked to be index into array but was malformed in JSON path \"%s\""), *Part, *InPath); return; } } else { FPart NewPart; NewPart.Name = *Part; // basic path PathParts.Add(NewPart); } } } void FJsonPath::Append(FStringView Name) { FPart NewPart; NewPart.Name = Name; PathParts.Add(NewPart); } void FJsonPath::SetArrayIndex(int32 Index) { check(PathParts.Num() > 0); PathParts.Last().Index = Index; } FJsonPath FJsonPath::GetSubPath(int32 NumParts) const { FJsonPath SubPath; SubPath.PathParts.Reserve(NumParts); for (int32 Idx = 0; Idx < NumParts; ++Idx) { SubPath.PathParts.Add(PathParts[Idx]); } return MoveTemp(SubPath); } FString FJsonPath::ToString() const { TStringBuilder<256> StringBuilder; for (const FPart& Part : PathParts) { StringBuilder.Append(Part.Name); if (Part.Index != INDEX_NONE) { StringBuilder.Append(TEXT("[")); StringBuilder.Append(LexToString(Part.Index)); StringBuilder.Append(TEXT("]")); } } return StringBuilder.ToString(); } //----------------------------------------------------------------------------- FJsonConfig::FJsonConfig() { OverrideObject = MakeShared(); MergedObject = MakeShared(); } void FJsonConfig::SetParent(const TSharedPtr& Parent) { if (ParentConfig.IsValid()) { ParentConfig->OnConfigChanged.Unbind(); } ParentConfig = Parent; MergeThisWithParent(); if (ParentConfig.IsValid()) { ParentConfig->OnConfigChanged.BindRaw(this, &FJsonConfig::OnParentConfigChanged); } OnConfigChanged.ExecuteIfBound(); } void FJsonConfig::OnParentConfigChanged() { MergeThisWithParent(); } bool FJsonConfig::LoadFromFile(FStringView FilePath) { FString Contents; if (!FFileHelper::LoadFileToString(Contents, FilePath.GetData())) { return false; } if (!LoadFromString(Contents)) { UE_LOG(LogEditorConfig, Error, TEXT("Failed to load JSON file into JsonConfig %s"), FilePath.GetData()); return false; } return true; } bool FJsonConfig::LoadFromString(FStringView Content) { TSharedRef JsonReader = FJsonStringReader::Create(FString(Content)); if (!FJsonSerializer::Deserialize(JsonReader.Get(), OverrideObject)) { UE_LOG(LogEditorConfig, Error, TEXT("Failed to deserialize JSON string: %s"), *JsonReader->GetErrorMessage()); return false; } if (!OverrideObject.IsValid()) { return false; } OnConfigChanged.ExecuteIfBound(); return MergeThisWithParent(); } bool FJsonConfig::SaveToFile(FStringView FilePath) const { FString Contents; if (!SaveToString(Contents)) { return false; } return FFileHelper::SaveStringToFile(Contents, FilePath.GetData()); } bool FJsonConfig::SaveToString(FString& OutResult) const { if (!OverrideObject.IsValid()) { return false; } TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutResult); if (!FJsonSerializer::Serialize(OverrideObject.ToSharedRef(), Writer.Get())) { return false; } return true; } static TSharedPtr GetKeyField(const TSharedPtr& Value) { if (Value->Type == EJson::Object) { return Value->AsObject()->TryGetField(TEXT("$key")); } return TSharedPtr(); } static TSharedPtr GetValueField(const TSharedPtr& Value) { if (Value->Type == EJson::Object) { return Value->AsObject()->TryGetField(TEXT("$value")); } return TSharedPtr(); } static TSharedPtr FindValueInArray(const TArray>& Array, const TSharedPtr& Find) { if (Find->Type == EJson::Object) { TSharedPtr FindKey = GetKeyField(Find); if (FindKey.IsValid()) { const TSharedPtr* ExistingField = Array.FindByPredicate( [FindKey](const TSharedPtr& Value) { TSharedPtr ValueKey = GetKeyField(Value); if (ValueKey.IsValid()) { return FJsonValue::CompareEqual(*FindKey.Get(), *ValueKey.Get()); } return false; }); if (ExistingField != nullptr) { return *ExistingField; } } } else { const TSharedPtr* ExistingField = Array.FindByPredicate( [Find](const TSharedPtr& Value) { return FJsonValue::CompareEqual(*Find.Get(), *Value.Get()); }); if (ExistingField != nullptr) { return *ExistingField; } } return TSharedPtr(); } static bool RemoveValueFromArray(TArray>& Array, const TSharedPtr& Find) { if (Find->Type == EJson::Object) { TSharedPtr FindKey = GetKeyField(Find); if (FindKey.IsValid()) { return Array.RemoveAll( [FindKey](const TSharedPtr& Value) { TSharedPtr ValueKey = GetKeyField(Value); if (ValueKey.IsValid()) { return FJsonValue::CompareEqual(*FindKey.Get(), *ValueKey.Get()); } return false; }) > 0; } } return Array.RemoveAll( [Find](const TSharedPtr& Value) { return FJsonValue::CompareEqual(*Find.Get(), *Value.Get()); }) > 0; } static bool ApplyDeltaOperationsToArray(const TSharedPtr& OverrideObject, TArray>& DestArray) { const TArray>* OverrideSet = nullptr; if (OverrideObject->TryGetArrayField(TEXT("="), OverrideSet) && OverrideSet) // the latter part of this condition is just to satisfy static analysis { const TArray>& OverrideSetRef = *OverrideSet; DestArray.Reset(); DestArray.Reserve(OverrideSetRef.Num()); for (const TSharedPtr& SetValue : OverrideSetRef) { DestArray.Add(FJsonValue::Duplicate(SetValue)); } } const TArray>* OverrideAdd = nullptr; if (OverrideObject->TryGetArrayField(TEXT("+"), OverrideAdd) && OverrideAdd) { const TArray>& OverrideAddRef = *OverrideAdd; DestArray.Reserve(DestArray.Num() + OverrideAddRef.Num()); for (const TSharedPtr& AddValue : OverrideAddRef) { // check if this is a map with $key, $value fields if (AddValue->Type == EJson::Object) { TSharedPtr AddKey = GetKeyField(AddValue); if (AddKey.IsValid()) { TSharedPtr* ExistingField = DestArray.FindByPredicate( [AddKey](const TSharedPtr& Value) { TSharedPtr ValueKey = GetKeyField(Value); if (ValueKey.IsValid()) { return FJsonValue::CompareEqual(*ValueKey.Get(), *AddKey.Get()); } return false; }); if (ExistingField != nullptr) { // there was an existing field with the same key, override that value TSharedPtr AddValueField = GetValueField(AddValue); ensureAlways(AddValueField.IsValid()); TSharedPtr ExistingObject = (*ExistingField)->AsObject(); ExistingObject->SetField(TEXT("$value"), FJsonValue::Duplicate(AddValueField)); continue; } } } DestArray.Add(FJsonValue::Duplicate(AddValue)); } } const TArray>* OverrideRemove = nullptr; if (OverrideObject->TryGetArrayField(TEXT("-"), OverrideRemove) && OverrideRemove) { const TArray>& OverrideRemoveRef = *OverrideRemove; for (const TSharedPtr& RemoveValue : OverrideRemoveRef) { DestArray.RemoveAll( [RemoveValue](const TSharedPtr& Value) { TSharedPtr RemoveKey = GetKeyField(RemoveValue); TSharedPtr ExistingKey = GetKeyField(Value); if (RemoveKey.IsValid() && ExistingKey.IsValid()) { // the existing and remove values have $key fields, compare those return FJsonValue::CompareEqual(*RemoveKey.Get(), *ExistingKey.Get()); } if (ExistingKey.IsValid()) { // the existing value has a $key field, compare it with the remove value, // because we want to support the short-hand of "-": [ "Foo" ] instead of "-": [ { "$key": "Foo" } ] return FJsonValue::CompareEqual(*RemoveValue.Get(), *ExistingKey.Get()); } // just compare values directly return FJsonValue::CompareEqual(*RemoveValue.Get(), *Value.Get()); }); } } if (OverrideSet != nullptr && (OverrideAdd != nullptr || OverrideRemove != nullptr)) { UE_LOG(LogJsonConfig, Warning, TEXT("JSON container is malformed and contains both set and add/remove values.")); } return OverrideSet != nullptr || OverrideAdd != nullptr || OverrideRemove != nullptr; } static bool ApplyOverridesToObject(const TSharedPtr& Overrides, TSharedPtr& Dest) { for (const TPair>& Override : Overrides->Values) { TSharedPtr DestValue = Dest->TryGetField(Override.Key); if (!DestValue.IsValid()) { const TSharedPtr* OverrideObject = nullptr; if (Override.Value->TryGetObject(OverrideObject) && OverrideObject) { TArray> DestArray; if (ApplyDeltaOperationsToArray(*OverrideObject, DestArray)) { // was an array, DestArray is populated Dest->SetArrayField(Override.Key, DestArray); } else { TSharedPtr DestObject = MakeShared(); ApplyOverridesToObject(*OverrideObject, DestObject); Dest->SetObjectField(Override.Key, DestObject); } } else { Dest->SetField(Override.Key, FJsonValue::Duplicate(Override.Value)); } } else { switch (Override.Value->Type) { case EJson::Boolean: { bool OverrideBool, DestBool; if (Override.Value->TryGetBool(OverrideBool) && DestValue->TryGetBool(DestBool)) { Dest->SetBoolField(Override.Key, OverrideBool); } continue; } case EJson::Number: { double OverrideNumber, DestNumber; if (Override.Value->TryGetNumber(OverrideNumber) && DestValue->TryGetNumber(DestNumber)) { Dest->SetNumberField(Override.Key, OverrideNumber); } continue; } case EJson::String: { FString OverrideString, DestString; if (Override.Value->TryGetString(OverrideString) && DestValue->TryGetString(DestString)) { Dest->SetStringField(Override.Key, OverrideString); } continue; } case EJson::Array: { if (Override.Value->Type == EJson::Array && DestValue->Type == EJson::Array) { // our overrides has an array, so this is implicitly a set operation Dest->SetField(Override.Key, FJsonValue::Duplicate(Override.Value)); } continue; } case EJson::Object: { const TSharedPtr* OverrideObject = nullptr; if (Override.Value->TryGetObject(OverrideObject)) { check(OverrideObject); TArray>* DestArray = nullptr; TSharedPtr* DestObject = nullptr; if (DestValue->TryGetObject(DestObject) && DestObject) { ApplyOverridesToObject(*OverrideObject, *DestObject); } else if (DestValue->TryGetArray(DestArray) && DestArray) { ApplyDeltaOperationsToArray(*OverrideObject, *DestArray); } } continue; } default: ensureAlways(false); return false; } } } return true; } bool FJsonConfig::MergeThisWithParent() { MergedObject->Values.Reset(); // pre-fill with parent's values if we have one if (ParentConfig.IsValid()) { FJsonObject::Duplicate(ParentConfig->MergedObject, MergedObject); } // apply this object's overrides if we have any data if (OverrideObject.IsValid()) { return ApplyOverridesToObject(OverrideObject, MergedObject); } return true; } bool FJsonConfig::TryGetBool(const FJsonPath& Path, bool& OutValue) const { TSharedPtr JsonValue; if (!TryGetJsonValue(Path, JsonValue)) { return false; } return JsonValue->TryGetBool(OutValue); } bool FJsonConfig::TryGetString(const FJsonPath& Path, FString& OutValue) const { TSharedPtr JsonValue; if (!TryGetJsonValue(Path, JsonValue)) { return false; } return JsonValue->TryGetString(OutValue); } bool FJsonConfig::TryGetString(const FJsonPath& Path, FName& OutValue) const { TSharedPtr JsonValue; if (!TryGetJsonValue(Path, JsonValue)) { return false; } FString TempString; if (!JsonValue->TryGetString(TempString)) { return false; } OutValue = *TempString; return true; } bool FJsonConfig::TryGetString(const FJsonPath& Path, FText& OutValue) const { TSharedPtr JsonValue; if (!TryGetJsonValue(Path, JsonValue)) { return false; } FString TempString; if (!JsonValue->TryGetString(TempString)) { return false; } OutValue = FText::FromString(TempString); return true; } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetArrayHelper(Path, OutArray, [](const TSharedPtr& JsonValue, bool& OutBool) { return JsonValue->TryGetBool(OutBool); }); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetNumericArrayHelper(Path, OutArray); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetArrayHelper(Path, OutArray, [](const TSharedPtr& JsonValue, FString& OutString) { return JsonValue->TryGetString(OutString); }); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetArrayHelper(Path, OutArray, [](const TSharedPtr& JsonValue, FText& OutText) { FString StringValue; if (JsonValue->TryGetString(StringValue)) { OutText = FText::FromString(StringValue); return true; } return false; }); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray& OutArray) const { return TryGetArrayHelper(Path, OutArray, [](const TSharedPtr& JsonValue, FName& OutName) { FString StringValue; if (JsonValue->TryGetString(StringValue)) { OutName = *StringValue; return true; } return false; }); } bool FJsonConfig::TryGetArray(const FJsonPath& Path, TArray>& OutValue) const { TSharedPtr JsonValue; if (!TryGetJsonValue(Path, JsonValue)) { return false; } const TArray>* ValuePtr; if (!JsonValue->TryGetArray(ValuePtr)) { return false; } OutValue = *ValuePtr; return true; } bool FJsonConfig::TryGetMap(const FJsonPath& Path, TArray& OutMap) const { TArray> Array; if (!TryGetArray(Path, Array)) { return false; } OutMap.Reset(); OutMap.Reserve(Array.Num()); for (const TSharedPtr& Value : Array) { if (Value->Type != EJson::Object) { return false; } const TSharedPtr& Object = Value->AsObject(); TSharedPtr KeyField = Object->TryGetField(TEXT("$key")); TSharedPtr ValueField = Object->TryGetField(TEXT("$value")); if (!KeyField.IsValid() || !ValueField.IsValid()) { return false; } OutMap.Add(FJsonValuePair(KeyField, ValueField)); } return true; } bool FJsonConfig::TryGetJsonObject(const FJsonPath& Path, TSharedPtr& OutValue) const { TSharedPtr JsonValue; if (!TryGetJsonValue(Path, JsonValue)) { return false; } const TSharedPtr* ValuePtr; if (JsonValue->TryGetObject(ValuePtr)) { OutValue = *ValuePtr; return true; } return false; } TSharedPtr FJsonConfig::GetRootObject() const { return MergedObject; } static const TSharedPtr* FindPartInObject(const TSharedPtr& Object, const FJsonPath::FPart& PathPart) { const TSharedPtr* NextValue = Object->Values.Find(PathPart.Name); if (NextValue == nullptr) { // part of path didn't exist return nullptr; } if (PathPart.Index != INDEX_NONE) { const TArray>* ArrayPtr = nullptr; if ((*NextValue)->TryGetArray(ArrayPtr)) { if (!ArrayPtr->IsValidIndex(PathPart.Index)) { // index out of range return nullptr; } NextValue = &(*ArrayPtr)[PathPart.Index]; } } return NextValue; } static TSharedPtr* FindPartInObject(TSharedPtr& Object, const FJsonPath::FPart& PathPart) { return const_cast*>(FindPartInObject((const TSharedPtr&) Object, PathPart)); } bool FJsonConfig::TryGetJsonValue(const FJsonPath& Path, TSharedPtr& OutValue) const { if (!IsValid()) { return false; } const TSharedPtr* CurrentObject = &MergedObject; for (int32 Idx = 0; Idx < Path.Length() - 1; ++Idx) { const TSharedPtr* NextValue = FindPartInObject(*CurrentObject, Path[Idx]); if (NextValue == nullptr || !(*NextValue)->TryGetObject(CurrentObject)) { return false; } } const TSharedPtr* LastValue = FindPartInObject(*CurrentObject, Path[Path.Length() - 1]); if (LastValue == nullptr) { return false; } OutValue = *LastValue; return true; } static bool SetValueHelper(TSharedPtr& CurrentValue, const TSharedPtr& NewValue, const TSharedPtr& ParentValue); static bool SetArrayValueHelper(const TSharedPtr& CurrentValue, const TArray>& NewArray, const TSharedPtr& ParentValue) { TArray>* CurrentArray = nullptr; if (CurrentValue->TryGetArray(CurrentArray)) { // currently has array value so is implicitly a set, we can just overwrite it (*CurrentArray) = NewArray; return true; } TSharedPtr* CurrentObject = nullptr; if (!CurrentValue->TryGetObject(CurrentObject)) { return false; } // we generally want to steer clear of set fields, but if one already exists then we'll just use it TSharedPtr SetField = (*CurrentObject)->TryGetField(TEXT("=")); TArray>* SetArray = nullptr; if (SetField.IsValid() && SetField->TryGetArray(SetArray)) { (*SetArray) = NewArray; // remove add and remove fields (*CurrentObject)->RemoveField(TEXT("+")); (*CurrentObject)->RemoveField(TEXT("-")); return true; } // build up a diff relative to the parent array const TArray>* ParentArray = nullptr; if (ParentValue.IsValid()) { ParentValue->TryGetArray(ParentArray); } TArray> AddedValues; TArray> RemovedValues; if (ParentArray != nullptr) { // find all shared values TArray> SharedValues; SharedValues.Reserve(NewArray.Num()); for (const TSharedPtr& NewElement : NewArray) { TSharedPtr ExistingElement = FindValueInArray(*ParentArray, NewElement); if (ExistingElement.IsValid()) { SharedValues.Add(NewElement); } } // add all values in the new array that weren't shared to the added values AddedValues.Reserve(NewArray.Num() - SharedValues.Num()); for (const TSharedPtr& NewElement : NewArray) { TSharedPtr ExistingElement = FindValueInArray(SharedValues, NewElement); if (!ExistingElement.IsValid()) { AddedValues.Add(NewElement); } } // add all values in the parent array that weren't shared to the removed values RemovedValues.Reserve(ParentArray->Num() - SharedValues.Num()); for (const TSharedPtr& ParentElement : *ParentArray) { TSharedPtr ExistingElement = FindValueInArray(SharedValues, ParentElement); if (!ExistingElement.IsValid()) { RemovedValues.Add(ParentElement); } } } else { AddedValues = NewArray; } TSharedPtr AddField = (*CurrentObject)->TryGetField(TEXT("+")); if (AddField.IsValid()) { TArray>* AddArray = nullptr; if (ensureAlwaysMsgf(AddField->TryGetArray(AddArray), TEXT("Invalid JSON config: \"+\" field in JSON config was not an array."))) { (*AddArray) = AddedValues; } } else if (AddedValues.Num() > 0) { (*CurrentObject)->SetArrayField(TEXT("+"), AddedValues); } TSharedPtr RemoveField = (*CurrentObject)->TryGetField(TEXT("-")); if (RemoveField.IsValid()) { TArray>* RemoveArray = nullptr; if (ensureAlwaysMsgf(RemoveField->TryGetArray(RemoveArray), TEXT("Invalid JSON config: \"-\" field in JSON config was not an array."))) { (*RemoveArray) = RemovedValues; } } else if (RemovedValues.Num() > 0) { (*CurrentObject)->SetArrayField(TEXT("-"), RemovedValues); } return true; } static bool SetObjectValueHelper(const TSharedPtr& CurrentObject, const TSharedPtr& NewObject, const TSharedPtr& ParentValue) { TSharedPtr* ParentObject = nullptr; if (ParentValue.IsValid()) { ParentValue->TryGetObject(ParentObject); } if (ParentObject == nullptr) { // no parent, just set everything we were given into the object for (const TPair>& NewValuePair : NewObject->Values) { CurrentObject->SetField(NewValuePair.Key, NewValuePair.Value); } } else { for (const TPair>& NewValuePair : NewObject->Values) { TSharedPtr ParentField = (*ParentObject)->TryGetField(NewValuePair.Key); if (!ParentField.IsValid()) { // not set in parent, just set here CurrentObject->SetField(NewValuePair.Key, NewValuePair.Value); } else { // already set in parent, if we're identical we can just remove this field from this object if (FJsonValue::CompareEqual(*ParentField.Get(), *NewValuePair.Value.Get())) { CurrentObject->RemoveField(NewValuePair.Key); } else { TSharedPtr CurrentField = CurrentObject->TryGetField(NewValuePair.Key); if (!CurrentField.IsValid()) { CurrentObject->SetField(NewValuePair.Key, NewValuePair.Value); } else { SetValueHelper(CurrentField, NewValuePair.Value, ParentField); } } } } } return true; } bool SetValueHelper(TSharedPtr& CurrentValue, const TSharedPtr& NewValue, const TSharedPtr& ParentValue) { check(CurrentValue.IsValid()); check(NewValue.IsValid()); EJson ValueType = NewValue->Type; switch (ValueType) { case EJson::Number: case EJson::Boolean: case EJson::String: if (CurrentValue->Type != ValueType) { return false; } CurrentValue = NewValue; return true; case EJson::Array: if (CurrentValue->Type != EJson::Object && CurrentValue->Type != EJson::Array) { return false; } return SetArrayValueHelper(CurrentValue, NewValue->AsArray(), ParentValue); case EJson::Object: if (CurrentValue->Type != EJson::Object) { return false; } return SetObjectValueHelper(CurrentValue->AsObject(), NewValue->AsObject(), ParentValue); } return false; } bool FJsonConfig::SetJsonValueInMerged(const FJsonPath& Path, const TSharedPtr& NewValue) { TSharedPtr* CurrentObject = &MergedObject; for (int32 Idx = 0; Idx < Path.Length() - 1; ++Idx) { const FJsonPath::FPart& CurrentPart = Path[Idx]; const TSharedPtr* NextValue = FindPartInObject(*CurrentObject, CurrentPart); if (NextValue == nullptr || !(*NextValue)->TryGetObject(CurrentObject)) { if (CurrentPart.Index == INDEX_NONE) { // create an empty object (*CurrentObject)->SetObjectField(Path[Idx].Name, MakeShared()); } else { // array entry that doesn't exist return false; } } } const FJsonPath::FPart& LastPart = Path[Path.Length() - 1]; TSharedPtr* LastValue = FindPartInObject(*CurrentObject, LastPart); if (LastValue == nullptr) { if (LastPart.Index == INDEX_NONE) { // value doesn't exist and this isn't an array entry, should just set it (*CurrentObject)->SetField(LastPart.Name, NewValue); return true; } else { // array entry that doesn't exist return false; } } return SetValueHelper(*LastValue, NewValue, TSharedPtr()); } bool FJsonConfig::SetJsonValueInOverride(const FJsonPath& Path, const TSharedPtr& NewValue, const TSharedPtr& PreviousValue, const TSharedPtr& ParentValue) { TSharedPtr* CurrentObject = &OverrideObject; for (int32 Idx = 0; Idx < Path.Length() - 1; ++Idx) { const FJsonPath::FPart& CurrentPart = Path[Idx]; const TSharedPtr* NextValue = FindPartInObject(*CurrentObject, CurrentPart); if (NextValue == nullptr) { // since we know this isn't the last part, we can safely add an object here (*CurrentObject)->SetObjectField(CurrentPart.Name, MakeShared()); TSharedPtr CreatedValue = (*CurrentObject)->TryGetField(CurrentPart.Name); CreatedValue->TryGetObject(CurrentObject); } else if (!(*NextValue)->TryGetObject(CurrentObject)) { FString SubPath = Path.GetSubPath(Idx + 1).ToString(); ensureMsgf(false, TEXT("JSON value at \"%s\" not a JSON object in path: %s"), *SubPath, *Path.ToString()); } if (CurrentPart.Index != INDEX_NONE) { // if an index is set, there's an array in the path, eg. Foo[1].Bar ensureAlwaysMsgf(false, TEXT("The array in the middle of a path needs to be figured out...")); } } const FJsonPath::FPart& LastPart = Path[Path.Length() - 1]; TSharedPtr* LastValue = FindPartInObject(*CurrentObject, LastPart); if (LastValue == nullptr) { // field doesn't exist if (LastPart.Index == INDEX_NONE) { // this isn't an array entry, just set it now if (NewValue->Type == EJson::Object || NewValue->Type == EJson::Array) { // create a new empty object that we can diff against TSharedPtr CreatedValue = MakeShared(MakeShared()); (*CurrentObject)->SetField(LastPart.Name, CreatedValue); SetValueHelper(CreatedValue, NewValue, ParentValue); } else { (*CurrentObject)->SetField(LastPart.Name, NewValue); } return true; } else if (!PreviousValue.IsValid()) { // out of bounds return false; } else { // valid index, but no array field created in OverrideObject yet, create one (*CurrentObject)->SetObjectField(LastPart.Name, MakeShared()); } } if (LastPart.Index != INDEX_NONE) { // last part is an array, eg. Foo.Bar[0] - array should absolutely be created by now TSharedPtr ArrayField = (*CurrentObject)->TryGetField(LastPart.Name); check(ArrayField.IsValid()); TArray>* ArrayValue = nullptr; if (ArrayField->TryGetArray(ArrayValue)) { // override is an array, so implicitly a set if (ArrayValue->IsValidIndex(LastPart.Index)) { (*ArrayValue)[LastPart.Index] = NewValue; return true; } // index out of bounds return false; } TSharedPtr* ArrayObject = nullptr; if (ArrayField->TryGetObject(ArrayObject)) { // the field is an object, so we need to modify existing override fields // check if we have a set field TSharedPtr SetField = (*ArrayObject)->TryGetField(TEXT("=")); TArray>* SetContents = nullptr; if (SetField.IsValid() && SetField->TryGetArray(SetContents)) { if (SetContents->IsValidIndex(LastPart.Index)) { (*SetContents)[LastPart.Index] = NewValue; return true; } // index out of bounds return false; } // see if the previous value has already been removed in an override check(PreviousValue.IsValid()); TSharedPtr RemoveField = (*ArrayObject)->TryGetField(TEXT("-")); TArray>* RemoveContents = nullptr; if (RemoveField.IsValid() && RemoveField->TryGetArray(RemoveContents)) { TSharedPtr ExistingRemove = FindValueInArray(*RemoveContents, PreviousValue); if (!ExistingRemove.IsValid()) { // hasn't already been removed, remove it now RemoveContents->Add(PreviousValue); } // remove the new value from the remove list, otherwise we can end up in a situation where a value is added _and_ removed RemoveValueFromArray(*RemoveContents, NewValue); } else { // add a remove field with the previous value TArray> NewRemoveContents; NewRemoveContents.Add(PreviousValue); (*ArrayObject)->SetArrayField(TEXT("-"), NewRemoveContents); } // see if the new value has already been added in an override TSharedPtr AddField = (*ArrayObject)->TryGetField(TEXT("+")); TArray>* AddContents = nullptr; if (AddField.IsValid() && AddField->TryGetArray(AddContents)) { TSharedPtr ExistingAdd = FindValueInArray(*AddContents, NewValue); if (!ExistingAdd.IsValid()) { // hasn't already been added, add it now AddContents->Add(NewValue); } // remove the old value from the add list, otherwise we can end up in a situation where a value is added _and_ removed RemoveValueFromArray(*AddContents, PreviousValue); } else { // add an add field with the new value TArray> NewAddContents; NewAddContents.Add(NewValue); (*ArrayObject)->SetArrayField(TEXT("+"), NewAddContents); } } return true; } check(LastValue); return SetValueHelper(*LastValue, NewValue, ParentValue); } bool FJsonConfig::RemoveJsonValueFromOverride(const FJsonPath& Path, const TSharedPtr& CurrentValue) { TSharedPtr* CurrentObject = &OverrideObject; for (int32 Idx = 0; Idx < Path.Length() - 1; ++Idx) { const TSharedPtr* NextValue = FindPartInObject(*CurrentObject, Path[Idx]); if (NextValue == nullptr || !(*NextValue)->TryGetObject(CurrentObject)) { return false; } } const FJsonPath::FPart& LastPart = Path[Path.Length() - 1]; if (LastPart.Index != INDEX_NONE) { TSharedPtr ArrayField = (*CurrentObject)->TryGetField(LastPart.Name); if (!ArrayField.IsValid()) { return false; } TArray>* ArrayPtr = nullptr; if (ArrayField->TryGetArray(ArrayPtr)) { // array field is an array, so implicitly a set RemoveValueFromArray(*ArrayPtr, CurrentValue); return true; } TSharedPtr* ArrayObject = nullptr; if (ArrayField->TryGetObject(ArrayObject)) { { TSharedPtr SetField = (*ArrayObject)->TryGetField(TEXT("=")); TArray>* SetContents = nullptr; if (SetField.IsValid() && SetField->TryGetArray(SetContents)) { RemoveValueFromArray(*SetContents, CurrentValue); return true; } } { TSharedPtr AddField = (*ArrayObject)->TryGetField(TEXT("+")); TArray>* AddContents = nullptr; if (AddField.IsValid() && AddField->TryGetArray(AddContents)) { RemoveValueFromArray(*AddContents, CurrentValue); // remove the "+" array if it's now empty, since it's unnecessary if (AddContents->Num() == 0) { (*ArrayObject)->RemoveField(TEXT("+")); } } } // if there's a value set in the parent, then it might have been removed in this config, so we need to remove that TSharedPtr ParentValue; if (ParentConfig.IsValid()) { if (ParentConfig->TryGetJsonValue(Path, ParentValue)) { TSharedPtr RemoveField = (*ArrayObject)->TryGetField(TEXT("-")); TArray>* RemoveContents = nullptr; if (RemoveField.IsValid() && RemoveField->TryGetArray(RemoveContents)) { RemoveValueFromArray(*RemoveContents, ParentValue); // remove the "-" array if it's now empty, since it's unnecessary if (RemoveContents->Num() == 0) { (*ArrayObject)->RemoveField(TEXT("-")); } } } } } return true; } (*CurrentObject)->RemoveField(LastPart.Name); return true; } static bool ShouldAlwaysKeep(const FJsonPath& Path) { static const TCHAR* AlwaysKeep[] = { TEXT("$type") }; if (Path.Length() > 0) { const FJsonPath::FPart& LastPart = Path[Path.Length() - 1]; for (const TCHAR* Key : AlwaysKeep) { if (LastPart.Name == Key) { return true; } } } return false; } bool FJsonConfig::SetJsonValue(const FJsonPath& Path, const TSharedPtr& NewValue) { TSharedPtr PreviousValue; TryGetJsonValue(Path, PreviousValue); TSharedPtr MergedValue = FJsonValue::Duplicate(NewValue); if (!SetJsonValueInMerged(Path, MergedValue)) { return false; } TSharedPtr ParentValue; // this was a valid change in the merged object // we need to either add or remove it from this object, depending on if it differs from our parent bool bShouldRemove = false; if (!ShouldAlwaysKeep(Path)) { if (ParentConfig.IsValid()) { if (ParentConfig->TryGetJsonValue(Path, ParentValue)) { if (FJsonValue::CompareEqual(*ParentValue.Get(), *NewValue.Get())) { // same as inherited, remove it from this bShouldRemove = true; } } } } if (bShouldRemove) { return RemoveJsonValueFromOverride(Path, PreviousValue); } TSharedPtr OverrideValue = FJsonValue::Duplicate(NewValue); if (!SetJsonValueInOverride(Path, OverrideValue, PreviousValue, ParentValue)) { return false; } OnConfigChanged.ExecuteIfBound(); return true; } bool FJsonConfig::SetString(const FJsonPath& Path, const FText& Value) { TSharedPtr JsonValue = MakeShared(Value.ToString()); return SetJsonValue(Path, JsonValue); } bool FJsonConfig::SetString(const FJsonPath& Path, FStringView Value) { TSharedPtr JsonValue = MakeShared(FString(Value)); return SetJsonValue(Path, JsonValue); } bool FJsonConfig::SetBool(const FJsonPath& Path, bool Value) { TSharedPtr JsonValue = MakeShared(Value); return SetJsonValue(Path, JsonValue); } bool FJsonConfig::SetJsonObject(const FJsonPath& Path, const TSharedPtr& Object) { TSharedPtr JsonValue = MakeShared(Object); return SetJsonValue(Path, JsonValue); } bool FJsonConfig::SetJsonArray(const FJsonPath& Path, const TArray>& Array) { TSharedPtr JsonValue = MakeShared(Array); return SetJsonValue(Path, JsonValue); } bool FJsonConfig::HasOverride(const FJsonPath& Path) const { if (!IsValid()) { return false; } if (!Path.IsValid()) { return false; } const TSharedPtr* CurrentObject = &OverrideObject; for (int32 Idx = 0; Idx < Path.Length() - 1; ++Idx) { const TSharedPtr* NextValue = FindPartInObject(*CurrentObject, Path[Idx]); if (NextValue == nullptr || !(*NextValue)->TryGetObject(CurrentObject)) { return false; } } const TSharedPtr* LastValue = FindPartInObject(*CurrentObject, Path[Path.Length() - 1]); if (LastValue == nullptr) { return false; } return true; } bool FJsonConfig::SetRootObject(const TSharedPtr& Object) { if (!Object.IsValid()) { return false; } TArray NewKeys; Object->Values.GenerateKeyArray(NewKeys); TArray ExistingKeys; OverrideObject->Values.GenerateKeyArray(ExistingKeys); for (const TPair>& Pair : Object->Values) { FJsonPath Path(Pair.Key); SetJsonValue(Path, Pair.Value); } // remove all keys that are not in the new object for (const FString& Key : ExistingKeys) { if (!NewKeys.Contains(Key)) { OverrideObject->Values.Remove(Key); } } MergeThisWithParent(); OnConfigChanged.ExecuteIfBound(); return true; } }