MetaCore/tests/MetaCoreSceneTests.cpp
2026-07-27 17:02:26 +08:00

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;
}