307 lines
16 KiB
C++
307 lines
16 KiB
C++
#include "MetaCoreScene/MetaCorePrefab.h"
|
|
#include "MetaCoreScene/MetaCoreScenePackage.h"
|
|
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
|
|
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
|
#include "MetaCoreFoundation/MetaCorePackage.h"
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
|
|
namespace {
|
|
|
|
using namespace MetaCore;
|
|
|
|
void Expect(bool condition, const char* message) {
|
|
if (!condition) {
|
|
std::cerr << "FAILED: " << message << '\n';
|
|
std::exit(1);
|
|
}
|
|
}
|
|
|
|
MetaCoreSceneDocument MakeScene(const char* name, std::size_t objectCount = 1) {
|
|
MetaCoreSceneDocument document;
|
|
document.SceneGuid = MetaCoreAssetGuid::Generate();
|
|
document.Name = name;
|
|
for (std::size_t index = 0; index < objectCount; ++index) {
|
|
MetaCoreGameObjectData object;
|
|
object.Id = index + 1;
|
|
object.ParentId = index == 0 ? 0 : index;
|
|
object.SceneGuid = document.SceneGuid;
|
|
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, object.Id);
|
|
object.Name = std::string(name) + std::to_string(index);
|
|
document.GameObjects.push_back(std::move(object));
|
|
}
|
|
return document;
|
|
}
|
|
|
|
void TickUntilSettled(MetaCoreSceneWorld& world, std::uint64_t request) {
|
|
for (int iteration = 0; iteration < 1000; ++iteration) {
|
|
world.Tick();
|
|
const auto status = world.GetLoadStatus(request);
|
|
if (status && status->State != MetaCoreSceneLoadState::Loading) return;
|
|
std::this_thread::yield();
|
|
}
|
|
Expect(false, "asynchronous scene request did not settle");
|
|
}
|
|
|
|
void TestSceneJsonAndPackageVersions() {
|
|
const auto root = std::filesystem::temp_directory_path() / "MetaCoreSceneTests";
|
|
std::filesystem::create_directories(root);
|
|
const auto jsonPath = root / "Versioned.mcscene.json";
|
|
const auto packagePath = root / "Versioned.mcscene";
|
|
auto document = MakeScene("Versioned", 2);
|
|
MetaCoreSubSceneDocument sub;
|
|
sub.SceneGuid = MetaCoreAssetGuid::Generate();
|
|
sub.ScenePath = "Scenes/Cell.mcscene.json";
|
|
sub.LoadPolicy = MetaCoreSubSceneLoadPolicy::Distance;
|
|
sub.PreloadDistance = 25.0F;
|
|
sub.UnloadDistance = 30.0F;
|
|
document.SubScenes.push_back(sub);
|
|
document.ComponentVersions.push_back({"Transform", 2});
|
|
|
|
const auto registry = MetaCoreBuildScenePackageTypeRegistry();
|
|
Expect(MetaCoreSceneSerializer::SaveSceneToJson(jsonPath, document, registry), "versioned scene JSON save");
|
|
const auto jsonLoaded = MetaCoreSceneSerializer::LoadSceneFromJson(jsonPath, registry);
|
|
Expect(jsonLoaded && jsonLoaded->SchemaVersion == GMetaCoreSceneSchemaVersion, "scene JSON version round trip");
|
|
Expect(jsonLoaded->SceneGuid == document.SceneGuid, "scene GUID round trip");
|
|
Expect(jsonLoaded->GameObjects[1].StableObjectGuid == document.GameObjects[1].StableObjectGuid, "stable object GUID round trip");
|
|
Expect(jsonLoaded->SubScenes.size() == 1 && jsonLoaded->SubScenes[0].SceneGuid == sub.SceneGuid, "SubScene JSON round trip");
|
|
|
|
MetaCorePrefabDocument prefab;
|
|
prefab.Name = "VersionedPrefab";
|
|
prefab.PrefabGuid = MetaCoreAssetGuid::Generate();
|
|
prefab.GameObjects = document.GameObjects;
|
|
prefab.GameObjects[0].SceneGuid = {};
|
|
prefab.GameObjects[1].SceneGuid = {};
|
|
prefab.Overrides.push_back({
|
|
prefab.GameObjects[0].StableObjectGuid, "Name", "Value",
|
|
MetaCorePrefabOverrideOperation::SetProperty, "\"Renamed\""
|
|
});
|
|
const auto prefabPath = root / "Versioned.mcprefab.json";
|
|
Expect(MetaCoreSceneSerializer::SavePrefabToJson(prefabPath, prefab, registry), "versioned prefab JSON save");
|
|
const auto prefabLoaded = MetaCoreSceneSerializer::LoadPrefabFromJson(prefabPath, registry);
|
|
Expect(prefabLoaded && prefabLoaded->SchemaVersion == GMetaCorePrefabSchemaVersion,
|
|
"prefab JSON version round trip");
|
|
Expect(prefabLoaded->PrefabGuid == prefab.PrefabGuid && prefabLoaded->Overrides.size() == 1,
|
|
"prefab GUID and override round trip");
|
|
|
|
MetaCoreSceneDocument legacy;
|
|
legacy.Name = "LegacyDeterministic";
|
|
legacy.GameObjects.push_back(MetaCoreGameObjectData{});
|
|
legacy.GameObjects[0].Id = 42;
|
|
const auto legacyFirstPath = root / "LegacyFirst.mcscene.json";
|
|
const auto legacySecondPath = root / "LegacySecond.mcscene.json";
|
|
Expect(MetaCoreSceneSerializer::SaveSceneToJson(legacyFirstPath, legacy, registry), "legacy scene first migration save");
|
|
Expect(MetaCoreSceneSerializer::SaveSceneToJson(legacySecondPath, legacy, registry), "legacy scene second migration save");
|
|
const auto legacyFirst = MetaCoreSceneSerializer::LoadSceneFromJson(legacyFirstPath, registry);
|
|
const auto legacySecond = MetaCoreSceneSerializer::LoadSceneFromJson(legacySecondPath, registry);
|
|
Expect(legacyFirst && legacySecond && legacyFirst->SceneGuid == legacySecond->SceneGuid,
|
|
"legacy scene GUID migration is deterministic");
|
|
Expect(legacyFirst->GameObjects[0].StableObjectGuid == legacySecond->GameObjects[0].StableObjectGuid,
|
|
"legacy object GUID migration is deterministic");
|
|
|
|
Expect(MetaCoreWriteScenePackage(packagePath, document), "versioned scene package save");
|
|
const auto packageLoaded = MetaCoreReadScenePackage(packagePath);
|
|
Expect(packageLoaded && packageLoaded->SceneGuid == document.SceneGuid, "scene package round trip");
|
|
const auto package = MetaCoreReadPackageFile(packagePath, registry);
|
|
Expect(package && package->Header.PackageGuid == document.SceneGuid, "scene package uses scene GUID");
|
|
Expect(package->Dependencies.size() == 1 && package->Dependencies[0].AssetGuid == sub.SceneGuid, "scene package dependency table");
|
|
Expect(!package->CustomVersions.empty() && package->CustomVersions[0].Key == "MetaCore.Scene", "scene package custom version");
|
|
}
|
|
|
|
void TestAdditiveWorldAndQualifiedReferences() {
|
|
auto first = MakeScene("Root", 2);
|
|
auto second = MakeScene("Cell", 1);
|
|
first.GameObjects[0].PhysicsConstraint = MetaCorePhysicsConstraintComponent{};
|
|
first.GameObjects[0].PhysicsConstraint->TargetObjectReference = {
|
|
second.SceneGuid,
|
|
second.GameObjects[0].StableObjectGuid,
|
|
MetaCoreObjectReferenceStrength::Hard
|
|
};
|
|
|
|
std::unordered_map<MetaCoreAssetGuid, MetaCoreSceneDocument, MetaCoreAssetGuidHasher> documents;
|
|
documents.emplace(second.SceneGuid, second);
|
|
MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
|
|
const auto iterator = documents.find(guid);
|
|
return iterator == documents.end() ? std::nullopt : std::optional(iterator->second);
|
|
});
|
|
|
|
Expect(world.LoadDocumentNow(first, MetaCoreSceneLoadMode::Replace), "replace load root scene");
|
|
const auto sourceRoot = world.ResolveObject({
|
|
first.SceneGuid, first.GameObjects[0].StableObjectGuid, MetaCoreObjectReferenceStrength::Soft
|
|
});
|
|
Expect(sourceRoot, "source root resolves before additive load");
|
|
Expect(world.GetLoadedScenes().size() == 1 && world.GetScene().GetGameObjects().size() == 2, "root scene object count");
|
|
const auto requestStatusesBefore = world.GetLoadedScenes();
|
|
(void)requestStatusesBefore;
|
|
for (int iteration = 0; iteration < 1000 && !world.IsSceneLoaded(second.SceneGuid); ++iteration) {
|
|
world.Tick();
|
|
std::this_thread::yield();
|
|
}
|
|
Expect(world.IsSceneLoaded(second.SceneGuid), "hard reference loads target scene");
|
|
Expect(world.GetScene().GetGameObjects().size() == 3, "additive objects share composite ECS");
|
|
Expect(!world.UnloadScene(second.SceneGuid), "hard referenced scene cannot unload");
|
|
|
|
const auto targetReference = MetaCoreObjectReference{
|
|
second.SceneGuid,
|
|
second.GameObjects[0].StableObjectGuid,
|
|
MetaCoreObjectReferenceStrength::Soft
|
|
};
|
|
const auto target = world.ResolveObject(targetReference);
|
|
Expect(target, "qualified object reference resolves");
|
|
const auto rebuilt = world.BuildObjectReference(target.GetId());
|
|
Expect(rebuilt.SceneGuid == second.SceneGuid && rebuilt.StableObjectGuid == targetReference.StableObjectGuid,
|
|
"runtime object builds stable qualified reference");
|
|
|
|
const auto captured = world.CaptureSceneDocument(first.SceneGuid);
|
|
Expect(captured && captured->GameObjects.size() == 2, "capture filters one loaded scene");
|
|
Expect(world.SetActiveScene(second.SceneGuid) && world.GetActiveSceneGuid() == second.SceneGuid, "active additive scene");
|
|
Expect(world.MoveSubtreeToScene(sourceRoot.GetId(), second.SceneGuid), "move subtree across scenes");
|
|
const auto movedCapture = world.CaptureSceneDocument(second.SceneGuid);
|
|
Expect(movedCapture && movedCapture->GameObjects.size() == 3, "move subtree updates target scene ownership");
|
|
const auto worldSnapshot = world.CaptureWorldSnapshot();
|
|
Expect(world.UnloadScene(first.SceneGuid, true), "hard reference source can force unload");
|
|
Expect(world.UnloadScene(second.SceneGuid), "hard residency releases after reference source unloads");
|
|
world.RestoreWorldSnapshot(worldSnapshot);
|
|
Expect(world.GetLoadedScenes().size() == 2 && world.GetScene().GetGameObjects().size() == 3,
|
|
"world snapshot restores complete scene set");
|
|
Expect(world.GetActiveSceneGuid() == second.SceneGuid, "world snapshot restores active scene");
|
|
}
|
|
|
|
void TestAsynchronousLoadCancelAndStreaming() {
|
|
auto root = MakeScene("StreamingRoot");
|
|
auto cell = MakeScene("StreamingCell");
|
|
MetaCoreSubSceneDocument sub;
|
|
sub.SceneGuid = cell.SceneGuid;
|
|
sub.LoadPolicy = MetaCoreSubSceneLoadPolicy::Distance;
|
|
sub.BoundsCenter = {100.0F, 0.0F, 0.0F};
|
|
sub.BoundsExtent = {5.0F, 5.0F, 5.0F};
|
|
sub.PreloadDistance = 10.0F;
|
|
sub.UnloadDistance = 20.0F;
|
|
root.SubScenes.push_back(sub);
|
|
|
|
MetaCoreSceneWorld world([cell](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
|
|
return guid == cell.SceneGuid ? std::optional(cell) : std::nullopt;
|
|
});
|
|
Expect(world.LoadDocumentNow(root, MetaCoreSceneLoadMode::Replace), "streaming root load");
|
|
world.UpdateStreaming({glm::vec3(90.0F, 0.0F, 0.0F)});
|
|
for (int iteration = 0; iteration < 1000 && !world.IsSceneLoaded(cell.SceneGuid); ++iteration) {
|
|
world.Tick();
|
|
std::this_thread::yield();
|
|
}
|
|
Expect(world.IsSceneLoaded(cell.SceneGuid), "distance cell loads near threshold");
|
|
world.UpdateStreaming({glm::vec3(111.0F, 0.0F, 0.0F)});
|
|
Expect(world.IsSceneLoaded(cell.SceneGuid), "distance hysteresis keeps cell before unload threshold");
|
|
world.UpdateStreaming({glm::vec3(130.0F, 0.0F, 0.0F)});
|
|
Expect(!world.IsSceneLoaded(cell.SceneGuid), "distance cell unloads beyond threshold");
|
|
|
|
const auto request = world.RequestLoad(cell.SceneGuid);
|
|
const auto duplicateRequest = world.RequestLoad(cell.SceneGuid);
|
|
Expect(duplicateRequest == request, "duplicate asynchronous scene requests are coalesced");
|
|
Expect(world.CancelLoad(request), "pending asynchronous load cancels");
|
|
TickUntilSettled(world, request);
|
|
Expect(world.GetLoadStatus(request)->State == MetaCoreSceneLoadState::Cancelled, "cancelled request remains cancelled");
|
|
}
|
|
|
|
void TestSceneDependencyValidation() {
|
|
auto root = MakeScene("DependencyRoot");
|
|
auto child = MakeScene("DependencyChild");
|
|
root.SubScenes.push_back({child.SceneGuid});
|
|
child.SubScenes.push_back({root.SceneGuid});
|
|
std::unordered_map<MetaCoreAssetGuid, MetaCoreSceneDocument, MetaCoreAssetGuidHasher> documents{
|
|
{root.SceneGuid, root}, {child.SceneGuid, child}
|
|
};
|
|
MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
|
|
const auto iterator = documents.find(guid);
|
|
return iterator == documents.end() ? std::nullopt : std::optional(iterator->second);
|
|
});
|
|
const auto issues = world.ValidateDependencies(root);
|
|
Expect(std::any_of(issues.begin(), issues.end(), [](const std::string& issue) {
|
|
return issue.find("cycle") != std::string::npos;
|
|
}), "recursive SubScene cycle is rejected");
|
|
}
|
|
|
|
void TestPrefabVariantNestedAndOverrides() {
|
|
MetaCorePrefabDocument base;
|
|
base.PrefabGuid = MetaCoreAssetGuid::Generate();
|
|
base.Name = "Base";
|
|
MetaCoreGameObjectData root;
|
|
root.Id = 1;
|
|
root.StableObjectGuid = MetaCoreBuildDeterministicGuid(base.PrefabGuid, 1);
|
|
root.Name = "BaseRoot";
|
|
base.GameObjects.push_back(root);
|
|
|
|
MetaCorePrefabDocument nested;
|
|
nested.PrefabGuid = MetaCoreAssetGuid::Generate();
|
|
nested.Name = "Nested";
|
|
MetaCoreGameObjectData nestedRoot;
|
|
nestedRoot.Id = 1;
|
|
nestedRoot.StableObjectGuid = MetaCoreBuildDeterministicGuid(nested.PrefabGuid, 1);
|
|
nestedRoot.Name = "NestedRoot";
|
|
nested.GameObjects.push_back(nestedRoot);
|
|
|
|
MetaCorePrefabDocument variant;
|
|
variant.PrefabGuid = MetaCoreAssetGuid::Generate();
|
|
variant.BasePrefabGuid = base.PrefabGuid;
|
|
variant.Name = "Variant";
|
|
variant.Overrides.push_back({
|
|
root.StableObjectGuid, "Transform", "Position", MetaCorePrefabOverrideOperation::SetProperty, "[1,2,3]"
|
|
});
|
|
MetaCoreNestedPrefabDocument nestedInstance;
|
|
nestedInstance.MountNodeGuid = root.StableObjectGuid;
|
|
nestedInstance.PrefabAssetGuid = nested.PrefabGuid;
|
|
nestedInstance.InstanceGuid = MetaCoreAssetGuid::Generate();
|
|
variant.NestedPrefabs.push_back(nestedInstance);
|
|
|
|
std::unordered_map<MetaCoreAssetGuid, MetaCorePrefabDocument, MetaCoreAssetGuidHasher> prefabs{
|
|
{base.PrefabGuid, base}, {nested.PrefabGuid, nested}, {variant.PrefabGuid, variant}
|
|
};
|
|
MetaCorePrefabResolver resolver([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCorePrefabDocument> {
|
|
const auto iterator = prefabs.find(guid);
|
|
return iterator == prefabs.end() ? std::nullopt : std::optional(iterator->second);
|
|
});
|
|
|
|
const auto resolved = resolver.Resolve(variant.PrefabGuid);
|
|
Expect(resolved && resolved->Document.GameObjects.size() == 2, "variant resolves base and nested prefab");
|
|
Expect(resolved->Document.GameObjects[0].Transform.Position == glm::vec3(1.0F, 2.0F, 3.0F), "variant property override");
|
|
Expect(resolved->Document.GameObjects[1].ParentId == resolved->Document.GameObjects[0].Id, "nested prefab mounts below target node");
|
|
|
|
const auto instanceGuid = MetaCoreAssetGuid::Generate();
|
|
const auto firstInstance = resolver.Instantiate(variant.PrefabGuid, instanceGuid);
|
|
const auto secondInstance = resolver.Instantiate(variant.PrefabGuid, instanceGuid);
|
|
Expect(firstInstance && secondInstance && firstInstance->size() == secondInstance->size(), "prefab instantiate succeeds");
|
|
for (std::size_t index = 0; index < firstInstance->size(); ++index)
|
|
Expect((*firstInstance)[index].StableObjectGuid == (*secondInstance)[index].StableObjectGuid,
|
|
"same instance GUID yields stable prefab object GUIDs");
|
|
|
|
const std::vector<MetaCorePrefabOverrideDocument> structuralOverrides{
|
|
{root.StableObjectGuid, "Camera", "", MetaCorePrefabOverrideOperation::AddComponent, "{}"},
|
|
{root.StableObjectGuid, "", "", MetaCorePrefabOverrideOperation::AddChild,
|
|
R"({"Name":"InstanceChild","Position":[4,5,6]})"}
|
|
};
|
|
const auto structuralInstance = resolver.Instantiate(
|
|
variant.PrefabGuid, MetaCoreAssetGuid::Generate(), structuralOverrides
|
|
);
|
|
Expect(structuralInstance && structuralInstance->size() == 3,
|
|
"instance structural override adds a child");
|
|
Expect((*structuralInstance)[0].Camera.has_value(), "instance component override adds a component");
|
|
|
|
prefabs[base.PrefabGuid].BasePrefabGuid = variant.PrefabGuid;
|
|
Expect(!resolver.Resolve(variant.PrefabGuid), "prefab inheritance cycle rejected");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
TestSceneJsonAndPackageVersions();
|
|
TestAdditiveWorldAndQualifiedReferences();
|
|
TestAsynchronousLoadCancelAndStreaming();
|
|
TestSceneDependencyValidation();
|
|
TestPrefabVariantNestedAndOverrides();
|
|
std::cout << "MetaCoreSceneTests passed\n";
|
|
return 0;
|
|
}
|