feat: add prefabs
This commit is contained in:
@@ -109,6 +109,9 @@ set(SRC_FILES
|
|||||||
"src/Assets/AssetManager.cpp"
|
"src/Assets/AssetManager.cpp"
|
||||||
|
|
||||||
"src/Serialization/SceneSerializer.cpp"
|
"src/Serialization/SceneSerializer.cpp"
|
||||||
|
"src/Serialization/ObjectSerialization.cpp"
|
||||||
|
"src/Serialization/PrefabSerializer.cpp"
|
||||||
|
"src/Serialization/PrefabCache.cpp"
|
||||||
"src/Serialization/ComponentRegistry.cpp"
|
"src/Serialization/ComponentRegistry.cpp"
|
||||||
|
|
||||||
"src/Util/DeltaTime.cpp"
|
"src/Util/DeltaTime.cpp"
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ struct SceneEnvironment {
|
|||||||
float skyboxRotation{0.0f};
|
float skyboxRotation{0.0f};
|
||||||
};
|
};
|
||||||
class SceneSerializer;
|
class SceneSerializer;
|
||||||
|
class PrefabSerializer;
|
||||||
class PhysicsWorld;
|
class PhysicsWorld;
|
||||||
|
|
||||||
class Scene final {
|
class Scene final {
|
||||||
friend Scene& SceneManager::CreateScene(const std::string& name);
|
friend Scene& SceneManager::CreateScene(const std::string& name);
|
||||||
friend class SceneSerializer;
|
friend class SceneSerializer;
|
||||||
|
friend class PrefabSerializer;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// void Add(std::shared_ptr<GameObject> object);
|
// void Add(std::shared_ptr<GameObject> object);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#ifndef DESTRUM_OBJECTSERIALIZATION_H
|
||||||
|
#define DESTRUM_OBJECTSERIALIZATION_H
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
|
|
||||||
|
class GameObject;
|
||||||
|
class Scene;
|
||||||
|
|
||||||
|
namespace ObjectSerialization {
|
||||||
|
using ObjectMap = std::unordered_map<ObjectId, GameObject*>;
|
||||||
|
using ValidationError = std::optional<std::string>;
|
||||||
|
|
||||||
|
constexpr int CurrentObjectVersion = 2;
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::string> FindNonFinite(const nlohmann::json& value, const std::string& path);
|
||||||
|
|
||||||
|
[[nodiscard]] ValidationError ValidateObjects(const nlohmann::json& objects, int version);
|
||||||
|
|
||||||
|
void WarnMissingAssets(const nlohmann::json& objects, const std::filesystem::path& source);
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json SerializeObjects(const std::vector<const GameObject*>& objects);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<const GameObject*> CollectHierarchy(const GameObject& root);
|
||||||
|
|
||||||
|
struct BuiltObjects {
|
||||||
|
std::vector<GameObject*> objects;
|
||||||
|
ObjectMap idMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] BuiltObjects BuildObjects(Scene& scene, const nlohmann::json& objects,
|
||||||
|
const ObjectMap& externalObjects = {});
|
||||||
|
|
||||||
|
[[nodiscard]] bool WriteFileAtomically(const std::filesystem::path& path, const std::string& contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //DESTRUM_OBJECTSERIALIZATION_H
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef DESTRUM_PREFABCACHE_H
|
||||||
|
#define DESTRUM_PREFABCACHE_H
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Singleton.h>
|
||||||
|
|
||||||
|
class PrefabCache final : public Singleton<PrefabCache> {
|
||||||
|
friend class Singleton<PrefabCache>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
[[nodiscard]] std::shared_ptr<const nlohmann::json> Get(std::string_view path);
|
||||||
|
void Invalidate(const std::filesystem::path& path);
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
[[nodiscard]] static std::filesystem::path ResolvePath(std::string_view path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
PrefabCache() = default;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::shared_ptr<const nlohmann::json> prefab;
|
||||||
|
std::filesystem::file_time_type writeTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] static std::string MakeKey(const std::filesystem::path& path);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, Entry> m_Prefabs;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //DESTRUM_PREFABCACHE_H
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef DESTRUM_PREFABSERIALIZER_H
|
||||||
|
#define DESTRUM_PREFABSERIALIZER_H
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
class GameObject;
|
||||||
|
class Scene;
|
||||||
|
class Transform;
|
||||||
|
|
||||||
|
class PrefabSerializer final {
|
||||||
|
public:
|
||||||
|
static constexpr int CurrentPrefabVersion = 1;
|
||||||
|
|
||||||
|
[[nodiscard]] static std::optional<nlohmann::json> Capture(const GameObject& root);
|
||||||
|
static bool Save(const GameObject& root, const std::filesystem::path& path);
|
||||||
|
|
||||||
|
[[nodiscard]] static std::optional<nlohmann::json> LoadFile(const std::filesystem::path& path);
|
||||||
|
[[nodiscard]] static std::optional<std::string> Validate(const nlohmann::json& prefab);
|
||||||
|
|
||||||
|
static GameObject* InstantiateFromJson(Scene& scene, const nlohmann::json& prefab, Transform* parent = nullptr);
|
||||||
|
static GameObject* Instantiate(Scene& scene, std::string_view path, Transform* parent = nullptr);
|
||||||
|
|
||||||
|
static GameObject* Duplicate(GameObject& object);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //DESTRUM_PREFABSERIALIZER_H
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
#include <destrum/Serialization/ObjectSerialization.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/Assets/AssetReference.h>
|
||||||
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using ObjectSerialization::ValidationError;
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsFiniteNumber(const json& value) {
|
||||||
|
if (!value.is_number()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::isfinite(value.get<double>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsFiniteArray(const json& value, std::size_t size) {
|
||||||
|
if (!value.is_array() || value.size() != size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& element : value) {
|
||||||
|
if (!IsFiniteNumber(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string DescribeObject(std::size_t index, const json& objectJson) {
|
||||||
|
std::string description = "objects[" + std::to_string(index) + "]";
|
||||||
|
if (objectJson.is_object() && objectJson.contains("name") && objectJson.at("name").is_string()) {
|
||||||
|
description += " \"" + objectJson.at("name").get<std::string>() + "\"";
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] ValidationError ValidateTransform(const json& objectJson, int version) {
|
||||||
|
if (!objectJson.contains("transform")) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& transform = objectJson.at("transform");
|
||||||
|
if (!transform.is_object()) {
|
||||||
|
return "transform must be an object";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 1 wrote an empty placeholder transform object.
|
||||||
|
if (version <= 1 && transform.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char* key : {"position", "rotation", "scale"}) {
|
||||||
|
if (!transform.contains(key)) {
|
||||||
|
return std::string{"transform is missing \""} + key + "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsFiniteArray(transform.at("position"), 3)) {
|
||||||
|
return "transform.position must be 3 finite numbers";
|
||||||
|
}
|
||||||
|
if (!IsFiniteArray(transform.at("scale"), 3)) {
|
||||||
|
return "transform.scale must be 3 finite numbers";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& rotation = transform.at("rotation");
|
||||||
|
if (!IsFiniteArray(rotation, 3) && !IsFiniteArray(rotation, 4)) {
|
||||||
|
return "transform.rotation must be 3 (euler) or 4 (quaternion) finite numbers";
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] ValidationError ValidateComponents(const json& objectJson) {
|
||||||
|
if (!objectJson.contains("components")) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& components = objectJson.at("components");
|
||||||
|
if (!components.is_array()) {
|
||||||
|
return "components must be an array";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < components.size(); ++i) {
|
||||||
|
const auto& componentJson = components.at(i);
|
||||||
|
const std::string where = "components[" + std::to_string(i) + "]";
|
||||||
|
|
||||||
|
if (!componentJson.is_object() || !componentJson.contains("type") ||
|
||||||
|
!componentJson.at("type").is_string()) {
|
||||||
|
return where + " must be an object with a string \"type\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string type = componentJson.at("type").get<std::string>();
|
||||||
|
const std::string named = where + " " + type;
|
||||||
|
|
||||||
|
if (componentJson.contains("enabled") && !componentJson.at("enabled").is_boolean()) {
|
||||||
|
return named + ": \"enabled\" must be a boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentJson.contains("data")) {
|
||||||
|
const auto& data = componentJson.at("data");
|
||||||
|
if (!data.is_object()) {
|
||||||
|
return named + ": \"data\" must be an object, got " + std::string{data.type_name()};
|
||||||
|
}
|
||||||
|
if (auto bad = ObjectSerialization::FindNonFinite(data, "data")) {
|
||||||
|
return named + ": " + *bad + " is not a finite number";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ComponentFactory::IsRegistered(type)) {
|
||||||
|
return named + ": unknown component type";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool AssetFileExists(const std::string& path) {
|
||||||
|
if (path.empty() || PrimitiveMeshes::IsPrimitiveKey(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path fullPath;
|
||||||
|
if (path.find("://") != std::string::npos) {
|
||||||
|
try {
|
||||||
|
fullPath = AssetFS::GetInstance().GetFullPath(path);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fullPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::filesystem::exists(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollectHierarchyRecursive(const GameObject& object, std::vector<const GameObject*>& out,
|
||||||
|
std::unordered_set<const GameObject*>& visited) {
|
||||||
|
if (object.IsBeingDestroyed() || !visited.insert(&object).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_back(&object);
|
||||||
|
for (const Transform* child : object.GetTransform().GetChildren()) {
|
||||||
|
if (child != nullptr && child->GetOwner() != nullptr) {
|
||||||
|
CollectHierarchyRecursive(*child->GetOwner(), out, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ObjectSerialization {
|
||||||
|
// Returns the JSON path of the first NaN/Inf, nlohmann would write it as null
|
||||||
|
std::optional<std::string> FindNonFinite(const json& value, const std::string& path) {
|
||||||
|
if (value.is_number_float()) {
|
||||||
|
if (!std::isfinite(value.get<double>())) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (value.is_array()) {
|
||||||
|
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||||
|
if (auto bad = FindNonFinite(value.at(i), path + "[" + std::to_string(i) + "]")) {
|
||||||
|
return bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (value.is_object()) {
|
||||||
|
for (auto it = value.begin(); it != value.end(); ++it) {
|
||||||
|
if (auto bad = FindNonFinite(it.value(), path + "." + it.key())) {
|
||||||
|
return bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError ValidateObjects(const json& objects, int version) {
|
||||||
|
if (!objects.is_array()) {
|
||||||
|
return "\"objects\" must be an array";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_set<ObjectId> ids;
|
||||||
|
std::unordered_map<ObjectId, ObjectId> parents;
|
||||||
|
std::unordered_map<ObjectId, std::string> descriptions;
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < objects.size(); ++index) {
|
||||||
|
const auto& objectJson = objects.at(index);
|
||||||
|
const std::string where = DescribeObject(index, objectJson);
|
||||||
|
|
||||||
|
if (!objectJson.is_object()) {
|
||||||
|
return where + " must be an object";
|
||||||
|
}
|
||||||
|
if (!objectJson.contains("id") || !objectJson.at("id").is_number_unsigned()) {
|
||||||
|
return where + " must have a non-negative integer \"id\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObjectId id = objectJson.at("id").get<ObjectId>();
|
||||||
|
if (id == InvalidObjectId || id >= std::numeric_limits<ObjectId>::max() - 1) {
|
||||||
|
return where + " uses reserved id " + std::to_string(id);
|
||||||
|
}
|
||||||
|
if (!ids.insert(id).second) {
|
||||||
|
return where + " reuses id " + std::to_string(id);
|
||||||
|
}
|
||||||
|
descriptions.emplace(id, where);
|
||||||
|
|
||||||
|
if (objectJson.contains("name") && !objectJson.at("name").is_string()) {
|
||||||
|
return where + ": \"name\" must be a string";
|
||||||
|
}
|
||||||
|
if (objectJson.contains("active") && !objectJson.at("active").is_boolean()) {
|
||||||
|
return where + ": \"active\" must be a boolean";
|
||||||
|
}
|
||||||
|
if (auto error = ValidateTransform(objectJson, version)) {
|
||||||
|
return where + ": " + *error;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectId parent = InvalidObjectId;
|
||||||
|
if (objectJson.contains("parent")) {
|
||||||
|
if (!objectJson.at("parent").is_number_unsigned()) {
|
||||||
|
return where + ": \"parent\" must be an object id";
|
||||||
|
}
|
||||||
|
parent = objectJson.at("parent").get<ObjectId>();
|
||||||
|
}
|
||||||
|
if (parent == id) {
|
||||||
|
return where + " is its own parent";
|
||||||
|
}
|
||||||
|
parents.emplace(id, parent);
|
||||||
|
|
||||||
|
if (auto error = ValidateComponents(objectJson)) {
|
||||||
|
return where + " " + *error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [id, parent] : parents) {
|
||||||
|
if (parent != InvalidObjectId && !ids.contains(parent)) {
|
||||||
|
return descriptions.at(id) + " has missing parent id " + std::to_string(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_set<ObjectId> visited;
|
||||||
|
ObjectId current = id;
|
||||||
|
while (current != InvalidObjectId) {
|
||||||
|
if (!visited.insert(current).second) {
|
||||||
|
return descriptions.at(id) + " is part of a parent cycle";
|
||||||
|
}
|
||||||
|
const auto parentIt = parents.find(current);
|
||||||
|
if (parentIt == parents.end()) {
|
||||||
|
return descriptions.at(id) + " has missing ancestor id " + std::to_string(current);
|
||||||
|
}
|
||||||
|
current = parentIt->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WarnMissingAssets(const json& objects, const std::filesystem::path& source) {
|
||||||
|
const auto report = [&](const std::string& objectName, const std::string& detail) {
|
||||||
|
spdlog::warn("Asset warning ({}): {} references \"{}\" which could not be found",
|
||||||
|
source.string(), objectName, detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& objectJson : objects) {
|
||||||
|
const std::string objectName = objectJson.value("name", "GameObject");
|
||||||
|
if (!objectJson.contains("components")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& componentJson : objectJson.at("components")) {
|
||||||
|
if (!componentJson.contains("data")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& data = componentJson.at("data");
|
||||||
|
|
||||||
|
// MeshRendererComponent: meshKey / materialKey are asset cache keys.
|
||||||
|
for (const char* key : {"meshKey", "materialKey"}) {
|
||||||
|
if (data.contains(key) && data.at(key).is_string()) {
|
||||||
|
const std::string cacheKey = data.at(key).get<std::string>();
|
||||||
|
if (cacheKey.empty()) continue;
|
||||||
|
const auto asset = AssetReference::fromCacheKey(cacheKey);
|
||||||
|
// Runtime-only keys like "BulbMaterial#2" are not file paths
|
||||||
|
const bool looksLikePath = asset &&
|
||||||
|
asset->path.find_first_of(":/\\") != std::string::npos;
|
||||||
|
if (looksLikePath && !AssetFileExists(asset->path)) {
|
||||||
|
report(objectName, cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic: any nested object with a "path" field is an AssetReference.
|
||||||
|
std::function<void(const json&)> walk = [&](const json& node) {
|
||||||
|
if (node.is_object()) {
|
||||||
|
if (node.contains("path") && node.at("path").is_string()) {
|
||||||
|
const std::string path = node.at("path").get<std::string>();
|
||||||
|
if (!path.empty() && !AssetFileExists(path)) {
|
||||||
|
report(objectName, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||||
|
walk(it.value());
|
||||||
|
}
|
||||||
|
} else if (node.is_array()) {
|
||||||
|
for (const auto& element : node) {
|
||||||
|
walk(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const GameObject*> CollectHierarchy(const GameObject& root) {
|
||||||
|
std::vector<const GameObject*> objects;
|
||||||
|
std::unordered_set<const GameObject*> visited;
|
||||||
|
CollectHierarchyRecursive(root, objects, visited);
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
json SerializeObjects(const std::vector<const GameObject*>& objects) {
|
||||||
|
const std::unordered_set<const GameObject*> included(objects.begin(), objects.end());
|
||||||
|
|
||||||
|
json result = json::array();
|
||||||
|
for (const GameObject* objectPtr : objects) {
|
||||||
|
if (objectPtr == nullptr || objectPtr->IsBeingDestroyed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameObject& object = *objectPtr;
|
||||||
|
if (object.GetComponent<Rigidbody>() != nullptr &&
|
||||||
|
object.GetComponent<Collider>() == nullptr) {
|
||||||
|
throw std::runtime_error("\"" + object.GetName() + "\" has a Rigidbody but no Collider");
|
||||||
|
}
|
||||||
|
const Transform& transform = object.GetTransform();
|
||||||
|
|
||||||
|
json objectJson;
|
||||||
|
objectJson["id"] = object.GetId();
|
||||||
|
objectJson["name"] = object.GetName();
|
||||||
|
objectJson["active"] = object.IsActive();
|
||||||
|
objectJson["transform"] = {
|
||||||
|
{"position", transform.GetLocalPosition()},
|
||||||
|
{"rotation", transform.GetLocalRotation()},
|
||||||
|
{"scale", transform.GetLocalScale()}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Transform* parent = transform.GetParent();
|
||||||
|
const GameObject* parentObject = parent != nullptr ? parent->GetOwner() : nullptr;
|
||||||
|
objectJson["parent"] = parentObject != nullptr && included.contains(parentObject) &&
|
||||||
|
!parentObject->IsBeingDestroyed()
|
||||||
|
? parentObject->GetId()
|
||||||
|
: InvalidObjectId;
|
||||||
|
|
||||||
|
objectJson["components"] = json::array();
|
||||||
|
for (const auto& componentPtr : object.GetComponents()) {
|
||||||
|
if (!componentPtr || componentPtr->IsBeingDestroyed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component& component = *componentPtr;
|
||||||
|
json data;
|
||||||
|
try {
|
||||||
|
data = component.Serialize();
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
throw std::runtime_error("failed to serialize " + component.GetTypeName() +
|
||||||
|
" on \"" + object.GetName() + "\": " + exception.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.is_null()) {
|
||||||
|
data = json::object();
|
||||||
|
}
|
||||||
|
if (auto bad = FindNonFinite(data, "data")) {
|
||||||
|
throw std::runtime_error(component.GetTypeName() + " on \"" + object.GetName() +
|
||||||
|
"\": " + *bad + " is NaN or Inf");
|
||||||
|
}
|
||||||
|
|
||||||
|
objectJson["components"].push_back({
|
||||||
|
{"type", component.GetTypeName()},
|
||||||
|
{"enabled", component.isEnabled()},
|
||||||
|
{"data", std::move(data)}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(std::move(objectJson));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltObjects BuildObjects(Scene& scene, const json& objects, const ObjectMap& externalObjects) {
|
||||||
|
BuiltObjects built;
|
||||||
|
std::vector<const json*> objectJsonList;
|
||||||
|
std::string context = "objects";
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const auto& objectJson : objects) {
|
||||||
|
const ObjectId id = objectJson.at("id").get<ObjectId>();
|
||||||
|
const std::string name = objectJson.value("name", "GameObject");
|
||||||
|
const bool active = objectJson.value("active", true);
|
||||||
|
|
||||||
|
// File ids are local to the file, every loaded object gets a fresh runtime id
|
||||||
|
GameObject* object = scene.CreateGameObject(name);
|
||||||
|
object->SetActive(active);
|
||||||
|
|
||||||
|
built.objects.push_back(object);
|
||||||
|
built.idMap.emplace(id, object);
|
||||||
|
objectJsonList.push_back(&objectJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < objectJsonList.size(); ++i) {
|
||||||
|
const json& objectJson = *objectJsonList[i];
|
||||||
|
GameObject* object = built.objects[i];
|
||||||
|
context = "\"" + object->GetName() + "\" transform";
|
||||||
|
|
||||||
|
if (objectJson.contains("transform") &&
|
||||||
|
!objectJson.at("transform").empty()) {
|
||||||
|
const auto& transformJson = objectJson.at("transform");
|
||||||
|
const auto& rotation = transformJson.at("rotation");
|
||||||
|
|
||||||
|
object->GetTransform().SetLocalPosition(transformJson.at("position").get<glm::vec3>());
|
||||||
|
|
||||||
|
if (rotation.size() == 4) {
|
||||||
|
object->GetTransform().SetLocalRotation(rotation.get<glm::quat>());
|
||||||
|
} else {
|
||||||
|
// Version 1 scene files used three Euler angles in
|
||||||
|
// degrees. Continue reading that format safely.
|
||||||
|
object->GetTransform().SetLocalRotation(rotation.get<glm::vec3>());
|
||||||
|
}
|
||||||
|
|
||||||
|
object->GetTransform().SetLocalScale(transformJson.at("scale").get<glm::vec3>());
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObjectId parentId = objectJson.value("parent", InvalidObjectId);
|
||||||
|
if (parentId != InvalidObjectId) {
|
||||||
|
object->GetTransform().SetParent(&built.idMap.at(parentId)->GetTransform(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < objectJsonList.size(); ++i) {
|
||||||
|
const json& objectJson = *objectJsonList[i];
|
||||||
|
GameObject* object = built.objects[i];
|
||||||
|
if (!objectJson.contains("components")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& componentJson : objectJson.at("components")) {
|
||||||
|
const std::string type = componentJson.at("type").get<std::string>();
|
||||||
|
context = type + " on \"" + object->GetName() + "\"";
|
||||||
|
Component* component = ComponentFactory::Create(type, *object);
|
||||||
|
if (component == nullptr) {
|
||||||
|
throw std::runtime_error("unknown component type");
|
||||||
|
}
|
||||||
|
|
||||||
|
component->SetEnabled(componentJson.value("enabled", true));
|
||||||
|
if (componentJson.contains("data")) {
|
||||||
|
component->Deserialize(componentJson.at("data"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectMap referenceMap = built.idMap;
|
||||||
|
referenceMap.insert(externalObjects.begin(), externalObjects.end());
|
||||||
|
|
||||||
|
for (GameObject* object : built.objects) {
|
||||||
|
for (const auto& componentPtr : object->GetComponents()) {
|
||||||
|
if (componentPtr) {
|
||||||
|
context = componentPtr->GetTypeName() + " on \"" + object->GetName() + "\" references";
|
||||||
|
componentPtr->ResolveReferences(referenceMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
for (GameObject* object : built.objects) {
|
||||||
|
object->Destroy();
|
||||||
|
}
|
||||||
|
throw std::runtime_error(context + ": " + exception.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return built;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WriteFileAtomically(const std::filesystem::path& path, const std::string& contents) {
|
||||||
|
std::filesystem::path tempPath = path;
|
||||||
|
tempPath += ".tmp";
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream file(tempPath, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
spdlog::error("Failed to open file for writing: {}", tempPath.string());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file << contents;
|
||||||
|
file.flush();
|
||||||
|
if (!file.good()) {
|
||||||
|
spdlog::error("Failed to write file: {}", tempPath.string());
|
||||||
|
file.close();
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(tempPath, ignored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::rename(tempPath, path, error);
|
||||||
|
if (error) {
|
||||||
|
spdlog::error("Failed to replace file {}: {}", path.string(), error.message());
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(tempPath, ignored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#include <destrum/Serialization/PrefabCache.h>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
#include <destrum/Serialization/PrefabSerializer.h>
|
||||||
|
|
||||||
|
std::shared_ptr<const nlohmann::json> PrefabCache::Get(std::string_view path) {
|
||||||
|
std::filesystem::path fullPath;
|
||||||
|
try {
|
||||||
|
fullPath = ResolvePath(path);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Cannot resolve prefab path \"{}\": {}", path, exception.what());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code error;
|
||||||
|
const auto writeTime = std::filesystem::last_write_time(fullPath, error);
|
||||||
|
if (error) {
|
||||||
|
spdlog::error("Prefab file not found: {}", fullPath.string());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string key = MakeKey(fullPath);
|
||||||
|
if (const auto it = m_Prefabs.find(key); it != m_Prefabs.end() && it->second.writeTime == writeTime) {
|
||||||
|
return it->second.prefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto prefab = PrefabSerializer::LoadFile(fullPath);
|
||||||
|
if (!prefab) {
|
||||||
|
m_Prefabs.erase(key);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto shared = std::make_shared<const nlohmann::json>(std::move(*prefab));
|
||||||
|
m_Prefabs[key] = Entry{shared, writeTime};
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrefabCache::Invalidate(const std::filesystem::path& path) {
|
||||||
|
m_Prefabs.erase(MakeKey(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrefabCache::Clear() {
|
||||||
|
m_Prefabs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path PrefabCache::ResolvePath(std::string_view path) {
|
||||||
|
if (path.find("://") != std::string_view::npos) {
|
||||||
|
return AssetFS::GetInstance().GetFullPath(path);
|
||||||
|
}
|
||||||
|
return std::filesystem::path{path};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PrefabCache::MakeKey(const std::filesystem::path& path) {
|
||||||
|
std::error_code error;
|
||||||
|
const auto absolute = std::filesystem::absolute(path, error);
|
||||||
|
return (error ? path : absolute).lexically_normal().generic_string();
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#include <destrum/Serialization/PrefabSerializer.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
|
#include <destrum/Serialization/ObjectSerialization.h>
|
||||||
|
#include <destrum/Serialization/PrefabCache.h>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
GameObject* InstantiateWith(Scene& scene, const json& prefab, Transform* parent,
|
||||||
|
const ObjectSerialization::ObjectMap& externalObjects) {
|
||||||
|
if (scene.IsBeingUnloaded()) {
|
||||||
|
spdlog::error("Cannot instantiate a prefab into a scene that is being unloaded: {}", scene.GetName());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (auto error = PrefabSerializer::Validate(prefab)) {
|
||||||
|
spdlog::error("Cannot instantiate invalid prefab: {}", *error);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* parentObject = parent != nullptr ? parent->GetOwner() : nullptr;
|
||||||
|
if (parentObject != nullptr && parentObject->GetScene() != &scene) {
|
||||||
|
spdlog::error("Cannot instantiate a prefab under \"{}\", it belongs to another scene",
|
||||||
|
parentObject->GetName());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto built = ObjectSerialization::BuildObjects(scene, prefab.at("objects"), externalObjects);
|
||||||
|
GameObject* root = built.objects.front();
|
||||||
|
if (parent != nullptr) {
|
||||||
|
root->GetTransform().SetParent(parent, false);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to instantiate prefab \"{}\" ({})",
|
||||||
|
prefab.value("name", "Prefab"), exception.what());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<json> PrefabSerializer::Capture(const GameObject& root) {
|
||||||
|
if (root.IsBeingDestroyed()) {
|
||||||
|
spdlog::error("Cannot capture a prefab from destroyed object \"{}\"", root.GetName());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
json prefab;
|
||||||
|
prefab["version"] = CurrentPrefabVersion;
|
||||||
|
prefab["name"] = root.GetName();
|
||||||
|
try {
|
||||||
|
prefab["objects"] = ObjectSerialization::SerializeObjects(ObjectSerialization::CollectHierarchy(root));
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Cannot capture prefab \"{}\": {}", root.GetName(), exception.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto error = Validate(prefab)) {
|
||||||
|
spdlog::error("Cannot capture prefab \"{}\", it would not load back: {}", root.GetName(), *error);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return prefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PrefabSerializer::Save(const GameObject& root, const std::filesystem::path& path) {
|
||||||
|
const auto prefab = Capture(root);
|
||||||
|
if (!prefab) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string contents;
|
||||||
|
try {
|
||||||
|
contents = prefab->dump(4);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to encode prefab \"{}\": {}", root.GetName(), exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ObjectSerialization::WriteFileAtomically(path, contents)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PrefabCache::GetInstance().Invalidate(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<json> PrefabSerializer::LoadFile(const std::filesystem::path& path) {
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
spdlog::error("Failed to open prefab file for reading: {}", path.string());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
json prefab;
|
||||||
|
try {
|
||||||
|
file >> prefab;
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to parse prefab file {}: {}", path.string(), exception.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto error = Validate(prefab)) {
|
||||||
|
spdlog::error("Invalid prefab data in {}: {}", path.string(), *error);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectSerialization::WarnMissingAssets(prefab.at("objects"), path);
|
||||||
|
return prefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> PrefabSerializer::Validate(const json& prefab) {
|
||||||
|
RegisterEngineComponents();
|
||||||
|
|
||||||
|
if (!prefab.is_object()) {
|
||||||
|
return "root must be an object";
|
||||||
|
}
|
||||||
|
if (!prefab.contains("version") || !prefab.at("version").is_number_integer()) {
|
||||||
|
return "\"version\" must be an integer";
|
||||||
|
}
|
||||||
|
const int version = prefab.at("version").get<int>();
|
||||||
|
if (version < 1 || version > CurrentPrefabVersion) {
|
||||||
|
return "unsupported prefab version " + std::to_string(version);
|
||||||
|
}
|
||||||
|
if (prefab.contains("name") && !prefab.at("name").is_string()) {
|
||||||
|
return "\"name\" must be a string";
|
||||||
|
}
|
||||||
|
if (!prefab.contains("objects") || !prefab.at("objects").is_array() || prefab.at("objects").empty()) {
|
||||||
|
return "prefab must contain a non-empty \"objects\" array";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& objects = prefab.at("objects");
|
||||||
|
if (auto error = ObjectSerialization::ValidateObjects(objects, ObjectSerialization::CurrentObjectVersion)) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < objects.size(); ++i) {
|
||||||
|
const bool isRoot = objects.at(i).value("parent", InvalidObjectId) == InvalidObjectId;
|
||||||
|
if (i == 0 && !isRoot) {
|
||||||
|
return "objects[0] must be the prefab root";
|
||||||
|
}
|
||||||
|
if (i != 0 && isRoot) {
|
||||||
|
return "objects[" + std::to_string(i) + "] is a second root, a prefab has exactly one";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* PrefabSerializer::InstantiateFromJson(Scene& scene, const json& prefab, Transform* parent) {
|
||||||
|
return InstantiateWith(scene, prefab, parent, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* PrefabSerializer::Instantiate(Scene& scene, std::string_view path, Transform* parent) {
|
||||||
|
const auto prefab = PrefabCache::GetInstance().Get(path);
|
||||||
|
if (!prefab) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return InstantiateWith(scene, *prefab, parent, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* PrefabSerializer::Duplicate(GameObject& object) {
|
||||||
|
Scene* scene = object.GetScene();
|
||||||
|
if (scene == nullptr) {
|
||||||
|
spdlog::error("Cannot duplicate \"{}\", it is not in a scene", object.GetName());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto prefab = Capture(object);
|
||||||
|
if (!prefab) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// References to objects outside the copied hierarchy keep pointing at the originals
|
||||||
|
ObjectSerialization::ObjectMap sceneObjects;
|
||||||
|
for (const auto* list : {&scene->m_objects, &scene->m_pendingAdditions}) {
|
||||||
|
for (const auto& sceneObject : *list) {
|
||||||
|
if (sceneObject && !sceneObject->IsBeingDestroyed()) {
|
||||||
|
sceneObjects.emplace(sceneObject->GetId(), sceneObject.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return InstantiateWith(*scene, *prefab, object.GetTransform().GetParent(), sceneObjects);
|
||||||
|
}
|
||||||
@@ -1,171 +1,26 @@
|
|||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <limits>
|
|
||||||
#include <optional>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/ObjectModel/Component.h>
|
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/ObjectModel/ObjectId.h>
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
|
||||||
#include <destrum/Assets/AssetReference.h>
|
|
||||||
#include <destrum/Components/Physics/Collider.h>
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
|
||||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
|
||||||
#include <destrum/Serialization/ComponentFactory.h>
|
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
#include <destrum/Serialization/ObjectSerialization.h>
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using ValidationError = std::optional<std::string>;
|
using ObjectSerialization::ValidationError;
|
||||||
|
|
||||||
constexpr int CurrentSceneVersion = 2;
|
constexpr int CurrentSceneVersion = ObjectSerialization::CurrentObjectVersion;
|
||||||
|
|
||||||
[[nodiscard]] bool IsFiniteNumber(const json& value) {
|
|
||||||
if (!value.is_number()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::isfinite(value.get<double>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool IsFiniteArray(const json& value, std::size_t size) {
|
|
||||||
if (!value.is_array() || value.size() != size) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& element : value) {
|
|
||||||
if (!IsFiniteNumber(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the JSON path of the first NaN/Inf, nlohmann would write it as null
|
|
||||||
[[nodiscard]] std::optional<std::string> FindNonFinite(const json& value, const std::string& path) {
|
|
||||||
if (value.is_number_float()) {
|
|
||||||
if (!std::isfinite(value.get<double>())) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
if (value.is_array()) {
|
|
||||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
|
||||||
if (auto bad = FindNonFinite(value.at(i), path + "[" + std::to_string(i) + "]")) {
|
|
||||||
return bad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
if (value.is_object()) {
|
|
||||||
for (auto it = value.begin(); it != value.end(); ++it) {
|
|
||||||
if (auto bad = FindNonFinite(it.value(), path + "." + it.key())) {
|
|
||||||
return bad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] std::string DescribeObject(std::size_t index, const json& objectJson) {
|
|
||||||
std::string description = "objects[" + std::to_string(index) + "]";
|
|
||||||
if (objectJson.is_object() && objectJson.contains("name") && objectJson.at("name").is_string()) {
|
|
||||||
description += " \"" + objectJson.at("name").get<std::string>() + "\"";
|
|
||||||
}
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] ValidationError ValidateTransform(const json& objectJson, int version) {
|
|
||||||
if (!objectJson.contains("transform")) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& transform = objectJson.at("transform");
|
|
||||||
if (!transform.is_object()) {
|
|
||||||
return "transform must be an object";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version 1 wrote an empty placeholder transform object.
|
|
||||||
if (version <= 1 && transform.empty()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const char* key : {"position", "rotation", "scale"}) {
|
|
||||||
if (!transform.contains(key)) {
|
|
||||||
return std::string{"transform is missing \""} + key + "\"";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsFiniteArray(transform.at("position"), 3)) {
|
|
||||||
return "transform.position must be 3 finite numbers";
|
|
||||||
}
|
|
||||||
if (!IsFiniteArray(transform.at("scale"), 3)) {
|
|
||||||
return "transform.scale must be 3 finite numbers";
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& rotation = transform.at("rotation");
|
|
||||||
if (!IsFiniteArray(rotation, 3) && !IsFiniteArray(rotation, 4)) {
|
|
||||||
return "transform.rotation must be 3 (euler) or 4 (quaternion) finite numbers";
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] ValidationError ValidateComponents(const json& objectJson) {
|
|
||||||
if (!objectJson.contains("components")) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& components = objectJson.at("components");
|
|
||||||
if (!components.is_array()) {
|
|
||||||
return "components must be an array";
|
|
||||||
}
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < components.size(); ++i) {
|
|
||||||
const auto& componentJson = components.at(i);
|
|
||||||
const std::string where = "components[" + std::to_string(i) + "]";
|
|
||||||
|
|
||||||
if (!componentJson.is_object() || !componentJson.contains("type") ||
|
|
||||||
!componentJson.at("type").is_string()) {
|
|
||||||
return where + " must be an object with a string \"type\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string type = componentJson.at("type").get<std::string>();
|
|
||||||
const std::string named = where + " " + type;
|
|
||||||
|
|
||||||
if (componentJson.contains("enabled") && !componentJson.at("enabled").is_boolean()) {
|
|
||||||
return named + ": \"enabled\" must be a boolean";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentJson.contains("data")) {
|
|
||||||
const auto& data = componentJson.at("data");
|
|
||||||
if (!data.is_object()) {
|
|
||||||
return named + ": \"data\" must be an object, got " + std::string{data.type_name()};
|
|
||||||
}
|
|
||||||
if (auto bad = FindNonFinite(data, "data")) {
|
|
||||||
return named + ": " + *bad + " is not a finite number";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ComponentFactory::IsRegistered(type)) {
|
|
||||||
return named + ": unknown component type";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] ValidationError ValidateSceneJson(const json& root) {
|
[[nodiscard]] ValidationError ValidateSceneJson(const json& root) {
|
||||||
if (!root.is_object()) {
|
if (!root.is_object()) {
|
||||||
@@ -194,186 +49,7 @@ namespace {
|
|||||||
return "\"currentCamera\" must be an object id";
|
return "\"currentCamera\" must be an object id";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_set<ObjectId> ids;
|
return ObjectSerialization::ValidateObjects(root.at("objects"), version);
|
||||||
std::unordered_map<ObjectId, ObjectId> parents;
|
|
||||||
std::unordered_map<ObjectId, std::string> descriptions;
|
|
||||||
|
|
||||||
const auto& objects = root.at("objects");
|
|
||||||
for (std::size_t index = 0; index < objects.size(); ++index) {
|
|
||||||
const auto& objectJson = objects.at(index);
|
|
||||||
const std::string where = DescribeObject(index, objectJson);
|
|
||||||
|
|
||||||
if (!objectJson.is_object()) {
|
|
||||||
return where + " must be an object";
|
|
||||||
}
|
|
||||||
if (!objectJson.contains("id") || !objectJson.at("id").is_number_unsigned()) {
|
|
||||||
return where + " must have a non-negative integer \"id\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
const ObjectId id = objectJson.at("id").get<ObjectId>();
|
|
||||||
if (id == InvalidObjectId || id >= std::numeric_limits<ObjectId>::max() - 1) {
|
|
||||||
return where + " uses reserved id " + std::to_string(id);
|
|
||||||
}
|
|
||||||
if (!ids.insert(id).second) {
|
|
||||||
return where + " reuses id " + std::to_string(id);
|
|
||||||
}
|
|
||||||
descriptions.emplace(id, where);
|
|
||||||
|
|
||||||
if (objectJson.contains("name") && !objectJson.at("name").is_string()) {
|
|
||||||
return where + ": \"name\" must be a string";
|
|
||||||
}
|
|
||||||
if (objectJson.contains("active") && !objectJson.at("active").is_boolean()) {
|
|
||||||
return where + ": \"active\" must be a boolean";
|
|
||||||
}
|
|
||||||
if (auto error = ValidateTransform(objectJson, version)) {
|
|
||||||
return where + ": " + *error;
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectId parent = InvalidObjectId;
|
|
||||||
if (objectJson.contains("parent")) {
|
|
||||||
if (!objectJson.at("parent").is_number_unsigned()) {
|
|
||||||
return where + ": \"parent\" must be an object id";
|
|
||||||
}
|
|
||||||
parent = objectJson.at("parent").get<ObjectId>();
|
|
||||||
}
|
|
||||||
if (parent == id) {
|
|
||||||
return where + " is its own parent";
|
|
||||||
}
|
|
||||||
parents.emplace(id, parent);
|
|
||||||
|
|
||||||
if (auto error = ValidateComponents(objectJson)) {
|
|
||||||
return where + " " + *error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& [id, parent] : parents) {
|
|
||||||
if (parent != InvalidObjectId && !ids.contains(parent)) {
|
|
||||||
return descriptions.at(id) + " has missing parent id " + std::to_string(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<ObjectId> visited;
|
|
||||||
ObjectId current = id;
|
|
||||||
while (current != InvalidObjectId) {
|
|
||||||
if (!visited.insert(current).second) {
|
|
||||||
return descriptions.at(id) + " is part of a parent cycle";
|
|
||||||
}
|
|
||||||
const auto parentIt = parents.find(current);
|
|
||||||
if (parentIt == parents.end()) {
|
|
||||||
return descriptions.at(id) + " has missing ancestor id " + std::to_string(current);
|
|
||||||
}
|
|
||||||
current = parentIt->second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool AssetFileExists(const std::string& path) {
|
|
||||||
if (path.empty() || PrimitiveMeshes::IsPrimitiveKey(path)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::path fullPath;
|
|
||||||
if (path.find("://") != std::string::npos) {
|
|
||||||
try {
|
|
||||||
fullPath = AssetFS::GetInstance().GetFullPath(path);
|
|
||||||
} catch (const std::exception&) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fullPath = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::filesystem::exists(fullPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) {
|
|
||||||
const auto report = [&](const std::string& objectName, const std::string& detail) {
|
|
||||||
spdlog::warn("Scene asset warning ({}): {} references \"{}\" which could not be found",
|
|
||||||
scenePath.string(), objectName, detail);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& objectJson : root.at("objects")) {
|
|
||||||
const std::string objectName = objectJson.value("name", "GameObject");
|
|
||||||
if (!objectJson.contains("components")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& componentJson : objectJson.at("components")) {
|
|
||||||
if (!componentJson.contains("data")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto& data = componentJson.at("data");
|
|
||||||
|
|
||||||
// MeshRendererComponent: meshKey / materialKey are asset cache keys.
|
|
||||||
for (const char* key : {"meshKey", "materialKey"}) {
|
|
||||||
if (data.contains(key) && data.at(key).is_string()) {
|
|
||||||
const std::string cacheKey = data.at(key).get<std::string>();
|
|
||||||
if (cacheKey.empty()) continue;
|
|
||||||
const auto asset = AssetReference::fromCacheKey(cacheKey);
|
|
||||||
// Runtime-only keys like "BulbMaterial#2" are not file paths
|
|
||||||
const bool looksLikePath = asset &&
|
|
||||||
asset->path.find_first_of(":/\\") != std::string::npos;
|
|
||||||
if (looksLikePath && !AssetFileExists(asset->path)) {
|
|
||||||
report(objectName, cacheKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic: any nested object with a "path" field is an AssetReference.
|
|
||||||
std::function<void(const json&)> walk = [&](const json& node) {
|
|
||||||
if (node.is_object()) {
|
|
||||||
if (node.contains("path") && node.at("path").is_string()) {
|
|
||||||
const std::string path = node.at("path").get<std::string>();
|
|
||||||
if (!path.empty() && !AssetFileExists(path)) {
|
|
||||||
report(objectName, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
|
||||||
walk(it.value());
|
|
||||||
}
|
|
||||||
} else if (node.is_array()) {
|
|
||||||
for (const auto& element : node) {
|
|
||||||
walk(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
walk(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool WriteFileAtomically(const std::filesystem::path& path, const std::string& contents) {
|
|
||||||
std::filesystem::path tempPath = path;
|
|
||||||
tempPath += ".tmp";
|
|
||||||
|
|
||||||
{
|
|
||||||
std::ofstream file(tempPath, std::ios::binary | std::ios::trunc);
|
|
||||||
if (!file.is_open()) {
|
|
||||||
spdlog::error("Failed to open scene file for writing: {}", tempPath.string());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
file << contents;
|
|
||||||
file.flush();
|
|
||||||
if (!file.good()) {
|
|
||||||
spdlog::error("Failed to write scene file: {}", tempPath.string());
|
|
||||||
file.close();
|
|
||||||
std::error_code ignored;
|
|
||||||
std::filesystem::remove(tempPath, ignored);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::error_code error;
|
|
||||||
std::filesystem::rename(tempPath, path, error);
|
|
||||||
if (error) {
|
|
||||||
spdlog::error("Failed to replace scene file {}: {}", path.string(), error.message());
|
|
||||||
std::error_code ignored;
|
|
||||||
std::filesystem::remove(tempPath, ignored);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,72 +84,22 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
{"skyboxRotation", environment.skyboxRotation},
|
{"skyboxRotation", environment.skyboxRotation},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::vector<const GameObject*> objects;
|
||||||
|
objects.reserve(scene.GetObjects().size());
|
||||||
for (const auto& objectPtr : scene.GetObjects()) {
|
for (const auto& objectPtr : scene.GetObjects()) {
|
||||||
if (!objectPtr || objectPtr->IsBeingDestroyed()) {
|
if (objectPtr && !objectPtr->IsBeingDestroyed()) {
|
||||||
continue;
|
objects.push_back(objectPtr.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameObject& object = *objectPtr;
|
|
||||||
if (object.GetComponent<Rigidbody>() != nullptr &&
|
|
||||||
object.GetComponent<Collider>() == nullptr) {
|
|
||||||
spdlog::error("Cannot save object with a Rigidbody but no Collider: {}", object.GetName());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Transform& transform = object.GetTransform();
|
|
||||||
|
|
||||||
json objectJson;
|
|
||||||
objectJson["id"] = object.GetId();
|
|
||||||
objectJson["name"] = object.GetName();
|
|
||||||
objectJson["active"] = object.IsActive();
|
|
||||||
objectJson["transform"] = {
|
|
||||||
{"position", transform.GetLocalPosition()},
|
|
||||||
{"rotation", transform.GetLocalRotation()},
|
|
||||||
{"scale", transform.GetLocalScale()}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Transform* parent = transform.GetParent();
|
|
||||||
objectJson["parent"] = parent != nullptr && parent->GetOwner() != nullptr &&
|
|
||||||
parent->GetOwner()->GetScene() == &scene &&
|
|
||||||
!parent->GetOwner()->IsBeingDestroyed()
|
|
||||||
? parent->GetOwner()->GetId()
|
|
||||||
: InvalidObjectId;
|
|
||||||
|
|
||||||
objectJson["components"] = json::array();
|
|
||||||
for (const auto& componentPtr : object.GetComponents()) {
|
|
||||||
if (!componentPtr || componentPtr->IsBeingDestroyed()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component& component = *componentPtr;
|
|
||||||
json data;
|
|
||||||
try {
|
|
||||||
data = component.Serialize();
|
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
spdlog::error("Failed to serialize {} on \"{}\": {}",
|
|
||||||
component.GetTypeName(), object.GetName(), exception.what());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.is_null()) {
|
|
||||||
data = json::object();
|
|
||||||
}
|
|
||||||
if (auto bad = FindNonFinite(data, "data")) {
|
|
||||||
spdlog::error("Cannot save {} on \"{}\": {} is NaN or Inf",
|
|
||||||
component.GetTypeName(), object.GetName(), *bad);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
objectJson["components"].push_back({
|
|
||||||
{"type", component.GetTypeName()},
|
|
||||||
{"enabled", component.isEnabled()},
|
|
||||||
{"data", std::move(data)}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
root["objects"].push_back(std::move(objectJson));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto bad = FindNonFinite(root, "scene")) {
|
try {
|
||||||
|
root["objects"] = ObjectSerialization::SerializeObjects(objects);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto bad = ObjectSerialization::FindNonFinite(root, "scene")) {
|
||||||
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -492,7 +118,7 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return WriteFileAtomically(path, contents);
|
return ObjectSerialization::WriteFileAtomically(path, contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) {
|
SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) {
|
||||||
@@ -520,7 +146,7 @@ SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateAssetReferences(result.root, path);
|
ObjectSerialization::WarnMissingAssets(result.root.at("objects"), path);
|
||||||
result.success = true;
|
result.success = true;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -552,26 +178,12 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
// No physics world is created for the staging scene - physics bodies are
|
// No physics world is created for the staging scene - physics bodies are
|
||||||
// registered directly in the live scene's world once loading succeeds.
|
// registered directly in the live scene's world once loading succeeds.
|
||||||
Scene stagingScene(scene.GetName(), nullptr);
|
Scene stagingScene(scene.GetName(), nullptr);
|
||||||
std::unordered_map<ObjectId, GameObject*> idMap;
|
ObjectSerialization::ObjectMap idMap;
|
||||||
std::vector<const json*> objectJsonList;
|
|
||||||
std::vector<GameObject*> registeredObjects;
|
std::vector<GameObject*> registeredObjects;
|
||||||
std::string context = "scene";
|
std::string context = "scene";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const auto& objectJson : root.at("objects")) {
|
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
||||||
const ObjectId id = objectJson.at("id").get<ObjectId>();
|
|
||||||
const std::string name = objectJson.value("name", "GameObject");
|
|
||||||
const bool active = objectJson.value("active", true);
|
|
||||||
|
|
||||||
// File ids are local to the file, every loaded object gets a fresh runtime id
|
|
||||||
GameObject* object = stagingScene.CreateGameObject(name);
|
|
||||||
object->SetActive(active);
|
|
||||||
|
|
||||||
idMap.emplace(id, object);
|
|
||||||
objectJsonList.push_back(&objectJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback(0.4f);
|
|
||||||
|
|
||||||
// Move objects from pending to objects list without physics registration
|
// Move objects from pending to objects list without physics registration
|
||||||
// (staging scene has no physics world).
|
// (staging scene has no physics world).
|
||||||
@@ -582,73 +194,6 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
}
|
}
|
||||||
stagingScene.m_pendingAdditions.clear();
|
stagingScene.m_pendingAdditions.clear();
|
||||||
|
|
||||||
for (const json* objectJson : objectJsonList) {
|
|
||||||
const ObjectId id = objectJson->at("id").get<ObjectId>();
|
|
||||||
GameObject* object = idMap.at(id);
|
|
||||||
context = "\"" + object->GetName() + "\" transform";
|
|
||||||
|
|
||||||
if (objectJson->contains("transform") &&
|
|
||||||
!objectJson->at("transform").empty()) {
|
|
||||||
const auto& transformJson = objectJson->at("transform");
|
|
||||||
const auto& rotation = transformJson.at("rotation");
|
|
||||||
|
|
||||||
object->GetTransform().SetLocalPosition(transformJson.at("position").get<glm::vec3>());
|
|
||||||
|
|
||||||
if (rotation.size() == 4) {
|
|
||||||
object->GetTransform().SetLocalRotation(rotation.get<glm::quat>());
|
|
||||||
} else {
|
|
||||||
// Version 1 scene files used three Euler angles in
|
|
||||||
// degrees. Continue reading that format safely.
|
|
||||||
object->GetTransform().SetLocalRotation(rotation.get<glm::vec3>());
|
|
||||||
}
|
|
||||||
|
|
||||||
object->GetTransform().SetLocalScale(transformJson.at("scale").get<glm::vec3>());
|
|
||||||
}
|
|
||||||
|
|
||||||
const ObjectId parentId = objectJson->value("parent", InvalidObjectId);
|
|
||||||
if (parentId != InvalidObjectId) {
|
|
||||||
object->GetTransform().SetParent(&idMap.at(parentId)->GetTransform(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback(0.5f);
|
|
||||||
|
|
||||||
for (const json* objectJson : objectJsonList) {
|
|
||||||
GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>());
|
|
||||||
if (!objectJson->contains("components")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& componentJson : objectJson->at("components")) {
|
|
||||||
const std::string type = componentJson.at("type").get<std::string>();
|
|
||||||
context = type + " on \"" + object->GetName() + "\"";
|
|
||||||
Component* component = ComponentFactory::Create(type, *object);
|
|
||||||
if (component == nullptr) {
|
|
||||||
throw std::runtime_error("unknown component type");
|
|
||||||
}
|
|
||||||
|
|
||||||
component->SetEnabled(componentJson.value("enabled", true));
|
|
||||||
if (componentJson.contains("data")) {
|
|
||||||
component->Deserialize(componentJson.at("data"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback(0.6f);
|
|
||||||
|
|
||||||
for (const auto& objectPtr : stagingScene.GetObjects()) {
|
|
||||||
if (!objectPtr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& componentPtr : objectPtr->GetComponents()) {
|
|
||||||
if (componentPtr) {
|
|
||||||
context = componentPtr->GetTypeName() + " on \"" + objectPtr->GetName() + "\" references";
|
|
||||||
componentPtr->ResolveReferences(idMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback(0.7f);
|
if (progressCallback) progressCallback(0.7f);
|
||||||
|
|
||||||
// Register the replacement bodies in the live physics world before
|
// Register the replacement bodies in the live physics world before
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
#include <destrum/Serialization/ComponentFactory.h>
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
|
#include <destrum/Serialization/PrefabSerializer.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
[[nodiscard]] bool IsInspectableObject(const GameObject* object) {
|
[[nodiscard]] bool IsInspectableObject(const GameObject* object) {
|
||||||
@@ -242,6 +243,14 @@ namespace ImGuiUtils {
|
|||||||
if (ImGui::Checkbox("Active", &active)) {
|
if (ImGui::Checkbox("Active", &active)) {
|
||||||
selectedObject->SetActive(active);
|
selectedObject->SetActive(active);
|
||||||
}
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
const bool duplicateShortcut = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||||
|
ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_D);
|
||||||
|
if (ImGui::Button("Duplicate") || duplicateShortcut) {
|
||||||
|
if (GameObject* duplicate = PrefabSerializer::Duplicate(*selectedObject)) {
|
||||||
|
selectedObjectId = duplicate->GetId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Transform* parent = selectedObject->GetTransform().GetParent();
|
const Transform* parent = selectedObject->GetTransform().GetParent();
|
||||||
if (parent != nullptr && parent->GetOwner() != nullptr) {
|
if (parent != nullptr && parent->GetOwner() != nullptr) {
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ private:
|
|||||||
void saveCurrentScene();
|
void saveCurrentScene();
|
||||||
void loadScene();
|
void loadScene();
|
||||||
void createEmptyScene();
|
void createEmptyScene();
|
||||||
|
void saveSelectedPrefab();
|
||||||
|
void instantiatePrefab();
|
||||||
void findPlayer();
|
void findPlayer();
|
||||||
void submitDebugDraws();
|
void submitDebugDraws();
|
||||||
void submitColliderDebugLines();
|
void submitColliderDebugLines();
|
||||||
@@ -62,6 +64,7 @@ private:
|
|||||||
|
|
||||||
char scenePath[512]{"scenes/debug_scene.json"};
|
char scenePath[512]{"scenes/debug_scene.json"};
|
||||||
char newSceneName[128]{"EmptyScene"};
|
char newSceneName[128]{"EmptyScene"};
|
||||||
|
char prefabPath[512]{"prefabs/selected.prefab"};
|
||||||
std::string sceneStatus;
|
std::string sceneStatus;
|
||||||
bool renderColliders{false};
|
bool renderColliders{false};
|
||||||
bool render2DShapes{true};
|
bool render2DShapes{true};
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
||||||
#include <destrum/Graphics/Managers/Renderer2D.h>
|
#include <destrum/Graphics/Managers/Renderer2D.h>
|
||||||
#include <destrum/Input/InputManager.h>
|
#include <destrum/Input/InputManager.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Serialization/PrefabSerializer.h>
|
||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
|
|
||||||
#include <components/FirstPersonController.h>
|
#include <components/FirstPersonController.h>
|
||||||
@@ -120,6 +122,20 @@ void LightKeeper::drawDebugMenuBar()
|
|||||||
saveCurrentScene();
|
saveCurrentScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextDisabled("Prefab");
|
||||||
|
|
||||||
|
ImGui::SetNextItemWidth(320.0f);
|
||||||
|
ImGui::InputText("Prefab path", prefabPath, sizeof(prefabPath));
|
||||||
|
|
||||||
|
const bool hasSelection = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject() != nullptr;
|
||||||
|
if (ImGui::MenuItem("Save Selected as Prefab", nullptr, false, hasSelection)) {
|
||||||
|
saveSelectedPrefab();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Instantiate Prefab")) {
|
||||||
|
instantiatePrefab();
|
||||||
|
}
|
||||||
|
|
||||||
if (!sceneStatus.empty()) {
|
if (!sceneStatus.empty()) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::TextWrapped("%s", sceneStatus.c_str());
|
ImGui::TextWrapped("%s", sceneStatus.c_str());
|
||||||
@@ -270,6 +286,43 @@ void LightKeeper::saveCurrentScene()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LightKeeper::saveSelectedPrefab()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
|
||||||
|
if (selected == nullptr) {
|
||||||
|
sceneStatus = "Select an object to save as a prefab";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto path = ResolveScenePath(prefabPath, m_params.exeDir);
|
||||||
|
const auto parent = path.parent_path();
|
||||||
|
if (!parent.empty()) {
|
||||||
|
std::filesystem::create_directories(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneStatus = PrefabSerializer::Save(*selected, path)
|
||||||
|
? "Saved prefab \"" + selected->GetName() + "\" to " + path.string()
|
||||||
|
: "Failed to save prefab to " + path.string();
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
sceneStatus = std::string{"Prefab save failed: "} + exception.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LightKeeper::instantiatePrefab()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const auto path = ResolveScenePath(prefabPath, m_params.exeDir);
|
||||||
|
const GameObject* instance = PrefabSerializer::Instantiate(
|
||||||
|
SceneManager::GetInstance().GetCurrentScene(), path.string());
|
||||||
|
sceneStatus = instance != nullptr
|
||||||
|
? "Instantiated \"" + instance->GetName() + "\" from " + path.string()
|
||||||
|
: "Failed to instantiate prefab " + path.string();
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
sceneStatus = std::string{"Prefab instantiate failed: "} + exception.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LightKeeper::loadScene()
|
void LightKeeper::loadScene()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// The visible ground plane is at y = -1
|
// The visible ground plane is at y = -1
|
||||||
@@ -268,11 +269,12 @@ namespace {
|
|||||||
float top;
|
float top;
|
||||||
};
|
};
|
||||||
|
|
||||||
void CreateWall(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const std::string& name,
|
std::vector<GameObject*> CreateWall(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const std::string& name,
|
||||||
const glm::vec3& floorCenter, float length, float height, bool alongX, MaterialID materialID,
|
const glm::vec3& floorCenter, float length, float height, bool alongX, MaterialID materialID,
|
||||||
const std::optional<WallOpening>& opening = std::nullopt)
|
const std::optional<WallOpening>& opening = std::nullopt)
|
||||||
{
|
{
|
||||||
constexpr float thickness = 0.3f;
|
constexpr float thickness = 0.3f;
|
||||||
|
std::vector<GameObject*> pieces;
|
||||||
|
|
||||||
const auto createPiece = [&](const std::string& suffix, float from, float to, float bottom, float top)
|
const auto createPiece = [&](const std::string& suffix, float from, float to, float bottom, float top)
|
||||||
{
|
{
|
||||||
@@ -285,13 +287,14 @@ namespace {
|
|||||||
const glm::vec3 size = alongX
|
const glm::vec3 size = alongX
|
||||||
? glm::vec3{to - from, top - bottom, thickness}
|
? glm::vec3{to - from, top - bottom, thickness}
|
||||||
: glm::vec3{thickness, top - bottom, to - from};
|
: glm::vec3{thickness, top - bottom, to - from};
|
||||||
CreateBox(scene, gfxDevice, resources, name + suffix, center, size, materialID, RigidbodyType::Static);
|
pieces.push_back(
|
||||||
|
CreateBox(scene, gfxDevice, resources, name + suffix, center, size, materialID, RigidbodyType::Static));
|
||||||
};
|
};
|
||||||
|
|
||||||
const float halfLength = length * 0.5f;
|
const float halfLength = length * 0.5f;
|
||||||
if (!opening) {
|
if (!opening) {
|
||||||
createPiece("", -halfLength, halfLength, 0.0f, height);
|
createPiece("", -halfLength, halfLength, 0.0f, height);
|
||||||
return;
|
return pieces;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float openingStart = opening->offset - opening->width * 0.5f;
|
const float openingStart = opening->offset - opening->width * 0.5f;
|
||||||
@@ -300,6 +303,7 @@ namespace {
|
|||||||
createPiece("_Right", openingEnd, halfLength, 0.0f, height);
|
createPiece("_Right", openingEnd, halfLength, 0.0f, height);
|
||||||
createPiece("_Below", openingStart, openingEnd, 0.0f, opening->bottom);
|
createPiece("_Below", openingStart, openingEnd, 0.0f, opening->bottom);
|
||||||
createPiece("_Above", openingStart, openingEnd, opening->top, height);
|
createPiece("_Above", openingStart, openingEnd, opening->top, height);
|
||||||
|
return pieces;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameObject* CreateCeilingLight(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
|
GameObject* CreateCeilingLight(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
|
||||||
@@ -336,26 +340,40 @@ namespace {
|
|||||||
constexpr float doorOffset = -1.5f;
|
constexpr float doorOffset = -1.5f;
|
||||||
constexpr float doorWidth = 1.2f;
|
constexpr float doorWidth = 1.2f;
|
||||||
|
|
||||||
CreateBox(scene, gfxDevice, resources, "House_Floor", center + glm::vec3{0.0f, 0.03f, 0.0f},
|
GameObject* house = scene.CreateGameObject("House");
|
||||||
{width, 0.06f, depth}, floorMaterial, RigidbodyType::Static);
|
house->GetTransform().SetWorldPosition(center);
|
||||||
|
const auto addToHouse = [&](GameObject* object)
|
||||||
|
{
|
||||||
|
object->GetTransform().SetParent(&house->GetTransform(), true);
|
||||||
|
};
|
||||||
|
const auto addWallToHouse = [&](const std::vector<GameObject*>& pieces)
|
||||||
|
{
|
||||||
|
for (GameObject* piece : pieces) {
|
||||||
|
addToHouse(piece);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
CreateWall(scene, gfxDevice, resources, "House_FrontWall", center + glm::vec3{0.0f, 0.0f, depth * 0.5f},
|
addToHouse(CreateBox(scene, gfxDevice, resources, "House_Floor", center + glm::vec3{0.0f, 0.03f, 0.0f},
|
||||||
width + 0.3f, wallHeight, true, wallMaterial,
|
{width, 0.06f, depth}, floorMaterial, RigidbodyType::Static));
|
||||||
WallOpening{.offset = doorOffset, .width = doorWidth, .bottom = 0.0f, .top = 2.2f});
|
|
||||||
CreateWall(scene, gfxDevice, resources, "House_BackWall", center - glm::vec3{0.0f, 0.0f, depth * 0.5f},
|
|
||||||
width + 0.3f, wallHeight, true, wallMaterial,
|
|
||||||
WallOpening{.offset = 1.5f, .width = 1.4f, .bottom = 1.0f, .top = 2.0f});
|
|
||||||
CreateWall(scene, gfxDevice, resources, "House_LeftWall", center - glm::vec3{width * 0.5f, 0.0f, 0.0f},
|
|
||||||
depth - 0.3f, wallHeight, false, wallMaterial);
|
|
||||||
CreateWall(scene, gfxDevice, resources, "House_RightWall", center + glm::vec3{width * 0.5f, 0.0f, 0.0f},
|
|
||||||
depth - 0.3f, wallHeight, false, wallMaterial,
|
|
||||||
WallOpening{.offset = 0.0f, .width = 1.6f, .bottom = 1.0f, .top = 2.0f});
|
|
||||||
|
|
||||||
CreateBox(scene, gfxDevice, resources, "House_Roof", center + glm::vec3{0.0f, wallHeight + 0.15f, 0.0f},
|
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_FrontWall", center + glm::vec3{0.0f, 0.0f, depth * 0.5f},
|
||||||
{width + 0.8f, 0.3f, depth + 0.8f}, roofMaterial, RigidbodyType::Static);
|
width + 0.3f, wallHeight, true, wallMaterial,
|
||||||
|
WallOpening{.offset = doorOffset, .width = doorWidth, .bottom = 0.0f, .top = 2.2f}));
|
||||||
|
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_BackWall", center - glm::vec3{0.0f, 0.0f, depth * 0.5f},
|
||||||
|
width + 0.3f, wallHeight, true, wallMaterial,
|
||||||
|
WallOpening{.offset = 1.5f, .width = 1.4f, .bottom = 1.0f, .top = 2.0f}));
|
||||||
|
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_LeftWall", center - glm::vec3{width * 0.5f, 0.0f, 0.0f},
|
||||||
|
depth - 0.3f, wallHeight, false, wallMaterial));
|
||||||
|
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_RightWall", center + glm::vec3{width * 0.5f, 0.0f, 0.0f},
|
||||||
|
depth - 0.3f, wallHeight, false, wallMaterial,
|
||||||
|
WallOpening{.offset = 0.0f, .width = 1.6f, .bottom = 1.0f, .top = 2.0f}));
|
||||||
|
|
||||||
const GameObject* ceilingLight =
|
addToHouse(CreateBox(scene, gfxDevice, resources, "House_Roof", center + glm::vec3{0.0f, wallHeight + 0.15f, 0.0f},
|
||||||
|
{width + 0.8f, 0.3f, depth + 0.8f}, roofMaterial, RigidbodyType::Static));
|
||||||
|
|
||||||
|
GameObject* ceilingLight =
|
||||||
CreateCeilingLight(scene, gfxDevice, resources, center + glm::vec3{0.0f, wallHeight - 0.06f, 0.0f});
|
CreateCeilingLight(scene, gfxDevice, resources, center + glm::vec3{0.0f, wallHeight - 0.06f, 0.0f});
|
||||||
|
addToHouse(ceilingLight);
|
||||||
|
|
||||||
// Inside, next to the door
|
// Inside, next to the door
|
||||||
Material switchMaterial = Material::SimpleColor(glm::vec3{0.9f}, "LightSwitchMaterial");
|
Material switchMaterial = Material::SimpleColor(glm::vec3{0.9f}, "LightSwitchMaterial");
|
||||||
@@ -369,6 +387,7 @@ namespace {
|
|||||||
auto* switchButton = lightSwitch->AddComponent<InteractableButton>();
|
auto* switchButton = lightSwitch->AddComponent<InteractableButton>();
|
||||||
switchButton->SetCycleColors(false);
|
switchButton->SetCycleColors(false);
|
||||||
switchButton->AddTarget(*ceilingLight);
|
switchButton->AddTarget(*ceilingLight);
|
||||||
|
addToHouse(lightSwitch);
|
||||||
|
|
||||||
GameObject* sofa = ModelSpawner::SpawnStaticModel(
|
GameObject* sofa = ModelSpawner::SpawnStaticModel(
|
||||||
scene, gfxDevice, resources, "game://sofa.glb", StaticModelOptions(),
|
scene, gfxDevice, resources, "game://sofa.glb", StaticModelOptions(),
|
||||||
@@ -377,6 +396,7 @@ namespace {
|
|||||||
sofa->GetTransform().SetWorldPosition({-3.7, -1, -17.5});
|
sofa->GetTransform().SetWorldPosition({-3.7, -1, -17.5});
|
||||||
sofa->GetTransform().SetWorldRotation({180, 0, 180});
|
sofa->GetTransform().SetWorldRotation({180, 0, 180});
|
||||||
sofa->GetTransform().SetWorldScale({3, 3, 3});
|
sofa->GetTransform().SetWorldScale({3, 3, 3});
|
||||||
|
addToHouse(sofa);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
#include <destrum/Serialization/ComponentFactory.h>
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
#include <destrum/Serialization/PrefabCache.h>
|
||||||
|
#include <destrum/Serialization/PrefabSerializer.h>
|
||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
#include <destrum/Physics/SimplePhysicsWorld.h>
|
#include <destrum/Physics/SimplePhysicsWorld.h>
|
||||||
#include <destrum/Physics/JoltPhysicsWorld.h>
|
#include <destrum/Physics/JoltPhysicsWorld.h>
|
||||||
@@ -1615,6 +1617,134 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
SceneManager::GetInstance().Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameObject* buildTurret(Scene& scene, const GameObject& door)
|
||||||
|
{
|
||||||
|
GameObject* turret = scene.CreateGameObject("turret");
|
||||||
|
turret->GetTransform().SetLocalPosition(glm::vec3{5.0f, 0.0f, 0.0f});
|
||||||
|
GameObject* barrel = scene.CreateGameObject("barrel");
|
||||||
|
barrel->GetTransform().SetParent(&turret->GetTransform(), false);
|
||||||
|
barrel->GetTransform().SetLocalPosition(glm::vec3{0.0f, 1.0f, 0.0f});
|
||||||
|
barrel->AddComponent<TestActivatable>();
|
||||||
|
|
||||||
|
auto* button = turret->AddComponent<InteractableButton>();
|
||||||
|
button->SetCycleColors(false);
|
||||||
|
button->AddTarget(*barrel);
|
||||||
|
button->AddTarget(door);
|
||||||
|
return turret;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* onlyChild(GameObject& object)
|
||||||
|
{
|
||||||
|
const auto& children = object.GetTransform().GetChildren();
|
||||||
|
return children.size() == 1 ? children.front()->GetOwner() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPrefabRoundTrip()
|
||||||
|
{
|
||||||
|
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||||
|
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||||
|
|
||||||
|
Scene& source = SceneManager::GetInstance().CreateScene("prefab source");
|
||||||
|
GameObject* door = source.CreateGameObject("door");
|
||||||
|
door->AddComponent<TestActivatable>();
|
||||||
|
GameObject* turret = buildTurret(source, *door);
|
||||||
|
source.CommitPendingAdditions();
|
||||||
|
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_turret.prefab";
|
||||||
|
check(PrefabSerializer::Save(*turret, path), "a hierarchy must save as a prefab");
|
||||||
|
|
||||||
|
Scene& target = SceneManager::GetInstance().CreateScene("prefab target");
|
||||||
|
GameObject* holder = target.CreateGameObject("holder");
|
||||||
|
GameObject* first = PrefabSerializer::Instantiate(target, path.string());
|
||||||
|
GameObject* second = PrefabSerializer::Instantiate(target, path.string(), &holder->GetTransform());
|
||||||
|
target.CommitPendingAdditions();
|
||||||
|
check(first != nullptr && second != nullptr, "a saved prefab must instantiate");
|
||||||
|
check(target.GetObjects().size() == 5, "each instance must create the root and its child");
|
||||||
|
check(first != second && first->GetId() != turret->GetId(), "instances must get fresh objects");
|
||||||
|
check(first->GetName() == "turret" && first->GetTransform().GetParent() == nullptr,
|
||||||
|
"an instance without a parent must be a scene root");
|
||||||
|
check(second->GetTransform().GetParent() == &holder->GetTransform(),
|
||||||
|
"an instance must be parented to the given transform");
|
||||||
|
check(close(first->GetTransform().GetLocalPosition().x, 5.0f),
|
||||||
|
"the root transform must come from the prefab");
|
||||||
|
|
||||||
|
for (GameObject* instance : {first, second}) {
|
||||||
|
GameObject* barrel = onlyChild(*instance);
|
||||||
|
check(barrel != nullptr && barrel->GetName() == "barrel" &&
|
||||||
|
close(barrel->GetTransform().GetLocalPosition().y, 1.0f),
|
||||||
|
"prefab children must keep their local transform");
|
||||||
|
|
||||||
|
const auto* button = instance->GetComponent<InteractableButton>();
|
||||||
|
check(button != nullptr && button->GetTargets().size() == 1 &&
|
||||||
|
button->GetTargets().front().Get(target) == barrel,
|
||||||
|
"references inside the prefab must point at the instance's own objects, outside ones are dropped");
|
||||||
|
}
|
||||||
|
|
||||||
|
check(PrefabCache::GetInstance().Get(path.string()) == PrefabCache::GetInstance().Get(path.string()),
|
||||||
|
"a cached prefab must be parsed once");
|
||||||
|
const auto before = PrefabCache::GetInstance().Get(path.string());
|
||||||
|
turret->SetName("tower");
|
||||||
|
check(PrefabSerializer::Save(*turret, path), "a prefab must save over an existing file");
|
||||||
|
const auto after = PrefabCache::GetInstance().Get(path.string());
|
||||||
|
check(after != before && after->at("name") == "tower", "saving a prefab must refresh the cache");
|
||||||
|
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
check(PrefabSerializer::Instantiate(target, path.string()) == nullptr,
|
||||||
|
"instantiating a missing prefab must fail");
|
||||||
|
|
||||||
|
PrefabCache::GetInstance().Clear();
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPrefabDuplicate()
|
||||||
|
{
|
||||||
|
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||||
|
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||||
|
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("duplicate");
|
||||||
|
GameObject* door = scene.CreateGameObject("door");
|
||||||
|
door->AddComponent<TestActivatable>();
|
||||||
|
GameObject* holder = scene.CreateGameObject("holder");
|
||||||
|
GameObject* turret = buildTurret(scene, *door);
|
||||||
|
turret->GetTransform().SetParent(&holder->GetTransform(), false);
|
||||||
|
|
||||||
|
GameObject* copy = PrefabSerializer::Duplicate(*turret);
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
check(copy != nullptr && copy != turret && copy->GetName() == "turret",
|
||||||
|
"duplicate must create a new object");
|
||||||
|
check(copy->GetTransform().GetParent() == &holder->GetTransform(),
|
||||||
|
"a duplicate must keep the original's parent");
|
||||||
|
|
||||||
|
GameObject* barrel = onlyChild(*copy);
|
||||||
|
check(barrel != nullptr && barrel != onlyChild(*turret), "a duplicate must copy children");
|
||||||
|
|
||||||
|
const auto* button = copy->GetComponent<InteractableButton>();
|
||||||
|
check(button != nullptr && button->GetTargets().size() == 2 &&
|
||||||
|
button->GetTargets()[0].Get(scene) == barrel &&
|
||||||
|
button->GetTargets()[1].Get(scene) == door,
|
||||||
|
"a duplicate must remap inner references and keep outer ones");
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPrefabValidation()
|
||||||
|
{
|
||||||
|
using nlohmann::json;
|
||||||
|
check(PrefabSerializer::Validate(json::object()).has_value(), "an empty prefab must be rejected");
|
||||||
|
|
||||||
|
const json twoRoots = json::parse(R"({"version":1,"objects":[{"id":1},{"id":2}]})");
|
||||||
|
check(PrefabSerializer::Validate(twoRoots).has_value(), "a prefab with two roots must be rejected");
|
||||||
|
|
||||||
|
const json childFirst = json::parse(R"({"version":1,"objects":[{"id":2,"parent":1},{"id":1}]})");
|
||||||
|
check(PrefabSerializer::Validate(childFirst).has_value(), "the prefab root must come first");
|
||||||
|
|
||||||
|
const json valid = json::parse(R"({"version":1,"objects":[{"id":1},{"id":2,"parent":1}]})");
|
||||||
|
check(!PrefabSerializer::Validate(valid).has_value(), "a single-root prefab must validate");
|
||||||
|
|
||||||
|
const json future = json::parse(R"({"version":99,"objects":[{"id":1}]})");
|
||||||
|
check(PrefabSerializer::Validate(future).has_value(), "an unknown prefab version must be rejected");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -1663,6 +1793,9 @@ int main()
|
|||||||
testSceneFileIdsAreLocal();
|
testSceneFileIdsAreLocal();
|
||||||
testButtonTargetsRoundTrip();
|
testButtonTargetsRoundTrip();
|
||||||
testButtonDropsMissingTargets();
|
testButtonDropsMissingTargets();
|
||||||
|
testPrefabRoundTrip();
|
||||||
|
testPrefabDuplicate();
|
||||||
|
testPrefabValidation();
|
||||||
std::cout << "destrum tests passed\n";
|
std::cout << "destrum tests passed\n";
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user