feat: add ObjectRef for scene loading to not rely on runtime id's

This commit is contained in:
2026-10-18 17:15:51 +02:00
parent 94fa2bb4b2
commit d21c2ccfd7
10 changed files with 262 additions and 35 deletions
+4
View File
@@ -1,5 +1,8 @@
add_executable(destrum_tests
destrum_tests.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Activatable.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Interactable.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/InteractableButton.cpp
)
set_target_properties(destrum_tests PROPERTIES
@@ -12,6 +15,7 @@ target_compile_definitions(destrum_tests PRIVATE
SDL_MAIN_HANDLED
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
)
target_include_directories(destrum_tests PRIVATE "${CMAKE_SOURCE_DIR}/lightkeeper/include")
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
destrum_enable_warnings(destrum_tests)
+145
View File
@@ -7,6 +7,7 @@
#include <iostream>
#include <memory>
#include <string>
#include <unordered_set>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/packing.hpp>
@@ -44,6 +45,9 @@
#include <destrum/Graphics/imageLoader.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <components/Activatable.h>
#include <components/InteractableButton.h>
namespace {
class TestComponent final : public Component {
public:
@@ -53,6 +57,18 @@ namespace {
std::string GetTypeName() const override { return "TestComponent"; }
};
class TestActivatable final : public Activatable {
public:
explicit TestActivatable(GameObject& owner) : Activatable(owner, "TestActivatable") {}
std::string GetTypeName() const override { return "TestActivatable"; }
int activations{0};
protected:
void OnActivate(GameObject&) override { ++activations; }
};
class RemovingListener final : public EventListener {
public:
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
@@ -1473,6 +1489,132 @@ namespace {
!PrimitiveMeshes::MakeFromKey("Box").has_value(),
"invalid primitive keys must not build a mesh");
}
void testSceneFileIdsAreLocal()
{
const auto path = std::filesystem::temp_directory_path() / "destrum_local_ids_scene.json";
std::ofstream{path}
<< R"({"version":2,"currentCamera":2,"objects":[)"
R"({"id":1,"name":"root"},)"
R"({"id":2,"name":"child","parent":1,"components":[{"type":"CameraComponent"}]}]})";
Scene& first = SceneManager::GetInstance().CreateScene("local ids a");
Scene& second = SceneManager::GetInstance().CreateScene("local ids b");
check(SceneSerializer::Load(first, path), "the first copy of the scene must load");
check(SceneSerializer::Load(second, path), "the second copy of the scene must load");
std::filesystem::remove(path);
first.CommitPendingAdditions();
second.CommitPendingAdditions();
std::unordered_set<ObjectId> ids;
std::size_t count = 0;
for (const Scene* scene : {&first, &second}) {
for (const auto& object : scene->GetObjects()) {
ids.insert(object->GetId());
++count;
}
}
check(count == 4 && ids.size() == 4, "loading one file into two scenes must not duplicate ids");
for (const Scene* scene : {&first, &second}) {
const CameraComponent* camera = scene->GetCurrentCamera();
check(camera != nullptr && camera->GetGameObject()->GetName() == "child" &&
camera->GetGameObject()->GetScene() == scene,
"the current camera must be remapped to the object in its own scene");
const Transform* parent = camera->GetGameObject()->GetTransform().GetParent();
check(parent != nullptr && parent->GetOwner()->GetName() == "root" &&
parent->GetOwner()->GetScene() == scene,
"parents must be remapped within each loaded scene");
}
const GameObject* fresh = first.CreateGameObject("fresh");
check(!ids.contains(fresh->GetId()), "objects created after a load must get unused ids");
SceneManager::GetInstance().Destroy();
}
GameObject* findByName(const Scene& scene, const std::string& name)
{
for (const auto& object : scene.GetObjects()) {
if (object->GetName() == name) {
return object.get();
}
}
return nullptr;
}
void testButtonTargetsRoundTrip()
{
ComponentFactory::Register<InteractableButton>("InteractableButton");
ComponentFactory::Register<TestActivatable>("TestActivatable");
Scene& scene = SceneManager::GetInstance().CreateScene("button targets");
GameObject* door = scene.CreateGameObject("door");
door->AddComponent<TestActivatable>();
GameObject* lamp = scene.CreateGameObject("lamp");
lamp->AddComponent<TestActivatable>();
GameObject* buttonObject = scene.CreateGameObject("button");
auto* button = buttonObject->AddComponent<InteractableButton>();
button->SetCycleColors(false);
button->AddTarget(*door);
button->AddTarget(*lamp);
button->AddTarget(*door);
scene.CommitPendingAdditions();
check(button->GetTargets().size() == 2, "a button must not target the same object twice");
const auto path = std::filesystem::temp_directory_path() / "destrum_button_targets_scene.json";
check(SceneSerializer::Save(scene, path), "the button scene must save");
check(SceneSerializer::Load(scene, path), "the button scene must load");
Scene& copy = SceneManager::GetInstance().CreateScene("button targets copy");
check(SceneSerializer::Load(copy, path), "the button scene must load into a second scene");
std::filesystem::remove(path);
for (Scene* loaded : {&scene, &copy}) {
loaded->CommitPendingAdditions();
GameObject* loadedButton = findByName(*loaded, "button");
auto* loadedComponent = loadedButton != nullptr ? loadedButton->GetComponent<InteractableButton>() : nullptr;
check(loadedComponent != nullptr, "the button must survive a round-trip");
const auto& targets = loadedComponent->GetTargets();
check(targets.size() == 2, "button targets must survive a round-trip");
GameObject* loadedDoor = targets[0].Get(*loaded);
GameObject* loadedLamp = targets[1].Get(*loaded);
check(loadedDoor == findByName(*loaded, "door") && loadedDoor != nullptr &&
loadedLamp == findByName(*loaded, "lamp") && loadedLamp != nullptr,
"button targets must point at the loaded objects in their own scene");
loadedComponent->Interact(*loadedButton);
check(loadedDoor->GetComponent<TestActivatable>()->activations == 1 &&
loadedLamp->GetComponent<TestActivatable>()->activations == 1,
"pressing a loaded button must activate its targets");
}
SceneManager::GetInstance().Destroy();
}
void testButtonDropsMissingTargets()
{
ComponentFactory::Register<InteractableButton>("InteractableButton");
Scene& scene = SceneManager::GetInstance().CreateScene("missing targets");
const auto path = std::filesystem::temp_directory_path() / "destrum_missing_targets_scene.json";
std::ofstream{path}
<< R"({"version":2,"objects":[{"id":5,"name":"button","components":)"
R"([{"type":"InteractableButton","data":{"cycleColors":false,"targets":[5,99]}}]}]})";
check(SceneSerializer::Load(scene, path), "a button with a dangling target must still load");
std::filesystem::remove(path);
scene.CommitPendingAdditions();
const GameObject* object = scene.GetObjects().front().get();
const auto* button = object->GetComponent<InteractableButton>();
check(button != nullptr && button->GetTargets().size() == 1 &&
button->GetTargets().front().GetId() == object->GetId(),
"dangling targets must be dropped and valid ones remapped");
SceneManager::GetInstance().Destroy();
}
}
int main()
@@ -1518,6 +1660,9 @@ int main()
testSceneLoadDropsMissingCamera();
testGlmJson();
testPrimitiveMeshKeys();
testSceneFileIdsAreLocal();
testButtonTargetsRoundTrip();
testButtonDropsMissingTargets();
std::cout << "destrum tests passed\n";
return EXIT_SUCCESS;
}