载入中...
搜索中...
未找到
EditorHostHotReload.cpp
浏览该文件的文档.
1#include "ui/EditorHost.h"
2
3#include "common/config.h"
4
5#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
6
7namespace eve::ui {
8
9std::string EditorHost::reloadResource(const std::string&) {
10 return "error: editor host hot reload unavailable on this platform";
11}
12
13std::string EditorHost::hotReloadStatus() const {
14 return "{\"enabled\":false,\"watchCount\":0,\"reloadCount\":0,\"failureCount\":0}";
15}
16
18
19} // namespace eve::ui
20
21#else
22
23#include "common/Module.h"
24#include "common/Runtime.h"
25#include "common/ScriptModule.h"
26
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>
32
33#include <algorithm>
34#include <cctype>
35#include <exception>
36#include <filesystem>
37#include <fstream>
38#include <map>
39#include <mutex>
40#include <sstream>
41#include <string>
42
43namespace eve::ui {
44namespace {
45
46struct HotReloadState {
47 int watchCount = 0;
48 int reloadCount = 0;
49 int failureCount = 0;
50 std::string lastPath;
51 std::string lastResult = "idle";
52};
53
54std::mutex g_hotReloadMutex;
55HotReloadState g_hotReloadState;
56
57std::string stringify(const Poco::Dynamic::Var& value) {
58 std::ostringstream out;
59 Poco::JSON::Stringifier::stringify(value, out, 0, 0);
60 return out.str();
61}
62
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;
65}
66
67std::string normalizedRelativePath(const std::string& root, const std::string& input, std::filesystem::path* absolute) {
68 if (input.empty()) return {};
69 std::error_code ec;
70 const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, ec);
71 if (ec) return {};
72 std::filesystem::path candidate(input);
73 if (!candidate.is_absolute()) candidate = canonicalRoot / candidate;
74 candidate = std::filesystem::weakly_canonical(candidate, ec);
75 if (ec) return {};
76
77 std::filesystem::path relative = std::filesystem::relative(candidate, canonicalRoot, ec);
78 if (ec || relative.empty()) return {};
79 for (const auto& part : relative) {
80 if (part == "..") return {};
81 }
82 if (absolute) *absolute = candidate;
83 std::string path = relative.generic_string();
84 while (path.rfind("./", 0) == 0) path.erase(0, 2);
85 return path;
86}
87
88std::string readText(const std::filesystem::path& path) {
89 std::ifstream in(path, std::ios::binary);
90 if (!in) return {};
91 return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
92}
93
94std::map<std::string, Poco::Dynamic::Var> editorValues(EditorHost& host, const std::string& editorId) {
95 std::map<std::string, Poco::Dynamic::Var> values;
96 try {
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;
102 if (object) {
103 for (const auto& key : object->getNames()) values[key] = object->get(key);
104 }
105 } catch (...) {
106 }
107 return values;
108}
109
110void restoreEditorValues(EditorHost& host, const std::string& editorId,
111 const std::map<std::string, Poco::Dynamic::Var>& values) {
112 for (const auto& [widget, value] : values) host.restoreEditorValue(editorId, widget, stringify(value));
113}
114
115std::string editorVmName(EditorHost& host, const std::string& editorId) {
116 try {
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", "");
125 }
126 } catch (...) {
127 }
128 return {};
129}
130
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();
136}
137
138std::string editorIdFromSource(const std::string& source) {
139 try {
140 Poco::JSON::Parser parser;
141 const Poco::JSON::Object::Ptr root = parser.parse(source).extract<Poco::JSON::Object::Ptr>();
142 return root ? root->optValue<std::string>("id", "") : std::string();
143 } catch (...) {
144 return {};
145 }
146}
147
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;
154 else
155 ++g_hotReloadState.reloadCount;
156}
157
158} // namespace
159
160std::string EditorHost::reloadResource(const std::string& input) {
161 if (!running()) return "error: host not started";
162 std::filesystem::path absolute;
163 const std::string path = normalizedRelativePath(gameRoot(), input, &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);
168 return result;
169 }
170
171 const std::string source = readText(absolute);
172 if (source.empty()) {
173 const std::string result = "error: resource is empty: " + path;
174 recordReload(path, result);
175 return result;
176 }
177
178 std::string 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()) {
184 const std::string sourceId = editorIdFromSource(source);
185 if (sourceId.empty()) {
186 result = "error: editor resource requires a valid id";
187 } else if (sourceId != editorId) {
188 result = "error: editor id must match resource filename: " + editorId;
189 } else {
190 const auto values = editorValues(*this, editorId);
191 result = applyEditor(source);
192 if (result.rfind("error:", 0) != 0) restoreEditorValues(*this, editorId, values);
193 }
194 } else if (!vmEditorId.empty()) {
195 const std::string vmName = editorVmName(*this, vmEditorId);
196 if (vmName.empty()) {
197 result = "error: editor ViewModel not found for: " + vmEditorId;
198 } else {
199 const auto values = editorValues(*this, vmEditorId);
200 result = registerVM(vmName, source, "game:/" + path);
201 if (result.rfind("error:", 0) != 0) restoreEditorValues(*this, vmEditorId, values);
202 }
203 } else if (path == "mcp.nut" || (path.rfind("mcp/", 0) == 0 && endsWith(path, ".nut"))) {
204 const std::string uri = "game:/" + path;
205 if (Runtime* rt = ModuleManager::runtime()) {
206 try {
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);
212 return result;
213 }
214 }
215 result = runScript(source, uri);
216 } else {
217 result = "error: unsupported MCP host resource: " + path;
218 }
219 recordReload(path, result);
220 return result;
221}
222
223std::string EditorHost::hotReloadStatus() const {
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));
233}
234
236 std::lock_guard<std::mutex> lock(g_hotReloadMutex);
237 g_hotReloadState.watchCount = std::max(0, count);
238}
239
240} // namespace eve::ui
241
242#endif
double value
int root
Definition AnimSmr.cpp:119
Vec3 relative
Definition AnimSmr.cpp:164
std::string lastPath
int reloadCount
int failureCount
std::string lastResult
int watchCount
std::map< std::string, Var > values
std::string widget
std::string vmName
std::uint64_t sourceId
EvpackChunkInput input
Definition Evpack.cpp:170
std::uint32_t key
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::string uri
std::string string
std::uint32_t count
Json object
UIHostHandle host
const UnitySourceAsset & source
static Runtime * runtime()
Active runtime associated with the last expose() call, or nullptr. @ownership Borrowed; the embedding...
Definition Module.cpp:292
EVENGINE_API_FOUNDATION public API.
Definition Runtime.h:189
void setHotReloadWatchCount(int count)
Set active project watch count reported by hotReloadStatus().
bool running() const
Running.
Definition EditorHost.h:49
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.
Definition EditorHost.h:53
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.
Definition Json.cpp:563