Saving assets as text

I did some research into this and implemented two related features for my project:

  1. When saving a .uasset file it will dump it to a text file (using the same API that the editor’s diff tool uses) that sits right next to the .uasset file. This way you can commit it to version control and your history will show somewhat useful information. Keep in mind this doesn’t work well for some Blueprints as I’ve found occasionally a bunch of stuff will change in the text representation even though I made one tiny change to the Blueprint.
  2. When right clicking on an asset in the context drawer there’s a “Dump to text file” menu which will dump it to a temporary file and open it in your default .txt editor.

Here’s the code in a UE plugin. Note that right now #1 above is filtered to only run on UMassEntityConfigAsset but you can easily change this in the TODO below.

#include "ProjectM.h"

#include <ContentBrowserModule.h>
#include <AssetToolsModule.h>
#include <Kismet/KismetSystemLibrary.h>
#include "UObject/ObjectSaveContext.h"
#include <MassEntityConfigAsset.h>

#define LOCTEXT_NAMESPACE "FProjectMModule"

static void DumpToTextFile(const FAssetData& SelectedAsset)
{
	UObject* Asset = SelectedAsset.GetAsset();
	FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools");
	FString TempFilename = AssetToolsModule.Get().DumpAssetToTempFile(Asset);
	FPlatformProcess::LaunchFileInDefaultExternalApplication(*TempFilename);
}

static void AddDumpToFileMenuEntry(FMenuBuilder& MenuBuilder, const TArray<FAssetData> SelectedAssets)
{
	MenuBuilder.BeginSection("ProjetM Asset Context", LOCTEXT("ASSET_CONTEXT", "ProjectM"));
	{
		MenuBuilder.AddMenuEntry(
			LOCTEXT("DumpToTextFileLabel", "Dump to text file"),
			LOCTEXT("DumpToTextFileTooltip", "Dump asset to text file"),
			FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Edit"),
			FUIAction(FExecuteAction::CreateLambda([SelectedAssets]()
			{
				DumpToTextFile(SelectedAssets[0]);
			})),
			NAME_None,
			EUserInterfaceActionType::Button);
	}
	MenuBuilder.EndSection();
}

static TSharedRef<FExtender> OnExtendAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets)
{
	TSharedRef<FExtender> Extender = MakeShared<FExtender>();
	Extender->AddMenuExtension(
		"CommonAssetActions",
		EExtensionHook::After,
		nullptr,
		FMenuExtensionDelegate::CreateStatic(&AddDumpToFileMenuEntry, SelectedAssets)
	);
	return Extender;
}

void FProjectMModule::StartupModule()
{
	FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
	TArray<FContentBrowserMenuExtender_SelectedAssets>& CBMenuAssetExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
	CBMenuAssetExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateStatic(&OnExtendAssetSelectionMenu));
	
	UPackage::PackageSavedWithContextEvent.AddStatic(&FProjectMModule::DumpAssetsToTextFileOnSave);
}

// We do this so that we can better leverage git on various .uasset binary files.
/*static*/ void FProjectMModule::DumpAssetsToTextFileOnSave(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext ObjectSaveContext)
{
	TArray<UObject*> Objects;
	GetObjectsWithPackage(Package, Objects, false);
	FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools");
	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
	for (UObject* Asset : Objects)
	{
		// For now only do this for MassEntityConfigAsset. TODO: Add more types here.
		if (!Asset->IsA<UMassEntityConfigAsset>())
		{
			continue;
		}

		FString TempFilename = AssetToolsModule.Get().DumpAssetToTempFile(Asset);
		FString AssetPath = UKismetSystemLibrary::GetSystemPath(Asset);
		FString TextFilePath = FPaths::ChangeExtension(AssetPath, TEXT("txt"));
		const bool bDidCopy = PlatformFile.CopyFile(*TextFilePath, *TempFilename);
		check(bDidCopy);
	}
}

void FProjectMModule::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
	// we call this function before unloading the module.
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FProjectMModule, ProjectM)