Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export

This commit is contained in:
Emil
2026-09-18 03:40:15 +03:00
parent 5c6b24d34d
commit 999686a896
125 changed files with 16086 additions and 1714 deletions
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <faset/core/json.hpp>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
namespace faset::editor {
struct BuildConfig {
std::filesystem::path project_root;
std::filesystem::path engine_root;
std::filesystem::path build_directory;
std::filesystem::path cache_root;
// Separate CMake caches prevent an export from changing the active development build.
std::string configuration{"Debug"};
std::string export_configuration{"Release"};
std::string cmake{"cmake"};
std::string generator{"Ninja"};
std::vector<std::string> configure_arguments;
};
struct JobStatus {
std::string id, kind, state{"queued"}, stage{"queued"};
double progress{};
std::string log, error;
Json result = Json::object();
bool finished() const {
return state == "succeeded" || state == "failed" || state == "cancelled";
}
Json json() const;
};
// One serialized build/cook worker per project. GUI and MCP use the same service.
// Input scenes must be resolved authoring snapshots, independent of live runtime state.
class BuildService {
public:
explicit BuildService(BuildConfig);
~BuildService();
BuildService(const BuildService&) = delete;
BuildService& operator=(const BuildService&) = delete;
void scaffold(const std::string& name, int dimension);
std::string start_build();
std::string start_cook(Json resolved_scene);
// Publishes output/generations/<id>; current.json changes only after all validation succeeds.
std::string start_export(Json resolved_scene, const std::filesystem::path& output_directory);
JobStatus job(const std::string& id) const;
std::vector<JobStatus> jobs() const;
void cancel(const std::string& id);
JobStatus wait(const std::string& id);
const BuildConfig& config() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
void write_cooked_scene(const std::filesystem::path& path, const Json& resolved_scene);
} // namespace faset::editor
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <faset/authoring/service.hpp>
#include <functional>
#include <map>
#include <string>
namespace faset::editor {
class Commands {
public:
using Handler = std::function<Json(const Json&)>;
explicit Commands(authoring::AuthoringService& authoring);
void add(std::string name, std::string description, Json input_schema, Handler handler,
bool read_only = false);
Json list() const;
void remove(const std::string& name);
Json call(const std::string& name, const Json& arguments);
Json resolved_scene(const std::string& document) const;
authoring::AuthoringService& authoring() {
return authoring_;
}
static Json object_schema(Json properties, Json required = Json::array());
private:
struct Command {
Json descriptor;
Handler handler;
};
authoring::AuthoringService& authoring_;
std::map<std::string, Command> commands_;
};
} // namespace faset::editor
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <faset/editor/session.hpp>
#include <faset/ui/ui.hpp>
#include <memory>
namespace faset::editor {
// Owns authoring presentation only. Player execution and MCP transport stay in
// the application.
class EditorUI {
public:
EditorUI(Session&, render::Renderer&, const std::filesystem::path& font,
const std::filesystem::path& styles);
~EditorUI();
EditorUI(const EditorUI&) = delete;
EditorUI& operator=(const EditorUI&) = delete;
void frame(const std::vector<render::Event>&);
const render::Snapshot& snapshot() const;
ui::Context& widgets();
const std::string& current_document() const;
void select_document(const std::string& document);
const std::string& selected_entity() const;
void select_entity(const std::string& entity);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace faset::editor
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <faset/editor/commands.hpp>
#include <optional>
#include <string>
#include <vector>
namespace faset::editor {
// MCP is compiled exclusively into the Editor / headless authoring executable.
class McpServer {
public:
explicit McpServer(Commands& commands) : commands_(commands) {}
std::optional<Json> handle(const Json& message);
Json parse_error() const;
private:
Commands& commands_;
bool initialized_ = false, ready_ = false;
};
class StdioTransport {
public:
// Nonblocking: GUI and MCP can share the same authoring session and event loop.
std::vector<std::string> poll();
bool closed() const noexcept {
return closed_;
}
void send(const Json& message);
private:
std::string buffer_;
bool closed_ = false;
};
} // namespace faset::editor
+42
View File
@@ -0,0 +1,42 @@
#pragma once
/* Exact-build native Editor SDK. No STL types or ownership cross this ABI. */
#include <faset/editor/sdk_build.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define FASET_EDITOR_API_VERSION 1u
#if defined(_WIN32)
#define FASET_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FASET_PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
/* UTF-8 JSON is borrowed for the duration of a call. write() copies output in
the receiving module. Return zero on success; errors use {code,message}. */
typedef void (*FasetWrite)(void* receiver, const char* utf8, uint64_t length);
typedef int (*FasetCommand)(void* user, const char* arguments_json, FasetWrite write,
void* receiver);
typedef struct FasetEditorHost {
uint32_t api_version;
uint32_t struct_size;
const char* build_fingerprint;
void* context;
int (*register_command)(void* context, const char* descriptor_json, FasetCommand callback,
void* user);
int (*register_panel)(void* context, const char* panel_json);
int (*invoke_command)(void* context, const char* name, const char* arguments_json,
FasetWrite write, void* receiver);
void (*log)(void* context, const char* utf8);
} FasetEditorHost;
typedef struct FasetEditorPlugin {
uint32_t api_version;
uint32_t struct_size;
const char* build_fingerprint;
void* user;
void (*shutdown)(void* user);
} FasetEditorPlugin;
/* Required exported symbol. Called once at startup, after dependencies load. */
typedef int (*FasetPluginEntry)(const FasetEditorHost* host, FasetEditorPlugin* plugin);
#ifdef __cplusplus
}
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <faset/editor/commands.hpp>
#include <functional>
#include <memory>
namespace faset::editor {
class PluginManager {
public:
using Logger = std::function<void(std::string)>;
explicit PluginManager(Commands&, Logger);
~PluginManager();
PluginManager(const PluginManager&) = delete;
PluginManager& operator=(const PluginManager&) = delete;
// Discover and validate the complete dependency graph before loading code.
// Individual failures are reported; dependents are never loaded after one.
void load(const std::filesystem::path& directory);
Json status() const;
Json panels() const;
static std::string fingerprint();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace faset::editor
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <faset/assets/asset_pipeline.hpp>
#include <faset/core/process.hpp>
#include <faset/editor/build_service.hpp>
#include <faset/editor/commands.hpp>
#include <faset/editor/plugins.hpp>
#include <memory>
#include <mutex>
#include <thread>
namespace faset::editor {
struct SessionConfig {
std::filesystem::path project_root, engine_root, binary_directory;
};
class Session {
public:
explicit Session(SessionConfig);
~Session();
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
authoring::AuthoringService& authoring() {
return authoring_;
}
Commands& commands() {
return commands_;
}
void poll();
const std::vector<std::string>& logs() const {
return logs_;
}
Json project() const;
Json plugin_panels() const {
return plugins_ ? plugins_->panels() : Json::array();
}
void scaffold(const std::string& name, int dimension);
const SessionConfig& config() const {
return config_;
}
bool playing() const {
return bool(player_);
}
void log(std::string message);
private:
struct ImportTask;
void register_commands();
Json assets_list() const;
Json jobs() const;
Json job(const std::string& id) const;
void load_schema(const std::filesystem::path& path);
void launch_player(Json scene, const std::filesystem::path& executable);
void stop_player();
SessionConfig config_;
authoring::AuthoringService authoring_;
Commands commands_;
assets::AssetPipeline assets_;
BuildService builds_;
std::map<std::string, std::shared_ptr<ImportTask>> imports_;
std::vector<std::jthread> workers_;
std::vector<std::string> logs_;
std::map<std::string, std::string> observed_jobs_;
std::unique_ptr<Process> player_;
std::filesystem::path control_path_;
std::uint64_t control_sequence_ = 0;
std::string pending_play_job_;
Json pending_play_scene_;
std::unique_ptr<PluginManager> plugins_;
};
} // namespace faset::editor