Checkpoint 4: harden native paths, authoring workflows and Player lifecycle
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#include <chrono>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/editor/editor_ui.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
using namespace faset;
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
void check(bool value, const char* message) {
|
||||
if (!value)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
Json geometry(const render::DrawItem& draw) {
|
||||
Json positions = Json::array();
|
||||
for (const auto& vertex : draw.mesh->vertices)
|
||||
positions.push_back(vertex.position);
|
||||
return positions;
|
||||
}
|
||||
int run(int argc, char** argv) {
|
||||
try {
|
||||
check(argc == 3, "Usage: faset_blender_editor_probe FIXTURE_DIRECTORY NEW_PROJECT");
|
||||
const auto fixture = path_from_utf8(argv[1]), project = path_from_utf8(argv[2]);
|
||||
check(!fs::exists(project), "Use a new project directory for the Blender UI probe");
|
||||
const auto source = project / "Assets/door";
|
||||
fs::create_directories(source);
|
||||
editor::Session session({project, path_from_utf8(FASET_TEST_ENGINE), project});
|
||||
render::Renderer renderer({.width = 1280,
|
||||
.height = 900,
|
||||
.title = "Blender reimport",
|
||||
.headless = true,
|
||||
.validation = true});
|
||||
editor::EditorUI ui(session, renderer,
|
||||
path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf",
|
||||
path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json");
|
||||
auto import = [&](const char* stage) {
|
||||
fs::copy(fixture / stage, source,
|
||||
fs::copy_options::recursive | fs::copy_options::overwrite_existing);
|
||||
const auto job = session.commands()
|
||||
.call("faset_import", {{"path", "Assets/door/manifest.json"}})
|
||||
.at("job");
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
ui.frame({});
|
||||
auto result = session.commands().call("faset_job", {{"id", job}});
|
||||
if (result.at("state") != "queued" && result.at("state") != "running")
|
||||
return result;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("Blender import timed out");
|
||||
};
|
||||
const auto initial = import("initial");
|
||||
check(initial.at("state") == "succeeded", "Initial Blender import failed");
|
||||
const auto id = initial.at("result").at("asset_id");
|
||||
const auto document = ui.current_document();
|
||||
auto current = session.authoring().query(document);
|
||||
Json operations = Json::array();
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
auto entity = authoring::make_entity(session.authoring().schemas(), "Placed door");
|
||||
entity["components"][0]["fields"]["position"] = {index ? 3 : -3, 0, 0};
|
||||
entity["components"].push_back(
|
||||
{{"id", new_id()},
|
||||
{"type", "faset.mesh"},
|
||||
{"version", 1},
|
||||
{"fields",
|
||||
{{"asset", id},
|
||||
{"primitive", "asset"},
|
||||
{"color", index ? Json{0.5, 0.8, 0.3, 1} : Json{0.3, 0.5, 0.9, 1}}}}});
|
||||
// Missing gameplay packages stay opaque; reimport must not remove them.
|
||||
entity["components"].push_back({{"id", new_id()},
|
||||
{"type", "project.door"},
|
||||
{"version", 2},
|
||||
{"fields", {{"locked", index == 0}, {"speed", 2.5}}}});
|
||||
auto body = session.authoring().schemas().default_fields("faset.rigid_body_3d");
|
||||
body["body_type"] = "static";
|
||||
body["friction"] = index ? 0.75 : 0.25;
|
||||
entity["components"].push_back({{"id", new_id()},
|
||||
{"type", "faset.rigid_body_3d"},
|
||||
{"version", 1},
|
||||
{"fields", body}});
|
||||
operations.push_back({{"op", "entity.create"}, {"entity", entity}});
|
||||
}
|
||||
session.authoring().transact(document, current.at("revision"), operations);
|
||||
current = session.authoring().query(document);
|
||||
ui.frame({});
|
||||
check(ui.snapshot().draws.size() >= 2, "Both placed Blender meshes must render");
|
||||
const auto first = ui.snapshot().draws[0], second = ui.snapshot().draws[1];
|
||||
check(first.mesh == second.mesh && first.model != second.model,
|
||||
"Two independent placements must share the imported mesh");
|
||||
renderer.render(ui.snapshot());
|
||||
renderer.capture(project / "initial.ppm");
|
||||
const auto renamed = import("renamed");
|
||||
check(renamed.at("state") == "succeeded", "Renamed Blender import failed");
|
||||
// Exercise the real editor's generation watcher, without manually clearing SceneView.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
|
||||
ui.frame({});
|
||||
const auto first_after = ui.snapshot().draws[0], second_after = ui.snapshot().draws[1];
|
||||
check(geometry(first) != geometry(first_after) &&
|
||||
geometry(second) != geometry(second_after),
|
||||
"Both live Editor instances must receive changed Blender geometry");
|
||||
check(first_after.mesh == second_after.mesh, "New instances share one cooked mesh");
|
||||
check(first.model == first_after.model && second.model == second_after.model &&
|
||||
first.color == first_after.color && second.color == second_after.color,
|
||||
"Reimport must preserve each instance's placement and tint");
|
||||
check(session.authoring().query(document) == current,
|
||||
"Reimport must preserve authoring IDs, gameplay fields and revision");
|
||||
renderer.render(ui.snapshot());
|
||||
renderer.capture(project / "renamed.ppm");
|
||||
const auto removed = import("removed");
|
||||
check(removed.at("state") == "conflict", "Removed Blender output must conflict");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
|
||||
ui.frame({});
|
||||
check(geometry(ui.snapshot().draws[0]) == geometry(first_after) &&
|
||||
geometry(ui.snapshot().draws[1]) == geometry(second_after),
|
||||
"Conflict must keep both last-good visible meshes");
|
||||
check(session.authoring().query(document) == current,
|
||||
"Import conflict must preserve authoring");
|
||||
check(renderer.stats().validation_errors == 0, "Vulkan validation failed");
|
||||
atomic_write_json(project / "probe.json",
|
||||
{{"status", "passed"},
|
||||
{"instances", 2},
|
||||
{"live_geometry_updated", true},
|
||||
{"placement_and_tint_preserved", true},
|
||||
{"opaque_gameplay_preserved", true},
|
||||
{"physics_fields_preserved", true},
|
||||
{"conflict_keeps_visible_generation", true},
|
||||
{"validation_enabled", renderer.stats().validation_enabled},
|
||||
{"validation_errors", renderer.stats().validation_errors}});
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t** argv) {
|
||||
return run_utf8_main(argc, argv, run);
|
||||
}
|
||||
#else
|
||||
int main(int argc, char** argv) {
|
||||
return run(argc, argv);
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user