fix: alot of stuff changed. Mostly bugfixxes / architechture changes

This commit is contained in:
2026-08-09 02:24:17 +02:00
parent 4b6f773c0c
commit 05384debbf
98 changed files with 5461 additions and 1751 deletions
+16
View File
@@ -0,0 +1,16 @@
add_executable(destrum_tests
destrum_tests.cpp
)
set_target_properties(destrum_tests PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
target_compile_definitions(destrum_tests PRIVATE SDL_MAIN_HANDLED)
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
destrum_enable_warnings(destrum_tests)
add_test(NAME destrum_tests COMMAND destrum_tests)
+363
View File
@@ -0,0 +1,363 @@
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <destrum/Event.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/SceneSerializer.h>
#include <destrum/Physics/SimplePhysicsWorld.h>
#include <destrum/Physics/JoltPhysicsWorld.h>
#include <destrum/Components/Physics/SphereCollider.h>
#include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
namespace {
class TestComponent final : public Component {
public:
explicit TestComponent(GameObject& owner) : Component(owner, "TestComponent") {}
void Update() override {}
std::string GetTypeName() const override { return "TestComponent"; }
};
class RemovingListener final : public EventListener {
public:
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
: victim(victim) {}
void OnEvent(int) {
if (victim != nullptr) {
victim->reset();
victim = nullptr;
}
++calls;
}
int calls{0};
private:
std::unique_ptr<RemovingListener>* victim;
};
void check(bool condition, const char* message)
{
if (!condition) {
std::cerr << "FAILED: " << message << '\n';
std::exit(EXIT_FAILURE);
}
}
bool close(float a, float b)
{
return std::abs(a - b) < 0.001f;
}
void testTransforms()
{
GameObject parent{"parent"};
GameObject child{"child"};
parent.GetTransform().SetLocalPosition({10.f, 2.f, -4.f});
parent.GetTransform().SetLocalRotation({0.f, 0.f, 90.f});
parent.GetTransform().SetLocalScale({2.f, 2.f, 2.f});
child.GetTransform().SetLocalPosition({1.f, 0.f, 0.f});
child.GetTransform().SetParent(&parent.GetTransform(), true);
const auto worldPosition = child.GetTransform().GetWorldPosition();
check(close(worldPosition.x, 1.f) && close(worldPosition.y, 0.f),
"reparenting must preserve world position");
child.GetTransform().SetWorldPosition({20.f, 10.f, 0.f});
const auto movedPosition = child.GetTransform().GetWorldPosition();
check(close(movedPosition.x, 20.f) && close(movedPosition.y, 10.f),
"world position must account for parent rotation and scale");
parent.Destroy();
check(parent.IsBeingDestroyed() && child.IsBeingDestroyed(),
"destroying a parent must mark descendants");
}
void testSceneRoundTrip()
{
Scene& scene = SceneManager::GetInstance().CreateScene("round-trip");
GameObject* parent = scene.CreateGameObject("parent");
GameObject* child = scene.CreateGameObject("child");
child->GetTransform().SetLocalPosition({1.f, 2.f, 3.f});
child->GetTransform().SetParent(&parent->GetTransform(), false);
scene.CommitPendingAdditions();
const auto path = std::filesystem::temp_directory_path() / "destrum_scene_test.json";
check(SceneSerializer::Save(scene, path), "scene save must succeed");
check(SceneSerializer::Load(scene, path), "scene load must succeed");
std::filesystem::remove(path);
check(scene.GetObjects().size() == 2, "scene round-trip must preserve objects");
const auto* loadedChild = scene.GetObjects().at(1).get();
check(loadedChild->GetTransform().GetParent() != nullptr,
"scene round-trip must preserve hierarchy");
check(close(loadedChild->GetTransform().GetLocalPosition().x, 1.f),
"scene round-trip must preserve local transforms");
SceneManager::GetInstance().Destroy();
}
void testEventMutation()
{
Event<int> event;
int calls = 0;
event.AddListener([&](int) { ++calls; });
event.AddListener([&](int) {
++calls;
event.AddListener([&](int) { ++calls; });
});
event.Invoke(1);
check(calls == 2, "event mutation must not invalidate the current invocation");
event.Invoke(1);
check(calls == 5, "new event listeners must run on subsequent invocations");
}
void testEventRemoval()
{
Event<int> event;
std::unique_ptr<RemovingListener> victim;
RemovingListener remover{&victim};
victim = std::make_unique<RemovingListener>(nullptr);
event.AddListener(&remover, &RemovingListener::OnEvent);
event.AddListener(victim.get(), &RemovingListener::OnEvent);
event.Invoke(1);
check(remover.calls == 1, "event remover must run once");
check(victim == nullptr, "event listener must be removable during invocation");
}
void testComponentRemoval()
{
Scene& scene = SceneManager::GetInstance().CreateScene("component-removal");
GameObject* object = scene.CreateGameObject("object");
object->AddComponent<TestComponent>();
scene.CommitPendingAdditions();
check(object->GetComponent<TestComponent>() != nullptr,
"test component must be discoverable before removal");
check(object->DestroyComponent<TestComponent>() != nullptr,
"destroying a component must find it");
scene.CleanupDestroyedGameObjects();
check(object->GetComponent<TestComponent>() == nullptr,
"destroyed components must be removed at the scene boundary");
SceneManager::GetInstance().Destroy();
}
void testAssetPathValidation()
{
const auto root = std::filesystem::temp_directory_path() / "destrum_asset_test";
std::filesystem::create_directories(root / "assets" / "engine");
std::filesystem::create_directories(root / "assets" / "game");
std::ofstream{root / "assets" / "engine" / "asset.txt"} << "asset";
std::ofstream{root / "assets" / "engine" / "cooked.asset"} << "cooked";
std::ofstream{root / "assets" / "engine" / "manifest.json"}
<< R"({"version":1,"assets":[{"src":"asset.txt","out":"cooked.asset","type":"raw","mtime_epoch_ns":-5020137004799419216,"size_bytes":355972}]})";
auto& assetFs = AssetFS::GetInstance();
assetFs.Init(root);
check(std::filesystem::exists(assetFs.GetFullPath("engine://asset.txt")),
"valid virtual asset paths must resolve");
check(assetFs.GetCookedPathForFile("engine://asset.txt").filename() == "cooked.asset",
"manifest output paths must resolve to cooked assets");
bool rejected = false;
try {
(void)assetFs.GetFullPath("engine://../outside.txt");
} catch (const std::exception&) {
rejected = true;
}
check(rejected, "asset traversal must be rejected");
const auto outside = root / "outside";
const auto link = root / "assets" / "engine" / "link";
std::filesystem::create_directories(outside);
std::error_code symlinkError;
std::filesystem::create_directory_symlink(outside, link, symlinkError);
if (!symlinkError) {
bool symlinkEscapeRejected = false;
try {
(void)assetFs.GetFullPath("engine://link/missing.txt");
} catch (const std::exception&) {
symlinkEscapeRejected = true;
}
check(symlinkEscapeRejected,
"symlink traversal must be rejected for missing targets");
}
std::filesystem::remove_all(root);
assetFs.Reset();
}
void testLegacySceneLoad()
{
Scene& scene = SceneManager::GetInstance().CreateScene("legacy");
const auto path = std::filesystem::temp_directory_path() / "destrum_legacy_scene.json";
std::ofstream{path}
<< R"({"version":1,"name":"legacy","objects":[{"id":9001,"name":"object","active":true,"transform":{},"parent":0,"components":[]}]})";
check(SceneSerializer::Load(scene, path),
"version 1 placeholder transforms must remain loadable");
check(scene.GetObjects().size() == 1,
"legacy scene load must create its object");
std::filesystem::remove(path);
SceneManager::GetInstance().Destroy();
}
void testSceneLoadRollback()
{
Scene& scene = SceneManager::GetInstance().CreateScene("rollback");
scene.CreateGameObject("original");
scene.CommitPendingAdditions();
const auto path = std::filesystem::temp_directory_path() / "destrum_bad_scene.json";
std::ofstream{path}
<< R"({"version":2,"name":"bad","objects":[{"id":9101,"name":"bad","transform":{"position":[0,0,0],"rotation":[0,0,0,1],"scale":[1,1,1]},"parent":0,"components":[{"type":"Rigidbody","enabled":true,"data":{}}]}]})";
check(!SceneSerializer::Load(scene, path),
"scene loading must reject a rigidbody without a collider");
check(scene.GetObjects().size() == 1 && scene.GetObjects().at(0)->GetName() == "original",
"failed scene loading must preserve the live scene");
std::filesystem::remove(path);
SceneManager::GetInstance().Destroy();
}
void testPhysicsWorldUnits()
{
SimplePhysicsWorld world{glm::vec3{0.0f}};
GameObject object{"scaled-physics"};
object.GetTransform().SetLocalScale(glm::vec3{0.5f});
object.AddComponent<SphereCollider>(1.0f);
Rigidbody* rigidbody = object.AddComponent<Rigidbody>();
world.RegisterRigidbody(*rigidbody);
PhysicsRaycastHit hit;
check(world.Raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f, hit),
"scaled physics bodies must remain raycastable");
check(close(hit.distance, 1.0f),
"physics collider dimensions must remain in world units");
object.GetTransform().SetLocalPosition({10.0f, 0.0f, 0.0f});
world.RefreshRigidbody(*rigidbody);
check(world.Raycast({8.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f, hit),
"refreshing a physics body must use the current transform position");
check(close(hit.distance, 1.0f),
"refreshed physics bodies must remain aligned with rendered transforms");
}
void testJoltSphereCollisions()
{
JoltPhysicsWorld world{{
.workerThreadCount = 1,
.gravity = {0.0f, -9.81f, 0.0f}
}};
GameObject floor{"floor"};
floor.GetTransform().SetLocalPosition({0.0f, -1.0f, 0.0f});
floor.AddComponent<BoxCollider>(glm::vec3{10.0f, 0.5f, 10.0f});
auto* floorBody = floor.AddComponent<Rigidbody>();
floorBody->SetType(RigidbodyType::Static);
world.RegisterRigidbody(*floorBody);
GameObject first{"first"};
first.AddComponent<SphereCollider>(1.5f);
auto* firstBody = first.AddComponent<Rigidbody>();
world.RegisterRigidbody(*firstBody);
first.GetTransform().SetLocalPosition({0.0f, 100.0f, 0.0f});
first.GetTransform().SetLocalScale(glm::vec3{0.015f});
world.RefreshRigidbody(*firstBody);
GameObject second{"second"};
second.AddComponent<SphereCollider>(1.5f);
auto* secondBody = second.AddComponent<Rigidbody>();
world.RegisterRigidbody(*secondBody);
second.GetTransform().SetLocalPosition({0.0f, 100.0f, 0.0f});
second.GetTransform().SetLocalScale(glm::vec3{0.015f});
world.RefreshRigidbody(*secondBody);
for (int step = 0; step < 240; ++step) {
world.Step(1.0f / 60.0f);
world.SyncDynamicBodiesToTransforms();
}
check(first.GetTransform().GetWorldPosition().y > 0.8f &&
second.GetTransform().GetWorldPosition().y > 0.8f,
"Jolt spheres must collide with the ground plane");
check(glm::distance(first.GetTransform().GetWorldPosition(),
second.GetTransform().GetWorldPosition()) > 2.5f,
"Jolt spheres must collide with one another instead of occupying the same point");
}
void testSceneJoltSphereCollisions()
{
Scene& scene = SceneManager::GetInstance().CreateScene("scene-physics");
GameObject* floor = scene.CreateGameObject("floor");
floor->GetTransform().SetLocalPosition({0.0f, -1.0f, 0.0f});
floor->AddComponent<BoxCollider>(glm::vec3{10.0f, 0.5f, 10.0f});
auto* floorBody = floor->AddComponent<Rigidbody>();
floorBody->SetType(RigidbodyType::Static);
GameObject* first = scene.CreateGameObject("first");
first->AddComponent<SphereCollider>(1.5f);
first->AddComponent<Rigidbody>();
first->GetTransform().SetWorldPosition({0.0f, 100.0f, 0.0f});
first->GetTransform().SetWorldScale(glm::vec3{0.015f});
GameObject* second = scene.CreateGameObject("second");
second->AddComponent<SphereCollider>(1.5f);
second->AddComponent<Rigidbody>();
second->GetTransform().SetWorldPosition({0.0f, 100.0f, 0.0f});
second->GetTransform().SetWorldScale(glm::vec3{0.015f});
scene.CommitPendingAdditions();
scene.GetPhysics().RefreshGameObject(*first);
scene.GetPhysics().RefreshGameObject(*second);
for (int step = 0; step < 240; ++step) {
scene.FixedUpdate(1.0f / 60.0f);
}
check(first->GetTransform().GetWorldPosition().y > 0.8f &&
second->GetTransform().GetWorldPosition().y > 0.8f,
"scene physics spheres must collide with the ground plane");
check(glm::distance(first->GetTransform().GetWorldPosition(),
second->GetTransform().GetWorldPosition()) > 2.5f,
"scene physics spheres must not overlap after settling");
SceneManager::GetInstance().Destroy();
}
}
int main()
{
testTransforms();
testSceneRoundTrip();
testEventMutation();
testEventRemoval();
testComponentRemoval();
testAssetPathValidation();
testLegacySceneLoad();
testSceneLoadRollback();
testPhysicsWorldUnits();
testJoltSphereCollisions();
testSceneJoltSphereCollisions();
std::cout << "destrum tests passed\n";
return EXIT_SUCCESS;
}