Checkpoint 3: deliver playable samples and complete native authoring workflows
This commit is contained in:
@@ -306,7 +306,9 @@ struct BuildService::Impl {
|
||||
atomic_write_json(schema_file, schema);
|
||||
copy_required_file(player, staging / ("faset_player" + executable_suffix()));
|
||||
copy_required_file(exporter, staging / ("faset_schema_exporter" + executable_suffix()));
|
||||
for (const auto* file : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv"})
|
||||
for (const auto* file : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
||||
"shadowMain.reflection.json"})
|
||||
copy_required_file(native_directory / "shaders" / file, staging / "shaders" / file);
|
||||
copy_runtime_libraries(job, player, staging, native_directory, configuration);
|
||||
Json manifest{{"format", "faset.build"},
|
||||
@@ -520,7 +522,9 @@ struct BuildService::Impl {
|
||||
auto build_directory = fs::path(built.at("directory").get<std::string>());
|
||||
copy_required_file(build_directory / ("faset_player" + executable_suffix()),
|
||||
staging / ("faset_player" + executable_suffix()));
|
||||
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv"})
|
||||
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
||||
"shadowMain.reflection.json"})
|
||||
copy_required_file(build_directory / "shaders" / shader,
|
||||
staging / "shaders" / shader);
|
||||
for (const auto& entry : fs::directory_iterator(build_directory)) {
|
||||
|
||||
+36
-3
@@ -67,8 +67,13 @@ Json Commands::call(const std::string& name, const Json& arguments) {
|
||||
}
|
||||
Json Commands::resolved_scene(const std::string& id) const {
|
||||
const auto result = authoring::resolve_templates(
|
||||
authoring_.query(id).at("scene"), authoring_.schemas(),
|
||||
[&](const std::string& path) { return read_json(project_path(authoring_.root(), path)); });
|
||||
authoring_.query(id).at("scene"), authoring_.schemas(), [&](const std::string& path) {
|
||||
const auto relative = std::filesystem::path(path).lexically_normal();
|
||||
for (const auto& document : authoring_.documents())
|
||||
if (document.at("path") == relative.generic_string())
|
||||
return authoring_.query(document.at("id")).at("scene");
|
||||
return read_json(project_path(authoring_.root(), relative));
|
||||
});
|
||||
return {{"scene", result.scene}, {"conflicts", result.conflicts}};
|
||||
}
|
||||
Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authoring) {
|
||||
@@ -109,7 +114,10 @@ Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authorin
|
||||
add("faset_scene_edit",
|
||||
"Apply one atomic authoring batch with optimistic revision checking and one Undo step. "
|
||||
"Operations: entity.create/rename/delete/duplicate/reparent; component.add/remove/set; "
|
||||
"scene.rename; template.instance/override/revert/suppress/add/reparent. Use persistent IDs "
|
||||
"scene.rename/simulation; "
|
||||
"template.instance/override/revert/suppress/restore/add/addition_set/reparent/remove/"
|
||||
"source_set. Use "
|
||||
"persistent IDs "
|
||||
"from document_query and schema. An idempotency_key retries the same payload in this "
|
||||
"session.",
|
||||
object_schema({{"document", text},
|
||||
@@ -138,6 +146,31 @@ Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authorin
|
||||
"is not a live game query.",
|
||||
object_schema({{"document", text}}, {"document"}),
|
||||
[&](const Json& args) { return resolved_scene(args.at("document")); }, true);
|
||||
add(
|
||||
"faset_simulation_get",
|
||||
"Read scene simulation settings with defaults: fixed_delta seconds, max_catch_up_ticks, "
|
||||
"physics_substeps and gravity.",
|
||||
object_schema({{"document", text}}, {"document"}),
|
||||
[&](const Json& args) {
|
||||
const auto document = authoring_.query(args.at("document"));
|
||||
auto settings = authoring::default_simulation_settings();
|
||||
settings.update(document.at("scene").value("simulation", Json::object()));
|
||||
return Json{{"document", document.at("id")},
|
||||
{"revision", document.at("revision")},
|
||||
{"settings", settings}};
|
||||
},
|
||||
true);
|
||||
add("faset_simulation_set",
|
||||
"Update scene simulation settings as one Undo transaction. Values apply when the Player "
|
||||
"next starts.",
|
||||
object_schema(
|
||||
{{"document", text}, {"revision", integer}, {"settings", {{"type", "object"}}}},
|
||||
{"document", "revision", "settings"}),
|
||||
[&](const Json& args) {
|
||||
return authoring_.transact(
|
||||
args.at("document"), args.at("revision"),
|
||||
Json::array({{{"op", "scene.simulation"}, {"value", args.at("settings")}}}));
|
||||
});
|
||||
add("faset_recovery_restore",
|
||||
"Restore a recovery journal, including an unsaved new scene. If the document is already "
|
||||
"open, pass its current revision. External file changes are never overwritten.",
|
||||
|
||||
+1029
-105
File diff suppressed because it is too large
Load Diff
+49
-4
@@ -1,11 +1,13 @@
|
||||
#include <algorithm>
|
||||
#include <faset/editor/mcp.hpp>
|
||||
#include <iostream>
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -126,7 +128,6 @@ std::vector<std::string> StdioTransport::poll() {
|
||||
if (type == FILE_TYPE_PIPE) {
|
||||
if (!PeekNamedPipe(input, nullptr, 0, nullptr, &available, nullptr)) {
|
||||
closed_ = true;
|
||||
return lines;
|
||||
}
|
||||
} else if (type == FILE_TYPE_DISK)
|
||||
available = sizeof(bytes);
|
||||
@@ -169,7 +170,51 @@ std::vector<std::string> StdioTransport::poll() {
|
||||
return lines;
|
||||
}
|
||||
void StdioTransport::send(const Json& value) {
|
||||
std::cout << value.dump() << '\n';
|
||||
std::cout.flush();
|
||||
const auto bytes = value.dump() + '\n';
|
||||
std::size_t offset = 0;
|
||||
#ifdef _WIN32
|
||||
const auto output = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
while (offset < bytes.size()) {
|
||||
DWORD written = 0;
|
||||
const auto count = static_cast<DWORD>(std::min<std::size_t>(bytes.size() - offset, 65536));
|
||||
if (!WriteFile(output, bytes.data() + offset, count, &written, nullptr) || written == 0) {
|
||||
closed_ = true;
|
||||
return;
|
||||
}
|
||||
offset += written;
|
||||
}
|
||||
#else
|
||||
// A disconnected MCP client must not terminate the GUI with SIGPIPE. Block
|
||||
// it only on this thread during the write, preserving the process signal
|
||||
// policy and any SIGPIPE that was already pending for the caller.
|
||||
sigset_t blocked, previous, pending;
|
||||
sigemptyset(&blocked);
|
||||
sigaddset(&blocked, SIGPIPE);
|
||||
if (pthread_sigmask(SIG_BLOCK, &blocked, &previous) != 0) {
|
||||
closed_ = true;
|
||||
return;
|
||||
}
|
||||
sigpending(&pending);
|
||||
const bool alreadyPending = sigismember(&pending, SIGPIPE) == 1;
|
||||
bool brokenPipe = false;
|
||||
while (offset < bytes.size()) {
|
||||
const auto written = ::write(STDOUT_FILENO, bytes.data() + offset, bytes.size() - offset);
|
||||
if (written > 0)
|
||||
offset += static_cast<std::size_t>(written);
|
||||
else if (written < 0 && errno == EINTR)
|
||||
continue;
|
||||
else {
|
||||
brokenPipe = written < 0 && errno == EPIPE;
|
||||
closed_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (brokenPipe && !alreadyPending) {
|
||||
const timespec noWait{};
|
||||
while (sigtimedwait(&blocked, nullptr, &noWait) == -1 && errno == EINTR) {
|
||||
}
|
||||
}
|
||||
pthread_sigmask(SIG_SETMASK, &previous, nullptr);
|
||||
#endif
|
||||
}
|
||||
} // namespace faset::editor
|
||||
|
||||
+23
-7
@@ -14,11 +14,25 @@
|
||||
|
||||
namespace faset::editor {
|
||||
namespace {
|
||||
void append(void* context, const char* bytes, std::uint64_t size) {
|
||||
auto& output = *static_cast<std::string*>(context);
|
||||
require(size <= 8 * 1024 * 1024 && output.size() + size <= 8 * 1024 * 1024,
|
||||
"plugin.output_limit", "Plugin response exceeds 8 MiB");
|
||||
output.append(bytes, static_cast<std::size_t>(size));
|
||||
struct ResponseBuffer {
|
||||
std::string bytes;
|
||||
bool failed = false;
|
||||
};
|
||||
void append(void* context, const char* bytes, std::uint64_t size) noexcept {
|
||||
auto& output = *static_cast<ResponseBuffer*>(context);
|
||||
if (output.failed)
|
||||
return;
|
||||
if ((!bytes && size) || size > 8 * 1024 * 1024 ||
|
||||
output.bytes.size() + size > 8 * 1024 * 1024) {
|
||||
output.failed = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (size)
|
||||
output.bytes.append(bytes, static_cast<std::size_t>(size));
|
||||
} catch (...) {
|
||||
output.failed = true;
|
||||
}
|
||||
}
|
||||
Json response(const std::string& bytes) {
|
||||
auto value = Json::parse(bytes);
|
||||
@@ -237,11 +251,13 @@ struct PluginManager::Impl {
|
||||
commands.add(
|
||||
name, descriptor.at("description"), descriptor.at("inputSchema"),
|
||||
[registration](const Json& arguments) {
|
||||
std::string output;
|
||||
ResponseBuffer output;
|
||||
const auto input = arguments.dump();
|
||||
const int result =
|
||||
registration.callback(registration.user, input.c_str(), append, &output);
|
||||
const auto value = response(output);
|
||||
require(!output.failed, "plugin.output_limit",
|
||||
"Cannot collect plugin response (maximum 8 MiB)");
|
||||
const auto value = response(output.bytes);
|
||||
if (result != 0)
|
||||
throw Error(value.value("code", std::string("plugin.failed")),
|
||||
value.value("message", std::string("Plugin command failed")),
|
||||
|
||||
+171
-52
@@ -58,6 +58,10 @@ Session::Session(SessionConfig config)
|
||||
if (std::filesystem::exists(schema))
|
||||
try {
|
||||
load_schema(schema);
|
||||
const auto state = config_.project_root / ".faset/schema-state.json";
|
||||
if (std::filesystem::exists(state))
|
||||
schema_source_signature_ =
|
||||
read_json(state).value("source_signature", std::string());
|
||||
} catch (const std::exception& error) {
|
||||
log(std::string("Schema load failed: ") + error.what());
|
||||
}
|
||||
@@ -77,8 +81,26 @@ void Session::log(std::string value) {
|
||||
}
|
||||
Json Session::project() const {
|
||||
const auto path = config_.project_root / "project.faset.json";
|
||||
if (std::filesystem::exists(path))
|
||||
return read_json(path);
|
||||
if (std::filesystem::exists(path)) {
|
||||
const auto value = read_json(path);
|
||||
require(value.is_object() && value.value("format", "") == "faset.project" &&
|
||||
value.value("version", 0) == 1,
|
||||
"project.version", "Unsupported project format or version");
|
||||
require(value.contains("name") && value.at("name").is_string() &&
|
||||
!value.at("name").get<std::string>().empty(),
|
||||
"project.name", "Project name must be a nonempty string");
|
||||
const auto dimension = value.value("dimension", 3);
|
||||
require(dimension == 2 || dimension == 3, "project.dimension",
|
||||
"Project dimension must be 2 or 3");
|
||||
if (value.contains("start_scene")) {
|
||||
require(value.at("start_scene").is_string(), "project.start_scene",
|
||||
"Project start_scene must be a relative path");
|
||||
const auto scene = value.at("start_scene").get<std::string>();
|
||||
if (!scene.empty())
|
||||
project_path(config_.project_root, scene);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return {{"format", "faset.project"},
|
||||
{"version", 1},
|
||||
{"name", config_.project_root.filename().string()},
|
||||
@@ -105,6 +127,26 @@ Json Session::assets_list() const {
|
||||
}
|
||||
return {{"assets", list}};
|
||||
}
|
||||
std::string Session::source_signature() const {
|
||||
const auto directory = config_.project_root / "Scripts";
|
||||
std::vector<std::filesystem::path> files;
|
||||
if (std::filesystem::exists(directory))
|
||||
for (const auto& file : std::filesystem::recursive_directory_iterator(directory))
|
||||
if (file.is_regular_file())
|
||||
files.push_back(file.path());
|
||||
std::sort(files.begin(), files.end());
|
||||
std::string contents;
|
||||
for (const auto& file : files)
|
||||
contents +=
|
||||
file.lexically_relative(directory).generic_string() + ":" + sha256_file(file) + "\n";
|
||||
return sha256(contents);
|
||||
}
|
||||
Json Session::schema_status() const {
|
||||
return {{"loaded", schema_loaded_},
|
||||
{"stale", !schema_loaded_ || schema_source_signature_ != source_signature() ||
|
||||
!schema_error_.empty()},
|
||||
{"error", schema_error_}};
|
||||
}
|
||||
Json Session::jobs() const {
|
||||
Json list = Json::array();
|
||||
for (const auto& item : builds_.jobs())
|
||||
@@ -124,6 +166,8 @@ void Session::load_schema(const std::filesystem::path& path) {
|
||||
const auto output = config_.project_root / ".faset/schema.json";
|
||||
if (std::filesystem::weakly_canonical(path) != std::filesystem::weakly_canonical(output))
|
||||
atomic_write_json(output, value);
|
||||
schema_loaded_ = true;
|
||||
schema_error_.clear();
|
||||
log("Gameplay schema loaded");
|
||||
}
|
||||
void Session::launch_player(Json scene, const std::filesystem::path& executable) {
|
||||
@@ -170,20 +214,35 @@ void Session::poll() {
|
||||
if (observed_jobs_[value.id] == value.state)
|
||||
continue;
|
||||
observed_jobs_[value.id] = value.state;
|
||||
if (value.state == "failed")
|
||||
log(value.kind + " failed: " + value.error);
|
||||
bool schema_valid = true;
|
||||
if (value.state == "failed") {
|
||||
schema_error_ = value.error;
|
||||
log(value.kind + " failed: " + value.error +
|
||||
"; previous gameplay metadata remains available and is marked stale");
|
||||
}
|
||||
if (value.state == "succeeded") {
|
||||
log(value.kind + " completed");
|
||||
if (value.result.contains("schema"))
|
||||
try {
|
||||
load_schema(value.result.at("schema").get<std::string>());
|
||||
// This signature represents the sources submitted with this job, not later
|
||||
// edits.
|
||||
if (value.result.contains("source_signature"))
|
||||
schema_source_signature_ =
|
||||
value.result.at("source_signature").get<std::string>();
|
||||
else if (submitted_sources_.contains(value.id))
|
||||
schema_source_signature_ = submitted_sources_.at(value.id);
|
||||
atomic_write_json(config_.project_root / ".faset/schema-state.json",
|
||||
{{"source_signature", schema_source_signature_}});
|
||||
} catch (const std::exception& error) {
|
||||
schema_valid = false;
|
||||
schema_error_ = error.what();
|
||||
log(std::string("Schema update failed: ") + error.what());
|
||||
}
|
||||
}
|
||||
if (value.id == pending_play_job_ && value.finished()) {
|
||||
pending_play_job_.clear();
|
||||
if (value.state == "succeeded")
|
||||
if (value.state == "succeeded" && schema_valid)
|
||||
try {
|
||||
const auto executable =
|
||||
value.result.value("player", (builds_.config().build_directory /
|
||||
@@ -242,67 +301,125 @@ void Session::register_commands() {
|
||||
commands_.add(
|
||||
"faset_project", "Read the authoring project's settings.", schema(Json::object()),
|
||||
[&](const Json&) { return project(); }, true);
|
||||
commands_.add(
|
||||
"faset_project_settings_get", "Read project settings and their content revision.",
|
||||
schema(Json::object()),
|
||||
[&](const Json&) {
|
||||
const auto value = project();
|
||||
return Json{{"settings", value}, {"revision", sha256(value.dump())}};
|
||||
},
|
||||
true);
|
||||
commands_.add(
|
||||
"faset_project_settings_set",
|
||||
"Save project name, initial scene dimension or start scene with an expected content "
|
||||
"revision. Applies on the next project open; does not change the active scene or its Undo "
|
||||
"history.",
|
||||
schema({{"revision", text}, {"settings", {{"type", "object"}}}}, {"revision", "settings"}),
|
||||
[&](const Json& args) {
|
||||
auto value = project();
|
||||
require(args.at("revision") == sha256(value.dump()), "revision.conflict",
|
||||
"Project settings changed; reload them before saving");
|
||||
const auto& changes = args.at("settings");
|
||||
for (const auto& [key, field] : changes.items()) {
|
||||
require(key == "name" || key == "dimension" || key == "start_scene",
|
||||
"project.setting", "Unknown editable project setting: " + key);
|
||||
if (key == "name")
|
||||
require(field.is_string() && !field.get<std::string>().empty(), "project.name",
|
||||
"Project name must be a nonempty string");
|
||||
else if (key == "dimension")
|
||||
require(field.is_number_integer() && (field == 2 || field == 3),
|
||||
"project.dimension", "Initial scene dimension must be 2 or 3");
|
||||
else {
|
||||
require(field.is_string() && !field.get<std::string>().empty(),
|
||||
"project.start_scene", "Choose a saved scene inside the project");
|
||||
const auto file = project_path(config_.project_root, field.get<std::string>());
|
||||
require(std::filesystem::is_regular_file(file), "project.start_scene",
|
||||
"Save the start scene before selecting it in Project settings");
|
||||
const auto scene = read_json(file);
|
||||
require(scene.value("format", "") == "faset.scene" &&
|
||||
scene.value("version", 0) == 1,
|
||||
"project.start_scene",
|
||||
"The start scene must be a supported Faset scene");
|
||||
}
|
||||
value[key] = field;
|
||||
}
|
||||
if (!value.contains("id"))
|
||||
value["id"] = new_id();
|
||||
atomic_write_json(config_.project_root / "project.faset.json", value);
|
||||
log("Project settings saved; changes apply on next project open");
|
||||
return Json{{"settings", value}, {"revision", sha256(value.dump())}};
|
||||
});
|
||||
commands_.add(
|
||||
"faset_assets", "List imported asset manifests and resource identities.",
|
||||
schema(Json::object()), [&](const Json&) { return assets_list(); }, true);
|
||||
commands_.add("faset_import",
|
||||
"Import GLB/glTF or a Blender export manifest relative to this project. Returns "
|
||||
"a cancellable job ID; failure retains the last successful generation.",
|
||||
schema({{"path", text},
|
||||
{"settings", {{"type", "object"}}},
|
||||
{"allow_removed_outputs", boolean}},
|
||||
{"path"}),
|
||||
[&](const Json& args) {
|
||||
assets::ImportRequest request;
|
||||
request.source =
|
||||
project_path(config_.project_root, args.at("path").get<std::string>());
|
||||
request.settings = args.value("settings", Json(nullptr));
|
||||
request.allow_removed_outputs = args.value("allow_removed_outputs", false);
|
||||
auto task = std::make_shared<ImportTask>();
|
||||
task->id = "import-" + new_id();
|
||||
imports_[task->id] = task;
|
||||
workers_.emplace_back([this, task, request] {
|
||||
{
|
||||
std::lock_guard lock(task->mutex);
|
||||
task->state = "running";
|
||||
}
|
||||
try {
|
||||
const auto result = assets_.import_asset(request, *task->job);
|
||||
std::lock_guard lock(task->mutex);
|
||||
task->state =
|
||||
result.status == assets::ImportStatus::succeeded ? "succeeded"
|
||||
commands_.add(
|
||||
"faset_import",
|
||||
"Import PNG/JPEG, GLB/glTF, or a Blender export manifest relative to this project. Returns "
|
||||
"a cancellable job ID; failure retains the last successful generation.",
|
||||
schema({{"path", text},
|
||||
{"settings", {{"type", "object"}}},
|
||||
{"allow_removed_outputs", boolean}},
|
||||
{"path"}),
|
||||
[&](const Json& args) {
|
||||
assets::ImportRequest request;
|
||||
request.source = project_path(config_.project_root, args.at("path").get<std::string>());
|
||||
request.settings = args.value("settings", Json(nullptr));
|
||||
request.allow_removed_outputs = args.value("allow_removed_outputs", false);
|
||||
auto task = std::make_shared<ImportTask>();
|
||||
task->id = "import-" + new_id();
|
||||
imports_[task->id] = task;
|
||||
workers_.emplace_back([this, task, request] {
|
||||
{
|
||||
std::lock_guard lock(task->mutex);
|
||||
task->state = "running";
|
||||
}
|
||||
try {
|
||||
const auto result = assets_.import_asset(request, *task->job);
|
||||
std::lock_guard lock(task->mutex);
|
||||
task->state = result.status == assets::ImportStatus::succeeded ? "succeeded"
|
||||
: result.status == assets::ImportStatus::cancelled ? "cancelled"
|
||||
: result.status == assets::ImportStatus::conflict ? "conflict"
|
||||
: "failed";
|
||||
task->result = {{"asset_id", result.asset_id},
|
||||
{"generation", result.generation},
|
||||
{"diagnostics", result.diagnostics},
|
||||
{"cache_hit", result.cache_hit},
|
||||
{"manifest", result.manifest}};
|
||||
for (const auto& message : result.diagnostics)
|
||||
task->error += message + "\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::lock_guard lock(task->mutex);
|
||||
task->state = "failed";
|
||||
task->error = error.what();
|
||||
}
|
||||
});
|
||||
return Json{{"job", task->id}};
|
||||
});
|
||||
task->result = {{"asset_id", result.asset_id},
|
||||
{"generation", result.generation},
|
||||
{"diagnostics", result.diagnostics},
|
||||
{"cache_hit", result.cache_hit},
|
||||
{"manifest", result.manifest}};
|
||||
for (const auto& message : result.diagnostics)
|
||||
task->error += message + "\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::lock_guard lock(task->mutex);
|
||||
task->state = "failed";
|
||||
task->error = error.what();
|
||||
}
|
||||
});
|
||||
return Json{{"job", task->id}};
|
||||
});
|
||||
commands_.add(
|
||||
"faset_schema_status",
|
||||
"Report whether the last successful gameplay schema matches current project scripts. "
|
||||
"Failed builds retain metadata but mark it stale.",
|
||||
schema(Json::object()), [&](const Json&) { return schema_status(); }, true);
|
||||
commands_.add("faset_build",
|
||||
"Incrementally compile C++ gameplay and export its metadata in separate native "
|
||||
"processes. Returns a job ID.",
|
||||
schema(Json::object()),
|
||||
[&](const Json&) { return Json{{"job", builds_.start_build()}}; });
|
||||
schema(Json::object()), [&](const Json&) {
|
||||
const auto signature = source_signature();
|
||||
const auto id = builds_.start_build();
|
||||
submitted_sources_[id] = signature;
|
||||
return Json{{"job", id}};
|
||||
});
|
||||
commands_.add("faset_export",
|
||||
"Build, validate and export a resolved authoring snapshot to a project-relative "
|
||||
"output directory. Returns a job ID.",
|
||||
schema({{"document", text}, {"output", text}}, {"document", "output"}),
|
||||
[&](const Json& args) {
|
||||
return Json{{"job", builds_.start_export(
|
||||
resolved_or_throw(commands_, args.at("document")),
|
||||
project_path(config_.project_root,
|
||||
args.at("output").get<std::string>()))}};
|
||||
const auto signature = source_signature();
|
||||
const auto id = builds_.start_export(
|
||||
resolved_or_throw(commands_, args.at("document")),
|
||||
project_path(config_.project_root, args.at("output").get<std::string>()));
|
||||
submitted_sources_[id] = signature;
|
||||
return Json{{"job", id}};
|
||||
});
|
||||
commands_.add(
|
||||
"faset_jobs", "List editor import/build/export jobs and their progress.",
|
||||
@@ -326,7 +443,9 @@ void Session::register_commands() {
|
||||
schema({{"document", text}}, {"document"}), [&](const Json& args) {
|
||||
stop_player();
|
||||
pending_play_scene_ = resolved_or_throw(commands_, args.at("document"));
|
||||
const auto signature = source_signature();
|
||||
pending_play_job_ = builds_.start_build();
|
||||
submitted_sources_[pending_play_job_] = signature;
|
||||
return Json{{"job", pending_play_job_}, {"play_pending", true}};
|
||||
});
|
||||
commands_.add(
|
||||
|
||||
Reference in New Issue
Block a user