514 lines
14 KiB
C++
514 lines
14 KiB
C++
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
|
|
|
|
#include "JsonAsset.h"
|
|
#if USE_EDITOR
|
|
#include "Engine/Platform/File.h"
|
|
#include "Engine/Platform/FileSystem.h"
|
|
#include "Engine/Core/Types/DataContainer.h"
|
|
#include "Engine/Level/Level.h"
|
|
#else
|
|
#include "Storage/ContentStorageManager.h"
|
|
#endif
|
|
#include "Content.h"
|
|
#include "FlaxEngine.Gen.h"
|
|
#include "Cache/AssetsCache.h"
|
|
#include "Engine/Core/Log.h"
|
|
#include "Engine/Core/Config/Settings.h"
|
|
#include "Engine/Serialization/JsonTools.h"
|
|
#include "Engine/Serialization/JsonWriters.h"
|
|
#include "Engine/Content/Factories/JsonAssetFactory.h"
|
|
#include "Engine/Core/Cache.h"
|
|
#include "Engine/Debug/Exceptions/JsonParseException.h"
|
|
#include "Engine/Profiler/ProfilerCPU.h"
|
|
#include "Engine/Scripting/Scripting.h"
|
|
#include "Engine/Scripting/ManagedCLR/MClass.h"
|
|
#include "Engine/Scripting/ManagedCLR/MField.h"
|
|
#include "Engine/Utilities/StringConverter.h"
|
|
#include "Engine/Threading/Threading.h"
|
|
|
|
JsonAssetBase::JsonAssetBase(const SpawnParams& params, const AssetInfo* info)
|
|
: Asset(params, info)
|
|
, _path(info->Path)
|
|
, Data(nullptr)
|
|
, DataEngineBuild(0)
|
|
{
|
|
}
|
|
|
|
String JsonAssetBase::GetData() const
|
|
{
|
|
if (Data == nullptr)
|
|
return String::Empty;
|
|
PROFILE_CPU_NAMED("JsonAsset.GetData");
|
|
rapidjson_flax::StringBuffer buffer;
|
|
OnGetData(buffer);
|
|
return String((const char*)buffer.GetString(), (int32)buffer.GetSize());
|
|
}
|
|
|
|
void JsonAssetBase::SetData(const StringView& value)
|
|
{
|
|
if (!IsLoaded())
|
|
return;
|
|
PROFILE_CPU_NAMED("JsonAsset.SetData");
|
|
const StringAnsi dataJson(value);
|
|
ScopeLock lock(Locker);
|
|
const StringView dataTypeName = DataTypeName;
|
|
if (Init(dataTypeName, dataJson))
|
|
{
|
|
LOG(Error, "Failed to set Json asset data.");
|
|
}
|
|
}
|
|
|
|
bool JsonAssetBase::Init(const StringView& dataTypeName, const StringAnsiView& dataJson)
|
|
{
|
|
unload(true);
|
|
DataTypeName = dataTypeName;
|
|
DataEngineBuild = FLAXENGINE_VERSION_BUILD;
|
|
|
|
// Parse json document
|
|
{
|
|
PROFILE_CPU_NAMED("Json.Parse");
|
|
Document.Parse(dataJson.Get(), dataJson.Length());
|
|
}
|
|
if (Document.HasParseError())
|
|
{
|
|
Log::JsonParseException(Document.GetParseError(), Document.GetErrorOffset());
|
|
return true;
|
|
}
|
|
Data = &Document;
|
|
_isVirtualDocument = true;
|
|
|
|
// Load asset-specific data
|
|
return loadAsset() != LoadResult::Ok;
|
|
}
|
|
|
|
void JsonAssetBase::OnGetData(rapidjson_flax::StringBuffer& buffer) const
|
|
{
|
|
PrettyJsonWriter writerObj(buffer);
|
|
Data->Accept(writerObj.GetWriter());
|
|
}
|
|
|
|
const String& JsonAssetBase::GetPath() const
|
|
{
|
|
#if USE_EDITOR
|
|
return _path;
|
|
#else
|
|
// In build all assets are packed into packages so use ID for original path lookup
|
|
return Content::GetRegistry()->GetEditorAssetPath(_id);
|
|
#endif
|
|
}
|
|
|
|
uint64 JsonAssetBase::GetMemoryUsage() const
|
|
{
|
|
Locker.Lock();
|
|
uint64 result = Asset::GetMemoryUsage();
|
|
result += sizeof(JsonAssetBase) - sizeof(Asset);
|
|
if (Data)
|
|
result += Document.GetAllocator().Capacity();
|
|
Locker.Unlock();
|
|
return result;
|
|
}
|
|
|
|
#if USE_EDITOR
|
|
|
|
void FindIds(ISerializable::DeserializeStream& node, Array<Guid>& output, Array<String>& files, rapidjson_flax::Value* nodeName = nullptr)
|
|
{
|
|
if (node.IsObject())
|
|
{
|
|
for (auto i = node.MemberBegin(); i != node.MemberEnd(); ++i)
|
|
{
|
|
FindIds(i->value, output, files, &i->name);
|
|
}
|
|
}
|
|
else if (node.IsArray())
|
|
{
|
|
for (rapidjson::SizeType i = 0; i < node.Size(); i++)
|
|
{
|
|
FindIds(node[i], output, files);
|
|
}
|
|
}
|
|
else if (node.IsString() && node.GetStringLength() != 0)
|
|
{
|
|
if (node.GetStringLength() == 32)
|
|
{
|
|
// Try parse as Guid in format `N` (32 hex chars)
|
|
Guid id;
|
|
if (!Guid::Parse(node.GetStringAnsiView(), id))
|
|
{
|
|
output.Add(id);
|
|
return;
|
|
}
|
|
}
|
|
if (node.GetStringLength() < 512 &&
|
|
(!nodeName || nodeName->GetStringAnsiView() != "ImportPath")) // Ignore path in ImportPath from ModelPrefab (TODO: resave prefabs/scenes before cooking to get rid of editor-only data)
|
|
{
|
|
// Try to detect file paths
|
|
String path = node.GetText();
|
|
if (FileSystem::FileExists(path))
|
|
{
|
|
files.Add(MoveTemp(path));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void JsonAssetBase::GetReferences(const StringAnsiView& json, Array<Guid>& assets)
|
|
{
|
|
ISerializable::SerializeDocument document;
|
|
document.Parse(json.Get(), json.Length());
|
|
if (document.HasParseError())
|
|
return;
|
|
Array<String> files;
|
|
FindIds(document, assets, files);
|
|
}
|
|
|
|
bool JsonAssetBase::Save(const StringView& path) const
|
|
{
|
|
// Validate state
|
|
if (WaitForLoaded())
|
|
{
|
|
LOG(Error, "Asset loading failed. Cannot save it.");
|
|
return true;
|
|
}
|
|
if (IsVirtual() && path.IsEmpty())
|
|
{
|
|
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
|
|
return true;
|
|
}
|
|
ScopeLock lock(Locker);
|
|
|
|
// Serialize to json to the buffer
|
|
rapidjson_flax::StringBuffer buffer;
|
|
PrettyJsonWriter writerObj(buffer);
|
|
Save(writerObj);
|
|
|
|
// Save json to file
|
|
if (File::WriteAllBytes(path.HasChars() ? path : StringView(GetPath()), (byte*)buffer.GetString(), (int32)buffer.GetSize()))
|
|
{
|
|
LOG(Error, "Cannot save \'{0}\'", ToString());
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool JsonAssetBase::Save(JsonWriter& writer) const
|
|
{
|
|
// Validate state
|
|
if (WaitForLoaded())
|
|
{
|
|
LOG(Error, "Asset loading failed. Cannot save it.");
|
|
return true;
|
|
}
|
|
ScopeLock lock(Locker);
|
|
|
|
writer.StartObject();
|
|
{
|
|
// Json resource header
|
|
writer.JKEY("ID");
|
|
writer.Guid(GetID());
|
|
writer.JKEY("TypeName");
|
|
writer.String(DataTypeName);
|
|
writer.JKEY("EngineBuild");
|
|
writer.Int(FLAXENGINE_VERSION_BUILD);
|
|
|
|
// Json resource data
|
|
rapidjson_flax::StringBuffer dataBuffer;
|
|
OnGetData(dataBuffer);
|
|
writer.JKEY("Data");
|
|
writer.RawValue(dataBuffer.GetString(), (int32)dataBuffer.GetSize());
|
|
}
|
|
writer.EndObject();
|
|
|
|
return false;
|
|
}
|
|
|
|
void JsonAssetBase::GetReferences(Array<Guid>& assets, Array<String>& files) const
|
|
{
|
|
if (Data == nullptr)
|
|
return;
|
|
|
|
// Unified way to find asset references inside a generic asset.
|
|
// This could deserialize managed/unmanaged object or load actors in case of SceneAsset or PrefabAsset.
|
|
// But this would be performance killer.
|
|
// The fastest way is to just iterate through the json and find all the Guid values.
|
|
// It produces many invalid ids (like refs to scene objects).
|
|
// But it's super fast, super low-memory and doesn't involve any advanced systems integration.
|
|
|
|
FindIds(*Data, assets, files);
|
|
}
|
|
|
|
#endif
|
|
|
|
Asset::LoadResult JsonAssetBase::loadAsset()
|
|
{
|
|
if (IsVirtual() || _isVirtualDocument)
|
|
return LoadResult::Ok;
|
|
|
|
// Load data (raw json file in editor, cooked asset in build game)
|
|
#if USE_EDITOR
|
|
BytesContainer data;
|
|
if (File::ReadAllBytes(_path, data))
|
|
{
|
|
LOG(Warning, "Filed to load json asset data. {0}", ToString());
|
|
return LoadResult::CannotLoadData;
|
|
}
|
|
if (data.Length() == 0)
|
|
{
|
|
return LoadResult::MissingDataChunk;
|
|
}
|
|
#else
|
|
// Get the asset storage container but don't load it now
|
|
const auto storage = ContentStorageManager::GetStorage(_path, true);
|
|
if (!storage)
|
|
return LoadResult::CannotLoadStorage;
|
|
|
|
// Load header
|
|
AssetInitData initData;
|
|
if (storage->LoadAssetHeader(GetID(), initData))
|
|
return LoadResult::CannotLoadInitData;
|
|
|
|
// Load the actual data
|
|
auto chunk = initData.Header.Chunks[0];
|
|
if (chunk == nullptr)
|
|
return LoadResult::MissingDataChunk;
|
|
if (storage->LoadAssetChunk(chunk))
|
|
return LoadResult::CannotLoadData;
|
|
auto& data = chunk->Data;
|
|
#endif
|
|
|
|
// Parse json document
|
|
{
|
|
PROFILE_CPU_NAMED("Json.Parse");
|
|
Document.Parse(data.Get<char>(), data.Length());
|
|
}
|
|
if (Document.HasParseError())
|
|
{
|
|
Log::JsonParseException(Document.GetParseError(), Document.GetErrorOffset());
|
|
return LoadResult::CannotLoadData;
|
|
}
|
|
|
|
// Gather information from the header
|
|
const auto id = JsonTools::GetGuid(Document, "ID");
|
|
if (id != _id)
|
|
{
|
|
LOG(Warning, "Invalid json asset id. Asset: {0}, serialized: {1}.", _id, id);
|
|
return LoadResult::InvalidData;
|
|
}
|
|
DataTypeName = JsonTools::GetString(Document, "TypeName");
|
|
DataEngineBuild = JsonTools::GetInt(Document, "EngineBuild", FLAXENGINE_VERSION_BUILD);
|
|
auto dataMember = Document.FindMember("Data");
|
|
if (dataMember == Document.MemberEnd())
|
|
{
|
|
LOG(Warning, "Missing json asset data.");
|
|
return LoadResult::InvalidData;
|
|
}
|
|
Data = &dataMember->value;
|
|
|
|
return LoadResult::Ok;
|
|
}
|
|
|
|
void JsonAssetBase::unload(bool isReloading)
|
|
{
|
|
ISerializable::SerializeDocument tmp;
|
|
Document.Swap(tmp);
|
|
Data = nullptr;
|
|
DataTypeName.Clear();
|
|
DataEngineBuild = 0;
|
|
_isVirtualDocument = false;
|
|
}
|
|
|
|
#if USE_EDITOR
|
|
|
|
void JsonAssetBase::onRename(const StringView& newPath)
|
|
{
|
|
ScopeLock lock(Locker);
|
|
|
|
// Rename
|
|
_path = newPath;
|
|
}
|
|
|
|
#endif
|
|
|
|
REGISTER_JSON_ASSET(JsonAsset, "FlaxEngine.JsonAsset", true);
|
|
|
|
JsonAsset::JsonAsset(const SpawnParams& params, const AssetInfo* info)
|
|
: JsonAssetBase(params, info)
|
|
, Instance(nullptr)
|
|
{
|
|
}
|
|
|
|
uint64 JsonAsset::GetMemoryUsage() const
|
|
{
|
|
Locker.Lock();
|
|
uint64 result = JsonAssetBase::GetMemoryUsage();
|
|
result += sizeof(JsonAsset) - sizeof(JsonAssetBase);
|
|
if (Instance && InstanceType)
|
|
result += InstanceType.GetType().Size;
|
|
Locker.Unlock();
|
|
return result;
|
|
}
|
|
|
|
Asset::LoadResult JsonAsset::loadAsset()
|
|
{
|
|
const auto result = JsonAssetBase::loadAsset();
|
|
if (result != LoadResult::Ok || IsInternalType())
|
|
return result;
|
|
|
|
if (CreateInstance())
|
|
return LoadResult::Failed;
|
|
|
|
#if USE_EDITOR
|
|
// Reload instance when module with this type gets reloaded
|
|
Level::ScriptsReloadStart.Bind<JsonAsset, &JsonAsset::OnScriptsReloadStart>(this);
|
|
Level::ScriptsReloaded.Bind<JsonAsset, &JsonAsset::OnScriptsReloaded>(this);
|
|
#endif
|
|
|
|
// Destroy instance on scripting shutdown (eg. asset from scripts)
|
|
Scripting::ScriptsUnload.Bind<JsonAsset, &JsonAsset::DeleteInstance>(this);
|
|
|
|
return LoadResult::Ok;
|
|
}
|
|
|
|
void JsonAsset::unload(bool isReloading)
|
|
{
|
|
#if USE_EDITOR
|
|
Level::ScriptsReloadStart.Unbind<JsonAsset, &JsonAsset::OnScriptsReloadStart>(this);
|
|
Level::ScriptsReloaded.Unbind<JsonAsset, &JsonAsset::OnScriptsReloaded>(this);
|
|
#endif
|
|
Scripting::ScriptsUnload.Unbind<JsonAsset, &JsonAsset::DeleteInstance>(this);
|
|
DeleteInstance();
|
|
_isAfterReload |= isReloading;
|
|
|
|
JsonAssetBase::unload(isReloading);
|
|
}
|
|
|
|
void JsonAsset::onLoaded_MainThread()
|
|
{
|
|
JsonAssetBase::onLoaded_MainThread();
|
|
|
|
// Special case for Settings assets to flush them after edited and saved in Editor
|
|
const StringAsANSI<> dataTypeNameAnsi(DataTypeName.Get(), DataTypeName.Length());
|
|
const auto typeHandle = Scripting::FindScriptingType(StringAnsiView(dataTypeNameAnsi.Get(), DataTypeName.Length()));
|
|
if (Instance && typeHandle && typeHandle.IsSubclassOf(SettingsBase::TypeInitializer) && _isAfterReload)
|
|
{
|
|
_isAfterReload = false;
|
|
((SettingsBase*)Instance)->Apply();
|
|
}
|
|
}
|
|
|
|
bool JsonAsset::CreateInstance()
|
|
{
|
|
ScopeLock lock(Locker);
|
|
if (Instance)
|
|
return false;
|
|
|
|
// Try to scripting type for this data
|
|
const StringAsANSI<> dataTypeNameAnsi(DataTypeName.Get(), DataTypeName.Length());
|
|
const auto typeHandle = Scripting::FindScriptingType(StringAnsiView(dataTypeNameAnsi.Get(), DataTypeName.Length()));
|
|
if (typeHandle)
|
|
{
|
|
auto& type = typeHandle.GetType();
|
|
|
|
// Ensure that object can deserialized
|
|
const ScriptingType::InterfaceImplementation* interface = type.GetInterface(ISerializable::TypeInitializer);
|
|
if (!interface)
|
|
{
|
|
LOG(Warning, "Cannot deserialize {0} from Json Asset because it doesn't implement ISerializable interface.", type.ToString());
|
|
return false;
|
|
}
|
|
auto modifier = Cache::ISerializeModifier.Get();
|
|
modifier->EngineBuild = DataEngineBuild;
|
|
|
|
// Create object
|
|
switch (type.Type)
|
|
{
|
|
case ScriptingTypes::Class:
|
|
case ScriptingTypes::Structure:
|
|
{
|
|
const auto instance = Allocator::Allocate(type.Size);
|
|
if (!instance)
|
|
return true;
|
|
Instance = instance;
|
|
if (type.Type == ScriptingTypes::Class)
|
|
{
|
|
_dtor = type.Class.Dtor;
|
|
type.Class.Ctor(instance);
|
|
}
|
|
else
|
|
{
|
|
_dtor = type.Struct.Dtor;
|
|
type.Struct.Ctor(instance);
|
|
}
|
|
|
|
// Deserialize object
|
|
((ISerializable*)((byte*)instance + interface->VTableOffset))->Deserialize(*Data, modifier.Value);
|
|
break;
|
|
}
|
|
case ScriptingTypes::Script:
|
|
{
|
|
const ScriptingObjectSpawnParams params(Guid::New(), typeHandle);
|
|
const auto instance = type.Script.Spawn(params);
|
|
if (!instance)
|
|
return true;
|
|
Instance = instance;
|
|
_dtor = nullptr;
|
|
|
|
// Deserialize object
|
|
ToInterface<ISerializable>(instance)->Deserialize(*Data, modifier.Value);
|
|
break;
|
|
}
|
|
}
|
|
InstanceType = typeHandle;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void JsonAsset::DeleteInstance()
|
|
{
|
|
ScopeLock lock(Locker);
|
|
|
|
// C# instance
|
|
MObject* object = GetManagedInstance();
|
|
MClass* klass = GetClass();
|
|
if (object && klass)
|
|
{
|
|
const MField* field = klass->GetField("_instance");
|
|
if (field)
|
|
field->SetValue(object, nullptr);
|
|
}
|
|
|
|
// C++ instance
|
|
if (!Instance)
|
|
return;
|
|
if (_dtor)
|
|
{
|
|
_dtor(Instance);
|
|
_dtor = nullptr;
|
|
Allocator::Free(Instance);
|
|
}
|
|
else
|
|
{
|
|
Delete((ScriptingObject*)Instance);
|
|
}
|
|
InstanceType = ScriptingTypeHandle();
|
|
Instance = nullptr;
|
|
}
|
|
|
|
#if USE_EDITOR
|
|
|
|
void JsonAsset::OnScriptsReloadStart()
|
|
{
|
|
DeleteInstance();
|
|
}
|
|
|
|
void JsonAsset::OnScriptsReloaded()
|
|
{
|
|
if (CreateInstance())
|
|
{
|
|
LOG(Warning, "Failed to reload {0} instance {1}.", ToString(), DataTypeName);
|
|
}
|
|
}
|
|
|
|
#endif
|