feat: add reflection
This commit is contained in:
@@ -135,6 +135,7 @@ set(SRC_FILES
|
|||||||
"src/Serialization/MaterialSerialization.cpp"
|
"src/Serialization/MaterialSerialization.cpp"
|
||||||
"src/Serialization/PrefabCache.cpp"
|
"src/Serialization/PrefabCache.cpp"
|
||||||
"src/Serialization/ComponentRegistry.cpp"
|
"src/Serialization/ComponentRegistry.cpp"
|
||||||
|
"src/Serialization/Reflect.cpp"
|
||||||
|
|
||||||
"src/Util/DeltaTime.cpp"
|
"src/Util/DeltaTime.cpp"
|
||||||
"src/Util/ImGuiUtils.cpp"
|
"src/Util/ImGuiUtils.cpp"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <destrum/ObjectModel/Object.h>
|
#include <destrum/ObjectModel/Object.h>
|
||||||
#include <destrum/ObjectModel/ObjectId.h>
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
|
|
||||||
|
class FieldVisitor;
|
||||||
class GameObject;
|
class GameObject;
|
||||||
class Transform;
|
class Transform;
|
||||||
|
|
||||||
@@ -51,15 +52,16 @@ public:
|
|||||||
// Serialization.
|
// Serialization.
|
||||||
virtual std::string GetTypeName() const = 0;
|
virtual std::string GetTypeName() const = 0;
|
||||||
|
|
||||||
virtual nlohmann::json Serialize() const {
|
// The defaults save, load, resolve and draw the fields listed by Reflect
|
||||||
return nlohmann::json::object();
|
virtual void Reflect(FieldVisitor&) {
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Deserialize(const nlohmann::json&) {
|
virtual void OnFieldsEdited() {
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void ResolveReferences(const ObjectMap&) {
|
virtual nlohmann::json Serialize() const;
|
||||||
}
|
virtual void Deserialize(const nlohmann::json& data);
|
||||||
|
virtual void ResolveReferences(const ObjectMap& objects);
|
||||||
|
|
||||||
// Trigger callbacks called when this component's owner overlaps a sensor.
|
// Trigger callbacks called when this component's owner overlaps a sensor.
|
||||||
// Only called when the owner has a Collider set as a trigger (or overlaps one).
|
// Only called when the owner has a Collider set as a trigger (or overlaps one).
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
#ifndef DESTRUM_REFLECT_H
|
||||||
|
#define DESTRUM_REFLECT_H
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/ObjectRef.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
|
class Component;
|
||||||
|
class GameObject;
|
||||||
|
class Scene;
|
||||||
|
|
||||||
|
// Designated initializers have to follow this order
|
||||||
|
struct FieldOptions {
|
||||||
|
float speed = 0.1f;
|
||||||
|
std::optional<double> min;
|
||||||
|
std::optional<double> max;
|
||||||
|
const char* format = nullptr;
|
||||||
|
bool color = false;
|
||||||
|
bool readOnly = false;
|
||||||
|
bool hidden = false;
|
||||||
|
bool transient = false;
|
||||||
|
const char* label = nullptr;
|
||||||
|
std::span<const char* const> enumNames;
|
||||||
|
std::function<bool(const GameObject&)> filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FieldOps {
|
||||||
|
void (*save)(const void* value, nlohmann::json& out, const FieldOptions& options);
|
||||||
|
void (*load)(void* value, const nlohmann::json& in, const FieldOptions& options);
|
||||||
|
bool (*draw)(const char* label, void* value, const FieldOptions& options, const Scene* scene);
|
||||||
|
void (*resolve)(void* value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace reflect_detail {
|
||||||
|
enum class Scalar { S8, U8, S16, U16, S32, U32, S64, U64, Float, Double };
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
consteval Scalar ScalarOf()
|
||||||
|
{
|
||||||
|
if constexpr (std::same_as<T, float>) return Scalar::Float;
|
||||||
|
else if constexpr (std::same_as<T, double>) return Scalar::Double;
|
||||||
|
else if constexpr (std::is_signed_v<T>) {
|
||||||
|
if constexpr (sizeof(T) == 1) return Scalar::S8;
|
||||||
|
else if constexpr (sizeof(T) == 2) return Scalar::S16;
|
||||||
|
else if constexpr (sizeof(T) == 4) return Scalar::S32;
|
||||||
|
else return Scalar::S64;
|
||||||
|
} else {
|
||||||
|
if constexpr (sizeof(T) == 1) return Scalar::U8;
|
||||||
|
else if constexpr (sizeof(T) == 2) return Scalar::U16;
|
||||||
|
else if constexpr (sizeof(T) == 4) return Scalar::U32;
|
||||||
|
else return Scalar::U64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawScalar(const char* label, Scalar type, void* value, int components, const void* min, const void* max, const FieldOptions& options);
|
||||||
|
bool DrawEnum(const char* label, std::int64_t& value, const FieldOptions& options);
|
||||||
|
|
||||||
|
void BeginList(const char* label);
|
||||||
|
[[nodiscard]] bool ListRemoveButton(std::size_t index);
|
||||||
|
void EndListRow();
|
||||||
|
[[nodiscard]] bool ListAddButton();
|
||||||
|
|
||||||
|
[[nodiscard]] std::string PrettifyFieldName(std::string_view key);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept Numeric = std::is_arithmetic_v<T> && !std::same_as<T, bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, bool& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, std::string& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, glm::vec2& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, glm::vec3& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, glm::vec4& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, ObjectRef& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
bool DrawField(const char* label, std::vector<ObjectRef>& value, const FieldOptions& options, const Scene* scene);
|
||||||
|
|
||||||
|
template <reflect_detail::Numeric T>
|
||||||
|
bool DrawField(const char* label, T& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
const T min = options.min ? static_cast<T>(*options.min) : T{};
|
||||||
|
const T max = options.max ? static_cast<T>(*options.max) : T{};
|
||||||
|
return reflect_detail::DrawScalar(label, reflect_detail::ScalarOf<T>(), &value, 1,
|
||||||
|
options.min ? &min : nullptr, options.max ? &max : nullptr, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class E>
|
||||||
|
requires std::is_enum_v<E>
|
||||||
|
bool DrawField(const char* label, E& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
auto raw = static_cast<std::int64_t>(value);
|
||||||
|
if (!reflect_detail::DrawEnum(label, raw, options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = static_cast<E>(raw);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
bool DrawField(const char* label, std::vector<T>& values, const FieldOptions& options, const Scene* scene)
|
||||||
|
{
|
||||||
|
reflect_detail::BeginList(label);
|
||||||
|
bool changed = false;
|
||||||
|
std::optional<std::size_t> removeIndex;
|
||||||
|
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||||
|
if (reflect_detail::ListRemoveButton(i)) {
|
||||||
|
removeIndex = i;
|
||||||
|
}
|
||||||
|
changed |= DrawField("##value", values[i], options, scene);
|
||||||
|
reflect_detail::EndListRow();
|
||||||
|
}
|
||||||
|
if (removeIndex) {
|
||||||
|
values.erase(values.begin() + static_cast<std::ptrdiff_t>(*removeIndex));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (reflect_detail::ListAddButton()) {
|
||||||
|
values.emplace_back();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveField(ObjectRef& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
|
||||||
|
void ResolveField(std::vector<ObjectRef>& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept Reflectable = std::is_constructible_v<nlohmann::json, const T&> &&
|
||||||
|
requires(T& value, const FieldOptions& options, const Scene* scene) {
|
||||||
|
{ DrawField("", value, options, scene) } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept HasReferences = requires(T& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped) {
|
||||||
|
ResolveField(value, objects, dropped);
|
||||||
|
};
|
||||||
|
|
||||||
|
template <Reflectable T>
|
||||||
|
struct FieldCodec {
|
||||||
|
static void Save(const void* value, nlohmann::json& out, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
const T& typed = *static_cast<const T*>(value);
|
||||||
|
if constexpr (std::is_enum_v<T>) {
|
||||||
|
const auto index = static_cast<std::size_t>(typed);
|
||||||
|
if (index < options.enumNames.size()) {
|
||||||
|
out = options.enumNames[index];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = typed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Load(void* value, const nlohmann::json& in, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
T& typed = *static_cast<T*>(value);
|
||||||
|
if constexpr (std::is_enum_v<T>) {
|
||||||
|
if (in.is_string()) {
|
||||||
|
const auto name = in.get<std::string>();
|
||||||
|
for (std::size_t i = 0; i < options.enumNames.size(); ++i) {
|
||||||
|
if (name == options.enumNames[i]) {
|
||||||
|
typed = static_cast<T>(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw std::runtime_error("unknown value \"" + name + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typed = in.get<T>();
|
||||||
|
if constexpr (reflect_detail::Numeric<T>) {
|
||||||
|
if (options.min && typed < static_cast<T>(*options.min)) typed = static_cast<T>(*options.min);
|
||||||
|
if (options.max && typed > static_cast<T>(*options.max)) typed = static_cast<T>(*options.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Draw(const char* label, void* value, const FieldOptions& options, const Scene* scene)
|
||||||
|
{
|
||||||
|
return DrawField(label, *static_cast<T*>(value), options, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Resolve(void* value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
|
||||||
|
{
|
||||||
|
if constexpr (HasReferences<T>) {
|
||||||
|
ResolveField(*static_cast<T*>(value), objects, dropped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr FieldOps Ops{&Save, &Load, &Draw, HasReferences<T> ? &Resolve : nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
class FieldVisitor {
|
||||||
|
public:
|
||||||
|
virtual ~FieldVisitor() = default;
|
||||||
|
|
||||||
|
template <Reflectable T>
|
||||||
|
void Field(std::string_view key, T& value, const FieldOptions& options = {})
|
||||||
|
{
|
||||||
|
Visit(key, &value, options, FieldCodec<T>::Ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JsonWriter final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] nlohmann::json& Result() { return m_Result; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
nlohmann::json m_Result = nlohmann::json::object();
|
||||||
|
};
|
||||||
|
|
||||||
|
class JsonReader final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
explicit JsonReader(const nlohmann::json& data) : m_Data(data) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const nlohmann::json& m_Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ReferenceResolver final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
ReferenceResolver(const Component& owner, const ObjectRef::ObjectMap& objects) : m_Owner(owner), m_Objects(objects) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Component& m_Owner;
|
||||||
|
const ObjectRef::ObjectMap& m_Objects;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InspectorDrawer final : public FieldVisitor {
|
||||||
|
public:
|
||||||
|
explicit InspectorDrawer(const Scene* scene) : m_Scene(scene) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool Changed() const { return m_Changed; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Scene* m_Scene;
|
||||||
|
bool m_Changed{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESTRUM_REFLECT_H
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
Component::Component(GameObject& pParent, const std::string& name)
|
Component::Component(GameObject& pParent, const std::string& name)
|
||||||
: Object(name),
|
: Object(name),
|
||||||
@@ -50,6 +51,28 @@ void Component::FixedUpdate(float fixedDt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Component::ImGuiInspector() {
|
void Component::ImGuiInspector() {
|
||||||
|
InspectorDrawer drawer(m_ParentGameObjectPtr != nullptr ? m_ParentGameObjectPtr->GetScene() : nullptr);
|
||||||
|
Reflect(drawer);
|
||||||
|
if (drawer.Changed()) {
|
||||||
|
OnFieldsEdited();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reflect also loads, so it can't be const, but the writer only reads the fields
|
||||||
|
nlohmann::json Component::Serialize() const {
|
||||||
|
JsonWriter writer;
|
||||||
|
const_cast<Component*>(this)->Reflect(writer);
|
||||||
|
return std::move(writer.Result());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Component::Deserialize(const nlohmann::json& data) {
|
||||||
|
JsonReader reader(data);
|
||||||
|
Reflect(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Component::ResolveReferences(const ObjectMap& objects) {
|
||||||
|
ReferenceResolver resolver(*this, objects);
|
||||||
|
Reflect(resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Component::ImGuiRender() {
|
void Component::ImGuiRender() {
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
[[nodiscard]] ImGuiDataType ToImGui(reflect_detail::Scalar type)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case reflect_detail::Scalar::S8: return ImGuiDataType_S8;
|
||||||
|
case reflect_detail::Scalar::U8: return ImGuiDataType_U8;
|
||||||
|
case reflect_detail::Scalar::S16: return ImGuiDataType_S16;
|
||||||
|
case reflect_detail::Scalar::U16: return ImGuiDataType_U16;
|
||||||
|
case reflect_detail::Scalar::S32: return ImGuiDataType_S32;
|
||||||
|
case reflect_detail::Scalar::U32: return ImGuiDataType_U32;
|
||||||
|
case reflect_detail::Scalar::S64: return ImGuiDataType_S64;
|
||||||
|
case reflect_detail::Scalar::U64: return ImGuiDataType_U64;
|
||||||
|
case reflect_detail::Scalar::Float: return ImGuiDataType_Float;
|
||||||
|
case reflect_detail::Scalar::Double: return ImGuiDataType_Double;
|
||||||
|
}
|
||||||
|
return ImGuiDataType_Float;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] ImGuiSliderFlags ClampFlags(const FieldOptions& options)
|
||||||
|
{
|
||||||
|
return options.min || options.max ? ImGuiSliderFlags_AlwaysClamp : ImGuiSliderFlags_None;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <int N>
|
||||||
|
bool DrawFloats(const char* label, float* value, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
if constexpr (N == 3) {
|
||||||
|
if (options.color) {
|
||||||
|
return ImGui::ColorEdit3(label, value);
|
||||||
|
}
|
||||||
|
} else if constexpr (N == 4) {
|
||||||
|
if (options.color) {
|
||||||
|
return ImGui::ColorEdit4(label, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float min = options.min ? static_cast<float>(*options.min) : 0.0f;
|
||||||
|
const float max = options.max ? static_cast<float>(*options.max) : 0.0f;
|
||||||
|
return reflect_detail::DrawScalar(label, reflect_detail::Scalar::Float, value, N,
|
||||||
|
options.min ? &min : nullptr, options.max ? &max : nullptr, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string ObjectLabel(const ObjectRef& ref, const Scene& scene)
|
||||||
|
{
|
||||||
|
const GameObject* object = ref.Get(scene);
|
||||||
|
return object != nullptr ? object->GetName() : "Missing object " + std::to_string(ref.GetId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace reflect_detail {
|
||||||
|
bool DrawScalar(const char* label, Scalar type, void* value, int components, const void* min, const void* max, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
return ImGui::DragScalarN(label, ToImGui(type), value, components, options.speed, min, max, options.format, ClampFlags(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawEnum(const char* label, std::int64_t& value, const FieldOptions& options)
|
||||||
|
{
|
||||||
|
if (options.enumNames.empty()) {
|
||||||
|
return ImGui::InputScalar(label, ImGuiDataType_S64, &value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto count = static_cast<std::int64_t>(options.enumNames.size());
|
||||||
|
const char* preview = value >= 0 && value < count ? options.enumNames[static_cast<std::size_t>(value)] : "?";
|
||||||
|
bool changed = false;
|
||||||
|
if (ImGui::BeginCombo(label, preview)) {
|
||||||
|
for (std::int64_t i = 0; i < count; ++i) {
|
||||||
|
if (ImGui::Selectable(options.enumNames[static_cast<std::size_t>(i)], i == value)) {
|
||||||
|
value = i;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeginList(const char* label)
|
||||||
|
{
|
||||||
|
ImGui::SeparatorText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ListRemoveButton(std::size_t index)
|
||||||
|
{
|
||||||
|
ImGui::PushID(static_cast<int>(index));
|
||||||
|
const bool clicked = ImGui::SmallButton("x");
|
||||||
|
ImGui::SameLine();
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndListRow()
|
||||||
|
{
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ListAddButton()
|
||||||
|
{
|
||||||
|
return ImGui::SmallButton("+");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PrettifyFieldName(std::string_view key)
|
||||||
|
{
|
||||||
|
std::string label;
|
||||||
|
label.reserve(key.size() + 4);
|
||||||
|
for (std::size_t i = 0; i < key.size(); ++i) {
|
||||||
|
const auto c = static_cast<unsigned char>(key[i]);
|
||||||
|
if (c == '_') {
|
||||||
|
label += ' ';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool wordStart = i > 0 && std::isupper(c) && !std::isupper(static_cast<unsigned char>(key[i - 1])) && key[i - 1] != '_';
|
||||||
|
if (wordStart) {
|
||||||
|
label += ' ';
|
||||||
|
}
|
||||||
|
label += static_cast<char>(label.empty() || label.back() == ' ' ? std::toupper(c) : c);
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, bool& value, const FieldOptions&, const Scene*)
|
||||||
|
{
|
||||||
|
return ImGui::Checkbox(label, &value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, std::string& value, const FieldOptions&, const Scene*)
|
||||||
|
{
|
||||||
|
return ImGuiUtils::InputString(label, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, glm::vec2& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
return DrawFloats<2>(label, &value.x, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, glm::vec3& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
return DrawFloats<3>(label, &value.x, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, glm::vec4& value, const FieldOptions& options, const Scene*)
|
||||||
|
{
|
||||||
|
return DrawFloats<4>(label, &value.x, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, ObjectRef& value, const FieldOptions& options, const Scene* scene)
|
||||||
|
{
|
||||||
|
if (scene == nullptr) {
|
||||||
|
ImGui::TextDisabled("%s: not in a scene", label);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ImGuiUtils::DrawObjectRefPicker(label, value, *scene, options.filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrawField(const char* label, std::vector<ObjectRef>& value, const FieldOptions& options, const Scene* scene)
|
||||||
|
{
|
||||||
|
reflect_detail::BeginList(label);
|
||||||
|
if (scene == nullptr) {
|
||||||
|
ImGui::TextDisabled("Not in a scene");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
std::optional<std::size_t> removeIndex;
|
||||||
|
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||||
|
if (reflect_detail::ListRemoveButton(i)) {
|
||||||
|
removeIndex = i;
|
||||||
|
}
|
||||||
|
if (value[i].Get(*scene) != nullptr) {
|
||||||
|
ImGui::TextUnformatted(ObjectLabel(value[i], *scene).c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled("%s", ObjectLabel(value[i], *scene).c_str());
|
||||||
|
}
|
||||||
|
reflect_detail::EndListRow();
|
||||||
|
}
|
||||||
|
if (removeIndex) {
|
||||||
|
value.erase(value.begin() + static_cast<std::ptrdiff_t>(*removeIndex));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto canAdd = [&](const GameObject& object) {
|
||||||
|
return !object.IsBeingDestroyed() && std::ranges::find(value, ObjectRef(object)) == value.end() &&
|
||||||
|
(!options.filter || options.filter(object));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui::BeginCombo("##Add", "Add...")) {
|
||||||
|
for (const auto& object : scene->GetObjects()) {
|
||||||
|
if (object == nullptr || !canAdd(*object)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ImGui::PushID(static_cast<int>(object->GetId()));
|
||||||
|
if (ImGui::Selectable(object->GetName().c_str())) {
|
||||||
|
value.emplace_back(*object);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::BeginDragDropTarget()) {
|
||||||
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
|
||||||
|
const GameObject* dropped = scene->FindGameObject(*static_cast<const ObjectId*>(payload->Data));
|
||||||
|
if (dropped != nullptr && canAdd(*dropped)) {
|
||||||
|
value.emplace_back(*dropped);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveField(ObjectRef& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
|
||||||
|
{
|
||||||
|
const ObjectId fileId = value.GetId();
|
||||||
|
if (!value.Resolve(objects)) {
|
||||||
|
dropped.push_back(fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveField(std::vector<ObjectRef>& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
|
||||||
|
{
|
||||||
|
std::erase_if(value, [&](ObjectRef& ref) {
|
||||||
|
const ObjectId fileId = ref.GetId();
|
||||||
|
if (!ref.Resolve(objects)) {
|
||||||
|
dropped.push_back(fileId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !ref.IsSet();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
|
||||||
|
{
|
||||||
|
if (!options.transient) {
|
||||||
|
ops.save(value, m_Result[std::string{key}], options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonReader::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
|
||||||
|
{
|
||||||
|
if (options.transient || !m_Data.is_object()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto it = m_Data.find(std::string{key});
|
||||||
|
if (it == m_Data.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ops.load(value, *it, options);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
throw std::runtime_error("\"" + std::string{key} + "\": " + exception.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReferenceResolver::Visit(std::string_view key, void* value, const FieldOptions&, const FieldOps& ops)
|
||||||
|
{
|
||||||
|
if (ops.resolve == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<ObjectId> dropped;
|
||||||
|
ops.resolve(value, m_Objects, dropped);
|
||||||
|
const GameObject* owner = m_Owner.GetGameObject();
|
||||||
|
for (const ObjectId id : dropped) {
|
||||||
|
spdlog::warn("{} on {}: {} references object {} which is not in the scene file, dropping it",
|
||||||
|
m_Owner.GetTypeName(), owner != nullptr ? owner->GetName() : "?", key, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorDrawer::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
|
||||||
|
{
|
||||||
|
if (options.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::string label = options.label != nullptr ? options.label : reflect_detail::PrettifyFieldName(key);
|
||||||
|
ImGui::PushID(key.data(), key.data() + key.size());
|
||||||
|
ImGui::BeginDisabled(options.readOnly);
|
||||||
|
m_Changed |= ops.draw(label.c_str(), value, options, m_Scene);
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
@@ -14,8 +14,7 @@ public:
|
|||||||
void Start() override;
|
void Start() override;
|
||||||
void Update(float dt) override;
|
void Update(float dt) override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
void Open() { m_IsOpen = true; }
|
void Open() { m_IsOpen = true; }
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ public:
|
|||||||
void Start() override;
|
void Start() override;
|
||||||
void Interact(GameObject& interactor) override;
|
void Interact(GameObject& interactor) override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
void Reflect(FieldVisitor& v) override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void OnFieldsEdited() override;
|
||||||
void ResolveReferences(const ObjectMap& objects) override;
|
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
[[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; }
|
[[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; }
|
||||||
@@ -35,7 +34,6 @@ public:
|
|||||||
private:
|
private:
|
||||||
void ApplyColor();
|
void ApplyColor();
|
||||||
void ActivateTargets(GameObject& interactor);
|
void ActivateTargets(GameObject& interactor);
|
||||||
void DrawTargetsInspector();
|
|
||||||
|
|
||||||
std::size_t m_ColorIndex{0};
|
std::size_t m_ColorIndex{0};
|
||||||
int m_PressCount{0};
|
int m_PressCount{0};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/Reflect.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
@@ -58,24 +58,13 @@ void Door::ApplyPosition()
|
|||||||
GetGameObject()->GetTransform().SetLocalPosition(m_ClosedPosition + GetOffsetFor(m_OpenAmount));
|
GetGameObject()->GetTransform().SetLocalPosition(m_ClosedPosition + GetOffsetFor(m_OpenAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json Door::Serialize() const
|
void Door::Reflect(FieldVisitor& v)
|
||||||
{
|
{
|
||||||
return {
|
v.Field("openOffset", m_OpenOffset, {.speed = 0.05f});
|
||||||
{"openOffset", m_OpenOffset},
|
v.Field("openDuration", m_OpenDuration, {.speed = 0.05f, .min = 0.0, .format = "%.2f s"});
|
||||||
{"openDuration", m_OpenDuration},
|
v.Field("toggleable", m_Toggleable);
|
||||||
{"toggleable", m_Toggleable},
|
v.Field("open", m_IsOpen, {.hidden = true});
|
||||||
{"open", m_IsOpen},
|
v.Field("openAmount", m_OpenAmount, {.min = 0.0, .max = 1.0, .hidden = true});
|
||||||
{"openAmount", m_OpenAmount},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void Door::Deserialize(const nlohmann::json& data)
|
|
||||||
{
|
|
||||||
m_OpenOffset = data.value("openOffset", m_OpenOffset);
|
|
||||||
m_OpenDuration = std::max(data.value("openDuration", m_OpenDuration), 0.0f);
|
|
||||||
m_Toggleable = data.value("toggleable", m_Toggleable);
|
|
||||||
m_IsOpen = data.value("open", m_IsOpen);
|
|
||||||
m_OpenAmount = std::clamp(data.value("openAmount", m_OpenAmount), 0.0f, 1.0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Door::ImGuiInspector()
|
void Door::ImGuiInspector()
|
||||||
@@ -85,7 +74,5 @@ void Door::ImGuiInspector()
|
|||||||
Toggle();
|
Toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::DragFloat3("Open Offset", &m_OpenOffset.x, 0.05f);
|
Activatable::ImGuiInspector();
|
||||||
ImGui::DragFloat("Open Duration", &m_OpenDuration, 0.05f, 0.0f, 10.0f, "%.2f s");
|
|
||||||
ImGui::Checkbox("Toggleable", &m_Toggleable);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
#include <destrum/Graphics/Renderer.h>
|
#include <destrum/Graphics/Renderer.h>
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
#include <destrum/Util/GameState.h>
|
#include <destrum/Util/GameState.h>
|
||||||
|
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
@@ -108,93 +108,28 @@ void InteractableButton::ApplyColor()
|
|||||||
renderer.updateMaterialGPU(meshRenderer->GetMaterialID());
|
renderer.updateMaterialGPU(meshRenderer->GetMaterialID());
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json InteractableButton::Serialize() const
|
void InteractableButton::Reflect(FieldVisitor& v)
|
||||||
{
|
{
|
||||||
return {
|
v.Field("colorIndex", m_ColorIndex, {.max = static_cast<double>(ButtonColors.size() - 1)});
|
||||||
{"colorIndex", m_ColorIndex},
|
v.Field("cycleColors", m_CycleColors);
|
||||||
{"cycleColors", m_CycleColors},
|
v.Field("pressCount", m_PressCount, {.readOnly = true});
|
||||||
{"pressCount", m_PressCount},
|
v.Field("targets", m_Targets, {.filter = [this](const GameObject& object) {
|
||||||
{"targets", m_Targets},
|
return &object != GetGameObject() && object.GetComponent<Activatable>() != nullptr;
|
||||||
};
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractableButton::Deserialize(const nlohmann::json& data)
|
void InteractableButton::OnFieldsEdited()
|
||||||
{
|
{
|
||||||
m_ColorIndex = data.value("colorIndex", m_ColorIndex) % ButtonColors.size();
|
if (m_CycleColors) {
|
||||||
m_CycleColors = data.value("cycleColors", m_CycleColors);
|
ApplyColor();
|
||||||
m_PressCount = data.value("pressCount", m_PressCount);
|
}
|
||||||
m_Targets = data.value("targets", std::vector<ObjectRef>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
void InteractableButton::ResolveReferences(const ObjectMap& objects)
|
|
||||||
{
|
|
||||||
std::erase_if(m_Targets, [&](ObjectRef& target) {
|
|
||||||
const ObjectId fileId = target.GetId();
|
|
||||||
if (!target.Resolve(objects)) {
|
|
||||||
spdlog::warn("{} targets object {} which is not in the scene file, dropping it", GetGameObject()->GetName(), fileId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !target.IsSet();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractableButton::ImGuiInspector()
|
void InteractableButton::ImGuiInspector()
|
||||||
{
|
{
|
||||||
ImGui::Text("Presses: %d", m_PressCount);
|
|
||||||
if (ImGui::Button("Press")) {
|
if (ImGui::Button("Press")) {
|
||||||
Interact(*GetGameObject());
|
Interact(*GetGameObject());
|
||||||
}
|
}
|
||||||
if (ImGui::Checkbox("Cycle Colors", &m_CycleColors) && m_CycleColors) {
|
|
||||||
ApplyColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawTargetsInspector();
|
Interactable::ImGuiInspector();
|
||||||
}
|
|
||||||
|
|
||||||
void InteractableButton::DrawTargetsInspector()
|
|
||||||
{
|
|
||||||
ImGui::SeparatorText("Targets");
|
|
||||||
|
|
||||||
const Scene* scene = GetGameObject()->GetScene();
|
|
||||||
if (scene == nullptr) {
|
|
||||||
ImGui::TextDisabled("Not in a scene");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<ObjectId> targetToRemove;
|
|
||||||
for (const ObjectRef& targetRef : m_Targets) {
|
|
||||||
const ObjectId targetId = targetRef.GetId();
|
|
||||||
const GameObject* target = targetRef.Get(*scene);
|
|
||||||
ImGui::PushID(static_cast<int>(targetId));
|
|
||||||
if (ImGui::SmallButton("x")) {
|
|
||||||
targetToRemove = targetId;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (target != nullptr) {
|
|
||||||
ImGui::TextUnformatted(target->GetName().c_str());
|
|
||||||
} else {
|
|
||||||
ImGui::TextDisabled("Missing object %llu", static_cast<unsigned long long>(targetId));
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
if (targetToRemove) {
|
|
||||||
RemoveTarget(*targetToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::BeginCombo("##AddTarget", "Add target...")) {
|
|
||||||
for (const auto& object : scene->GetObjects()) {
|
|
||||||
const bool alreadyTargeted = object != nullptr &&
|
|
||||||
std::ranges::find(m_Targets, ObjectRef(*object)) != m_Targets.end();
|
|
||||||
if (object == nullptr || object.get() == GetGameObject() || alreadyTargeted ||
|
|
||||||
object->GetComponent<Activatable>() == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ImGui::PushID(static_cast<int>(object->GetId()));
|
|
||||||
if (ImGui::Selectable(object->GetName().c_str())) {
|
|
||||||
AddTarget(*object);
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -9,7 +10,9 @@
|
|||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
#include <destrum/Serialization/MaterialSerialization.h>
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
|
#include <destrum/Serialization/Reflect.h>
|
||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
#include <destrum/Components/CameraComponent.h>
|
#include <destrum/Components/CameraComponent.h>
|
||||||
#include <destrum/Components/LightComponent.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()
|
void RunSerializationTests()
|
||||||
{
|
{
|
||||||
|
testReflectedRoundTrip();
|
||||||
|
testReflectedLoadClampsAndKeepsDefaults();
|
||||||
|
testReflectedReferencesResolve();
|
||||||
|
testFieldLabels();
|
||||||
testTonemapperNames();
|
testTonemapperNames();
|
||||||
testSceneRoundTrip();
|
testSceneRoundTrip();
|
||||||
testLegacySceneLoad();
|
testLegacySceneLoad();
|
||||||
|
|||||||
Reference in New Issue
Block a user