Try   HackMD

Unreal Engine

Paths

Windows

Shipping:

c:\Users\%USERNAME%\AppData\Local\<Project Name>\Saved\

Mac

Config:

~/Library/Preferences/Unreal Engine/
~/Library/Preferences/<packaged project name>/

Logs:

~/Library/Logs/Unreal Engine/
~/Library/Logs/<packaged project name>/

Save games/Rest:

~/Library/Application Support/Epic/

Crashes:

~/Library/Application Support/Epic/UnrealEngine/<Engine Version>/Saved/Crashes

Advanced Debugging in Unreal Engine

USTRUCT/UCLASS/UFUNCTION/UPARAM/UPROPERTY

UMG

Resourcces:

GPU

CPU

Configuration Files

The configuration file hierarchy is read in starting with Base.ini, with values in later files in the hierarchy overriding earlier values. All files in the Engine folder will be applied to all projects, while project-specific settings should be in files in the project directory.

Finally, all project-specific and platform-specific differences are saved out to [ProjectDirectory]/Saved/Config/[Platform]/[Category].ini

The below file hierarchy example is for the Engine category of configuration files.

  1. Engine/Config/Base.ini Base.ini is usually empty.

  2. Engine/Config/BaseEngine.ini

  3. Engine/Config/[Platform]/[Platform]Engine.ini

  4. [ProjectDirectory]/Config/DefaultEngine.ini

  5. [ProjectDirectory]/Config/[Platform]/[Platform]Engine.ini

  6. [ProjectDirectory]/Saved/Config/[Platform]/Engine.ini The configuration file in the Saved directory only stores the project-specific and platform-specific differences in the stack of configuration files.

Complete hierarchy

// Possible entries in a config hierarchy
enum class EConfigFileHierarchy : uint8
{
	// Engine/Config/Base.ini
	AbsoluteBase = 0,

	// Engine/Config/*.ini
	EngineDirBase,
	// Engine/Config/Platform/BasePlatform* ini
	EngineDir_BasePlatformParent,
	EngineDir_BasePlatform,
	// Engine/Config/NotForLicensees/*.ini
	EngineDirBase_NotForLicensees,
	// Engine/Config/NoRedist/*.ini -Not supported at this time.
	EngineDirBase_NoRedist,

	// Game/Config/*.ini
	GameDirDefault,
	// Game/Config/DedicatedServer*.ini
	GameDirDedicatedServer,
	// Game/Config/NotForLicensees/*.ini
	GameDirDefault_NotForLicensees,
	// Game/Config/NoRedist*.ini
	GameDirDefault_NoRedist,


	// Engine/Config/PlatformName/PlatformName*.ini
	EngineDir_PlatformParent,
	EngineDir_Platform,
	// Engine/Config/NotForLicensees/PlatformName/PlatformName*.ini
	EngineDir_PlatformParent_NotForLicensees,
	EngineDir_Platform_NotForLicensees,
	// Engine/Config/NoRedist/PlatformName/PlatformName*.ini
	EngineDir_PlatformParent_NoRedist,
	EngineDir_Platform_NoRedist,

	// Game/Config/PlatformName/PlatformName*.ini
	GameDir_PlatformParent,
	GameDir_Platform,
	// Game/Config/NotForLicensees/PlatformName/PlatformName*.ini
	GameDir_PlatformParent_NotForLicensees,
	GameDir_Platform_NotForLicensees,
	// Game/Config/NoRedist/PlatformName/PlatformName*.ini
	GameDir_PlatformParent_NoRedist,
	GameDir_Platform_NoRedist,

	// <UserSettingsDir|AppData>/Unreal Engine/Engine/Config/User*.ini
	UserSettingsDir_EngineDir_User,
	// <UserDir|Documents>/Unreal Engine/Engine/Config/User*.ini
	UserDir_User,
	// Game/Config/User*.ini
	GameDir_User,

	// Number of config files in hierarchy.
	NumHierarchyFiles,
};

In game console commands

What is a Console Variable? See Docs

Priority

