|
/** |
|
* Row structure for our data table. |
|
* The row name must be unique - this is the first column by default, unless otherwise specified on import. |
|
* |
|
* Note that if we're importing a DataTable from CSV, the column header (with spaces removed) must match the |
|
* property name *exactly*, for example: |
|
* - Column header: `Some Data` |
|
* - Property name: `SomeData` |
|
*/ |
|
struct FMyDataTableRow |
|
{ |
|
/** Some data */ |
|
UPROPERTY() |
|
int SomeData = 0; |
|
|
|
/** Some other data */ |
|
UPROPERTY() |
|
bool bMyBool = false; |
|
}; |
|
|
|
|
|
/** |
|
* A custom Data Asset that we will create & fill from our Data Table's row data. |
|
*/ |
|
class UMyDataAsset : UDataAsset |
|
{ |
|
/** We use this to track exactly which asset was created, since Key values must be unique */ |
|
UPROPERTY() |
|
FName Key; |
|
|
|
/** Some data */ |
|
UPROPERTY() |
|
int SomeData = 0; |
|
|
|
/** Some other data */ |
|
UPROPERTY() |
|
bool bMyBool = false; |
|
}; |
|
|
|
|
|
/** |
|
* Editor utility that creates assets from a data table. |
|
* Some functions in this Angelscript class are stubs that need to be implemented in a Blueprint subclass, |
|
* because the functionality is not (yet) exposed to the Angelscript bindings. |
|
* |
|
* So, to fully implement this, we need to create a Blueprint class based on this class, and implement the |
|
* following functions, using `AssetTools` and `AssetRegistry` as needed: |
|
* - `CreateAsset` |
|
* - `GetRowData` |
|
* - `GetExistingAssets` |
|
*/ |
|
class UAssetFromDataTableUtility : UAssetActionUtility |
|
{ |
|
/** |
|
* Only show the action utility when right-clicking on DataTable assets in the Content Browser. |
|
*/ |
|
UFUNCTION(BlueprintOverride) |
|
UClass GetSupportedClass() const |
|
{ |
|
return UDataTable::StaticClass(); |
|
} |
|
|
|
/** |
|
* Create assets from the Data Table - this is exposed to the right-click menu in Unreal. |
|
*/ |
|
UFUNCTION(CallInEditor) |
|
void CreateAssets() |
|
{ |
|
TArray<UObject> SelectedAssets = EditorUtility::GetSelectedAssets(); |
|
for (UObject Object : SelectedAssets) |
|
{ |
|
CreateAssetsFromDataTable(Cast<UDataTable>(Object)); |
|
} |
|
} |
|
|
|
/** |
|
* Generate new assets, or update existing assets, with data from the passed Data Table. |
|
*/ |
|
private void CreateAssetsFromDataTable(UDataTable InDataTable) |
|
{ |
|
const FString ReplaceName = InDataTable.GetName() + "." + InDataTable.GetName(); |
|
const FString BasePath = InDataTable.GetPathName().Replace(ReplaceName, "", ESearchCase::CaseSensitive); |
|
|
|
auto TransientDataAssets = GenerateTransientAssets(InDataTable, BasePath); |
|
|
|
// Examine existing data assets, we may need to delete or update them |
|
TArray<UMyDataAsset> ExistingAssets; |
|
GetExistingAssets(BasePath, ExistingAssets); |
|
TMap<FName, UMyDataAsset> FinalAssets; |
|
TArray<UMyDataAsset> AssetsToDelete; |
|
for (UMyDataAsset ExistingAsset : ExistingAssets) |
|
{ |
|
if (TransientDataAssets.Contains(ExistingAsset.Key)) |
|
{ |
|
FinalAssets.Add(ExistingAsset.Key, ExistingAsset); |
|
} |
|
else |
|
{ |
|
AssetsToDelete.Add(ExistingAsset); |
|
} |
|
} |
|
|
|
// Create new assets in the content browser if needed, update existing assets with data from the transients |
|
for (TMapIterator<FName, UMyDataAsset> It : TransientDataAssets) |
|
{ |
|
const FName Key = It.GetKey(); |
|
UMyDataAsset DataAsset; |
|
|
|
// Asset didn't already exist, so make a new one |
|
if (!FinalAssets.Find(Key, DataAsset)) |
|
{ |
|
const FString DataAssetName = "DA_" + Key.ToString().Replace(".", "_"); |
|
DataAsset = CreateAsset(DataAssetName, BasePath); |
|
DataAsset.CopyScriptPropertiesFrom(It.GetValue()); |
|
FinalAssets.Add(Key, DataAsset); |
|
} |
|
// Asset existed, so update any changed properties and mark the asset as dirty if it was modified |
|
else |
|
{ |
|
CopyData(DataAsset, It.GetValue()); |
|
} |
|
} |
|
|
|
// Delete assets that are no longer referenced |
|
for (UMyDataAsset DataAsset : AssetsToDelete) |
|
{ |
|
// TODO: Ideally we'd delete assets automatically, but this doesn't seem possible via AS or BP yet |
|
Warning("Key '" + DataAsset.Key + "' no longer exists, asset should be deleted: " + DataAsset.GetName()); |
|
} |
|
|
|
if (FinalAssets.Num() == 0) |
|
{ |
|
Error("Didn't create or find any Data Assets from the data in " + InDataTable.GetName() + "!"); |
|
} |
|
} |
|
|
|
/** |
|
* Generate a map of transient Data Assets for each unique key in the Data Table. |
|
*/ |
|
private TMap<FName, UMyDataAsset> GenerateTransientAssets(UDataTable InDataTable, const FString& BasePath) |
|
{ |
|
TArray<FName> AssetNames; |
|
DataTable::GetDataTableRowNames(InDataTable, AssetNames); |
|
|
|
TMap<FName, UMyDataAsset> TransientAssets; |
|
for (int i = 0; i < AssetNames.Num(); i++) |
|
{ |
|
const FName AssetName = AssetNames[i]; |
|
FMyDataTableRow Row = GetRowData(InDataTable, AssetName); |
|
|
|
// Find existing asset for this Key, or make a new one if not found |
|
UMyDataAsset DataAsset; |
|
if (TransientAssets.Find(AssetName, DataAsset) == false) |
|
{ |
|
DataAsset = Cast<UMyDataAsset>(NewObject(this, UMyDataAsset::StaticClass(), bTransient = true)); |
|
TransientAssets.Add(AssetName, DataAsset); |
|
} |
|
|
|
// Update the data in the Data Asset with the data from the Data Table Row. |
|
DataAsset.Key = AssetName; |
|
DataAsset.SomeData = Row.SomeData; |
|
DataAsset.bMyBool = Row.bMyBool; |
|
|
|
DataAsset.Modify(true); |
|
} |
|
return TransientAssets; |
|
} |
|
|
|
/** |
|
* Get the table row data for a given Key. |
|
* This has to be implemented in Blueprint since BP's "Get Data Table Row" function is templated. |
|
* |
|
* Note that we **could** do this entirely in Angelscript using `DataTable::GetDataTableColumnAsString` for |
|
* each column, and iterating through all the rows by index, but it'd be much more effort. This is easier! |
|
*/ |
|
protected |
|
UFUNCTION(BlueprintCallable, BlueprintEvent) |
|
FMyDataTableRow GetRowData(UDataTable DataTable, const FName RowName) |
|
{ |
|
FMyDataTableRow TableRow; |
|
return TableRow; |
|
} |
|
|
|
/** |
|
* Create a new DataAsset with the specified name at the specified path. |
|
* This has to be implemented in Blueprint since AssetTools currently isn't bound in Angelscript. |
|
*/ |
|
protected |
|
UFUNCTION(BlueprintCallable, BlueprintEvent) |
|
UMyDataAsset CreateAsset(const FString AssetName, const FString AssetPath) |
|
{ |
|
return nullptr; |
|
} |
|
|
|
/** |
|
* Find existing Data Assets at the specified path. |
|
* This has to be implemented in Blueprint since AssetRegistry currently isn't bound in Angelscript. |
|
*/ |
|
protected |
|
UFUNCTION(BlueprintCallable, BlueprintEvent) |
|
bool GetExistingAssets(const FString BasePath, TArray<UMyDataAsset>& OutDataAssets) |
|
{ |
|
return false; |
|
} |
|
|
|
/** |
|
* Update a Data Asset with data from another, and mark it as dirty if it was modified. |
|
*/ |
|
private void CopyData(UMyDataAsset DataAsset, UMyDataAsset CopyFromDataAsset) |
|
{ |
|
// If data is the same, no need to modify |
|
if (DataAsset.SomeData == CopyFromDataAsset.SomeData |
|
&& DataAsset.bMyBool == CopyFromDataAsset.bMyBool) |
|
{ |
|
return; |
|
} |
|
|
|
DataAsset.SomeData = CopyFromDataAsset.SomeData; |
|
DataAsset.bMyBool = CopyFromDataAsset.bMyBool; |
|
|
|
DataAsset.Modify(); |
|
} |
|
}; |