feat: move editor to project / add post processing etc

This commit is contained in:
2026-10-23 20:54:12 +02:00
parent f7e7c56392
commit a3d3da4a67
104 changed files with 11750 additions and 2675 deletions
+4 -2
View File
@@ -8,6 +8,7 @@ add_executable(destrum_tests
graphics_tests.cpp
ui_tests.cpp
gameplay_tests.cpp
editor_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
@@ -28,12 +29,13 @@ set_target_properties(destrum_tests PROPERTIES
target_compile_definitions(destrum_tests PRIVATE
SDL_MAIN_HANDLED
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
LIGHTKEEPER_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/lightkeeper/assets_src"
)
target_include_directories(destrum_tests PRIVATE "${CMAKE_SOURCE_DIR}/lightkeeper/include")
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
target_link_libraries(destrum_tests PRIVATE destrum::destrum destrum::editor)
destrum_enable_warnings(destrum_tests)
foreach(suite core serialization physics assets graphics ui gameplay)
foreach(suite core serialization physics assets graphics ui gameplay editor)
add_test(NAME destrum_tests.${suite} COMMAND destrum_tests ${suite})
endforeach()
+73
View File
@@ -5,7 +5,9 @@
#include <string>
#include <glm/gtc/packing.hpp>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetManager.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Assets/ModelFile.h>
#include <destrum/Assets/TextureFile.h>
@@ -16,6 +18,8 @@
#include <destrum/Components/Animator.h>
#include <destrum/Graphics/Material.h>
#include <destrum/Graphics/imageLoader.h>
#include <destrum/Serialization/MaterialSerialization.h>
#include <destrum/Serialization/ObjectSerialization.h>
#include "TestUtils.h"
@@ -447,6 +451,71 @@ namespace {
check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(),
"plain cache names must not be treated as file-backed assets");
}
void testAssetGuids()
{
const AssetGuid first = AssetGuid::Generate();
const AssetGuid second = AssetGuid::Generate();
check(first.IsValid() && second.IsValid() && first != second, "generated asset ids must be valid and unique");
const std::string text = first.ToString();
check(text.size() == 32 && AssetGuid::Parse(text) == first, "asset ids must round-trip through their text form");
check(!AssetGuid::Parse("1234").has_value() && !AssetGuid::Parse(std::string(32, 'z')).has_value() &&
!AssetGuid::Parse(std::string(32, '0')).has_value(),
"malformed or empty asset ids must be rejected");
}
void testAssetDatabaseFollowsMovedAssets()
{
auto& database = AssetDatabase::GetInstance();
database.Clear();
const AssetGuid guid = AssetGuid::Generate();
database.Register("game://models/crate.glb", guid);
check(database.FindGuid("game://models/crate.glb") == guid && database.FindPath(guid) == "game://models/crate.glb",
"registered assets must be found by path and by id");
const nlohmann::json reference = database.WriteReference("game://models/crate.glb", "mesh:0");
check(reference.at("guid") == guid.ToString() && reference.at("sub") == "mesh:0",
"references to known assets must carry the id and subresource");
check(!database.WriteReference("game://unknown.png").contains("guid"),
"assets without a meta file must not get an id outside a save");
database.Clear();
database.Register("game://props/crate.glb", guid);
const auto moved = database.ReadReference(reference);
check(moved.has_value() && moved->path == "game://props/crate.glb" && moved->subresource == "mesh:0",
"references must follow their id when the asset moved");
const auto byPath = database.ReadReference(nlohmann::json{{"path", "game://other.png"}});
check(byPath.has_value() && byPath->path == "game://other.png", "references without a known id must keep their path");
check(!database.ReadReference(nlohmann::json{{"guid", guid.ToString()}}).has_value(),
"a reference needs a path to fall back on");
database.Clear();
}
void testAssetTypes()
{
check(AssetTypes::IsModel("game://a/B.GLB") && AssetTypes::IsImage("x.Png") && AssetTypes::IsMaterial("m.dmat") &&
AssetTypes::IsScene("game://scenes/main.scene") && AssetTypes::IsPrefab("p.prefab") && AssetTypes::IsMeta("a.glb.meta"),
"asset types must be recognised by extension, ignoring case");
check(!AssetTypes::IsModel("model.glb.meta") && !AssetTypes::IsScene("scene.json"),
"only the last extension decides the asset type");
check(MaterialSerialization::IsFileKey("game://materials/a.dmat") && !MaterialSerialization::IsFileKey("local:3:1") &&
!MaterialSerialization::IsFileKey("game://a.glb#material:0"),
"only .dmat paths are material files");
}
void testMainSceneFileIsValid()
{
std::ifstream file{std::filesystem::path{LIGHTKEEPER_ASSETS_SRC_DIR} / "scenes" / "main.scene"};
const auto root = nlohmann::json::parse(file, nullptr, false);
check(!root.is_discarded() && root.at("version") == ObjectSerialization::CurrentObjectVersion &&
root.at("objects").size() > 100,
"the main scene must be saved in the current format with the whole level");
check(!MaterialSerialization::Validate(root.at("materials")).has_value(),
"the main scene's local materials must be valid");
}
}
void RunAssetsTests()
@@ -460,4 +529,8 @@ void RunAssetsTests()
testTextureFileRoundTrip();
testImageLoaderReadsCookedTextures();
testAssetReferenceCacheKeys();
testAssetGuids();
testAssetDatabaseFollowsMovedAssets();
testAssetTypes();
testMainSceneFileIsValid();
}
+68
View File
@@ -0,0 +1,68 @@
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <editor/UndoStack.h>
#include "TestUtils.h"
using namespace test;
namespace {
void testUndoRedoRestoresEdits()
{
Scene& scene = SceneManager::GetInstance().CreateScene("undo");
GameObject* box = scene.CreateGameObject("box");
box->GetTransform().SetLocalPosition({1.0f, 0.0f, 0.0f});
scene.CommitPendingAdditions();
UndoStack history;
history.Reset(scene);
check(!history.CanUndo() && !history.Record(scene), "an unchanged scene must not create an undo step");
findByName(scene, "box")->GetTransform().SetLocalPosition({5.0f, 0.0f, 0.0f});
scene.SetSelectedObject(findByName(scene, "box")->GetId());
check(history.Record(scene) && history.CanUndo() && !history.CanRedo(), "an edit must create an undo step");
check(history.Undo(scene), "undo must succeed");
scene.CommitPendingAdditions();
const GameObject* undone = findByName(scene, "box");
check(undone != nullptr && close(undone->GetTransform().GetLocalPosition().x, 1.0f), "undo must restore the earlier state");
check(scene.GetSelectedObject() == undone, "undo must keep the edited object selected");
check(history.CanRedo(), "undo must allow redo");
check(history.Redo(scene), "redo must succeed");
scene.CommitPendingAdditions();
check(close(findByName(scene, "box")->GetTransform().GetLocalPosition().x, 5.0f), "redo must bring the edit back");
check(!history.Record(scene), "reloading the scene for undo must not count as an edit");
scene.CreateGameObject("spawned");
scene.CommitPendingAdditions();
check(history.Record(scene) && !history.CanRedo(), "a new edit must drop the redo steps");
SceneManager::GetInstance().Destroy();
}
void testUndoHistoryIsCapped()
{
Scene& scene = SceneManager::GetInstance().CreateScene("undo cap");
GameObject* box = scene.CreateGameObject("box");
scene.CommitPendingAdditions();
UndoStack history;
history.Reset(scene);
for (std::size_t i = 0; i < UndoStack::MaxSteps + 10; ++i) {
box->GetTransform().SetLocalPosition({static_cast<float>(i + 1), 0.0f, 0.0f});
history.Record(scene);
}
check(history.UndoCount() == UndoStack::MaxSteps, "the undo history must keep only the newest steps");
SceneManager::GetInstance().Destroy();
}
}
void RunEditorTests()
{
testUndoRedoRestoresEdits();
testUndoHistoryIsCapped();
}
+2
View File
@@ -9,6 +9,7 @@ void RunAssetsTests();
void RunGraphicsTests();
void RunUiTests();
void RunGameplayTests();
void RunEditorTests();
namespace {
struct Suite {
@@ -24,6 +25,7 @@ namespace {
{"graphics", RunGraphicsTests},
{"ui", RunUiTests},
{"gameplay", RunGameplayTests},
{"editor", RunEditorTests},
};
}
+15 -1
View File
@@ -36,6 +36,8 @@ namespace {
environment.fogDensity = 0.05f;
environment.skyboxRotationSpeed = 0.5f;
environment.skyboxRotation = 1.0f;
environment.exposure = -1.5f;
environment.tonemapper = Tonemapper::Aces;
scene.Update(1.0f);
check(close(environment.skyboxRotation, 1.5f), "scene updates must spin the skybox");
@@ -56,7 +58,8 @@ namespace {
loadedEnvironment.fogColor == glm::vec3{0.6f, 0.7f, 0.8f} &&
close(loadedEnvironment.fogDensity, 0.05f) &&
close(loadedEnvironment.skyboxRotationSpeed, 0.5f) &&
close(loadedEnvironment.skyboxRotation, 1.5f),
close(loadedEnvironment.skyboxRotation, 1.5f) &&
close(loadedEnvironment.exposure, -1.5f) && loadedEnvironment.tonemapper == Tonemapper::Aces,
"scene round-trip must preserve the environment settings");
SceneManager::GetInstance().Destroy();
@@ -370,8 +373,19 @@ namespace {
}
}
namespace {
void testTonemapperNames()
{
for (const Tonemapper tonemapper : {Tonemapper::None, Tonemapper::Aces, Tonemapper::PbrNeutral}) {
check(ParseTonemapper(ToString(tonemapper)) == tonemapper, "tonemapper names must round-trip");
}
check(!ParseTonemapper("filmic").has_value(), "unknown tonemapper names must be rejected");
}
}
void RunSerializationTests()
{
testTonemapperNames();
testSceneRoundTrip();
testLegacySceneLoad();
testSceneLoadRollback();