Priority for console commands from lower to higher goes like this:

  1. SetByConstructor - When created, default
  2. SetByScalability - From Scalability.ini (lower priority than game settings so it's easier to override partially)
  3. SetByGameSetting - This is usually set from your Game Settings In Game UI or from a file
  4. SetByProjectSetting - Project settings (editor UI or from file, higher priority than game setting to allow to enforce some setting for this project)
  5. SetByDeviceProfile - Per device setting in DeviceProfiles.ini (e.g. specific iOS device, higher priority than per project to do device specific settings).
  6. SetByConsoleVariablesIni - Consolevariables.ini (for multiple projects) set in Engine/Config
  7. SetByCommandline - A minus command e.g. -VSync (very high priority to enforce the setting for the application)
  8. SetByCode - Least useful, likely a hack, maybe better to find the correct SetBy
  9. SetByConsole - Editor UI or console in game or editor

Constructor

See Creating / Registering a Console Variable

Basically ECVF_SetByConstructor === ECVF_Default.

Example:

static TAutoConsoleVariable<int32> CVarNormalMaps(
	TEXT("r.NormalMapsForStaticLighting"),
	0,
	TEXT("Whether to allow any static lighting to use normal maps for lighting computations."),
	ECVF_Default
);

Scalability Config

From Scalability.ini (lower priority than game settings so it’s easier to override partially).

Example:

[ViewDistanceQuality@0]
r.SkeletalMeshLODBias=2
r.ViewDistanceScale=0.4

[ViewDistanceQuality@1]
r.SkeletalMeshLODBias=1
r.ViewDistanceScale=0.6

Game Settings

This is usually set from your Game Settings In Game UI or from a file. The config is usually kept inside GameUserSettings.ini and see the contents of UGameUserSettings.

Examples of console variables that might be set by the game user settings:

r.FullScreenMode
r.VSync
r.HDR.EnableHDROutput

Project Settings

Console Variables Config

NOTE: Engine Only

Command Line

Code

Device Profiles

These console commands can be set in the DeviceProfiles.ini file.

Example:

[Android DeviceProfile]
DeviceType=Android
BaseProfileName=
+CVars=r.MobileContentScaleFactor=1
+CVars=r.BloomQuality=0
+CVars=r.DepthOfFieldQuality=0
+CVars=r.LightShaftQuality=0
+CVars=r.RefractionQuality=0
+CVars=r.ShadowQuality=2
+CVars=slate.AbsoluteIndices=1
+CVars=r.Vulkan.DelayAcquireBackBuffer=0
+CVars=r.Vulkan.RobustBufferAccess=1
+CVars=r.Vulkan.DescriptorSetLayoutMode=2

Console

(back tick) ` - Open the developer console.

This is the highest priority you can set for commands.

# View modes
viewmode <mode>
// Where <mode> can be:
    BrushWireframe - wireframes with brushes (crashes at runtime)
    Wireframe      - wireframes with BSP (crashes at runtime)

    Unlit              - unlit
    Lit                - lit
    Lit_DetailLighting - lit + detaillighting
    LightingOnly       - lit without materials

    LightComplexity    - colored according to light count
    ShaderComplexity   - colored according to shader complexity
    LightmapDensity    - colored according to world-space LightMap texture density

    // Colored according to light count
    LitLightmapDensity
    ReflectionOverride
    VisualizeBuffer

    StationaryLightOverlap - Colored according to stationary light overlap

    // collision
    CollisionPawn
    CollisionVisibility

    LODColoration                    - Colored according to the current LOD index
    QuadOverdraw                     - Colored according to the quad coverage
    PrimitiveDistanceAccuracy        - Visualize the accuracy of the primitive distance
                                       computed for texture streaming
    MeshUVDensityAccuracy            - Visualize the accuracy of the mesh UV densities
                                       computed for texture streaming
    ShaderComplexityWithQuadOverdraw - Colored according to shader complexity, including
                                       quad overdraw
    HLODColoration                   - Colored according to the current HLOD index
    GroupLODColoration               - Group item for LOD and HLOD coloration
    MaterialTextureScaleAccuracy     - Visualize the accuracy of the material texture
                                       scales used for texture streaming
    RequiredTextureResolution        - Compare the required texture resolution to the
                                       actual resolution

ShowFlag.Bones 1                   - shows bones of all stuff that has bones
ShowFlag.StationaryLightOverlap 1  - shows the overlap of stationary ligths
ShowFlag.Bounds 1                  - shows all bounds

# Debug
debug rendercrash | rendergpf - Crash the renderer
debug rendercheck             - Crash the renderthread via check(0) at your request
debug renderensure            - Crash by ensure() condition inside the renderer

debug threadcrash    - Crash inside worker thread via UE_LOG(Fatal)
debug threadcheck    - Crash worker thread via check()
debug threadgpf      - Crash worker thread via invalid memomory assigment
debug twothreadcrash - Crash two threads at once via UE_LOG(Fatal)
debug twothreadgpf   - Crash two threads at once via invalid memomory assigment
debug threadensure   - Crash worker thread via ensure()
debug threadfatal    - Crash thread via LowLevelFatalError()

debug crash        - Crash game via UE_LOG(Fatal)
debug check        - Crash game via check()
debug gpf          - Crash game via invalid memomory assigment
debug ensure       - Crash game via ensure()
debug fatal        - Crash game via LowLevelFatalError()

debug bufferoverrun - Crash via buffer overflow
debug crtinvalid    - Crash via invalid cast
debug eatmem        - Fills up all your computer memory

debug recurse       - Crash via infinite recursion
debug threadrecurse - Crash via infinite recursion in a separate thread

debug stackoverflow       - Crash via stack overflow via infinite recursion
debug threadstackoverflow - Crash via stack overflow in a separate thread

debug hitch         - Hitch the game for 1 second
debug renderhitch   - Hith the render for 1 second
debug softlock      - Hands the current thread
debug infiniteloop  - Hang the CPU forever (infinite loop)
debug sleep         - Sleep for 1 hour. This should crash after a few seconds in cooked builds.

debug audiogpf   - Crash auto thread via invalid memomory assigment
debug audiocheck - Crash audio thread via check()

# Choose graphics card
r.GraphicsAdapter      - User request to pick a specific graphics adapter (e.g. when using a integrated graphics card with a discrete one)
D3D12.GraphicsAdapter  - User request to pick a specific graphics adapter (e.g. when using a integrated graphics card with a discrete one)

# Other
WidgetReflector - opens up the widget reflector (works even in non editor builds)

Command line arguments

When running the game you can append these arguments:

-Windowed       - run in windowed mode
-FullScreen     - run in full screen mode
-ForceRes       - force the resolution
-ResX= | -ResY= - set the X and Y resolution
-WinX= | -WinY= - set the position of the game window in X/Y coordinates
-SaveWinPos     - saves the window position into the game user settings file
-Seconds        - set the maximum tick time
-Verbose        - set compiler to use verbose output. 

-VerifyGC | -NoVerifyGC - enables/disable garbage collection verification every 30s

-StatNamedEvents  - enable named stat events
-LoadTimeStats    - enable load time stats, shortcut for some stat gorup commands
-LoadTimeFile     - enable load time stats to a file, shortcut for some stat gorup commands

-LogThreadedParticleTicking - log the thread of the particles ticking
-VerifyDDC        - verifies the derived data cache (https://docs.unrealengine.com/en-us/Engine/Basics/DerivedDataCache)
-Debug            - running in debug
-NoAmbientActors  - mute ambient sound actors
-HDR | -NoHDR     - enables/disables HDR

-CleanCrashReports       - clean the crash reports
-CrashForUAT             - write UAT markers on crash
-NotInstalled            - similar to NotInstalledEngine
-Installed               - similar to InstalledEngine
-NotInstalledEngine      - tells it that the engine is not installed
-InstalledEngine         - tells that the engine is installed
-WaitForDebugger         - if was specified, halt startup and wait for a debugger to attach before continuing
-PromptRemoteDebug       - Prompt Remote Debug
-SlateDebug              - create slate test windows
-ScriptStackOnWarnings   - Show blueprint script stack for warnings, same config in Engine.ini

-EnableSound | -NoSound    - enables/disables sound
-VSync | -NoVSync           - enables/disables vsync

-OneThread               - runs with only one thread instead of multi-threading
-Threading               - enables threading
-NoThreading             - disables threading
-NoThreadTimeout         - stop hang detection
-NoConsole               - disables console ouput
-No<OnlineSubsystemName> - disables the <OnlineSubsystemName>
-NoTextureStreaming      - disable texture streaming. Highest quality textures are always loaded. 
-NoSplash                - Disable use of splash image when loading the game. 

-CmdLineFile=<cmd.txt>   - Only in Non Shipping and Editor. File to give to the executable with command line arguments
-Exec=<exec_file.txt>    - Executes the specified exec file. 
-ExecCmds="command1; command2;" - Executes deffered commands

-bForceSmokeTests - forces the smoke tests?

-Log              - show the log
-Silent           - disables output and feedback
-WarningsAsErrors - treat warnings as errors
-ForceLogFlush    - force a log flus after each line

-LogTimes      - display the log times in UTC format ELogTimes::UTC
-UTCLogTimes   - same as LogTimes
-NoLogTimes    - disable the log times
-LocalLogTimes - display the log times in local time format ELogTimes::Local
-LogTimeCode   - display the log times as timecode

-stdout                  - Enables the stdout device
-FullStdOutLogOutput      - Display all log levels to stdout
-AllowStdOutLogVerbosity = display only ELogVerbosity::Log to stdout

-NoScreenMessage - disables the screen messages
-NoLoadingScreen - disables the PreLoadingScreen on windows in non shipping builds
-NoEpicPortal    - disables some parts of the epic games launcher (NOTE does not seem to disable it completly)

-EnableAllPlugins                     - enables all the plugins
-ExceptPlugins=<comma separated list> - used in conjuction with -EnableAllPlugins
-NoEnginePlugins                      - disable all the engine plugins

-Benchmark
-FPS=<int> - Used with the benchmark command, fixed delta time

# Curl
-NoReuseConn - don't reuse connections
-NoTimeouts  - don't use timeouts I guess

# Linux
-UseHyperThreading          - enables the use of hyperthreading on linux
-WaitAndForkRequireResponse - wait for forks (fork())

# Steam
-NoSteam - set steamworks to not be used. 

# OpenGl/Vulkan
-OpenglDebug - debug opengl
-Opengl4     - force with opengl 4
-Opengl3     - force with opengl 3
-Opengl      - run with opengl
-Vulkan      - run with vulkan

# DirectX
-dxdebug | -d3debug | d3ddebug  - use a debug device
-d3dbreakonwarning      - breaks on warning, debug
-d3d10 | -dx10 | -sm4   - use directx 10 (default is directx 11)
-d3d11 | -dx11          - use directx 11
-d3d12 | -dx12          - use directx 12

-novendordevice         - apparently only AMD and directx 11
-AllowSoftwareRendering - allow to use software rendering fallback

-PreferAMD    - use AMD card
-PreferIntel  - use Intel card
-PreferNvidia - use Nvidia card

Logs

Project logs

Logging in Shipping Builds

Used by multiple files:

// inside header
DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity);

// inside cpp
DEFINE_LOG_CATEGORY(CategoryName);

Used in only one file, static log:

// inside cpp
DEFINE_LOG_CATEGORY_STATIC(CategoryName, DefaultVerbosity, CompileTimeVerbosity);

Where:

  • CategoryName - is simply the name for the new category you are defining.
  • DefaultVerbosity - is the verbosity level used when one is not specified in the ini files or on the command line. Anything more verbose than this will not be logged.
  • CompileTimeVerbosity - is the maximum verbosity to compile in the code. Anything more verbose than this will not be compiled.

Examples:

// Inside header
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, All, All);

// inside cpp
DEFINE_LOG_CATEGORY(LogMyGame);

// or static used only inside this cpp file
DEFINE_LOG_CATEGORY_STATIC(LogUsedOnlyInsideCPP, All, All);

JSON

NOTE: There is an issue with this serializer where in editor it converts the field to Field but in packaged game it converts the field to field.

#include "IStructSerializerBackend.h"
#include "StructSerializer.h"
#include "StructDeserializer.h"
#include "Backends/JsonStructDeserializerBackend.h"
#include "Serialization/BufferArchive.h"
#include "Serialization/BufferWriter.h"
#include "Backends/JsonStructSerializerBackend.h"

// Serializes a given USTRUCT to a JSON string using the default policy.
// @return A string holding the serialized object in JSON format
template<typename StructType>
static void SerializeToJSON(const StructType& Struct, FString& OutString)
{
    SerializeToJSON(&Struct, *Struct.StaticStruct(), OutString);
}
static void SerializeToJSON(const void* Struct, UStruct& TypeInfo, FString& OutString)
{
    // Read into this
    FBufferArchive BufferArchive;

    // Save to JSON
    const EStructSerializerBackendFlags SerializerFlags = EStructSerializerBackendFlags::Default;
    FStructSerializerPolicies SerializerPolicies;
    FJsonStructSerializerBackend Backend(BufferArchive, SerializerFlags);
    FStructSerializer::Serialize(Struct, TypeInfo, Backend, SerializerPolicies);

    if (BufferArchive.TotalSize() == 0)
    {
        return;
    }

    // Add string terminator
    // NOTE: we need two zeros here otherwise the conversion to string will not be right
    BufferArchive.Add(0);
    BufferArchive.Add(0);

    // Convert to String
    OutString = FString((TCHAR*)BufferArchive.GetData());
}

// Deserializes a given JSON String into a specific struct
// @return true if it could deserialize the json string into the struct
template<typename StructType>
static bool DeserializeFromJSON(const FString& JSONString, StructType& OutStruct)
{
    return DeserializeFromJSON(JSONString, &OutStruct, *StructType::StaticStruct());
}
static bool DeserializeFromJSON(const FString& JSONString, void* OutStruct, UStruct& TypeInfo)
{
    // Read from this
    const bool bInFreeOnClose = false;
    const bool bIsPersistent = false;
    FBufferReader BufferReader((void*)*JSONString, JSONString.Len() * sizeof(TCHAR), bInFreeOnClose, bIsPersistent);

    // Load from JSON
    FJsonStructDeserializerBackend Backend(BufferReader);
    FStructDeserializerPolicies DeserializerPolicies;
    DeserializerPolicies.MissingFields = EStructDeserializerErrorPolicies::Warning;
    return FStructDeserializer::Deserialize(OutStruct, TypeInfo, Backend, DeserializerPolicies);
}

Performance and profiling

Resources:

NOTE: that enabling any stats for live viewing affects the game thread performance

Stat commands

NOTE: To open the *.ue4stats file use the SessionFrontend Profiler Tab

stat <Category>

// Most useful categories
stat Unit      - Overall frame time as well as the game thread, rendering thread, and GPU times. 
stat UnitGraph - To see a graph with the stat unit data

// But to start/stop recording in packaged game
// The stats file is located inide [UE4ProjectFolder]\[ProjectName]\Saved\Profiling\UnrealStats. 
// Open the *.ue4stats file with the SessionFrontend from unreal.
stat StartFile - Starts a statistics capture, creating a new file in the Profiling directory. 
stat StopFile  - Finishes statistics capture that was started by the stat StartFile command, closing the file that was created in the Profiling directory. 

Memory profiling

https://pzurita.wordpress.com/2015/02/10/limitations-of-memory-tracking-features-in-unreal-engine-4/

Adding own stat categories in C++

See whole docs in Runtime/Core/Public/Stats/Stats.h

Stats system in the UE4 supports following stats types:

  • Cycle Counter - a generic cycle counter used to counting the number of cycles during the lifetime of the object
  • Float/Dword Counter - a counter that is cleared every frame
  • Float/Dword Accumulator - a counter that is not cleared every frame, persistent stat, but it can be reset
  • Memory - a special type of counter that is optimized for memory tracking

Each stat needs to be grouped, this usually corresponds with displaying the specified stat group i.e. 'stat statsystem' which displays stats' related data.

To define a stat group you need to use one of the following methods:

  • DECLARE_STATS_GROUP(GroupDesc, GroupId, GroupCat) - declares a stats group which is enabled by default
  • DECLARE_STATS_GROUP_VERBOSE(GroupDesc, GroupId ,GroupCat) - declares a stats group which is disabled by default
  • DECLARE_STATS_GROUP_MAYBE_COMPILED_OUT(GroupDesc, GroupId, GroupCat) - declares a stats group which is disabled by default and may be stripped by the compiler

where

  • GroupDesc is a text description of the group
  • GroupId is an UNIQUE id of the group
  • GroupCat is reserved for future use
  • CompileIn if set to true, the compiler may strip it out

Example:

DECLARE_STATS_GROUP(TEXT("Threading"), STATGROUP_Threading, STATCAT_Advanced);

Now, you can declare/define a stat.

A stat can be used only in one cpp file, in the function scope, in the module scope or can be used in the whole project.

For one file scope you need to use one of the following methods depending on the stat type.

  • DECLARE_CYCLE_STAT(CounterName, StatId, GroupId) - declares a cycle counter stat

  • DECLARE_SCOPE_CYCLE_COUNTER(CounterName, StatId, GroupId) - declares a cycle counter stat and uses it at the same time, it is limited to one function scope

  • QUICK_SCOPE_CYCLE_COUNTER(StatId) - declares a cycle counter stat that will belong to stat group called 'Quick'

where

  • CounterName is a text description of the stat
  • StatId is an UNIQUE id of the stat
  • GroupId is an id of the group that the stat will belong to, the GroupId from DECLARE_STATS_GROUP*
  • Pool is a platform specific memory pool, more details later
  • API is the *_API of module, can be empty if the stat will be used only in that module

Example:

// Declare the stat grup in the header of the file or at the top of the cpp file
DECLARE_STATS_GROUP(TEXT("Threading"), STATGROUP_Threading, STATCAT_Advanced);

// You can declare the stat counter at the top of the file as the category like this
DECLARE_CYCLE_STAT(TEXT("Threading Test"), STAT_ThreadingTest, STATGROUP_Threading);

// In the cpp file
Class::Method()
{
    SCOPE_CYCLE_COUNTER(STAT_ThreadingTest);
    // OR
    // You can declare and use in the same function with 
    DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Threading Test"), STAT_ThreadingTest, STATGROUP_Threading);
}

Debugging

Useful development

(back tick) ` - Open the developer console.

(apostrophe) ' - Open the gameplay debugger

F1 - F5 - change different view modes

Open the WidgetReflector - type in the developer console WidgetReflector

Blueprints

  • FFrame::KismetExecutionMessage
  • FMessageLog
  • FBlueprintCoreDelegates::ThrowScriptException
  • FScriptStackTracker

C++

  • FStackTracker
  • FDebug::DumpStackTraceToLog(VerbosityLevel)

Crashes

NOTE: To create a fake crash, type this into the console in game: debug crash

NOTE: To force log callstacks you can add -ForceLogCallstacks to the command line.

Steps:

  1. Go to Saved/Crashes/<Crash Folder>
  2. You can ignore the log file from the Crashes folder as it is not complete, check instead Saved/Logs/<Log file> this will have the call stack at the end.
  3. Open the project in Visual Studio then open the UE4Minidump.dmp file.
  4. Start debugging it via clicking on Debug with Native Only

.dmp files

NOTE: if you built the pdb file in another day than the exe was built, use this tool https://web.archive.org/web/20210205095232/https://www.debuginfo.com/tools/chkmatch.html

chkmatch -c .\Game-Win64-Shipping.exe .\Game-Win64-Shipping.pdb
chkmatch -m .\Game-Win64-Shipping.exe .\Game-Win64-Shipping.pdb

Visual studio

  1. Open the .dmp file inside VS
  2. Set the pdb/exe file path.
  3. Run the file

WinDBG

  1. Open the .dmp file inside VS
  2. Add the the debug files to the sympath .sympath+ c:\dev\illuvium\game-client\Packaged\RELEASE-Illuvium-PrivateBeta1-Win_24_May_2022\Illuvium\Binaries\Win64\
  3. .reload

Customize Structs

From Runtime/CoreUObject/Private/UObject/Class.cpp

// sample of how to customize structs
USTRUCT()
struct ENGINE_API FTestStruct
{
	GENERATED_USTRUCT_BODY()

	TMap<int32, double> Doubles;
	FTestStruct()
	{
		Doubles.Add(1, 1.5);
		Doubles.Add(2, 2.5);
	}
	void AddStructReferencedObjects(class FReferenceCollector& Collector) const
	{
		Collector.AddReferencedObject(AActor::StaticClass());
	}
	bool Serialize(FArchive& Ar)
	{
		Ar << Doubles;
		return true;
	}
	bool operator==(FTestStruct const& Other) const
	{
		if (Doubles.Num() != Other.Doubles.Num())
		{
			return false;
		}
		for (TMap<int32, double>::TConstIterator It(Doubles); It; ++It)
		{
			double const* OtherVal = Other.Doubles.Find(It.Key());
			if (!OtherVal || *OtherVal != It.Value() )
			{
				return false;
			}
		}
		return true;
	}
	bool Identical(FTestStruct const& Other, uint32 PortFlags) const
	{
		return (*this) == Other;
	}
	void operator=(FTestStruct const& Other)
	{
		Doubles.Empty(Other.Doubles.Num());
		for (TMap<int32, double>::TConstIterator It(Other.Doubles); It; ++It)
		{
			Doubles.Add(It.Key(), It.Value());
		}
	}
	bool ExportTextItem(FString& ValueStr, FTestStruct const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const
	{
		ValueStr += TEXT("(");
		for (TMap<int32, double>::TConstIterator It(Doubles); It; ++It)
		{
			ValueStr += FString::Printf( TEXT("(%d,%f)"),It.Key(), It.Value());
		}
		ValueStr += TEXT(")");
		return true;
	}
	bool ImportTextItem( const TCHAR*& Buffer, int32 PortFlags, UObject* Parent, FOutputDevice* ErrorText )
	{
		check(*Buffer == TEXT('('));
		Buffer++;
		Doubles.Empty();
		while (1)
		{
			const TCHAR* Start = Buffer;
			while (*Buffer && *Buffer != TEXT(','))
			{
				if (*Buffer == TEXT(')'))
				{
					break;
				}
				Buffer++;
			}
			if (*Buffer == TEXT(')'))
			{
				break;
			}
			int32 Key = FCString::Atoi(Start);
			if (*Buffer)
			{
				Buffer++;
			}
			Start = Buffer;
			while (*Buffer && *Buffer != TEXT(')'))
			{
				Buffer++;
			}
			double Value = FCString::Atod(Start);

			if (*Buffer)
			{
				Buffer++;
			}
			Doubles.Add(Key, Value);
		}
		if (*Buffer)
		{
			Buffer++;
		}
		return true;
	}
	bool SerializeFromMismatchedTag(struct FPropertyTag const& Tag, FArchive& Ar)
	{
		// no example of this provided, doesn't make sense
		return false;
	}
};

template<>
struct TStructOpsTypeTraits<FTestStruct> : public TStructOpsTypeTraitsBase2<FTestStruct>
{
	enum 
	{
		WithZeroConstructor = true,
		WithSerializer = true,
		WithPostSerialize = true,
		WithCopy = true,
		WithIdenticalViaEquality = true,
		//WithIdentical = true,
		WithExportTextItem = true,
		WithImportTextItem = true,
		WithAddStructReferencedObjects = true,
		WithSerializeFromMismatchedTag = true,
	};
};