Checkpoint 4: harden native paths, authoring workflows and Player lifecycle
This commit is contained in:
@@ -74,7 +74,7 @@ std::set<std::string> asset_references(const Json& scene) {
|
||||
void copy_required_file(const fs::path& source, const fs::path& target) {
|
||||
if (!fs::is_regular_file(source) || fs::is_symlink(source))
|
||||
throw std::runtime_error("Required package file missing or not a regular file: " +
|
||||
source.string());
|
||||
path_to_utf8(source));
|
||||
fs::create_directories(target.parent_path());
|
||||
fs::copy_file(source, target, fs::copy_options::overwrite_existing);
|
||||
}
|
||||
@@ -248,9 +248,9 @@ struct BuildService::Impl {
|
||||
fs::create_directories(native_directory);
|
||||
std::vector<std::string> arguments = {config.cmake,
|
||||
"-S",
|
||||
config.engine_root.string(),
|
||||
path_to_utf8(config.engine_root),
|
||||
"-B",
|
||||
native_directory.string(),
|
||||
path_to_utf8(native_directory),
|
||||
"-G",
|
||||
config.generator,
|
||||
"-DCMAKE_BUILD_TYPE=" + configuration,
|
||||
@@ -260,7 +260,7 @@ struct BuildService::Impl {
|
||||
"-DFASET_BUILD_RUNTIME=ON",
|
||||
"-DFASET_BUILD_ASSETS=ON",
|
||||
"-DFASET_GAMEPLAY_SOURCE_DIR=" +
|
||||
(config.project_root / "Scripts").string()};
|
||||
path_to_utf8(config.project_root / "Scripts")};
|
||||
bool compiler_overridden = false;
|
||||
for (const auto& arg : config.configure_arguments)
|
||||
if (arg.starts_with("-DCMAKE_CXX_COMPILER="))
|
||||
@@ -268,11 +268,11 @@ struct BuildService::Impl {
|
||||
if (!compiler_overridden) {
|
||||
#ifdef _WIN32
|
||||
auto compiler = find_executable("clang-cl");
|
||||
arguments.push_back("-DCMAKE_C_COMPILER=" + compiler.string());
|
||||
arguments.push_back("-DCMAKE_CXX_COMPILER=" + compiler.string());
|
||||
arguments.push_back("-DCMAKE_C_COMPILER=" + path_to_utf8(compiler));
|
||||
arguments.push_back("-DCMAKE_CXX_COMPILER=" + path_to_utf8(compiler));
|
||||
#else
|
||||
arguments.push_back("-DCMAKE_C_COMPILER=" + find_executable("clang").string());
|
||||
arguments.push_back("-DCMAKE_CXX_COMPILER=" + find_executable("clang++").string());
|
||||
arguments.push_back("-DCMAKE_C_COMPILER=" + path_to_utf8(find_executable("clang")));
|
||||
arguments.push_back("-DCMAKE_CXX_COMPILER=" + path_to_utf8(find_executable("clang++")));
|
||||
#endif
|
||||
}
|
||||
arguments.insert(arguments.end(), config.configure_arguments.begin(),
|
||||
@@ -281,7 +281,7 @@ struct BuildService::Impl {
|
||||
run(job, std::move(arguments), config.project_root);
|
||||
checkpoint(job, "Compiling and linking Player", .25);
|
||||
run(job,
|
||||
{config.cmake, "--build", native_directory.string(), "--config", configuration,
|
||||
{config.cmake, "--build", path_to_utf8(native_directory), "--config", configuration,
|
||||
"--parallel", "4", "--target", "faset_player", "faset_schema_exporter"},
|
||||
config.project_root);
|
||||
checkpoint(job, "Exporting gameplay schema", .58);
|
||||
@@ -292,7 +292,8 @@ struct BuildService::Impl {
|
||||
fs::create_directories(staging);
|
||||
try {
|
||||
const auto schema_file = staging / "schema.json";
|
||||
run(job, {exporter.string(), "--output", schema_file.string()}, config.project_root);
|
||||
run(job, {path_to_utf8(exporter), "--output", path_to_utf8(schema_file)},
|
||||
config.project_root);
|
||||
auto schema = read_json(schema_file);
|
||||
if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 ||
|
||||
!schema.contains("types") || !schema.at("types").is_array())
|
||||
@@ -324,11 +325,11 @@ struct BuildService::Impl {
|
||||
atomic_write_json(config.cache_root / "last_build.json",
|
||||
{{"generation", job.status.id}, {"fingerprint", fingerprint}});
|
||||
return {{"generation", job.status.id},
|
||||
{"directory", generation.string()},
|
||||
{"build_directory", native_directory.string()},
|
||||
{"directory", path_to_utf8(generation)},
|
||||
{"build_directory", path_to_utf8(native_directory)},
|
||||
{"configuration", configuration},
|
||||
{"player", (generation / ("faset_player" + executable_suffix())).string()},
|
||||
{"schema", (generation / "schema.json").string()},
|
||||
{"player", path_to_utf8(generation / ("faset_player" + executable_suffix()))},
|
||||
{"schema", path_to_utf8(generation / "schema.json")},
|
||||
{"fingerprint", fingerprint}};
|
||||
} catch (...) {
|
||||
std::error_code error;
|
||||
@@ -346,7 +347,7 @@ struct BuildService::Impl {
|
||||
{executable.parent_path(), native_directory, native_directory / configuration})
|
||||
if (fs::is_directory(root))
|
||||
for (const auto& entry : fs::directory_iterator(root)) {
|
||||
auto extension = entry.path().extension().string();
|
||||
auto extension = path_to_utf8(entry.path().extension());
|
||||
for (auto& c : extension)
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (entry.is_regular_file() && extension == ".dll")
|
||||
@@ -356,7 +357,8 @@ struct BuildService::Impl {
|
||||
(void)job;
|
||||
#else
|
||||
const auto output =
|
||||
run(job, {find_executable("ldd").string(), executable.string()}, config.project_root);
|
||||
run(job, {path_to_utf8(find_executable("ldd")), path_to_utf8(executable)},
|
||||
config.project_root);
|
||||
if (output.find("not found") != std::string::npos)
|
||||
throw std::runtime_error("Player has unresolved shared library dependencies");
|
||||
// SDL/physics/gameplay are linked statically. glibc/libstdc++/Vulkan are the host baseline.
|
||||
@@ -427,8 +429,8 @@ struct BuildService::Impl {
|
||||
fs::rename(staging, directory);
|
||||
atomic_write_json(config.cache_root / "last_cook.json", {{"generation", digest}});
|
||||
return {{"generation", digest},
|
||||
{"scene", (directory / "scene.fscene").string()},
|
||||
{"directory", directory.string()}};
|
||||
{"scene", path_to_utf8(directory / "scene.fscene")},
|
||||
{"directory", path_to_utf8(directory)}};
|
||||
} catch (...) {
|
||||
std::error_code error;
|
||||
fs::remove_all(staging, error);
|
||||
@@ -451,13 +453,14 @@ struct BuildService::Impl {
|
||||
for (const auto& entry : fs::directory_iterator(source)) {
|
||||
if (!entry.is_regular_file())
|
||||
continue;
|
||||
auto filename = entry.path().filename().string();
|
||||
auto filename = path_to_utf8(entry.path().filename());
|
||||
std::string upper = filename;
|
||||
for (auto& c : upper)
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
if (upper.starts_with("LICENSE") || upper.starts_with("COPYING") ||
|
||||
upper.starts_with("NOTICE")) {
|
||||
copy_required_file(entry.path(), destination / name / filename);
|
||||
copy_required_file(entry.path(),
|
||||
destination / name / entry.path().filename());
|
||||
copied = true;
|
||||
}
|
||||
}
|
||||
@@ -487,7 +490,7 @@ struct BuildService::Impl {
|
||||
"generations" / generation);
|
||||
auto target = destination / "assets" / id / "generations" / generation;
|
||||
for (const auto& file : manifest.at("files")) {
|
||||
auto relative = fs::path(file.at("path").get<std::string>());
|
||||
auto relative = path_from_utf8(file.at("path").get<std::string>());
|
||||
copy_required_file(project_path(source_directory, relative),
|
||||
project_path(target, relative));
|
||||
}
|
||||
@@ -508,7 +511,8 @@ struct BuildService::Impl {
|
||||
for (const auto& id : asset_references(job.scene))
|
||||
job.asset_manifests[id] = source.current_manifest(id);
|
||||
const auto built = build(job, true);
|
||||
validate_component_types(job.scene, read_json(built.at("schema").get<std::string>()));
|
||||
validate_component_types(job.scene,
|
||||
read_json(path_from_utf8(built.at("schema").get<std::string>())));
|
||||
auto output = fs::absolute(job.output);
|
||||
if (output.empty())
|
||||
throw std::runtime_error("An export destination is required");
|
||||
@@ -519,7 +523,7 @@ struct BuildService::Impl {
|
||||
try {
|
||||
checkpoint(job, "Cooking export snapshot", .72);
|
||||
write_cooked_scene(staging / "scene.fscene", job.scene);
|
||||
auto build_directory = fs::path(built.at("directory").get<std::string>());
|
||||
auto build_directory = path_from_utf8(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",
|
||||
@@ -528,7 +532,7 @@ struct BuildService::Impl {
|
||||
copy_required_file(build_directory / "shaders" / shader,
|
||||
staging / "shaders" / shader);
|
||||
for (const auto& entry : fs::directory_iterator(build_directory)) {
|
||||
auto extension = entry.path().extension().string();
|
||||
auto extension = path_to_utf8(entry.path().extension());
|
||||
for (auto& c : extension)
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (entry.is_regular_file() && extension == ".dll")
|
||||
@@ -536,7 +540,8 @@ struct BuildService::Impl {
|
||||
}
|
||||
checkpoint(job, "Packaging assets and notices", .80);
|
||||
package_assets(job, staging);
|
||||
package_notices(staging / "Notices", built.at("build_directory").get<std::string>());
|
||||
package_notices(staging / "Notices",
|
||||
path_from_utf8(built.at("build_directory").get<std::string>()));
|
||||
atomic_write(
|
||||
staging / "README.txt",
|
||||
"Run faset_player" + executable_suffix() +
|
||||
@@ -553,8 +558,9 @@ struct BuildService::Impl {
|
||||
checkpoint(job, "Validating packaged Player", .90);
|
||||
auto validation_output =
|
||||
run(job,
|
||||
{(staging / ("faset_player" + executable_suffix())).string(), "--validate",
|
||||
"--scene", (staging / "scene.fscene").string(), "--assets", staging.string()},
|
||||
{path_to_utf8(staging / ("faset_player" + executable_suffix())), "--validate",
|
||||
"--scene", path_to_utf8(staging / "scene.fscene"), "--assets",
|
||||
path_to_utf8(staging)},
|
||||
staging);
|
||||
Json files = Json::array();
|
||||
for (const auto& entry : fs::recursive_directory_iterator(staging)) {
|
||||
@@ -562,7 +568,7 @@ struct BuildService::Impl {
|
||||
throw std::runtime_error("Export contains a symlink");
|
||||
if (entry.is_regular_file())
|
||||
files.push_back(
|
||||
{{"path", entry.path().lexically_relative(staging).generic_string()},
|
||||
{{"path", generic_path_to_utf8(entry.path().lexically_relative(staging))},
|
||||
{"sha256", sha256_file(entry.path())},
|
||||
{"size", entry.file_size()}});
|
||||
}
|
||||
@@ -613,15 +619,16 @@ struct BuildService::Impl {
|
||||
{"version", 1},
|
||||
{"generation", job.status.id},
|
||||
{"directory", "generations/" + job.status.id}});
|
||||
return {{"directory", generation.string()},
|
||||
{"executable", (generation / ("faset_player" + executable_suffix())).string()},
|
||||
{"manifest", (generation / "manifest.json").string()},
|
||||
{"generation", job.status.id},
|
||||
{"build", built},
|
||||
{"schema", built.at("schema")},
|
||||
{"player", built.at("player")},
|
||||
{"build_directory", built.at("build_directory")},
|
||||
{"configuration", built.at("configuration")}};
|
||||
return {
|
||||
{"directory", path_to_utf8(generation)},
|
||||
{"executable", path_to_utf8(generation / ("faset_player" + executable_suffix()))},
|
||||
{"manifest", path_to_utf8(generation / "manifest.json")},
|
||||
{"generation", job.status.id},
|
||||
{"build", built},
|
||||
{"schema", built.at("schema")},
|
||||
{"player", built.at("player")},
|
||||
{"build_directory", built.at("build_directory")},
|
||||
{"configuration", built.at("configuration")}};
|
||||
} catch (...) {
|
||||
std::error_code error;
|
||||
fs::remove_all(staging, error);
|
||||
|
||||
@@ -68,9 +68,9 @@ 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) {
|
||||
const auto relative = std::filesystem::path(path).lexically_normal();
|
||||
const auto relative = path_from_utf8(path).lexically_normal();
|
||||
for (const auto& document : authoring_.documents())
|
||||
if (document.at("path") == relative.generic_string())
|
||||
if (document.at("path") == generic_path_to_utf8(relative))
|
||||
return authoring_.query(document.at("id")).at("scene");
|
||||
return read_json(project_path(authoring_.root(), relative));
|
||||
});
|
||||
@@ -92,7 +92,7 @@ Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authorin
|
||||
"Open a scene relative to the project. Set recover=true to load its saved recovery "
|
||||
"journal.",
|
||||
object_schema({{"path", text}, {"recover", boolean}}, {"path"}), [&](const Json& args) {
|
||||
return authoring_.open(args.at("path").get<std::string>(),
|
||||
return authoring_.open(path_from_utf8(args.at("path").get<std::string>()),
|
||||
args.value("recover", false));
|
||||
});
|
||||
add(
|
||||
@@ -104,7 +104,8 @@ Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authorin
|
||||
add("faset_document_save",
|
||||
"Atomically save an authoring document. Refuses to overwrite an externally modified file.",
|
||||
object_schema({{"document", text}, {"path", text}}, {"document"}), [&](const Json& args) {
|
||||
return authoring_.save(args.at("document"), args.value("path", std::string()));
|
||||
return authoring_.save(args.at("document"),
|
||||
path_from_utf8(args.value("path", std::string())));
|
||||
});
|
||||
add(
|
||||
"faset_schema",
|
||||
|
||||
+203
-67
@@ -174,6 +174,7 @@ struct EditorUI::Impl {
|
||||
std::uint64_t shown_revision = std::numeric_limits<std::uint64_t>::max();
|
||||
std::map<std::string, std::uint64_t> edit_revisions;
|
||||
std::map<std::string, Json> preview_fields;
|
||||
std::map<std::string, std::string> import_retries;
|
||||
std::filesystem::path layout_path;
|
||||
std::filesystem::path theme_source_path, layout_source_path;
|
||||
std::string attempted_theme, attempted_layout, applied_theme, applied_layout,
|
||||
@@ -196,6 +197,14 @@ struct EditorUI::Impl {
|
||||
std::uint64_t gizmo_revision = 0;
|
||||
std::string gizmo_component;
|
||||
ui::Rect viewport;
|
||||
float ui_scale = 1;
|
||||
unsigned layout_width = 0, layout_height = 0;
|
||||
float logical_width() const {
|
||||
return float(renderer.width()) / ui_scale;
|
||||
}
|
||||
float logical_height() const {
|
||||
return float(renderer.height()) / ui_scale;
|
||||
}
|
||||
Vec3 gizmo_origin{};
|
||||
render::Vec2 gizmo_drag_start{}, gizmo_drag_end{};
|
||||
float gizmo_world_length = 1;
|
||||
@@ -253,12 +262,13 @@ struct EditorUI::Impl {
|
||||
document = session.authoring()
|
||||
.create("Untitled", session.project().value("dimension", 3))
|
||||
.at("id");
|
||||
ui_scale = std::clamp(renderer.display_scale(), .5f, 4.f);
|
||||
refresh();
|
||||
}
|
||||
void persist_layout() {
|
||||
dock.set_size("scene", ui.find("scene_panel")->rect.width);
|
||||
dock.set_size("inspector", ui.find("inspector_panel")->rect.width);
|
||||
dock.set_size("bottom", ui.find("bottom_panel")->rect.height);
|
||||
dock.set_size("scene", ui.find("scene_panel")->rect.width / ui_scale);
|
||||
dock.set_size("inspector", ui.find("inspector_panel")->rect.width / ui_scale);
|
||||
dock.set_size("bottom", ui.find("bottom_panel")->rect.height / ui_scale);
|
||||
try {
|
||||
dock.save(layout_path);
|
||||
} catch (const std::exception& e) {
|
||||
@@ -897,7 +907,8 @@ struct EditorUI::Impl {
|
||||
}
|
||||
if (!open)
|
||||
try {
|
||||
source_scene = read_json(project_path(session.config().project_root, source));
|
||||
source_scene = read_json(
|
||||
project_path(session.config().project_root, path_from_utf8(source)));
|
||||
} catch (...) {
|
||||
return source;
|
||||
}
|
||||
@@ -983,7 +994,8 @@ struct EditorUI::Impl {
|
||||
}
|
||||
void instance_scene(const std::string& path) {
|
||||
try {
|
||||
auto data = read_json(project_path(session.config().project_root, path));
|
||||
auto data =
|
||||
read_json(project_path(session.config().project_root, path_from_utf8(path)));
|
||||
authoring::validate_scene(data, session.authoring().schemas());
|
||||
if (data.at("id") == document)
|
||||
throw std::runtime_error("A scene cannot instance itself");
|
||||
@@ -1019,7 +1031,7 @@ struct EditorUI::Impl {
|
||||
subtree.insert(item.at("id").get<std::string>()).second || changed;
|
||||
}
|
||||
auto result = call("faset_document_create",
|
||||
{{"name", std::filesystem::path(path).stem().string()},
|
||||
{{"name", path_to_utf8(path_from_utf8(path).stem())},
|
||||
{"dimension", current.at("scene").value("dimension", 3)}});
|
||||
if (result.is_null())
|
||||
return;
|
||||
@@ -1075,9 +1087,9 @@ struct EditorUI::Impl {
|
||||
const bool image = manifest.is_object() && manifest.value("kind", std::string()) == "image";
|
||||
const auto name =
|
||||
manifest.is_object()
|
||||
? std::filesystem::path(manifest.value("source", std::string("Imported asset")))
|
||||
.stem()
|
||||
.string()
|
||||
? path_to_utf8(
|
||||
path_from_utf8(manifest.value("source", std::string("Imported asset")))
|
||||
.stem())
|
||||
: "Imported asset";
|
||||
auto object = authoring::make_entity(session.authoring().schemas(), name);
|
||||
const auto entity_id = object.at("id").get<std::string>();
|
||||
@@ -1218,7 +1230,7 @@ struct EditorUI::Impl {
|
||||
std::function<void(Json, int)> append_group = [&](Json path, int depth) {
|
||||
const auto source = instance_source(path);
|
||||
auto& row = tree.add(Kind::TreeRow, "instance-" + path.dump(),
|
||||
"[T] " + std::filesystem::path(source).filename().string());
|
||||
"[T] " + path_to_utf8(path_from_utf8(source).filename()));
|
||||
row.indent = depth;
|
||||
row.selected = instance_selection == path;
|
||||
row.on_click = [this, path](Widget&) { select_instance(path); };
|
||||
@@ -1378,7 +1390,7 @@ struct EditorUI::Impl {
|
||||
const auto path = e->at("origin").at("path");
|
||||
const auto object = e->at("origin").at("object").get<std::string>();
|
||||
label(body, "object-origin",
|
||||
"Source: " + std::filesystem::path(instance_source(path)).filename().string());
|
||||
"Source: " + path_to_utf8(path_from_utf8(instance_source(path)).filename()));
|
||||
keep.insert("object-origin");
|
||||
button(body, "object-open-source", "Open source",
|
||||
[this, path, object] { open_template_source(path, object); });
|
||||
@@ -1565,9 +1577,8 @@ struct EditorUI::Impl {
|
||||
break;
|
||||
if (!entry.is_regular_file())
|
||||
continue;
|
||||
auto relative =
|
||||
std::filesystem::relative(entry.path(), session.config().project_root)
|
||||
.generic_string();
|
||||
auto relative = generic_path_to_utf8(
|
||||
std::filesystem::relative(entry.path(), session.config().project_root));
|
||||
if (relative.find(".faset-") != std::string::npos)
|
||||
continue;
|
||||
if (!asset_filter.empty() && relative.find(asset_filter) == std::string::npos)
|
||||
@@ -1602,11 +1613,10 @@ struct EditorUI::Impl {
|
||||
}
|
||||
for (const auto& asset : assets) {
|
||||
const auto id = asset.at("id").get<std::string>();
|
||||
const auto name = asset.contains("manifest")
|
||||
? std::filesystem::path(asset["manifest"].value("source", id))
|
||||
.filename()
|
||||
.string()
|
||||
: id;
|
||||
const auto name =
|
||||
asset.contains("manifest")
|
||||
? path_to_utf8(path_from_utf8(asset["manifest"].value("source", id)).filename())
|
||||
: id;
|
||||
auto& row = list.add(Kind::TreeRow, "asset-" + id, "Imported / " + name);
|
||||
row.layout.height = 25;
|
||||
row.indent = 1;
|
||||
@@ -1624,6 +1634,8 @@ struct EditorUI::Impl {
|
||||
trim_children(list, keep);
|
||||
}
|
||||
void refresh_bottom() {
|
||||
const auto jobs_response = call("faset_jobs");
|
||||
const auto all_jobs = jobs_response.is_null() ? Json::array() : jobs_response.at("jobs");
|
||||
for (const auto& panel : session.plugin_panels()) {
|
||||
const auto panel_id = panel.at("id").get<std::string>();
|
||||
const auto dock_id = "plugin-" + panel_id;
|
||||
@@ -1674,8 +1686,6 @@ struct EditorUI::Impl {
|
||||
ui.find("console-items")->visible = active_bottom == "console";
|
||||
ui.find("job-items")->visible = active_bottom == "jobs";
|
||||
ui.find("conflict-items")->visible = active_bottom == "conflicts";
|
||||
ui.find("tab-conflicts")->text =
|
||||
"Conflicts (" + std::to_string(template_conflicts.size()) + ")";
|
||||
auto& conflict_list = *ui.find("conflict-items");
|
||||
std::set<std::string> conflict_keep;
|
||||
std::size_t conflict_index = 0;
|
||||
@@ -1687,7 +1697,7 @@ struct EditorUI::Impl {
|
||||
const auto path = conflict.at("instance_path");
|
||||
auto& text = row.add(
|
||||
Kind::Label, id + "-text",
|
||||
code + " · " + std::filesystem::path(instance_source(path)).filename().string());
|
||||
code + " · " + path_to_utf8(path_from_utf8(instance_source(path)).filename()));
|
||||
text.layout.flex = 1;
|
||||
button(
|
||||
row, id + "-source", "Open source", [this, path] { open_template_source(path); },
|
||||
@@ -1716,9 +1726,113 @@ struct EditorUI::Impl {
|
||||
}
|
||||
conflict_keep.insert(id);
|
||||
}
|
||||
std::size_t import_conflicts = 0;
|
||||
for (const auto& job : all_jobs) {
|
||||
if (job.value("kind", std::string()) != "import" ||
|
||||
job.value("state", std::string()) != "conflict")
|
||||
continue;
|
||||
const auto job_id = job.at("id").get<std::string>();
|
||||
const auto& result = job.at("result");
|
||||
const auto request = job.value("request", Json::object());
|
||||
bool pending = false, superseded = false;
|
||||
std::string retry_error;
|
||||
if (const auto found = import_retries.find(job_id); found != import_retries.end())
|
||||
for (const auto& retry : all_jobs)
|
||||
if (retry.at("id") == found->second) {
|
||||
const auto state = retry.value("state", std::string());
|
||||
pending = state == "queued" || state == "running";
|
||||
superseded = state == "succeeded" || state == "conflict";
|
||||
if (state == "failed" || state == "cancelled")
|
||||
retry_error = retry.value("error", std::string("Retry cancelled"));
|
||||
}
|
||||
for (const auto& asset : assets)
|
||||
if (asset.at("id") == result.value("asset_id", std::string()) &&
|
||||
asset.contains("manifest") &&
|
||||
asset.at("manifest").at("generation") ==
|
||||
result.value("generation", std::string()))
|
||||
superseded = true;
|
||||
if (superseded)
|
||||
continue;
|
||||
++import_conflicts;
|
||||
const auto id = "import-conflict-" + job_id;
|
||||
auto& group = conflict_list.add(Kind::Column, id);
|
||||
group.layout.padding = 6;
|
||||
group.layout.gap = 3;
|
||||
conflict_keep.insert(id);
|
||||
label(group, id + "-title", "Import removal: " + request.value("path", job_id));
|
||||
label(group, id + "-note",
|
||||
"The previous asset stays active. Update affected references before accepting.");
|
||||
label(group, id + "-generation",
|
||||
"Reviewed generation: " + result.value("generation", std::string()));
|
||||
auto diagnostic = job.value("error", std::string());
|
||||
std::replace(diagnostic.begin(), diagnostic.end(), '\n', ' ');
|
||||
label(group, id + "-diagnostic", diagnostic);
|
||||
const auto removed = result.value("removed_output_ids", Json::array());
|
||||
auto& outputs = group.add(Kind::Column, id + "-outputs");
|
||||
outputs.layout.height = std::min(150.f, std::max(26.f, float(removed.size()) * 26));
|
||||
outputs.layout.scroll = true;
|
||||
outputs.layout.gap = 0;
|
||||
std::set<std::string> output_keep;
|
||||
for (const auto& output : removed) {
|
||||
const auto output_id = output.get<std::string>();
|
||||
std::string name;
|
||||
for (const auto& asset : assets)
|
||||
if (asset.at("id") == result.value("asset_id", std::string()) &&
|
||||
asset.contains("manifest"))
|
||||
for (const auto* collection : {"nodes", "meshes", "materials", "textures"})
|
||||
for (const auto& item :
|
||||
asset.at("manifest").value(collection, Json::array()))
|
||||
if (item.at("id") == output_id)
|
||||
name = item.value("name", std::string());
|
||||
const auto output_widget = id + "-output-" + output_id;
|
||||
label(outputs, output_widget, output_id + (name.empty() ? "" : " / " + name));
|
||||
outputs.find(output_widget)->layout.height = 26;
|
||||
output_keep.insert(output_widget);
|
||||
}
|
||||
trim_children(outputs, output_keep);
|
||||
auto& actions = group.add(Kind::Row, id + "-actions");
|
||||
actions.layout.height = 30;
|
||||
const auto retry = [this, job_id, request, result](bool accept) {
|
||||
Json arguments = {{"path", request.at("path")}, {"allow_removed_outputs", accept}};
|
||||
if (request.contains("settings") && !request.at("settings").is_null())
|
||||
arguments["settings"] = request.at("settings");
|
||||
if (accept) {
|
||||
arguments["expected_generation"] = result.at("generation");
|
||||
arguments["expected_active_generation"] = result.at("previous_generation");
|
||||
}
|
||||
auto submitted = call("faset_import", arguments);
|
||||
if (!submitted.is_null()) {
|
||||
import_retries[job_id] = submitted.at("job");
|
||||
status =
|
||||
accept
|
||||
? "Publishing the reviewed import; source changes require a new review"
|
||||
: "Reimporting source for a fresh review";
|
||||
}
|
||||
};
|
||||
button(
|
||||
actions, id + "-retry", "Reimport / review again", [retry] { retry(false); }, 185)
|
||||
.enabled = !pending && request.contains("path");
|
||||
button(
|
||||
actions, id + "-accept", "Accept reviewed removal", [retry] { retry(true); }, 190)
|
||||
.enabled = !pending && request.contains("path") && !removed.empty();
|
||||
button(
|
||||
actions, id + "-copy", "Copy removed IDs",
|
||||
[this, removed] { renderer.set_clipboard(removed.dump(2)); }, 145);
|
||||
auto detail = pending ? "Import in progress; the previous generation remains active."
|
||||
: "Accepting removes these outputs. Scene references are not "
|
||||
"remapped automatically.";
|
||||
label(group, id + "-detail", detail);
|
||||
if (!retry_error.empty()) {
|
||||
std::replace(retry_error.begin(), retry_error.end(), '\n', ' ');
|
||||
label(group, id + "-error", retry_error);
|
||||
} else
|
||||
group.remove(id + "-error");
|
||||
}
|
||||
ui.find("tab-conflicts")->text =
|
||||
"Conflicts (" + std::to_string(template_conflicts.size() + import_conflicts) + ")";
|
||||
if (conflict_keep.empty()) {
|
||||
label(conflict_list, "conflicts-empty",
|
||||
"No template conflicts. Missing targets preserve their overrides here.");
|
||||
"No template or import conflicts. Missing targets preserve their data here.");
|
||||
conflict_keep.insert("conflicts-empty");
|
||||
}
|
||||
trim_children(conflict_list, conflict_keep);
|
||||
@@ -1738,27 +1852,28 @@ struct EditorUI::Impl {
|
||||
keep.insert("log-empty");
|
||||
}
|
||||
trim_children(console, keep);
|
||||
auto result = call("faset_jobs");
|
||||
auto& jobs = *ui.find("job-items");
|
||||
keep.clear();
|
||||
if (!result.is_null())
|
||||
for (const auto& job : result.at("jobs")) {
|
||||
const auto id = job.at("id").get<std::string>();
|
||||
auto& row = jobs.add(Kind::Row, "job-" + id);
|
||||
row.layout.height = 28;
|
||||
keep.insert(row.id);
|
||||
const auto state = job.value("state", std::string());
|
||||
auto& text =
|
||||
row.add(Kind::Label, "job-text-" + id,
|
||||
job.value("kind", std::string("Job")) + " · " + state + " · " +
|
||||
job.value("stage", std::string()) + " " +
|
||||
std::to_string(int(job.value("progress", 0.0) * 100)) + "%");
|
||||
text.layout.flex = 1;
|
||||
auto& cancel = button(
|
||||
row, "job-cancel-" + id, "Cancel",
|
||||
[this, id] { call("faset_job_cancel", {{"id", id}}); }, 72);
|
||||
cancel.enabled = state == "queued" || state == "running";
|
||||
}
|
||||
for (const auto& job : all_jobs) {
|
||||
const auto id = job.at("id").get<std::string>();
|
||||
auto& row = jobs.add(Kind::Row, "job-" + id);
|
||||
row.layout.height = 28;
|
||||
keep.insert(row.id);
|
||||
const auto state = job.value("state", std::string());
|
||||
auto& text = row.add(Kind::Label, "job-text-" + id,
|
||||
job.value("kind", std::string("Job")) + " · " + state + " · " +
|
||||
job.value("stage", std::string()) + " " +
|
||||
std::to_string(int(job.value("progress", 0.0) * 100)) + "%");
|
||||
text.layout.flex = 1;
|
||||
auto& cancel = button(
|
||||
row, "job-cancel-" + id, "Cancel",
|
||||
[this, id] { call("faset_job_cancel", {{"id", id}}); }, 72);
|
||||
cancel.enabled = state == "queued" || state == "running";
|
||||
auto& review = button(
|
||||
row, "job-review-" + id, "Review removals", [this] { active_bottom = "conflicts"; },
|
||||
135);
|
||||
review.visible = job.value("kind", std::string()) == "import" && state == "conflict";
|
||||
}
|
||||
if (keep.empty()) {
|
||||
label(jobs, "jobs-empty", "No active import, build or export jobs.");
|
||||
keep.insert("jobs-empty");
|
||||
@@ -1788,8 +1903,8 @@ struct EditorUI::Impl {
|
||||
: "Project switching is unavailable while MCP is connected";
|
||||
auto* switching = ui.find("project-switch-dialog");
|
||||
switching->visible = project_switch_warning;
|
||||
switching->layout.x = std::max(0.f, (float(renderer.width()) - 500) * .5f);
|
||||
switching->layout.y = std::max(0.f, (float(renderer.height()) - 220) * .5f);
|
||||
switching->layout.x = std::max(0.f, (logical_width() - 500) * .5f);
|
||||
switching->layout.y = std::max(0.f, (logical_height() - 220) * .5f);
|
||||
for (const auto* id : {"menu-duplicate", "menu-delete"})
|
||||
ui.find(id)->visible = edit;
|
||||
for (const auto* id : {"help-one", "help-two", "help-three"})
|
||||
@@ -1805,7 +1920,7 @@ struct EditorUI::Impl {
|
||||
: 5;
|
||||
auto& command = *ui.find("palette");
|
||||
command.visible = palette;
|
||||
command.layout.x = std::max(0.f, (float(renderer.width()) - 570) / 2);
|
||||
command.layout.x = std::max(0.f, (logical_width() - 570) / 2);
|
||||
command.layout.y = 90;
|
||||
auto& list = *ui.find("palette-list");
|
||||
std::set<std::string> keep;
|
||||
@@ -1855,10 +1970,9 @@ struct EditorUI::Impl {
|
||||
if (++visited > 4000)
|
||||
break;
|
||||
if (entry.is_regular_file() &&
|
||||
entry.path().filename().string().ends_with(".scene.json"))
|
||||
saved.insert(
|
||||
std::filesystem::relative(entry.path(), session.config().project_root)
|
||||
.generic_string());
|
||||
path_to_utf8(entry.path().filename()).ends_with(".scene.json"))
|
||||
saved.insert(generic_path_to_utf8(std::filesystem::relative(
|
||||
entry.path(), session.config().project_root)));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
@@ -1867,7 +1981,8 @@ struct EditorUI::Impl {
|
||||
std::size_t index = 0;
|
||||
for (const auto& path : saved) {
|
||||
try {
|
||||
const auto scene = read_json(project_path(session.config().project_root, path));
|
||||
const auto scene =
|
||||
read_json(project_path(session.config().project_root, path_from_utf8(path)));
|
||||
if (scene.value("format", "") != "faset.scene" || scene.value("version", 0) != 1)
|
||||
continue;
|
||||
} catch (...) {
|
||||
@@ -1914,10 +2029,10 @@ struct EditorUI::Impl {
|
||||
void refresh_project_settings() {
|
||||
auto* panel = ui.find("project-settings-panel");
|
||||
panel->visible = project_settings_open;
|
||||
panel->layout.width = std::min(610.f, std::max(340.f, float(renderer.width()) - 40));
|
||||
panel->layout.height = std::min(550.f, std::max(300.f, float(renderer.height()) - 40));
|
||||
panel->layout.x = std::max(0.f, (float(renderer.width()) - panel->layout.width) * .5f);
|
||||
panel->layout.y = std::max(0.f, (float(renderer.height()) - panel->layout.height) * .5f);
|
||||
panel->layout.width = std::min(610.f, std::max(340.f, logical_width() - 40));
|
||||
panel->layout.height = std::min(550.f, std::max(300.f, logical_height() - 40));
|
||||
panel->layout.x = std::max(0.f, (logical_width() - panel->layout.width) * .5f);
|
||||
panel->layout.y = std::max(0.f, (logical_height() - panel->layout.height) * .5f);
|
||||
ui.find("project-settings-2d")->selected = project_settings_dimension == 2;
|
||||
ui.find("project-settings-3d")->selected = project_settings_dimension == 3;
|
||||
ui.find("project-settings-error")->text = project_settings_error;
|
||||
@@ -1928,7 +2043,7 @@ struct EditorUI::Impl {
|
||||
void refresh_simulation() {
|
||||
auto& panel = *ui.find("simulation-panel");
|
||||
panel.visible = simulation_open;
|
||||
panel.layout.x = std::max(0.f, (float(renderer.width()) - 470) / 2);
|
||||
panel.layout.x = std::max(0.f, (logical_width() - 470) / 2);
|
||||
panel.layout.y = 90;
|
||||
if (!simulation_open)
|
||||
return;
|
||||
@@ -1986,7 +2101,7 @@ struct EditorUI::Impl {
|
||||
void refresh_recovery() {
|
||||
auto& panel = *ui.find("recovery-panel");
|
||||
panel.visible = !recovery.empty();
|
||||
panel.layout.x = std::max(0.f, (float(renderer.width()) - 470) / 2);
|
||||
panel.layout.x = std::max(0.f, (logical_width() - 470) / 2);
|
||||
panel.layout.y = 100;
|
||||
std::set<std::string> keep;
|
||||
label(panel, "recovery-title", "Unsaved authoring recovery");
|
||||
@@ -2152,11 +2267,16 @@ struct EditorUI::Impl {
|
||||
const render::Color colors[3] = {
|
||||
{.88f, .35f, .34f, 1}, {.38f, .75f, .49f, 1}, {.4f, .58f, .92f, 1}};
|
||||
for (int axis = 0; axis < (is2d ? 2 : 3); ++axis) {
|
||||
line(gizmo_screen[0], gizmo_screen[axis + 1], colors[axis], 2);
|
||||
line(gizmo_screen[0], gizmo_screen[axis + 1], colors[axis], 2 * ui_scale);
|
||||
const auto end = gizmo_screen[axis + 1];
|
||||
if (viewport.contains(end[0], end[1]))
|
||||
rendered.ui_quads.push_back(
|
||||
{end[0] - 4, end[1] - 4, 8, 8, colors[axis], {}, {0, 0, 1, 1}});
|
||||
rendered.ui_quads.push_back({end[0] - 4 * ui_scale,
|
||||
end[1] - 4 * ui_scale,
|
||||
8 * ui_scale,
|
||||
8 * ui_scale,
|
||||
colors[axis],
|
||||
{},
|
||||
{0, 0, 1, 1}});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2196,12 +2316,12 @@ struct EditorUI::Impl {
|
||||
const auto* c = component(*e, "faset.transform");
|
||||
if (!c || c->value("version", 1) != 1)
|
||||
return false;
|
||||
float best = 8;
|
||||
float best = 8 * ui_scale;
|
||||
int axis = -1;
|
||||
for (int i = 0; i < (current.at("scene").value("dimension", 3) == 2 ? 2 : 3); ++i) {
|
||||
auto a = gizmo_screen[0], b = gizmo_screen[i + 1];
|
||||
const auto dx = b[0] - a[0], dy = b[1] - a[1], l = dx * dx + dy * dy;
|
||||
if (l < 16)
|
||||
if (l < 16 * ui_scale * ui_scale)
|
||||
continue;
|
||||
const auto t = std::clamp(((x - a[0]) * dx + (y - a[1]) * dy) / l, 0.f, 1.f);
|
||||
const auto d = std::hypot(x - a[0] - t * dx, y - a[1] - t * dy);
|
||||
@@ -2282,8 +2402,8 @@ struct EditorUI::Impl {
|
||||
if (camera_drag) {
|
||||
const bool is2d = current.at("scene").value("dimension", 3) == 2;
|
||||
if (camera_drag == 3 && !is2d) {
|
||||
yaw -= dx * .008f;
|
||||
pitch = std::clamp(pitch + dy * .008f, -1.5f, 1.5f);
|
||||
yaw -= dx / ui_scale * .008f;
|
||||
pitch = std::clamp(pitch + dy / ui_scale * .008f, -1.5f, 1.5f);
|
||||
} else {
|
||||
const auto right = Vec3{std::cos(yaw), 0, -std::sin(yaw)};
|
||||
const auto up =
|
||||
@@ -2367,7 +2487,7 @@ struct EditorUI::Impl {
|
||||
}
|
||||
}
|
||||
if (event.type == Type::MouseDown && !menu.empty() &&
|
||||
!ui.find("menu-popup")->rect.contains(event.x, event.y) && event.y > 36)
|
||||
!ui.find("menu-popup")->rect.contains(event.x, event.y) && event.y > 36 * ui_scale)
|
||||
menu.clear();
|
||||
if (event.type == Type::MouseMove || event.type == Type::MouseDown) {
|
||||
mouse_x = event.x;
|
||||
@@ -2427,15 +2547,31 @@ struct EditorUI::Impl {
|
||||
}
|
||||
}
|
||||
void frame(const std::vector<render::Event>& events_) {
|
||||
const auto next_scale = std::clamp(renderer.display_scale(), .5f, 4.f);
|
||||
const bool geometry_changed = next_scale != ui_scale || layout_width != renderer.width() ||
|
||||
layout_height != renderer.height();
|
||||
if (next_scale != ui_scale) {
|
||||
camera_drag = 0;
|
||||
if (gizmo_axis >= 0) {
|
||||
const auto field = gizmo_mode == "Move" ? "position"
|
||||
: gizmo_mode == "Rotate" ? "rotation"
|
||||
: "scale";
|
||||
preview_fields.erase(gizmo_component + "/" + field);
|
||||
gizmo_axis = -1;
|
||||
}
|
||||
}
|
||||
ui_scale = next_scale;
|
||||
layout_width = renderer.width();
|
||||
layout_height = renderer.height();
|
||||
session.poll();
|
||||
poll_presentation();
|
||||
refresh();
|
||||
ui.layout(float(renderer.width()), float(renderer.height()));
|
||||
if (rendered.scene_rect[2] == 0)
|
||||
ui.layout(float(renderer.width()), float(renderer.height()), ui_scale);
|
||||
if (rendered.scene_rect[2] == 0 || geometry_changed)
|
||||
build_snapshot();
|
||||
events(events_);
|
||||
refresh();
|
||||
ui.layout(float(renderer.width()), float(renderer.height()));
|
||||
ui.layout(float(renderer.width()), float(renderer.height()), ui_scale);
|
||||
build_snapshot();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,7 +202,8 @@ struct PluginManager::Impl {
|
||||
module->owner = this;
|
||||
module->manifest = manifest;
|
||||
module->directory = directory;
|
||||
const auto path = project_path(directory, manifest.at("library").get<std::string>());
|
||||
const auto path =
|
||||
project_path(directory, path_from_utf8(manifest.at("library").get<std::string>()));
|
||||
require(std::filesystem::is_regular_file(path), "plugin.library",
|
||||
"Plugin library is missing");
|
||||
#ifdef _WIN32
|
||||
@@ -307,8 +308,8 @@ void PluginManager::load(const std::filesystem::path& directory) {
|
||||
};
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory))
|
||||
if (entry.is_regular_file() &&
|
||||
entry.path().filename().string().ends_with(".faset-plugin.json")) {
|
||||
std::string id = entry.path().filename().string();
|
||||
path_to_utf8(entry.path().filename()).ends_with(".faset-plugin.json")) {
|
||||
std::string id = path_to_utf8(entry.path().filename());
|
||||
try {
|
||||
const auto manifest = read_json(entry.path());
|
||||
id = manifest.at("id");
|
||||
@@ -324,7 +325,8 @@ void PluginManager::load(const std::filesystem::path& directory) {
|
||||
manifest.at("build_fingerprint") == fingerprint(),
|
||||
"plugin.compatibility",
|
||||
"Plugin manifest does not match this Editor SDK; rebuild it");
|
||||
project_path(entry.path().parent_path(), manifest.at("library").get<std::string>());
|
||||
project_path(entry.path().parent_path(),
|
||||
path_from_utf8(manifest.at("library").get<std::string>()));
|
||||
sources.emplace(id, Source{manifest, entry.path().parent_path()});
|
||||
} catch (const std::exception& error) {
|
||||
failure(id, error.what());
|
||||
|
||||
+59
-21
@@ -34,7 +34,7 @@ struct Session::ImportTask {
|
||||
std::shared_ptr<assets::ImportJob> job = std::make_shared<assets::ImportJob>();
|
||||
mutable std::mutex mutex;
|
||||
std::string state = "queued", error;
|
||||
Json result = Json::object();
|
||||
Json result = Json::object(), request = Json::object();
|
||||
Json json() const {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto progress = job->progress();
|
||||
@@ -44,6 +44,7 @@ struct Session::ImportTask {
|
||||
{"stage", progress.stage},
|
||||
{"progress", progress.fraction},
|
||||
{"error", error},
|
||||
{"request", request},
|
||||
{"result", result}};
|
||||
}
|
||||
};
|
||||
@@ -97,13 +98,13 @@ Json Session::project() const {
|
||||
"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);
|
||||
project_path(config_.project_root, path_from_utf8(scene));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return {{"format", "faset.project"},
|
||||
{"version", 1},
|
||||
{"name", config_.project_root.filename().string()},
|
||||
{"name", path_to_utf8(config_.project_root.filename())},
|
||||
{"dimension", 3}};
|
||||
}
|
||||
void Session::scaffold(const std::string& name, int dimension) {
|
||||
@@ -117,12 +118,12 @@ Json Session::assets_list() const {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory))
|
||||
if (entry.is_directory()) {
|
||||
try {
|
||||
const auto id = entry.path().filename().string();
|
||||
const auto id = path_to_utf8(entry.path().filename());
|
||||
const auto manifest = assets_.current_manifest(id);
|
||||
list.push_back({{"id", id}, {"manifest", manifest}});
|
||||
} catch (const std::exception& error) {
|
||||
list.push_back(
|
||||
{{"id", entry.path().filename().string()}, {"error", error.what()}});
|
||||
{{"id", path_to_utf8(entry.path().filename())}, {"error", error.what()}});
|
||||
}
|
||||
}
|
||||
return {{"assets", list}};
|
||||
@@ -137,8 +138,8 @@ std::string Session::source_signature() const {
|
||||
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";
|
||||
contents += generic_path_to_utf8(file.lexically_relative(directory)) + ":" +
|
||||
sha256_file(file) + "\n";
|
||||
return sha256(contents);
|
||||
}
|
||||
Json Session::schema_status() const {
|
||||
@@ -181,8 +182,8 @@ void Session::launch_player(Json scene, const std::filesystem::path& executable)
|
||||
control_sequence_ = 0;
|
||||
ProcessOptions options;
|
||||
options.arguments = {
|
||||
executable.string(), "--scene", snapshot.string(), "--assets",
|
||||
assets_.cache_root().string(), "--control", control_path_.string()};
|
||||
path_to_utf8(executable), "--scene", path_to_utf8(snapshot), "--assets",
|
||||
path_to_utf8(assets_.cache_root()), "--control", path_to_utf8(control_path_)};
|
||||
options.working_directory = config_.project_root;
|
||||
player_ = std::make_unique<Process>(options);
|
||||
log("Play started in a separate Player process");
|
||||
@@ -194,10 +195,34 @@ void Session::stop_player() {
|
||||
pending_play_scene_ = nullptr;
|
||||
}
|
||||
if (player_) {
|
||||
player_->cancel();
|
||||
const auto result = player_->poll();
|
||||
log(result.output);
|
||||
// Allow the normal Player shutdown path (including gameplay OnDestroy) first.
|
||||
// Keep Stop synchronous and bounded so starting a new Play cannot overlap this one.
|
||||
bool finished = false;
|
||||
try {
|
||||
atomic_write_json(control_path_,
|
||||
{{"sequence", ++control_sequence_}, {"command", "stop"}});
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
|
||||
for (;;) {
|
||||
const auto result = player_->poll();
|
||||
log(result.output);
|
||||
if (!result.running) {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= deadline)
|
||||
break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
log(std::string("Graceful Player stop failed: ") + error.what());
|
||||
}
|
||||
if (!finished) {
|
||||
log("Player did not finish graceful shutdown; terminating the process");
|
||||
player_->cancel();
|
||||
log(player_->poll().output);
|
||||
}
|
||||
player_.reset();
|
||||
control_path_.clear();
|
||||
log("Play stopped; authoring scene unchanged");
|
||||
}
|
||||
}
|
||||
@@ -224,7 +249,7 @@ void Session::poll() {
|
||||
log(value.kind + " completed");
|
||||
if (value.result.contains("schema"))
|
||||
try {
|
||||
load_schema(value.result.at("schema").get<std::string>());
|
||||
load_schema(path_from_utf8(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"))
|
||||
@@ -245,10 +270,9 @@ void Session::poll() {
|
||||
if (value.state == "succeeded" && schema_valid)
|
||||
try {
|
||||
const auto executable =
|
||||
value.result.value("player", (builds_.config().build_directory /
|
||||
executable_name("faset_player"))
|
||||
.string());
|
||||
launch_player(pending_play_scene_, executable);
|
||||
value.result.value("player", path_to_utf8(builds_.config().build_directory /
|
||||
executable_name("faset_player")));
|
||||
launch_player(pending_play_scene_, path_from_utf8(executable));
|
||||
} catch (const std::exception& error) {
|
||||
log(std::string("Play failed: ") + error.what());
|
||||
}
|
||||
@@ -332,7 +356,8 @@ void Session::register_commands() {
|
||||
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>());
|
||||
const auto file = project_path(config_.project_root,
|
||||
path_from_utf8(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);
|
||||
@@ -358,15 +383,25 @@ void Session::register_commands() {
|
||||
"a cancellable job ID; failure retains the last successful generation.",
|
||||
schema({{"path", text},
|
||||
{"settings", {{"type", "object"}}},
|
||||
{"allow_removed_outputs", boolean}},
|
||||
{"allow_removed_outputs", boolean},
|
||||
{"expected_generation", text},
|
||||
{"expected_active_generation", text}},
|
||||
{"path"}),
|
||||
[&](const Json& args) {
|
||||
assets::ImportRequest request;
|
||||
request.source = project_path(config_.project_root, args.at("path").get<std::string>());
|
||||
request.source = project_path(config_.project_root,
|
||||
path_from_utf8(args.at("path").get<std::string>()));
|
||||
request.settings = args.value("settings", Json(nullptr));
|
||||
request.allow_removed_outputs = args.value("allow_removed_outputs", false);
|
||||
request.expected_generation = args.value("expected_generation", std::string());
|
||||
request.expected_active_generation =
|
||||
args.value("expected_active_generation", std::string());
|
||||
auto task = std::make_shared<ImportTask>();
|
||||
task->id = "import-" + new_id();
|
||||
task->request = {{"path", generic_path_to_utf8(std::filesystem::relative(
|
||||
request.source, config_.project_root))},
|
||||
{"settings", request.settings},
|
||||
{"allow_removed_outputs", request.allow_removed_outputs}};
|
||||
imports_[task->id] = task;
|
||||
workers_.emplace_back([this, task, request] {
|
||||
{
|
||||
@@ -382,6 +417,8 @@ void Session::register_commands() {
|
||||
: "failed";
|
||||
task->result = {{"asset_id", result.asset_id},
|
||||
{"generation", result.generation},
|
||||
{"previous_generation", result.previous_generation},
|
||||
{"removed_output_ids", result.removed_output_ids},
|
||||
{"diagnostics", result.diagnostics},
|
||||
{"cache_hit", result.cache_hit},
|
||||
{"manifest", result.manifest}};
|
||||
@@ -417,7 +454,8 @@ void Session::register_commands() {
|
||||
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>()));
|
||||
project_path(config_.project_root,
|
||||
path_from_utf8(args.at("output").get<std::string>())));
|
||||
submitted_sources_[id] = signature;
|
||||
return Json{{"job", id}};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user