389 lines
19 KiB
C++
389 lines
19 KiB
C++
#include "MetaCoreScene/MetaCorePrefab.h"
|
|
|
|
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
|
|
|
#include <algorithm>
|
|
#include <nlohmann/json.hpp>
|
|
#include <unordered_set>
|
|
|
|
namespace MetaCore {
|
|
|
|
namespace {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
glm::vec3 ReadVec3(std::string_view value, bool& valid) {
|
|
try {
|
|
const json parsed = json::parse(value);
|
|
if (!parsed.is_array() || parsed.size() < 3) return {};
|
|
valid = true;
|
|
return {parsed[0].get<float>(), parsed[1].get<float>(), parsed[2].get<float>()};
|
|
} catch (...) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
bool ReadScalar(std::string_view value, T& output) {
|
|
try {
|
|
output = json::parse(value).get<T>();
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
MetaCoreGameObjectData* FindNode(MetaCorePrefabDocument& document, const MetaCoreAssetGuid& nodeGuid) {
|
|
const auto iterator = std::find_if(document.GameObjects.begin(), document.GameObjects.end(), [&](const auto& object) {
|
|
return object.StableObjectGuid == nodeGuid;
|
|
});
|
|
return iterator == document.GameObjects.end() ? nullptr : &*iterator;
|
|
}
|
|
|
|
const MetaCoreGameObjectData* FindNode(const MetaCorePrefabDocument& document, const MetaCoreAssetGuid& nodeGuid) {
|
|
const auto iterator = std::find_if(document.GameObjects.begin(), document.GameObjects.end(), [&](const auto& object) {
|
|
return object.StableObjectGuid == nodeGuid;
|
|
});
|
|
return iterator == document.GameObjects.end() ? nullptr : &*iterator;
|
|
}
|
|
|
|
void NormalizePrefab(MetaCorePrefabDocument& document) {
|
|
if (!document.PrefabGuid.IsValid()) document.PrefabGuid = MetaCoreAssetGuid::Generate();
|
|
for (auto& object : document.GameObjects) {
|
|
if (!object.StableObjectGuid.IsValid())
|
|
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.PrefabGuid, object.Id);
|
|
}
|
|
document.SchemaVersion = GMetaCorePrefabSchemaVersion;
|
|
}
|
|
|
|
void MergeVariantObjects(MetaCorePrefabDocument& target, const MetaCorePrefabDocument& layer) {
|
|
for (const auto& object : layer.GameObjects) {
|
|
const auto iterator = std::find_if(target.GameObjects.begin(), target.GameObjects.end(), [&](const auto& current) {
|
|
return current.StableObjectGuid == object.StableObjectGuid;
|
|
});
|
|
if (iterator == target.GameObjects.end()) target.GameObjects.push_back(object);
|
|
else *iterator = object;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MetaCorePrefabOverrideRegistry::MetaCorePrefabOverrideRegistry() {
|
|
RegisterPropertyHandler("Name", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
|
|
return path == "Value" && ReadScalar(value, object.Name);
|
|
});
|
|
RegisterPropertyHandler("Transform", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
|
|
bool valid = false;
|
|
const glm::vec3 parsed = ReadVec3(value, valid);
|
|
if (!valid) return false;
|
|
if (path == "Position") object.Transform.Position = parsed;
|
|
else if (path == "RotationEulerDegrees") object.Transform.RotationEulerDegrees = parsed;
|
|
else if (path == "Scale") object.Transform.Scale = parsed;
|
|
else return false;
|
|
return true;
|
|
});
|
|
RegisterPropertyHandler("Active", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
|
|
if (path != "Active") return false;
|
|
if (!object.Active) object.Active = MetaCoreActiveComponent{};
|
|
return ReadScalar(value, object.Active->Active);
|
|
});
|
|
RegisterPropertyHandler("Camera", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
|
|
if (!object.Camera) object.Camera = MetaCoreCameraComponent{};
|
|
if (path == "FieldOfViewDegrees") return ReadScalar(value, object.Camera->FieldOfViewDegrees);
|
|
if (path == "NearClip") return ReadScalar(value, object.Camera->NearClip);
|
|
if (path == "FarClip") return ReadScalar(value, object.Camera->FarClip);
|
|
if (path == "Enabled") return ReadScalar(value, object.Camera->Enabled);
|
|
if (path == "IsPrimary") return ReadScalar(value, object.Camera->IsPrimary);
|
|
return false;
|
|
});
|
|
RegisterPropertyHandler("Light", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
|
|
if (!object.Light) object.Light = MetaCoreLightComponent{};
|
|
if (path == "Intensity") return ReadScalar(value, object.Light->Intensity);
|
|
if (path == "Enabled") return ReadScalar(value, object.Light->Enabled);
|
|
if (path == "Color") {
|
|
bool valid = false;
|
|
const auto parsed = ReadVec3(value, valid);
|
|
if (valid) object.Light->Color = parsed;
|
|
return valid;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
void MetaCorePrefabOverrideRegistry::RegisterPropertyHandler(std::string componentTypeId, PropertyHandler handler) {
|
|
PropertyHandlers_[std::move(componentTypeId)] = std::move(handler);
|
|
}
|
|
|
|
bool MetaCorePrefabOverrideRegistry::Apply(
|
|
MetaCorePrefabDocument& document,
|
|
const MetaCorePrefabOverrideDocument& overrideValue
|
|
) const {
|
|
MetaCoreGameObjectData* object = FindNode(document, overrideValue.TargetNodeGuid);
|
|
if (!object) return false;
|
|
switch (overrideValue.Operation) {
|
|
case MetaCorePrefabOverrideOperation::SetProperty: {
|
|
const auto handler = PropertyHandlers_.find(overrideValue.ComponentTypeId);
|
|
return handler != PropertyHandlers_.end() &&
|
|
handler->second(*object, overrideValue.PropertyPath, overrideValue.ValueJson);
|
|
}
|
|
case MetaCorePrefabOverrideOperation::AddComponent:
|
|
if (overrideValue.ComponentTypeId == "Active") object->Active = MetaCoreActiveComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Camera") object->Camera = MetaCoreCameraComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Light") object->Light = MetaCoreLightComponent{};
|
|
else if (overrideValue.ComponentTypeId == "MeshRenderer") object->MeshRenderer = MetaCoreMeshRendererComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Collider") object->Collider = MetaCoreColliderComponent{};
|
|
else if (overrideValue.ComponentTypeId == "RigidBody") object->RigidBody = MetaCoreRigidBodyComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Rotator") object->Rotator = MetaCoreRotatorComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Animator") object->Animator = MetaCoreAnimatorComponent{};
|
|
else if (overrideValue.ComponentTypeId == "TimelineDirector") object->TimelineDirector = MetaCoreTimelineDirectorComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Script") object->Script = MetaCoreScriptComponent{};
|
|
else if (overrideValue.ComponentTypeId == "UiRenderer") object->UiRenderer = MetaCoreUiRendererComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Interactable") object->Interactable = MetaCoreInteractableComponent{};
|
|
else if (overrideValue.ComponentTypeId == "PhysicsConstraint") object->PhysicsConstraint = MetaCorePhysicsConstraintComponent{};
|
|
else if (overrideValue.ComponentTypeId == "CharacterController") object->CharacterController = MetaCoreCharacterControllerComponent{};
|
|
else if (overrideValue.ComponentTypeId == "ReflectionProbe") object->ReflectionProbe = MetaCoreReflectionProbeComponent{};
|
|
else if (overrideValue.ComponentTypeId == "LodGroup") object->LodGroup = MetaCoreLodGroupComponent{};
|
|
else if (overrideValue.ComponentTypeId == "ParticleEmitter") object->ParticleEmitter = MetaCoreParticleEmitterComponent{};
|
|
else if (overrideValue.ComponentTypeId == "LineRenderer") object->LineRenderer = MetaCoreLineRendererComponent{};
|
|
else if (overrideValue.ComponentTypeId == "TrailRenderer") object->TrailRenderer = MetaCoreTrailRendererComponent{};
|
|
else if (overrideValue.ComponentTypeId == "DecalProjector") object->DecalProjector = MetaCoreDecalProjectorComponent{};
|
|
else if (overrideValue.ComponentTypeId == "Terrain") object->Terrain = MetaCoreTerrainComponent{};
|
|
else return false;
|
|
return true;
|
|
case MetaCorePrefabOverrideOperation::RemoveComponent:
|
|
if (overrideValue.ComponentTypeId == "Active") object->Active.reset();
|
|
else if (overrideValue.ComponentTypeId == "Camera") object->Camera.reset();
|
|
else if (overrideValue.ComponentTypeId == "Light") object->Light.reset();
|
|
else if (overrideValue.ComponentTypeId == "MeshRenderer") object->MeshRenderer.reset();
|
|
else if (overrideValue.ComponentTypeId == "Collider") object->Collider.reset();
|
|
else if (overrideValue.ComponentTypeId == "RigidBody") object->RigidBody.reset();
|
|
else if (overrideValue.ComponentTypeId == "Rotator") object->Rotator.reset();
|
|
else if (overrideValue.ComponentTypeId == "Animator") object->Animator.reset();
|
|
else if (overrideValue.ComponentTypeId == "TimelineDirector") object->TimelineDirector.reset();
|
|
else if (overrideValue.ComponentTypeId == "Script") object->Script.reset();
|
|
else if (overrideValue.ComponentTypeId == "UiRenderer") object->UiRenderer.reset();
|
|
else if (overrideValue.ComponentTypeId == "Interactable") object->Interactable.reset();
|
|
else if (overrideValue.ComponentTypeId == "PhysicsConstraint") object->PhysicsConstraint.reset();
|
|
else if (overrideValue.ComponentTypeId == "CharacterController") object->CharacterController.reset();
|
|
else if (overrideValue.ComponentTypeId == "ReflectionProbe") object->ReflectionProbe.reset();
|
|
else if (overrideValue.ComponentTypeId == "LodGroup") object->LodGroup.reset();
|
|
else if (overrideValue.ComponentTypeId == "ParticleEmitter") object->ParticleEmitter.reset();
|
|
else if (overrideValue.ComponentTypeId == "LineRenderer") object->LineRenderer.reset();
|
|
else if (overrideValue.ComponentTypeId == "TrailRenderer") object->TrailRenderer.reset();
|
|
else if (overrideValue.ComponentTypeId == "DecalProjector") object->DecalProjector.reset();
|
|
else if (overrideValue.ComponentTypeId == "Terrain") object->Terrain.reset();
|
|
else return false;
|
|
return true;
|
|
case MetaCorePrefabOverrideOperation::RemoveChild: {
|
|
std::unordered_set<MetaCoreId> removed{object->Id};
|
|
bool changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const auto& candidate : document.GameObjects)
|
|
if (removed.contains(candidate.ParentId) && removed.insert(candidate.Id).second) changed = true;
|
|
}
|
|
std::erase_if(document.GameObjects, [&](const auto& candidate) { return removed.contains(candidate.Id); });
|
|
return true;
|
|
}
|
|
case MetaCorePrefabOverrideOperation::Reparent: {
|
|
const auto parentGuid = MetaCoreAssetGuid::Parse(overrideValue.ValueJson);
|
|
if (!parentGuid) return false;
|
|
const auto* parent = FindNode(document, *parentGuid);
|
|
if (!parent || parent->Id == object->Id) return false;
|
|
object->ParentId = parent->Id;
|
|
return true;
|
|
}
|
|
case MetaCorePrefabOverrideOperation::AddChild: {
|
|
try {
|
|
const json value = json::parse(overrideValue.ValueJson);
|
|
MetaCoreGameObjectData child;
|
|
child.Id = 1;
|
|
for (const auto& current : document.GameObjects) child.Id = std::max(child.Id, current.Id + 1);
|
|
child.ParentId = object->Id;
|
|
child.Name = value.value("Name", std::string("GameObject"));
|
|
child.StableObjectGuid = MetaCoreAssetGuid::Parse(value.value("StableObjectGuid", std::string{}))
|
|
.value_or(MetaCoreBuildDeterministicGuid(document.PrefabGuid, child.Id));
|
|
if (value.contains("Position")) {
|
|
bool valid = false;
|
|
child.Transform.Position = ReadVec3(value["Position"].dump(), valid);
|
|
if (!valid) return false;
|
|
}
|
|
if (value.contains("RotationEulerDegrees")) {
|
|
bool valid = false;
|
|
child.Transform.RotationEulerDegrees = ReadVec3(value["RotationEulerDegrees"].dump(), valid);
|
|
if (!valid) return false;
|
|
}
|
|
if (value.contains("Scale")) {
|
|
bool valid = false;
|
|
child.Transform.Scale = ReadVec3(value["Scale"].dump(), valid);
|
|
if (!valid) return false;
|
|
}
|
|
document.GameObjects.push_back(std::move(child));
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
MetaCorePrefabResolver::MetaCorePrefabResolver(PrefabLoader loader, MetaCorePrefabOverrideRegistry registry)
|
|
: Loader_(std::move(loader)), OverrideRegistry_(std::move(registry)) {}
|
|
|
|
std::optional<MetaCoreResolvedPrefab> MetaCorePrefabResolver::Resolve(
|
|
const MetaCoreAssetGuid& prefabGuid,
|
|
std::string* error
|
|
) const {
|
|
std::vector<MetaCoreAssetGuid> stack;
|
|
return ResolveInternal(prefabGuid, stack, error);
|
|
}
|
|
|
|
std::optional<MetaCoreResolvedPrefab> MetaCorePrefabResolver::ResolveInternal(
|
|
const MetaCoreAssetGuid& prefabGuid,
|
|
std::vector<MetaCoreAssetGuid>& stack,
|
|
std::string* error
|
|
) const {
|
|
if (!prefabGuid.IsValid() || !Loader_) {
|
|
if (error) *error = "Prefab GUID is invalid or no loader is configured";
|
|
return std::nullopt;
|
|
}
|
|
if (std::find(stack.begin(), stack.end(), prefabGuid) != stack.end()) {
|
|
if (error) *error = "Prefab base/nesting dependency cycle at " + prefabGuid.ToString();
|
|
return std::nullopt;
|
|
}
|
|
auto layer = Loader_(prefabGuid);
|
|
if (!layer) {
|
|
if (error) *error = "Prefab asset is missing: " + prefabGuid.ToString();
|
|
return std::nullopt;
|
|
}
|
|
if (layer->SchemaVersion > GMetaCorePrefabSchemaVersion) {
|
|
if (error) *error = "Prefab schema is newer than this engine: " + prefabGuid.ToString();
|
|
return std::nullopt;
|
|
}
|
|
if (!layer->PrefabGuid.IsValid()) layer->PrefabGuid = prefabGuid;
|
|
NormalizePrefab(*layer);
|
|
stack.push_back(prefabGuid);
|
|
|
|
MetaCoreResolvedPrefab result;
|
|
if (layer->BasePrefabGuid.IsValid()) {
|
|
auto base = ResolveInternal(layer->BasePrefabGuid, stack, error);
|
|
if (!base) {
|
|
stack.pop_back();
|
|
return std::nullopt;
|
|
}
|
|
result = std::move(*base);
|
|
result.Document.PrefabGuid = layer->PrefabGuid;
|
|
result.Document.BasePrefabGuid = layer->BasePrefabGuid;
|
|
result.Document.Name = layer->Name;
|
|
MergeVariantObjects(result.Document, *layer);
|
|
} else {
|
|
result.Document = *layer;
|
|
}
|
|
result.DependencyChain.push_back(prefabGuid);
|
|
for (const auto& value : layer->Overrides)
|
|
if (!OverrideRegistry_.Apply(result.Document, value)) result.OrphanOverrides.push_back(value);
|
|
|
|
for (const auto& nested : layer->NestedPrefabs) {
|
|
auto nestedResult = ResolveInternal(nested.PrefabAssetGuid, stack, error);
|
|
if (!nestedResult) {
|
|
stack.pop_back();
|
|
return std::nullopt;
|
|
}
|
|
for (const auto& value : nested.Overrides)
|
|
if (!OverrideRegistry_.Apply(nestedResult->Document, value)) result.OrphanOverrides.push_back(value);
|
|
const auto* mount = FindNode(result.Document, nested.MountNodeGuid);
|
|
if (!mount) {
|
|
MetaCorePrefabOverrideDocument orphan;
|
|
orphan.TargetNodeGuid = nested.MountNodeGuid;
|
|
orphan.ComponentTypeId = "NestedPrefab";
|
|
orphan.ValueJson = nested.PrefabAssetGuid.ToString();
|
|
result.OrphanOverrides.push_back(std::move(orphan));
|
|
continue;
|
|
}
|
|
const MetaCoreAssetGuid instanceGuid = nested.InstanceGuid.IsValid()
|
|
? nested.InstanceGuid
|
|
: MetaCoreBuildDeterministicGuid(layer->PrefabGuid, nested.PrefabAssetGuid);
|
|
std::unordered_map<MetaCoreId, MetaCoreId> remap;
|
|
for (const auto& object : nestedResult->Document.GameObjects)
|
|
remap[object.Id] = MetaCoreIdGenerator::Generate();
|
|
for (auto object : nestedResult->Document.GameObjects) {
|
|
const MetaCoreId oldId = object.Id;
|
|
object.Id = remap.at(oldId);
|
|
if (object.ParentId == 0) object.ParentId = mount->Id;
|
|
else if (const auto parent = remap.find(object.ParentId); parent != remap.end()) object.ParentId = parent->second;
|
|
else {
|
|
if (error) *error = "Nested prefab contains a missing parent object";
|
|
stack.pop_back();
|
|
return std::nullopt;
|
|
}
|
|
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(instanceGuid, object.StableObjectGuid);
|
|
result.Document.GameObjects.push_back(std::move(object));
|
|
}
|
|
result.DependencyChain.insert(result.DependencyChain.end(), nestedResult->DependencyChain.begin(), nestedResult->DependencyChain.end());
|
|
result.OrphanOverrides.insert(result.OrphanOverrides.end(), nestedResult->OrphanOverrides.begin(), nestedResult->OrphanOverrides.end());
|
|
}
|
|
stack.pop_back();
|
|
return result;
|
|
}
|
|
|
|
std::optional<std::vector<MetaCoreGameObjectData>> MetaCorePrefabResolver::Instantiate(
|
|
const MetaCoreAssetGuid& prefabGuid,
|
|
const MetaCoreAssetGuid& instanceGuid,
|
|
const std::vector<MetaCorePrefabOverrideDocument>& instanceOverrides,
|
|
std::string* error
|
|
) const {
|
|
if (!instanceGuid.IsValid()) {
|
|
if (error) *error = "Prefab instance GUID is invalid";
|
|
return std::nullopt;
|
|
}
|
|
auto resolved = Resolve(prefabGuid, error);
|
|
if (!resolved) return std::nullopt;
|
|
for (const auto& value : instanceOverrides)
|
|
if (!OverrideRegistry_.Apply(resolved->Document, value)) resolved->OrphanOverrides.push_back(value);
|
|
|
|
std::unordered_map<MetaCoreId, MetaCoreId> remap;
|
|
for (const auto& object : resolved->Document.GameObjects) remap[object.Id] = MetaCoreIdGenerator::Generate();
|
|
for (auto& object : resolved->Document.GameObjects) {
|
|
const MetaCoreId oldId = object.Id;
|
|
const MetaCoreAssetGuid nodeGuid = object.StableObjectGuid;
|
|
object.Id = remap.at(oldId);
|
|
if (object.ParentId != 0) {
|
|
const auto parent = remap.find(object.ParentId);
|
|
if (parent == remap.end()) {
|
|
if (error) *error = "Prefab contains a missing parent object";
|
|
return std::nullopt;
|
|
}
|
|
object.ParentId = parent->second;
|
|
}
|
|
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(instanceGuid, nodeGuid);
|
|
MetaCorePrefabInstanceMetadata metadata;
|
|
metadata.PrefabAssetGuid = prefabGuid;
|
|
metadata.PrefabObjectId = oldId;
|
|
metadata.InstanceGuid = instanceGuid;
|
|
metadata.PrefabNodeGuid = nodeGuid;
|
|
object.PrefabInstance = metadata;
|
|
}
|
|
const MetaCoreId rootId = [&] {
|
|
const auto root = std::find_if(resolved->Document.GameObjects.begin(), resolved->Document.GameObjects.end(),
|
|
[](const auto& object) { return object.ParentId == 0; });
|
|
return root == resolved->Document.GameObjects.end() ? MetaCoreId{} : root->Id;
|
|
}();
|
|
for (auto& object : resolved->Document.GameObjects)
|
|
if (object.PrefabInstance) object.PrefabInstance->PrefabInstanceRootId = rootId;
|
|
return resolved->Document.GameObjects;
|
|
}
|
|
|
|
std::vector<std::string> MetaCorePrefabResolver::ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const {
|
|
std::string error;
|
|
const auto result = Resolve(prefabGuid, &error);
|
|
std::vector<std::string> issues;
|
|
if (!result) issues.push_back(error);
|
|
else if (!result->OrphanOverrides.empty())
|
|
issues.push_back("Prefab contains " + std::to_string(result->OrphanOverrides.size()) + " orphan override(s)");
|
|
return issues;
|
|
}
|
|
|
|
} // namespace MetaCore
|