Consider a UStruct with some basic type members, each one with a default value specified in the struct. Also consider a TSet or TMap with that struct as keys, used inside a UObject as a UProperty and marked with "Config". When SaveConfig() is called on the owner object, the serialization code attempts to determine, for each struct entry in the container, which of its members are different from their default values, and writes only them to the output file, while omitting the ones that are identical to the defaults. An issue arises for the keys of a TMap and for the sole entry of a TSet with a single element: the diff is performed against default-constructed struct members (always zero for numeric types, for example), instead of against a default-constructed instance of the struct. This results in the wrong value being loaded later when calling LoadConfig().
Note that, in addition to user-defined UObjects, this also affects built-in classes, such as the Remote Control plugin's "URemoteControlSettings" class, whose member "AllowlistedClients" is of type "TSet<FRCNetworkAddressRange>".
The problem has been tracked down by the user that reported the issue. Function UObject::SaveConfig() calls FSetProperty::ExportText_Internal() for single-element sets and FMapProperty::ExportText_Internal() for maps. Then, if "PPF_ExternalEditor" and "PPF_BlueprintDebugView" are not set, they call ExportTextItem_Direct() on the set items and ExportText_Internal() on the map pairs passing nullptr as the default value for diffing. Other code paths determine a PropDefault and pass it along for diffing.
Some relevant code lines in UE 5.5:
[Engine\Source\Runtime\CoreUObject\Private\UObject\Obj.cpp:3457] - Single-element set test
[Engine\Source\Runtime\CoreUObject\Private\UObject\PropertySet.cpp:614] - ExportTextItem_Direct() called with nullptr for default value
[Engine\Source\Runtime\CoreUObject\Private\UObject\PropertyMap.cpp:1000] - ExportText_Internal() called with nullptr for default value
=== Repro A ===
1. Open the repro project
2. Click on "MyActor" on the Outliner
3. On the Details panel, observe category "CFG"
3.1. A struct with 4 int32 members (A,B,C,D) is used in 4 places:
3.1.1. Inside a TArray
3.1.2. Inside a TSet
3.1.3. As keys of a TMap
3.1.4. As values of a TMap
3.2. Each struct member is an int32 with default 0
3.3. The struct itself has default (1,1,1,1)
3.4. The values stored in the actor are all (0,0,0,0)
4. Click on "Save Config"
5. Open file "<PROJECT>/Saved/Config/MyConfig.ini". Some contents:
5.1. MyArray=(A=0,B=0,C=0,D=0)
5.2. MySet=(())
5.3. MyMapKeys=(((), 0))
5.4. MyMapValues=((0, (A=0,B=0,C=0,D=0)))
6. Click on "Load Config"
6.1. Correct: The struct members on the TArray and the values of the TMap will remain (0,0,0,0)
6.2. Incorrect: The struct members on the TSet and the keys of the TMap will become (1,1,1,1)
7. On the Details panel, change all struct values to (1,1,1,1)
8. Click on "Save Config"
9. Open file "<PROJECT>/Saved/Config/MyConfig.ini". Some contents could be omitted but are not:
9.1. MyArray=(A=1,B=1,C=1,D=1)
9.2. MySet=((A=1,B=1,C=1,D=1))
9.3. MyMapKeys=(((A=1,B=1,C=1,D=1), 0))
9.4. MyMapValues=((0, ()))
=== Repro B ===
1. Open the repro project
2. Edit – Project Settings – Plugins – Remote Control – Remote Control – Security
2.1. Enable "Restrict Server Access"
2.2. Change "Range of Allowlisted Clients – Index [0] – Lower Bound" to (127.0.0.1)
2.3. Click button "Set as Default" on the top of the settings page. Confirm.
2.4. Incorrect: The changed "Lower Bound" becomes (127.168.1.1)
2.4.1. The default value of the struct is (192.168.1.1)
2.4.2. Both zeroes in (127.0.0.1) were not saved on the config file for being defaults of the uint8 type
2.4.3. The last "1" was saved on the config file, which is ok but was not needed
There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-230676 in the post.
1 |
Component | UE - Foundation - Core |
---|---|
Affects Versions | 5.5, 5.4.4 |
Target Fix | 5.6 |
Created | Nov 18, 2024 |
---|---|
Updated | Nov 27, 2024 |