feat: add reflection
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <destrum/ObjectModel/Object.h>
|
||||
#include <destrum/ObjectModel/ObjectId.h>
|
||||
|
||||
class FieldVisitor;
|
||||
class GameObject;
|
||||
class Transform;
|
||||
|
||||
@@ -51,15 +52,16 @@ public:
|
||||
// Serialization.
|
||||
virtual std::string GetTypeName() const = 0;
|
||||
|
||||
virtual nlohmann::json Serialize() const {
|
||||
return nlohmann::json::object();
|
||||
// The defaults save, load, resolve and draw the fields listed by Reflect
|
||||
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.
|
||||
// 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
|
||||
Reference in New Issue
Block a user