feat: add reflection
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -9,7 +10,9 @@
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Serialization/ComponentFactory.h>
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/Components/LightComponent.h>
|
||||
@@ -386,8 +389,157 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
enum class TestMode { Off, Slow, Fast };
|
||||
constexpr std::array<const char*, 3> TestModeNames{"Off", "Slow", "Fast"};
|
||||
|
||||
class ReflectedComponent final : public Component {
|
||||
public:
|
||||
explicit ReflectedComponent(GameObject& owner) : Component(owner, "ReflectedComponent") {}
|
||||
|
||||
std::string GetTypeName() const override { return "ReflectedComponent"; }
|
||||
|
||||
void Reflect(FieldVisitor& v) override
|
||||
{
|
||||
v.Field("flag", flag);
|
||||
v.Field("count", count, {.min = 0, .max = 10});
|
||||
v.Field("index", index);
|
||||
v.Field("speed", speed, {.min = 0.0});
|
||||
v.Field("ratio", ratio);
|
||||
v.Field("offset", offset);
|
||||
v.Field("tint", tint, {.color = true});
|
||||
v.Field("name", name);
|
||||
v.Field("mode", mode, {.enumNames = TestModeNames});
|
||||
v.Field("rawMode", rawMode);
|
||||
v.Field("values", values);
|
||||
v.Field("target", target);
|
||||
v.Field("targets", targets);
|
||||
v.Field("cache", cache, {.transient = true});
|
||||
}
|
||||
|
||||
bool flag{false};
|
||||
int count{3};
|
||||
std::size_t index{0};
|
||||
float speed{1.0f};
|
||||
double ratio{0.5};
|
||||
glm::vec3 offset{0.0f};
|
||||
glm::vec4 tint{1.0f};
|
||||
std::string name{"default"};
|
||||
TestMode mode{TestMode::Off};
|
||||
TestMode rawMode{TestMode::Off};
|
||||
std::vector<float> values;
|
||||
ObjectRef target;
|
||||
std::vector<ObjectRef> targets;
|
||||
int cache{0};
|
||||
};
|
||||
|
||||
[[nodiscard]] bool throwsMentioning(ReflectedComponent& component, const nlohmann::json& data, const char* key)
|
||||
{
|
||||
try {
|
||||
component.Deserialize(data);
|
||||
} catch (const std::exception& exception) {
|
||||
return std::string{exception.what()}.find(key) != std::string::npos;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void testReflectedRoundTrip()
|
||||
{
|
||||
GameObject object{"reflected"};
|
||||
auto* component = object.AddComponent<ReflectedComponent>();
|
||||
component->flag = true;
|
||||
component->count = 7;
|
||||
component->index = 4;
|
||||
component->speed = 2.5f;
|
||||
component->ratio = 0.25;
|
||||
component->offset = glm::vec3{1.0f, -2.0f, 3.5f};
|
||||
component->tint = glm::vec4{0.1f, 0.2f, 0.3f, 0.4f};
|
||||
component->name = "named";
|
||||
component->mode = TestMode::Fast;
|
||||
component->rawMode = TestMode::Slow;
|
||||
component->values = {1.0f, 2.0f};
|
||||
component->target = ObjectRef(ObjectId{12});
|
||||
component->targets = {ObjectRef(ObjectId{13}), ObjectRef(ObjectId{14})};
|
||||
component->cache = 99;
|
||||
|
||||
const nlohmann::json data = component->Serialize();
|
||||
check(data.at("mode") == "Fast", "enums with names must save their name");
|
||||
check(data.at("rawMode") == 1, "enums without names must save their value");
|
||||
check(data.at("targets") == nlohmann::json::array({13, 14}), "object refs must save their ids");
|
||||
check(!data.contains("cache"), "transient fields must not be saved");
|
||||
|
||||
GameObject loadedObject{"loaded"};
|
||||
auto* loaded = loadedObject.AddComponent<ReflectedComponent>();
|
||||
loaded->Deserialize(data);
|
||||
check(loaded->flag && loaded->count == 7 && loaded->index == 4, "bool and integer fields must round-trip");
|
||||
check(loaded->speed == 2.5f && loaded->ratio == 0.25, "floating point fields must round-trip");
|
||||
check(loaded->offset == component->offset && loaded->tint == component->tint, "vector fields must round-trip");
|
||||
check(loaded->name == "named", "string fields must round-trip");
|
||||
check(loaded->mode == TestMode::Fast && loaded->rawMode == TestMode::Slow, "enum fields must round-trip");
|
||||
check(loaded->values == component->values, "vector fields must round-trip");
|
||||
check(loaded->target == component->target && loaded->targets == component->targets, "object refs must round-trip");
|
||||
check(loaded->cache == 0, "transient fields must not be loaded");
|
||||
check(loaded->Serialize() == data, "a loaded component must save the same data");
|
||||
}
|
||||
|
||||
void testReflectedLoadClampsAndKeepsDefaults()
|
||||
{
|
||||
GameObject object{"reflected"};
|
||||
auto* component = object.AddComponent<ReflectedComponent>();
|
||||
component->Deserialize({{"count", 50}, {"speed", -3.0f}, {"unknown", true}});
|
||||
check(component->count == 10 && component->speed == 0.0f, "loading must clamp to min and max");
|
||||
check(component->name == "default" && component->ratio == 0.5, "missing keys must keep their defaults");
|
||||
|
||||
component->Deserialize({{"mode", 2}});
|
||||
check(component->mode == TestMode::Fast, "named enums must still load from a number");
|
||||
|
||||
check(throwsMentioning(*component, {{"mode", "Turbo"}}, "\"mode\""), "an unknown enum name must fail and name the field");
|
||||
check(throwsMentioning(*component, {{"offset", "up"}}, "\"offset\""), "a wrong type must fail and name the field");
|
||||
}
|
||||
|
||||
void testReflectedReferencesResolve()
|
||||
{
|
||||
ComponentFactory::Register<ReflectedComponent>("ReflectedComponent");
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("reflected references");
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_reflected_refs_scene.json";
|
||||
std::ofstream{path}
|
||||
<< R"({"version":2,"objects":[)"
|
||||
R"({"id":5,"name":"source","components":[{"type":"ReflectedComponent","data":{"target":7,"targets":[7,99,5]}}]},)"
|
||||
R"({"id":7,"name":"other","components":[{"type":"ReflectedComponent","data":{"target":42}}]}]})";
|
||||
|
||||
check(SceneSerializer::Load(scene, path), "a scene with reflected references must load");
|
||||
std::filesystem::remove(path);
|
||||
scene.CommitPendingAdditions();
|
||||
|
||||
const GameObject* source = findByName(scene, "source");
|
||||
const GameObject* other = findByName(scene, "other");
|
||||
check(source != nullptr && other != nullptr, "both objects must load");
|
||||
|
||||
const auto* sourceComponent = source->GetComponent<ReflectedComponent>();
|
||||
check(sourceComponent->target.Get(scene) == other, "an object ref must be remapped to the loaded object");
|
||||
check(sourceComponent->targets.size() == 2 && sourceComponent->targets[0].Get(scene) == other &&
|
||||
sourceComponent->targets[1].Get(scene) == source,
|
||||
"object ref lists must be remapped and drop missing objects");
|
||||
check(!other->GetComponent<ReflectedComponent>()->target.IsSet(), "a missing object ref must be cleared");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testFieldLabels()
|
||||
{
|
||||
check(reflect_detail::PrettifyFieldName("openDuration") == "Open Duration", "camelCase keys must become words");
|
||||
check(reflect_detail::PrettifyFieldName("cast_shadows") == "Cast Shadows", "snake_case keys must become words");
|
||||
check(reflect_detail::PrettifyFieldName("useHDR") == "Use HDR", "acronyms must stay together");
|
||||
}
|
||||
}
|
||||
|
||||
void RunSerializationTests()
|
||||
{
|
||||
testReflectedRoundTrip();
|
||||
testReflectedLoadClampsAndKeepsDefaults();
|
||||
testReflectedReferencesResolve();
|
||||
testFieldLabels();
|
||||
testTonemapperNames();
|
||||
testSceneRoundTrip();
|
||||
testLegacySceneLoad();
|
||||
|
||||
Reference in New Issue
Block a user