3#include "common/config.h"
5#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
10 return "error: editor host hot reload unavailable on this platform";
14 return "{\"enabled\":false,\"watchCount\":0,\"reloadCount\":0,\"failureCount\":0}";
27#include <Poco/Dynamic/Var.h>
28#include <Poco/JSON/Array.h>
29#include <Poco/JSON/Object.h>
30#include <Poco/JSON/Parser.h>
31#include <Poco/JSON/Stringifier.h>
46struct HotReloadState {
54std::mutex g_hotReloadMutex;
55HotReloadState g_hotReloadState;
58 std::ostringstream out;
59 Poco::JSON::Stringifier::stringify(
value, out, 0, 0);
63bool endsWith(
const std::string&
value,
const std::string& suffix) {
64 return value.size() >= suffix.size() &&
value.compare(
value.size() - suffix.size(), suffix.size(), suffix) == 0;
67std::string normalizedRelativePath(
const std::string&
root,
const std::string&
input, std::filesystem::path* absolute) {
68 if (
input.empty())
return {};
70 const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(
root, ec);
72 std::filesystem::path candidate(
input);
73 if (!candidate.is_absolute()) candidate = canonicalRoot / candidate;
74 candidate = std::filesystem::weakly_canonical(candidate, ec);
77 std::filesystem::path
relative = std::filesystem::relative(candidate, canonicalRoot, ec);
78 if (ec ||
relative.empty())
return {};
80 if (part ==
"..")
return {};
82 if (absolute) *absolute = candidate;
84 while (
path.rfind(
"./", 0) == 0)
path.erase(0, 2);
88std::string readText(
const std::filesystem::path&
path) {
89 std::ifstream in(
path, std::ios::binary);
91 return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
94std::map<std::string, Poco::Dynamic::Var> editorValues(EditorHost&
host,
const std::string& editorId) {
95 std::map<std::string, Poco::Dynamic::Var>
values;
97 Poco::JSON::Parser parser;
98 Poco::JSON::Object::Ptr
root = parser.parse(
host.editorState(editorId)).extract<Poco::JSON::Object::Ptr>();
99 Poco::JSON::Array::Ptr editors =
root ?
root->getArray(
"editors") :
nullptr;
100 Poco::JSON::Object::Ptr editor = editors && editors->size() ? editors->getObject(0) :
nullptr;
101 Poco::JSON::Object::Ptr
object = editor ? editor->getObject(
"values") :
nullptr;
110void restoreEditorValues(EditorHost&
host,
const std::string& editorId,
111 const std::map<std::string, Poco::Dynamic::Var>&
values) {
115std::string editorVmName(EditorHost&
host,
const std::string& editorId) {
117 Poco::JSON::Parser parser;
118 Poco::JSON::Object::Ptr
root = parser.parse(
host.listEditors()).extract<Poco::JSON::Object::Ptr>();
119 Poco::JSON::Array::Ptr editors =
root ?
root->getArray(
"editors") :
nullptr;
120 if (!editors)
return {};
121 for (
size_t i = 0; i < editors->size(); ++i) {
122 Poco::JSON::Object::Ptr editor = editors->getObject(
static_cast<unsigned>(i));
123 if (editor && editor->optValue<std::string>(
"id",
"") == editorId)
124 return editor->optValue<std::string>(
"vm",
"");
131std::string editorIdFromPath(
const std::string&
path,
const std::string& suffix) {
132 const std::string prefix =
"editors/";
133 if (
path.rfind(prefix, 0) != 0 || !endsWith(
path, suffix))
return {};
134 const std::string
id =
path.substr(prefix.size(),
path.size() - prefix.size() - suffix.size());
135 return id.find(
'/') == std::string::npos ?
id : std::string();
138std::string editorIdFromSource(
const std::string&
source) {
140 Poco::JSON::Parser parser;
141 const Poco::JSON::Object::Ptr
root = parser.parse(
source).extract<Poco::JSON::Object::Ptr>();
148void recordReload(
const std::string&
path,
const std::string& result) {
149 std::lock_guard<std::mutex> lock(g_hotReloadMutex);
150 g_hotReloadState.lastPath =
path;
151 g_hotReloadState.lastResult = result;
152 if (result.rfind(
"error:", 0) == 0)
153 ++g_hotReloadState.failureCount;
155 ++g_hotReloadState.reloadCount;
161 if (!
running())
return "error: host not started";
162 std::filesystem::path absolute;
164 if (
path.empty())
return "error: resource path is outside project root";
165 if (!std::filesystem::is_regular_file(absolute)) {
166 const std::string result =
"error: resource not found: " +
path;
167 recordReload(
path, result);
171 const std::string
source = readText(absolute);
173 const std::string result =
"error: resource is empty: " +
path;
174 recordReload(
path, result);
179 const std::string editorSuffix =
".editor.json";
180 const std::string vmSuffix =
".vm.nut";
181 const std::string editorId = editorIdFromPath(
path, editorSuffix);
182 const std::string vmEditorId = editorIdFromPath(
path, vmSuffix);
183 if (!editorId.empty()) {
186 result =
"error: editor resource requires a valid id";
188 result =
"error: editor id must match resource filename: " + editorId;
190 const auto values = editorValues(*
this, editorId);
192 if (result.rfind(
"error:", 0) != 0) restoreEditorValues(*
this, editorId,
values);
194 }
else if (!vmEditorId.empty()) {
195 const std::string
vmName = editorVmName(*
this, vmEditorId);
197 result =
"error: editor ViewModel not found for: " + vmEditorId;
199 const auto values = editorValues(*
this, vmEditorId);
201 if (result.rfind(
"error:", 0) != 0) restoreEditorValues(*
this, vmEditorId,
values);
203 }
else if (
path ==
"mcp.nut" || (
path.rfind(
"mcp/", 0) == 0 && endsWith(
path,
".nut"))) {
204 const std::string
uri =
"game:/" +
path;
207 for (
const auto& dep : rt->scriptModules().dependencies(
uri))
208 rt->scriptModules().reloadAffected(dep);
209 }
catch (
const std::exception&
error) {
210 result = std::string(
"error: ") +
error.what();
211 recordReload(
path, result);
217 result =
"error: unsupported MCP host resource: " +
path;
219 recordReload(
path, result);
224 std::lock_guard<std::mutex> lock(g_hotReloadMutex);
225 Poco::JSON::Object::Ptr out(
new Poco::JSON::Object());
226 out->set(
"enabled", g_hotReloadState.watchCount > 0);
227 out->set(
"watchCount", g_hotReloadState.watchCount);
228 out->set(
"reloadCount", g_hotReloadState.reloadCount);
229 out->set(
"failureCount", g_hotReloadState.failureCount);
230 out->set(
"lastPath", g_hotReloadState.lastPath);
231 out->set(
"lastResult", g_hotReloadState.lastResult);
232 return stringify(Poco::Dynamic::Var(out));
236 std::lock_guard<std::mutex> lock(g_hotReloadMutex);
237 g_hotReloadState.watchCount = std::max(0,
count);
std::map< std::string, Var > values
const UnitySourceAsset & source
static Runtime * runtime()
Active runtime associated with the last expose() call, or nullptr. @ownership Borrowed; the embedding...
EVENGINE_API_FOUNDATION public API.
void setHotReloadWatchCount(int count)
Set active project watch count reported by hotReloadStatus().
bool running() const
Running.
std::string applyEditor(const std::string &json)
JSON object/string: {id,title,vm,x,y,width,height,theme,children[]}.
const std::string & gameRoot() const
Game root.
std::string hotReloadStatus() const
Watch count, reload counters and latest diagnostic as compact JSON.
std::string registerVM(const std::string &name, const std::string &source, const std::string &sourceName={})
Compile Squirrel source in the host VM, then register table name.
std::string reloadResource(const std::string &path)
Reload mcp.nut, scripts under mcp, or editor VM and manifest files.
std::string runScript(const std::string &source, const std::string &sourceName={})
Compile and run Squirrel source in the host VM.
Result< std::string > stringify(const eve::Value &value)
Serialize the canonical owning Value as deterministic compact JSON.