Validate gameplay metadata before publishing build generations
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <faset/authoring/schema.hpp>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
|
||||
namespace faset::authoring {
|
||||
@@ -52,13 +53,25 @@ void SchemaRegistry::register_schema(const Json& value) {
|
||||
Json normalized = value;
|
||||
const auto id = value.at("id").get<std::string>();
|
||||
require(!id.empty(), "schema.invalid", "TypeId cannot be empty");
|
||||
require(value.value("version", 1) > 0, "schema.invalid", "Schema version must be positive");
|
||||
if (value.contains("version")) {
|
||||
const auto& version = value.at("version");
|
||||
require(version.is_number_integer() && version > 0 &&
|
||||
version <= std::numeric_limits<int>::max(),
|
||||
"schema.invalid", "Schema version must be a positive supported integer");
|
||||
}
|
||||
for (auto& [key, field] : normalized["fields"].items()) {
|
||||
require(field.is_object() && field.contains("default"), "schema.invalid",
|
||||
"Each field requires a typed default");
|
||||
require(field.value("id", key) == key, "schema.field_id",
|
||||
"Field map keys must be stable FieldIds");
|
||||
field["id"] = key;
|
||||
for (const auto* limit : {"min", "max"})
|
||||
if (field.contains(limit))
|
||||
require(field.at(limit).is_number() && std::isfinite(field.at(limit).get<double>()),
|
||||
"schema.invalid", "Field limits must be finite numbers");
|
||||
if (field.contains("enum"))
|
||||
require(field.at("enum").is_array(), "schema.invalid",
|
||||
"Field enum choices must be an array");
|
||||
validate_field(field["default"], field);
|
||||
}
|
||||
if (auto found = schemas_.find(id); found != schemas_.end())
|
||||
@@ -73,6 +86,22 @@ void SchemaRegistry::register_schemas(const Json& values) {
|
||||
candidate.register_schema(schema);
|
||||
*this = std::move(candidate);
|
||||
}
|
||||
SchemaRegistry gameplay_schemas(const Json& manifest) {
|
||||
require(manifest.is_array() || (manifest.is_object() && manifest.contains("types") &&
|
||||
manifest.at("types").is_array()),
|
||||
"schema.invalid", "Gameplay schemas require an array or a types manifest");
|
||||
const auto& types = manifest.is_array() ? manifest : manifest.at("types");
|
||||
auto result = builtin_schemas();
|
||||
for (const auto& schema : types) {
|
||||
require(schema.is_object() && schema.contains("id") && schema.at("id").is_string(),
|
||||
"schema.invalid", "Gameplay schema requires a TypeId");
|
||||
const auto id = schema.at("id").get<std::string>();
|
||||
require(!result.contains(id), "schema.duplicate_type",
|
||||
"Gameplay schema duplicates a builtin or gameplay TypeId: " + id);
|
||||
result.register_schema(schema);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool SchemaRegistry::contains(const std::string& type) const {
|
||||
return schemas_.contains(type);
|
||||
}
|
||||
|
||||
@@ -311,9 +311,7 @@ void AuthoringService::register_schemas(const Json& manifest) {
|
||||
}
|
||||
void AuthoringService::replace_external_schemas(const Json& manifest) {
|
||||
std::lock_guard lock(mutex_);
|
||||
auto candidate = builtin_schemas();
|
||||
candidate.register_schemas(manifest);
|
||||
schemas_ = std::move(candidate);
|
||||
schemas_ = gameplay_schemas(manifest);
|
||||
}
|
||||
void AuthoringService::apply(Json& scene, const Json& command) {
|
||||
require(command.is_object() && command.contains("op") && command["op"].is_string(),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <faset/assets/asset_data.hpp>
|
||||
#include <faset/authoring/schema.hpp>
|
||||
#include <faset/core/hash.hpp>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/core/process.hpp>
|
||||
@@ -33,23 +34,14 @@ void validate_scene(const Json& scene) {
|
||||
throw std::runtime_error("Scene dimension must be 2 or 3");
|
||||
}
|
||||
void validate_component_types(const Json& scene, const Json& schema) {
|
||||
std::map<std::string, int> versions;
|
||||
for (const auto* type : {"faset.transform", "faset.sprite", "faset.mesh", "faset.camera",
|
||||
"faset.light", "faset.rigid_body_2d", "faset.rigid_body_3d"})
|
||||
versions[type] = 1;
|
||||
for (const auto& type : schema.value("types", Json::array())) {
|
||||
auto id = type.at("id").get<std::string>();
|
||||
if (versions.contains(id))
|
||||
throw std::runtime_error("Gameplay schema duplicates a component type: " + id);
|
||||
versions[id] = type.value("version", 1);
|
||||
}
|
||||
const auto registry =
|
||||
schema.is_null() ? authoring::builtin_schemas() : authoring::gameplay_schemas(schema);
|
||||
for (const auto& entity : scene.at("entities"))
|
||||
for (const auto& component : entity.value("components", Json::array())) {
|
||||
const auto id = component.at("type").get<std::string>();
|
||||
auto found = versions.find(id);
|
||||
if (found == versions.end())
|
||||
if (!registry.contains(id))
|
||||
throw std::runtime_error("Cannot cook unresolved component type: " + id);
|
||||
if (component.value("version", 1) != found->second)
|
||||
if (component.value("version", 1) != registry.schema(id).value("version", 1))
|
||||
throw std::runtime_error("Migrate component '" + id +
|
||||
"' to the current gameplay schema before cooking");
|
||||
}
|
||||
@@ -298,6 +290,9 @@ struct BuildService::Impl {
|
||||
if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 ||
|
||||
!schema.contains("types") || !schema.at("types").is_array())
|
||||
throw std::runtime_error("SchemaExporter returned an invalid manifest");
|
||||
// Validate the complete metadata before publishing either the schema or
|
||||
// its Player generation. Session uses this same authoring contract.
|
||||
(void)authoring::gameplay_schemas(schema);
|
||||
std::string fingerprint = sha256_file(player) + sha256_file(exporter) +
|
||||
read_text(native_directory / "CMakeCache.txt");
|
||||
for (const auto& file : {"Gameplay.cpp", "Gameplay.hpp"})
|
||||
@@ -393,7 +388,7 @@ struct BuildService::Impl {
|
||||
checkpoint(job, "Validating scene and assets", .1);
|
||||
validate_scene(job.scene);
|
||||
validate_assets(job.scene);
|
||||
Json schema = Json::object();
|
||||
Json schema;
|
||||
if (fs::exists(config.cache_root / "last_build.json")) {
|
||||
auto pointer = read_json(config.cache_root / "last_build.json");
|
||||
auto schema_path = project_path(
|
||||
@@ -403,7 +398,9 @@ struct BuildService::Impl {
|
||||
}
|
||||
validate_component_types(job.scene, schema);
|
||||
auto source_hash = sha256(job.scene.dump());
|
||||
auto schema_fingerprint = schema.value("build_fingerprint", std::string("builtin-v1"));
|
||||
auto schema_fingerprint =
|
||||
schema.is_null() ? std::string("builtin-v1")
|
||||
: schema.value("build_fingerprint", std::string("builtin-v1"));
|
||||
auto digest = sha256(source_hash + schema_fingerprint);
|
||||
auto directory = config.cache_root / "cooked" / digest;
|
||||
auto staging = config.cache_root / "cooked" / (".staging-" + job.status.id);
|
||||
|
||||
Reference in New Issue
Block a user