Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
694676f
Add 'Reset to Defaults' button for mod configs
ryan-skabelund May 20, 2025
5a8cec2
Improved Reset to Default Functionality
ryan-skabelund Jun 7, 2025
7e499a6
Fix UConfigPropertyClass Reset to Default Crash
ryan-skabelund Jun 7, 2025
b7af332
Disable reset button with tooltip when settings are default
ryan-skabelund Jul 24, 2025
90be8fd
Update config property editor tooltips. Mark bParentSectionAllowsUser…
budak7273 Aug 3, 2025
c7a1b68
Prevent DefaultValue from appearing in the editor. Update editor tool…
budak7273 Aug 3, 2025
f317a4f
Unset 'Hidden' on ExampleMod root config property (how in the world h…
budak7273 Aug 3, 2025
26d0ed7
Fix enum default value display to use name instead of integer
ryan-skabelund Aug 3, 2025
601b096
Add section reset and collapse buttons
ryan-skabelund Aug 3, 2025
1dcdbb3
Tooltip for expand/collapse section button
budak7273 Aug 3, 2025
1656eb6
Fix example array elements requiring world reload, add name to first …
budak7273 Aug 3, 2025
276c5cc
Change array element tooltip to display position#, not index
budak7273 Aug 3, 2025
1961290
Switch confirm messages to format text instead of string append for i18n
budak7273 Aug 3, 2025
bf53f82
Fix extra space at the end of "Nested Horizontal" section name
budak7273 Aug 3, 2025
098f292
Fix array tooltip, improve array default value validator
ryan-skabelund Aug 3, 2025
8e4607e
Improve detection for resettable child properties in sections
ryan-skabelund Aug 3, 2025
31c4928
Add collapse/expand button to array widgets
ryan-skabelund Aug 4, 2025
b80eea4
Improve array reset button
ryan-skabelund Aug 4, 2025
936acd5
Config reset button to use `Update Values` instead of `Custom Construct`
ryan-skabelund Aug 4, 2025
b70f6c3
Consistent phrasing: "options". Add a tooltip for when the button is …
budak7273 Aug 6, 2025
b63aa08
Merge branch 'dev' into add-reset-config-button
ryan-skabelund Aug 9, 2025
2633181
Add comments for redundant tooltip cases
ryan-skabelund Aug 9, 2025
baff53b
Add tooltip for when only hidden config options remain resettable
ryan-skabelund Aug 10, 2025
4fca090
Add tooltip for config section with some non-default options that can…
ryan-skabelund Aug 10, 2025
038d6d2
Improve mod/section reset behavior and clarify related tooltips
ryan-skabelund Aug 12, 2025
ca0d54b
Migrate Value to DefaultValue
ryan-skabelund Oct 6, 2025
a914eba
Merge branch 'dev' into add-reset-config-button
ryan-skabelund Oct 6, 2025
2cbe591
Show border when config property section is collapsed
ryan-skabelund Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified Mods/ExampleMod/Content/ExampleModConfiguration.uasset
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified Mods/SML/Content/Interface/UI/Menu/Mods/Widget_Mod.uasset
Binary file not shown.
Binary file modified Mods/SML/Content/Interface/UI/Menu/Mods/Widget_ModConfig.uasset
Binary file not shown.
Binary file modified Mods/SML/Content/Interface/UI/Menu/Mods/Widget_ModList.uasset
Binary file not shown.
44 changes: 28 additions & 16 deletions Mods/SML/Source/SML/Private/Configuration/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,28 @@ const TCHAR* SMLConfigModVersionField = TEXT("SML_ModVersion_DoNotChange");

void UConfigManager::ReloadModConfigurations() {
UE_LOG(LogConfigManager, Display, TEXT("Reloading mod configurations..."));

for (const TPair<FConfigId, FRegisteredConfigurationData>& Pair : Configurations) {
LoadConfigurationInternal(Pair.Key, Pair.Value.RootValue, true);
}
}

void UConfigManager::SaveConfigurationInternal(const FConfigId& ConfigId) {
const FRegisteredConfigurationData& ConfigurationData = Configurations.FindChecked(ConfigId);

const URootConfigValueHolder* RootValue = ConfigurationData.RootValue;
URawFormatValue* RawFormatValue = RootValue->GetWrappedValue()->Serialize(GetTransientPackage());
checkf(RawFormatValue, TEXT("Root RawFormatValue returned NULL for config %s"), *ConfigId.ModReference);

//Root value should always be JsonObject, since root property is section property
const TSharedPtr<FJsonValue> JsonValue = FJsonRawFormatConverter::ConvertToJson(RawFormatValue);
check(JsonValue->Type == EJson::Object);
TSharedRef<FJsonObject> UnderlyingObject = JsonValue->AsObject().ToSharedRef();

//Record mod version so we can keep file system file schema up to date
FModInfo ModInfo;
UModLoadingLibrary* ModLoadingLibrary = GetGameInstance()->GetSubsystem<UModLoadingLibrary>();

if (ModLoadingLibrary->GetLoadedModInfo(ConfigId.ModReference, ModInfo)) {
const FString ModVersion = ModInfo.Version.ToString();
UnderlyingObject->SetStringField(SMLConfigModVersionField, ModVersion);
Expand All @@ -57,7 +57,7 @@ void UConfigManager::SaveConfigurationInternal(const FConfigId& ConfigId) {
const FString ConfigurationFilePath = GetConfigurationFilePath(ConfigId);
//Make sure configuration directory exists
FPlatformFileManager::Get().GetPlatformFile().CreateDirectoryTree(*FPaths::GetPath(ConfigurationFilePath));

if (!FFileHelper::SaveStringToFile(JsonOutputString, *ConfigurationFilePath)) {
UE_LOG(LogConfigManager, Error, TEXT("Failed to save configuration file to %s"), *ConfigurationFilePath);
return;
Expand Down Expand Up @@ -132,8 +132,8 @@ void UConfigManager::OnTimerManagerAvailable(FTimerManager* TimerManager) {
}

void UConfigManager::OnConfigMarkedDirty(FTimerManager* TimerManager) {
//Setup a timer which will force all changes into filesystem every in .2 seconds
//Setup a timer which will force all changes into filesystem every in .2 seconds

// I dont like this .. Creating a UObject here ..
// We want a Timer to Stop too frequent writing from incoming changes
// some way to get world context from MarkConfigurationDirty would be needed to reliably use Timers for this
Expand All @@ -154,7 +154,7 @@ void UConfigManager::ReinitializeCachedStructs(const FConfigId& ConfigId) {
#if OPTIMIZE_FILL_CONFIGURATION_STRUCT
const FRegisteredConfigurationData& ConfigurationData = Configurations.FindChecked(ConfigId);
URootConfigValueHolder* RootConfigValue = ConfigurationData.RootValue;

for (const TPair<UScriptStruct*, FReflectedObject>& Pair : ConfigurationData.CachedValues) {
RootConfigValue->GetWrappedValue()->FillConfigStructSelf(Pair.Value);
}
Expand Down Expand Up @@ -191,12 +191,24 @@ void UConfigManager::FillConfigurationStruct(const FConfigId& ConfigId, const FD
#endif
}

bool UConfigManager::ResetToDefault(const FConfigId& ConfigId) {
UConfigPropertySection* UserRootSection = GetConfigurationRootSection(ConfigId);
if (!UserRootSection) {
return false;
}
bool bReset = UserRootSection->ResetToDefault();
if (bReset) {
MarkConfigurationDirty(ConfigId);
}
return bReset;
}

UUserWidget* UConfigManager::CreateConfigurationWidget(const FConfigId& ConfigId, UUserWidget* Outer) {
FRegisteredConfigurationData* ConfigurationData = Configurations.Find(ConfigId);
if (ConfigurationData == NULL) {
return NULL;
}

UConfigPropertySection* RootValue = ConfigurationData->RootValue->GetWrappedValue();
return RootValue->CreateEditorWidget(Outer);
}
Expand All @@ -207,14 +219,14 @@ void UConfigManager::ReplaceConfigurationClass(FRegisteredConfigurationData* Exi

//Create new root section value from new configuration class
URootConfigValueHolder* RootConfigValueHolder = ExistingData->RootValue;

//Replace wrapped configuration section value with new root section, replace old configuration class
RootConfigValueHolder->UpdateWrappedValue(NewConfiguration.GetDefaultObject()->RootSection);
ExistingData->ConfigurationClass = NewConfiguration;

//Populate new configuration with data from previous one
RootConfigValueHolder->GetWrappedValue()->Deserialize(TempDataObject);

//Refresh all cached struct values with new data
ReinitializeCachedStructs(ExistingData->ConfigId);

Expand All @@ -238,7 +250,7 @@ bool IsCompatibleConfigurationClassChange(UClass* OldConfigurationClass, UClass*
}
}
#endif

//Otherwise, replace is not compatible
return false;
}
Expand All @@ -262,12 +274,12 @@ void UConfigManager::RegisterModConfiguration(TSubclassOf<UModConfiguration> Con
ReplaceConfigurationClass(ExistingData, Configuration);
return;
}

//Create root value and wrap it into config root handling marking config dirty
URootConfigValueHolder* RootConfigValueHolder = NewObject<URootConfigValueHolder>(this);
RootConfigValueHolder->SetupRootValue(this, ConfigId);
RootConfigValueHolder->UpdateWrappedValue(Configuration.GetDefaultObject()->RootSection);

//Register configuration inside all of the internal properties
Configurations.Add(ConfigId, FRegisteredConfigurationData{ConfigId, Configuration, RootConfigValueHolder});

Expand All @@ -287,7 +299,7 @@ UConfigPropertySection* UConfigManager::GetConfigurationRootSection(const FConfi

void UConfigManager::Initialize(FSubsystemCollectionBase& Collection) {
Collection.InitializeDependency<UModLoadingLibrary>();

//Subscribe to exit event so we make sure that pending saves are written to filesystem
FCoreDelegates::OnPreExit.AddUObject(this, &UConfigManager::FlushPendingSaves);
//Subscribe to timer manager availability delegate to be able to do periodic auto-saves
Expand Down
37 changes: 37 additions & 0 deletions Mods/SML/Source/SML/Private/Configuration/ConfigProperty.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,43 @@ void UConfigProperty::FillConfigStruct_Implementation(const FReflectedObject& Re
checkf(false, TEXT("FillConfigStruct not implemented"));
}

bool UConfigProperty::ResetToDefault_Implementation() {
checkf(false, TEXT("ResetToDefault not implemented"));
return false;
}

bool UConfigProperty::CanResetNow() const {
// Return false if this property does allow a user to reset it
if (!bAllowUserReset || !bParentSectionAllowsUserReset) {
return false;
}
// Return true if this property does not require a world reload
if (!bRequiresWorldReload) {
return true;
}
// Assume we can reset if for whatever reason we can't get the world
UWorld* World = GetWorld();
if (!World) {
return true;
}
// Check whether or not the user is in the main menu since requires world reload is enabled
// GetAuthGameMode is a server-only function, but in the case we're not the server, then we know we're definitely not in the main menu anyways
if (AFGGameMode* GameMode = World->GetAuthGameMode<AFGGameMode>()) {
return GameMode->IsMainMenuGameMode();
}
return false;
}

bool UConfigProperty::IsSetToDefaultValue_Implementation() const {
checkf(false, TEXT("IsSetToDefaultValue not implemented"));
return false;
}

FString UConfigProperty::GetDefaultValueAsString_Implementation() const {
checkf(false, TEXT("GetDefaultValueAsString not implemented"));
return TEXT("");
}

FConfigVariableDescriptor UConfigProperty::CreatePropertyDescriptor_Implementation(UConfigGenerationContext* Context, const FString& OuterPath) const {
checkf(false, TEXT("CreatePropertyDescriptor not implemented"));
return FConfigVariableDescriptor{};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
#include "Configuration/Properties/ConfigPropertyArray.h"
#include "Configuration/Properties/ConfigPropertyFloat.h"
#include "Configuration/CodeGeneration/ConfigVariableDescriptor.h"
#include "Configuration/CodeGeneration/ConfigVariableLibrary.h"
#include "Configuration/RawFileFormat/RawFormatValueArray.h"
#include "Reflection/BlueprintReflectedObject.h"

#define LOCTEXT_NAMESPACE "SML"

void UConfigPropertyArray::PostLoad() {
Super::PostLoad();
// Migrate to DefaultValues from Values [Remove only this if statement once migration is no longer needed]
// PostLoad runs before the user config is deserialized, but it still has the values from the CDO. If DefaultValues
// is set to the type default and Values is not, then Values has the old default value that we need to migrate.
if (DefaultValues.Num() == 0 && Values.Num() > 0) {
DefaultValues = Values; // Hand off the pointer, no need to duplicate. Values.Empty() below will not delete the objects
}
// Set initial value to default value. This runs before the user config is deserialized
// and ensures that if the user has never set a value, it's set to the default.
Values.Empty(DefaultValues.Num());
for (UConfigProperty* Property : DefaultValues) {
if (Property) {
UConfigProperty* Clone = DuplicateObject<UConfigProperty>(Property, this);
Values.Add(Clone);
} else {
Values.Add(nullptr);
}
}
}

UConfigProperty* UConfigPropertyArray::AddNewElement() {
checkf(DefaultValue, TEXT("Cannot add new element without default value defined"));
UConfigProperty* NewValueProperty = NewObject<UConfigProperty>(this, DefaultValue->GetClass(), NAME_None, RF_NoFlags, DefaultValue);
Expand Down Expand Up @@ -52,7 +74,7 @@ EDataValidationResult UConfigPropertyArray::IsDataValid(TArray<FText>& Validatio
FText::FromString(ConfigProperty ? ConfigProperty->GetClass()->GetName() : TEXT("None"))));
ValidationResult = EDataValidationResult::Invalid;
}
}
}
return ValidationResult;
}
#endif
Expand Down Expand Up @@ -80,9 +102,18 @@ void UConfigPropertyArray::Deserialize_Implementation(const URawFormatValue* Val
//Just iterate raw format array and deserialize each of its items
for (URawFormatValue* RawFormatValue : SerializedArray->GetUnderlyingArrayRef()) {
UConfigProperty* AllocatedValue = AddNewElement();
if (!bAllowUserReset || !bParentSectionAllowsUserReset) {
AllocatedValue->bParentSectionAllowsUserReset = false;
}
AllocatedValue->Deserialize(RawFormatValue);
}
}
// Set default values to inherit Allow User Reset
for (UConfigProperty* Property : DefaultValues) {
if (Property && (!bAllowUserReset || !bParentSectionAllowsUserReset)) {
Property->bParentSectionAllowsUserReset = false;
}
}
}

void UConfigPropertyArray::FillConfigStruct_Implementation(const FReflectedObject& ReflectedObject, const FString& VariableName) const {
Expand All @@ -95,8 +126,67 @@ void UConfigPropertyArray::FillConfigStruct_Implementation(const FReflectedObjec
}
}

void UConfigPropertyArray::HandleMarkDirty_Implementation()
{
bool UConfigPropertyArray::ResetToDefault_Implementation() {
if (!CanResetNow()) {
return false;
}
Values.Empty(DefaultValues.Num());
for (UConfigProperty* Property : DefaultValues) {
if (Property) {
UConfigProperty* Clone = DuplicateObject<UConfigProperty>(Property, this);
Values.Add(Clone);
} else {
Values.Add(nullptr);
}
}
MarkDirty();
return true;
}

bool UConfigPropertyArray::IsSetToDefaultValue_Implementation() const {
if (Values.Num() != DefaultValues.Num()) {
return false;
}
for (int32 i = 0; i < Values.Num(); i++) {
const UConfigProperty* UserProperty = Values[i];
const UConfigProperty* DefaultProperty = DefaultValues.IsValidIndex(i) ? DefaultValues[i] : nullptr;
if (!UserProperty || !DefaultProperty) {
return false;
}
// Check if the classes match and if the values are equal
if (UserProperty->GetClass() != DefaultProperty->GetClass()) {
return false;
}
// Special case for float properties to use FMath::IsNearlyEqual for comparison
if (const UConfigPropertyFloat* UserFloat = Cast<UConfigPropertyFloat>(UserProperty)) {
const UConfigPropertyFloat* DefaultFloat = Cast<UConfigPropertyFloat>(DefaultProperty);
if (!DefaultFloat || !FMath::IsNearlyEqual(UserFloat->Value, DefaultFloat->Value, SMALL_NUMBER)) {
return false;
}
}
// For other property types, compare describe values
else if (UserProperty->DescribeValue() != DefaultProperty->DescribeValue()) {
return false;
}
}
// MarkDirty();
return true;
}

FString UConfigPropertyArray::GetDefaultValueAsString_Implementation() const {
// A bit hacky, but Property->GetDefaultValueAsString() does not work here as the default
// values are not initialized within the DefaultValues array for whatever reason.
return FString::JoinBy(DefaultValues, TEXT(", "), [](const UConfigProperty* Property) -> FString {
if (!Property) {
return TEXT("null");
}
const FString d = Property->DescribeValue();
int32 i = d.Find(TEXT(" "));
return (i == INDEX_NONE || d.Len() < 2) ? d : d.Mid(i + 1, d.Len() - i - 2);
});
}

void UConfigPropertyArray::HandleMarkDirty_Implementation() {
MarkDirty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@
#include "Configuration/RawFileFormat/RawFormatValueBool.h"
#include "Reflection/BlueprintReflectedObject.h"

UConfigPropertyBool::UConfigPropertyBool() {
this->Value = false;
void UConfigPropertyBool::PostLoad() {
Super::PostLoad();
// Migrate to DefaultValue from Value [Remove only this if statement once migration is no longer needed]
// PostLoad runs before the user config is deserialized, but it still has the values from the CDO. If DefaultValue
// is set to the type default and Value is not, then Value has the old default value that we need to migrate.
if (DefaultValue == false && Value != DefaultValue) {
DefaultValue = Value;
}
// Set initial value to default value. This runs before the user config is deserialized
// and ensures that if the user has never set a value, it's set to the default.
Value = DefaultValue;
}

FString UConfigPropertyBool::DescribeValue_Implementation() const {
Expand All @@ -29,6 +38,23 @@ void UConfigPropertyBool::FillConfigStruct_Implementation(const FReflectedObject
ReflectedObject.SetBoolProperty(*VariableName, Value);
}

bool UConfigPropertyBool::ResetToDefault_Implementation() {
if (!CanResetNow()) {
return false;
}
Value = DefaultValue;
MarkDirty();
return true;
}

bool UConfigPropertyBool::IsSetToDefaultValue_Implementation() const {
return Value == DefaultValue;
}

FString UConfigPropertyBool::GetDefaultValueAsString_Implementation() const {
return DefaultValue ? TEXT("true") : TEXT("false");
}

FConfigVariableDescriptor UConfigPropertyBool::CreatePropertyDescriptor_Implementation(UConfigGenerationContext* Context, const FString& OuterPath) const {
return UConfigVariableLibrary::MakeConfigVariablePrimitive(EConfigVariableType::ECVT_Bool);
}
Loading
Loading