载入中...
搜索中...
未找到
EditorHost.cpp
浏览该文件的文档.
1#include "ui/EditorHost.h"
2
3#include "common/config.h"
4
5#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
6
7// The browser runtime trims Poco / DevTools / the host path. Keep EVUI
8// linkable with a no-op implementation; `eve mcp` is desktop-only anyway.
9namespace eve::ui {
10struct EditorHost::Impl {};
11
12EditorHost& EditorHost::instance() {
13 static EditorHost* inst = new EditorHost();
14 return *inst;
15}
16EditorHost::EditorHost() = default;
17EditorHost::~EditorHost() = default;
18void EditorHost::start(ssq::VM&, const std::string&, bool) {}
19void EditorHost::stop() {}
20bool EditorHost::isWindowOpen() const { return false; }
21std::string EditorHost::openWindow(const std::string&, int, int) {
22 return "error: editor host unavailable on this platform";
23}
24std::string EditorHost::closeWindow() { return "ok"; }
25std::string EditorHost::windowState() const { return "{}"; }
26std::string EditorHost::applyEditor(const std::string&) {
27 return "error: editor host unavailable on this platform";
28}
29std::string EditorHost::removeEditor(const std::string&) {
30 return "error: editor host unavailable on this platform";
31}
32std::string EditorHost::listEditors() const { return "{\"editors\":[]}"; }
33std::string EditorHost::editorState(const std::string&) const { return "{\"editors\":[]}"; }
35std::string EditorHost::setEditorValue(const std::string&, const std::string&, const std::string&) {
36 return "error: editor host unavailable on this platform";
37}
38std::string EditorHost::consumeEvents(const std::string&) { return "[]"; }
39std::string EditorHost::widgetRect(const std::string&, const std::string&) const {
40 return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
41}
42std::string EditorHost::registerVM(const std::string&, const std::string&, const std::string&) {
43 return "error: editor host unavailable on this platform";
44}
45std::string EditorHost::unregisterVM(const std::string&) {
46 return "error: editor host unavailable on this platform";
47}
48std::string EditorHost::saveEditor(const std::string&) {
49 return "error: editor host unavailable on this platform";
50}
51std::string EditorHost::unloadEditor(const std::string&) {
52 return "error: editor host unavailable on this platform";
53}
55std::string EditorHost::runScript(const std::string&, const std::string&) {
56 return "error: editor host unavailable on this platform";
57}
58std::string EditorHost::capture(const std::string&) {
59 return "error: editor host unavailable on this platform";
60}
61std::string EditorHost::status() const {
62 return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
63}
64void EditorHost::frame() {}
66void EditorHost::exposeScriptApi(ssq::VM&) {}
67} // namespace eve::ui
68
69#else // full implementation
70
71#include "common/Module.h"
72#include "common/Runtime.h"
74#include "common/ScriptError.h"
75#include "common/ScriptModule.h"
76#include "filesystem/FileData.h"
77#include "graphics/Graphics.h"
78#include "image/ImageData.h"
80#include "timer/Timer.h"
81#include "ui/UI.h"
83#include "window/Window.h"
84
85#include <Poco/Dynamic/Var.h>
86#include <Poco/Exception.h>
87#include <Poco/JSON/Array.h>
88#include <Poco/JSON/Object.h>
89#include <Poco/JSON/Parser.h>
90#include <Poco/JSON/Stringifier.h>
91
92#include <imgui.h>
93#include <imgui_internal.h>
94#include <simplesquirrel/simplesquirrel.hpp>
95#include <squirrel.h>
96
97#include <algorithm>
98#include <cstdlib>
99#include <cstring>
100#include <cstdio>
101#include <fstream>
102#include <filesystem>
103#include <fstream>
104#include <functional>
105#include <map>
106#include <sstream>
107#include <string>
108#include <utility>
109#include <vector>
110
111namespace eve::ui {
112namespace {
113
114using Poco::Dynamic::Var;
115using Poco::JSON::Array;
116using Poco::JSON::Object;
117using Poco::JSON::Parser;
118
119std::string jsonStringify(const Var& v) {
120 std::ostringstream oss;
121 Poco::JSON::Stringifier::stringify(v, oss, 0, 0);
122 return oss.str();
123}
124
125Object::Ptr parseObject(const std::string& json) {
126 Poco::JSON::Parser parser;
127 return parser.parse(json).extract<Object::Ptr>();
128}
129
130std::string strOf(Object::Ptr o, const char* key, const std::string& def = {}) {
131 if (!o || !o->has(key)) return def;
132 try {
133 return o->get(key).convert<std::string>();
134 } catch (...) {
135 return def;
136 }
137}
138
139int intOf(Object::Ptr o, const char* key, int def = 0) {
140 if (!o || !o->has(key)) return def;
141 try {
142 return o->get(key).convert<int>();
143 } catch (...) {
144 return def;
145 }
146}
147
148float floatOf(Object::Ptr o, const char* key, float def = 0.f) {
149 if (!o || !o->has(key)) return def;
150 try {
151 return static_cast<float>(o->get(key).convert<double>());
152 } catch (...) {
153 return def;
154 }
155}
156
157bool boolOf(Object::Ptr o, const char* key, bool def = false) {
158 if (!o || !o->has(key)) return def;
159 try {
160 return o->get(key).convert<bool>();
161 } catch (...) {
162 return def;
163 }
164}
165
166float varFloat(const Var& v, float def = 0.f) {
167 try {
168 return static_cast<float>(v.convert<double>());
169 } catch (...) {
170 return def;
171 }
172}
173
174bool varBool(const Var& v, bool def = false) {
175 try {
176 return v.convert<bool>();
177 } catch (...) {
178 return def;
179 }
180}
181
182std::string varString(const Var& v, const std::string& def = {}) {
183 try {
184 return v.convert<std::string>();
185 } catch (...) {
186 return def;
187 }
188}
189
190Array::Ptr varToArray(const Var& v) {
191 try {
192 return v.extract<Array::Ptr>();
193 } catch (...) {
194 return nullptr;
195 }
196}
197
198std::vector<float> varFloats(const Var& v) {
199 std::vector<float> out;
200 Array::Ptr arr = varToArray(v);
201 if (!arr) return out;
202 for (size_t i = 0; i < arr->size(); ++i) {
203 try {
204 out.push_back(
205 static_cast<float>(arr->get(static_cast<unsigned int>(i)).convert<double>()));
206 } catch (...) {
207 out.push_back(0.f);
208 }
209 }
210 return out;
211}
212
214std::vector<float> widgetFloatInit(const Var& v) {
215 std::vector<float> init = varFloats(v);
216 if (!init.empty()) return init;
217 try {
218 init.push_back(static_cast<float>(v.convert<double>()));
219 } catch (...) {
220 }
221 return init;
222}
223
224std::vector<std::string> varStrings(const Var& v) {
225 std::vector<std::string> out;
226 Array::Ptr arr = varToArray(v);
227 if (!arr) return out;
228 for (size_t i = 0; i < arr->size(); ++i)
229 out.push_back(varString(arr->get(static_cast<unsigned int>(i))));
230 return out;
231}
232
233// ---- raw Squirrel helpers -------------------------------------------------
234
235Var stackVar(HSQUIRRELVM vm, SQInteger index) {
236 switch (sq_gettype(vm, index)) {
237 case OT_NULL:
238 return Var();
239 case OT_BOOL: {
240 SQBool b = SQFalse;
241 if (SQ_SUCCEEDED(sq_getbool(vm, index, &b))) return Var(bool(b));
242 return Var();
243 }
244 case OT_INTEGER: {
245 SQInteger i = 0;
246 if (SQ_SUCCEEDED(sq_getinteger(vm, index, &i)))
247 return Var(static_cast<Poco::Int64>(i));
248 return Var();
249 }
250 case OT_FLOAT: {
251 SQFloat f = 0.f;
252 if (SQ_SUCCEEDED(sq_getfloat(vm, index, &f)))
253 return Var(static_cast<double>(f));
254 return Var();
255 }
256 case OT_STRING: {
257 const SQChar* s = nullptr;
258 if (SQ_SUCCEEDED(sq_getstring(vm, index, &s)) && s) return Var(std::string(s));
259 return Var();
260 }
261 case OT_ARRAY: {
262 HSQOBJECT obj;
263 if (SQ_FAILED(sq_getstackobj(vm, index, &obj))) return Var();
264 sq_pushobject(vm, obj);
265 sq_pushnull(vm);
266 Array::Ptr arr = new Array();
267 while (SQ_SUCCEEDED(sq_next(vm, -2))) {
268 arr->add(stackVar(vm, -1));
269 sq_pop(vm, 2);
270 }
271 sq_pop(vm, 1);
272 return Var(arr);
273 }
274 default:
275 return Var();
276 }
277}
278
279void pushVar(HSQUIRRELVM vm, const Var& v) {
280 if (v.isEmpty()) {
281 sq_pushnull(vm);
282 return;
283 }
284 try {
285 if (v.isBoolean()) {
286 sq_pushbool(vm, v.convert<bool>() ? SQTrue : SQFalse);
287 return;
288 }
289 if (v.isNumeric()) {
290 sq_pushfloat(vm, static_cast<SQFloat>(v.convert<double>()));
291 return;
292 }
293 if (v.isString()) {
294 const std::string s = v.convert<std::string>();
295 sq_pushstring(vm, s.c_str(), static_cast<SQInteger>(s.size()));
296 return;
297 }
298 if (v.isArray()) {
299 Array::Ptr arr;
300 try {
301 arr = v.extract<Array::Ptr>();
302 } catch (...) {
303 sq_pushnull(vm);
304 return;
305 }
306 sq_newarray(vm, 0);
307 if (arr) {
308 for (size_t i = 0; i < arr->size(); ++i) {
309 pushVar(vm, arr->get(static_cast<unsigned int>(i)));
310 if (SQ_FAILED(sq_arrayappend(vm, -2))) break;
311 }
312 }
313 return;
314 }
315 } catch (...) {
316 }
317 sq_pushnull(vm);
318}
319
321std::string runSquirrel(HSQUIRRELVM vm, const std::string& source, const char* name) {
322 const SQInteger top = sq_gettop(vm);
323 // ScriptCompiler::compileBuffer records `import` edges and compiles those
324 // modules. A raw sq_compilebuffer leaves them Compiled-but-not-Ready, so
325 // the import opcode then fails with "module was not instantiated".
327 static_cast<SQInteger>(source.size()),
328 name, SQTrue))) {
330 sq_settop(vm, top);
331 return text.empty() ? "compile failed" : ("compile failed: " + text);
332 }
333 if (Runtime* rt = ModuleManager::runtime()) {
334 try {
335 rt->scriptModules().instantiateDependencies(name);
336 } catch (const std::exception& error) {
337 sq_settop(vm, top);
338 return error.what();
339 }
340 }
341 sq_pushroottable(vm);
342 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
344 if (text.empty()) {
345 const SQChar* msg = nullptr;
346 if (sq_gettype(vm, -1) == OT_STRING) sq_getstring(vm, -1, &msg);
347 text = msg ? std::string(msg) : std::string{};
348 }
349 if (text.empty()) {
350 if (Runtime* rt = ModuleManager::runtime()) {
351 if (auto failure = rt->scriptModules().takeCompilationFailure()) text = *failure;
352 }
353 }
354 if (text.empty()) text = "unknown";
355 sq_settop(vm, top);
356 return "runtime failed: " + text;
357 }
358 sq_settop(vm, top);
359 return {};
360}
361
362void callRootFunc(HSQUIRRELVM vm, const std::string& name, const std::vector<Var>& args) {
363 const SQInteger top = sq_gettop(vm);
364 sq_pushroottable(vm);
365 sq_pushstring(vm, name.c_str(), static_cast<SQInteger>(name.size()));
366 if (SQ_FAILED(sq_get(vm, -2))) {
367 sq_settop(vm, top);
368 return; // hook not defined
369 }
370 if (sq_gettype(vm, -1) != OT_CLOSURE) {
371 sq_settop(vm, top);
372 return;
373 }
374 sq_pushroottable(vm);
375 for (const auto& a : args) pushVar(vm, a);
376 if (SQ_FAILED(sq_call(vm, static_cast<SQInteger>(args.size()) + 1, SQFalse, SQTrue)))
377 fprintf(stderr, "eve.host: %s() failed\n", name.c_str());
378 sq_settop(vm, top);
379}
380
381bool callTableFunc(HSQUIRRELVM vm, const ssq::Table& tbl, const std::string& name,
382 const std::vector<Var>& args, std::string* err) {
383 if (name.empty()) return true;
384 const SQInteger top = sq_gettop(vm);
385 try {
386 ssq::Function fn = tbl.findFunc(name.c_str());
387 sq_pushobject(vm, fn.getRaw());
388 sq_pushobject(vm, tbl.getRaw());
389 for (const auto& a : args) pushVar(vm, a);
390 if (SQ_FAILED(sq_call(vm, static_cast<SQInteger>(args.size()) + 1, SQFalse, SQTrue))) {
391 if (err) *err = "runtime failed: " + name;
392 sq_settop(vm, top);
393 return false;
394 }
395 sq_settop(vm, top);
396 return true;
397 } catch (const ssq::NotFoundException&) {
398 sq_settop(vm, top);
399 return true; // optional callback / command absent
400 } catch (const std::exception& e) {
401 if (err) *err = e.what();
402 sq_settop(vm, top);
403 return false;
404 }
405}
406
408bool splitBindPath(const std::string& bind, std::string& slot, std::string& nested) {
409 if (bind.rfind("vm.", 0) != 0) return false;
410 std::string rest = bind.substr(3);
411 const size_t dot = rest.find('.');
412 if (dot == std::string::npos) {
413 slot = rest;
414 nested.clear();
415 } else if (rest.find('.', dot + 1) == std::string::npos) {
416 slot = rest.substr(0, dot);
417 nested = rest.substr(dot + 1);
418 } else {
419 return false; // only one nesting level supported in v1
420 }
421 return !slot.empty();
422}
423
424Var objectToVar(ssq::VM& vm, const ssq::Object& o) {
425 // Push the object and convert through the stack so arrays recurse cleanly.
426 HSQUIRRELVM v = vm.getHandle();
427 const SQInteger top = sq_gettop(v);
428 sq_pushobject(v, o.getRaw());
429 Var out = stackVar(v, -1);
430 sq_settop(v, top);
431 return out;
432}
433
434Var readVMVar(ssq::VM& vm, const ssq::Table& tbl, const std::string& bind, bool* ok = nullptr) {
435 std::string slot, nested;
436 if (ok) *ok = false;
437 if (!splitBindPath(bind, slot, nested)) return Var();
438 try {
439 ssq::Object o = tbl.find(slot.c_str());
440 if (!nested.empty()) {
441 if (o.getType() != ssq::Type::TABLE) return Var();
442 o = o.toTable().find(nested.c_str());
443 }
444 if (ok) *ok = true;
445 return objectToVar(vm, o);
446 } catch (...) {
447 return Var();
448 }
449}
450
451void writeVMVar(ssq::VM& vm, const ssq::Table& tbl, const std::string& bind, const Var& value) {
452 std::string slot, nested;
453 if (!splitBindPath(bind, slot, nested)) return;
454 HSQUIRRELVM v = vm.getHandle();
455 const SQInteger top = sq_gettop(v);
456 if (nested.empty()) {
457 sq_pushobject(v, tbl.getRaw());
458 sq_pushstring(v, slot.c_str(), static_cast<SQInteger>(slot.size()));
459 pushVar(v, value);
460 sq_newslot(v, -3, false);
461 } else {
462 try {
463 ssq::Table t2 = tbl.find(slot.c_str()).toTable();
464 sq_pushobject(v, t2.getRaw());
465 sq_pushstring(v, nested.c_str(), static_cast<SQInteger>(nested.size()));
466 pushVar(v, value);
467 sq_newslot(v, -3, false);
468 } catch (...) {
469 }
470 }
471 sq_settop(v, top);
472}
473
474struct HostEvent {
475 std::string type; // "click" | "change" | "closed"
476 std::string widget;
477 Var value;
478};
479
480struct Editor {
481 std::string id;
482 std::string title;
483 std::string vmName;
484 Object::Ptr view;
485 float x = 0.f, y = 0.f, width = 0.f, height = 0.f;
486 bool resizable = true;
487 bool collapsible = true;
488 std::string layout = "vertical";
489 Object::Ptr theme;
490 std::map<std::string, Var> values;
491 std::vector<HostEvent> events;
492 std::map<std::string, ImVec2> rectMin, rectMax;
493 bool removed = false;
494};
495
496void forEachWidget(Object::Ptr parent, const std::function<void(Object::Ptr)>& fn) {
497 if (!parent) return;
498 Array::Ptr kids = parent->has("children") ? parent->getArray("children") : nullptr;
499 if (!kids) return;
500 for (size_t i = 0; i < kids->size(); ++i) {
501 Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
502 if (!w) continue;
503 fn(w);
504 forEachWidget(w, fn);
505 }
506}
507
508Object::Ptr findWidget(Object::Ptr parent, const std::string& id) {
509 if (!parent || id.empty()) return nullptr;
510 Array::Ptr kids = parent->has("children") ? parent->getArray("children") : nullptr;
511 if (!kids) return nullptr;
512 for (size_t i = 0; i < kids->size(); ++i) {
513 Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
514 if (!w) continue;
515 if (strOf(w, "id") == id) return w;
516 if (Object::Ptr sub = findWidget(w, id)) return sub;
517 }
518 return nullptr;
519}
520
521std::string widgetIdLabel(Object::Ptr w) {
522 const std::string id = strOf(w, "id");
523 const std::string label = strOf(w, "label");
524 if (label.empty()) return "##" + id;
525 if (id.empty()) return label;
526 return label + "##" + id;
527}
528
530std::string vmFuncName(const std::string& path) {
531 if (path.rfind("vm.", 0) == 0) return path.substr(3);
532 return path;
533}
534
535void recordRect(Editor& ed, const std::string& id) {
536 if (id.empty()) return;
537 ed.rectMin[id] = ImGui::GetItemRectMin();
538 ed.rectMax[id] = ImGui::GetItemRectMax();
539}
540
541void emitEvent(Editor& ed, const std::string& type, const std::string& widget, const Var& value) {
542 ed.events.push_back({type, widget, value});
543}
544
545void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids, const std::string& layout);
546void syncVMToView(EditorHost::Impl& I, Editor& ed);
547
548} // namespace
549
550// Out-of-line definition of the pimpl. Lives at eve::ui scope (not inside the
551// anonymous namespace) so it can be named as eve::ui::EditorHost::Impl.
553 ssq::VM* vm = nullptr;
554 bool allowWindow = true;
555 std::string rootDir;
558 eve::ui::UI* ui = nullptr;
561 bool windowOpen = false;
562 bool inFrame = false;
563 std::string windowTitle;
564 std::map<std::string, Editor> editors;
565 std::map<std::string, ssq::Table> vms;
566 std::map<std::string, std::string> vmSources;
567};
568
569// ---------------------------------------------------------------------------
570// public surface
571// ---------------------------------------------------------------------------
572
574 // Intentionally leaked: the host VM may be torn down after the singleton.
575 static EditorHost* inst = new EditorHost();
576 return *inst;
577}
578
579EditorHost::EditorHost() = default;
580EditorHost::~EditorHost() = default;
581
582void EditorHost::start(ssq::VM& vm, const std::string& gameRoot, bool allowWindow) {
583 if (running_) return;
584 impl_ = std::make_unique<Impl>();
585 impl_->vm = &vm;
586 impl_->allowWindow = allowWindow;
587 allowWindow_ = allowWindow;
588 gameRoot_ = gameRoot.empty() ? std::filesystem::current_path().string() : gameRoot;
589 impl_->rootDir = gameRoot_;
590 running_ = true;
592}
593
595 if (!running_) return;
596 closeWindow();
597 impl_.reset();
598 running_ = false;
599 exitRequested_ = false;
600}
601
602std::string EditorHost::openWindow(const std::string& title, int width, int height) {
603 if (!impl_) return "error: host not started";
604 auto& I = *impl_;
605 if (!I.allowWindow) return "error: window creation disabled";
606 try {
607 if (!I.win) I.win = ModuleManager::requireInstance<eve::window::Window>("Window");
608 if (!I.gfx) I.gfx = ModuleManager::requireInstance<eve::graphics::Graphics>("Graphics");
609 // setWindowSettings() always close()+SDL_CreateWindow. Reuse any live
610 // OS window (load.nut, a previous openWindow, or MCP attaching later).
611 const bool alreadyOpen = I.win->isOpen() || I.win->getHandle() != nullptr;
612 if (alreadyOpen) {
613 std::fprintf(stderr, "[editor-host] reuse existing window %dx%d\n",
614 I.win->getWidth(), I.win->getHeight());
615 if (!title.empty()) I.win->setWindowTitle(title);
616 } else {
618 s.width = static_cast<uint16_t>(width > 0 ? width : 1280);
619 s.height = static_cast<uint16_t>(height > 0 ? height : 800);
620 s.centered = true;
621 s.resizable = true;
622 std::fprintf(stderr, "[editor-host] create window %dx%d\n",
623 static_cast<int>(s.width), static_cast<int>(s.height));
624 if (!I.win->setWindowSettings(s)) return "error: setWindowSettings failed";
625 I.win->setWindowTitle(title.empty() ? "EVEngine AI Host" : title);
626 }
627 if (!I.ui) I.ui = ModuleManager::requireInstance<eve::ui::UI>("UI");
628 I.windowOpen = true;
629 I.windowTitle = title.empty() ? I.win->getWindowTitle() : title;
630 // Keep screen readback on while the host window is open: the AI's whole
631 // feedback loop is "render -> capture", so every presented frame should
632 // be available to eve_host_capture immediately (the Vulkan backend
633 // installs the present-copy hook as soon as readback is enabled).
634 if (I.gfx) I.gfx->setScreenReadbackEnabled(true);
635 return "ok";
636 } catch (const std::exception& e) {
637 return std::string("error: ") + e.what();
638 }
639}
640
642 if (!impl_) return "ok";
643 auto& I = *impl_;
644 if (I.windowOpen && I.win) {
645 try {
646 I.win->close();
647 } catch (...) {
648 }
649 if (I.ui) I.ui->shutdownBackend();
650 I.windowOpen = false;
651 }
652 return "ok";
653}
654
656 return impl_ && impl_->windowOpen;
657}
658
659std::string EditorHost::windowState() const {
660 if (!impl_ || !impl_->windowOpen) return "{\"open\":false}";
661 Object::Ptr o = new Object();
662 o->set("open", true);
663 o->set("title", impl_->windowTitle);
664 if (impl_->win) {
665 o->set("width", impl_->win->getWidth());
666 o->set("height", impl_->win->getHeight());
667 }
668 return jsonStringify(Var(o));
669}
670
671std::string EditorHost::applyEditor(const std::string& json) {
672 if (!impl_) return "error: host not started";
673 auto& I = *impl_;
674 try {
675 Object::Ptr root = parseObject(json);
676 const std::string id = strOf(root, "id");
677 if (id.empty()) return "error: editor requires id";
678 Editor ed;
679 ed.id = id;
680 ed.title = strOf(root, "title", id);
681 ed.vmName = strOf(root, "vm");
682 ed.view = root;
683 ed.x = static_cast<float>(intOf(root, "x", 0));
684 ed.y = static_cast<float>(intOf(root, "y", 0));
685 ed.width = static_cast<float>(intOf(root, "width", 0));
686 ed.height = static_cast<float>(intOf(root, "height", 0));
687 ed.resizable = boolOf(root, "resizable", true);
688 ed.collapsible = boolOf(root, "collapsible", true);
689 ed.layout = strOf(root, "layout", "vertical");
690 if (root->has("theme")) ed.theme = root->getObject("theme");
691 forEachWidget(root, [&](Object::Ptr w) {
692 const std::string wid = strOf(w, "id");
693 if (wid.empty()) return;
694 if (w->has("value")) ed.values[wid] = w->get("value");
695 });
696 I.editors[id] = std::move(ed);
697 if (!I.windowOpen && I.allowWindow) {
698 // Prefer the live window size (config.nut) over the 1280x800
699 // fallback so applyEditor cannot close()+recreate a different size.
700 int w = 1280;
701 int h = 800;
702 try {
703 if (!I.win) I.win = ModuleManager::requireInstance<eve::window::Window>("Window");
704 if (I.win->isOpen() || I.win->getHandle() != nullptr) {
705 w = I.win->getWidth();
706 h = I.win->getHeight();
707 }
708 } catch (...) {
709 }
710 std::string err = openWindow(ed.title + " - EVEngine AI Host", w, h);
711 if (err.rfind("error:", 0) == 0) return err;
712 }
713 return editorState(id);
714 } catch (const std::exception& e) {
715 return std::string("error: ") + e.what();
716 }
717}
718
719std::string EditorHost::removeEditor(const std::string& id) {
720 if (!impl_) return "error: host not started";
721 auto it = impl_->editors.find(id);
722 if (it == impl_->editors.end()) return "error: editor not found: " + id;
723 impl_->editors.erase(it);
724 return "ok";
725}
726
727std::string EditorHost::listEditors() const {
728 if (!impl_) return "{\"editors\":[]}";
729 Array::Ptr arr = new Array();
730 for (const auto& [k, ed] : impl_->editors) {
731 Object::Ptr o = new Object();
732 o->set("id", k);
733 o->set("title", ed.title);
734 o->set("vm", ed.vmName);
735 arr->add(o);
736 }
737 Object::Ptr root = new Object();
738 root->set("editors", arr);
739 return jsonStringify(Var(root));
740}
741
742namespace {
743
744Poco::JSON::Object::Ptr editorStateObject(const Editor& ed) {
745 Object::Ptr o = new Object();
746 o->set("id", ed.id);
747 o->set("title", ed.title);
748 o->set("vm", ed.vmName);
749 Object::Ptr vals = new Object();
750 for (const auto& [k, v] : ed.values) vals->set(k, v);
751 o->set("values", vals);
752 Array::Ptr evs = new Array();
753 for (const auto& e : ed.events) {
754 Object::Ptr eo = new Object();
755 eo->set("editor", ed.id);
756 eo->set("widget", e.widget);
757 eo->set("type", e.type);
758 if (!e.value.isEmpty()) eo->set("value", e.value);
759 evs->add(eo);
760 }
761 o->set("events", evs);
762 return o;
763}
764
765} // namespace
766
767std::string EditorHost::editorState(const std::string& id) const {
768 if (!impl_) return "{\"editors\":[]}";
769 const_cast<EditorHost*>(this)->syncBindings();
770 Array::Ptr arr = new Array();
771 if (!id.empty()) {
772 auto it = impl_->editors.find(id);
773 if (it == impl_->editors.end()) return "error: editor not found: " + id;
774 arr->add(editorStateObject(it->second));
775 } else {
776 for (const auto& [k, ed] : impl_->editors) arr->add(editorStateObject(ed));
777 }
778 Object::Ptr root = new Object();
779 root->set("editors", arr);
780 return jsonStringify(Var(root));
781}
782
784 if (!impl_) return;
785 for (auto& [k, ed] : impl_->editors) syncVMToView(*impl_, ed);
786}
787
788namespace {
789
790void pushViewToVM(EditorHost::Impl& I, Editor& ed, const std::string& widgetId, const Var& value) {
791 if (!I.vm || ed.vmName.empty()) return;
792 auto it = I.vms.find(ed.vmName);
793 if (it == I.vms.end()) return;
794 Object::Ptr w = findWidget(ed.view, widgetId);
795 if (!w) return;
796 const std::string bind = strOf(w, "bind");
797 if (!bind.empty()) writeVMVar(*I.vm, it->second, bind, value);
798 const std::string onChange = strOf(w, "onChange");
799 if (!onChange.empty()) {
800 std::string err;
801 callTableFunc(I.vm->getHandle(), it->second, vmFuncName(onChange),
802 {Var(widgetId), value}, &err);
803 if (!err.empty())
804 fprintf(stderr, "eve.host: onChange %s: %s\n", onChange.c_str(), err.c_str());
805 }
806}
807
808void setWidgetValue(EditorHost::Impl& I, Editor& ed, const std::string& widgetId,
809 const Var& value) {
810 ed.values[widgetId] = value;
811 emitEvent(ed, "change", widgetId, value);
812 pushViewToVM(I, ed, widgetId, value);
813}
814
815void resolveOneWay(EditorHost::Impl& I, Editor& ed, Object::Ptr w) {
816 if (!I.vm || ed.vmName.empty()) return;
817 auto it = I.vms.find(ed.vmName);
818 if (it == I.vms.end()) return;
819 const char* oneWay[] = {"bind:label", "bind:visible", "bind:enabled"};
820 for (const char* key : oneWay) {
821 const std::string bind = strOf(w, key);
822 if (bind.empty()) continue;
823 const Var v = readVMVar(*I.vm, it->second, bind);
824 if (v.isEmpty()) continue;
825 const std::string k(key);
826 if (k == "bind:label") w->set("label", varString(v));
827 else if (k == "bind:visible") w->set("visible", varBool(v, true));
828 else if (k == "bind:enabled") w->set("enabled", varBool(v, true));
829 }
830 const std::string optBind = strOf(w, "bind:options");
831 if (!optBind.empty()) {
832 const Var v = readVMVar(*I.vm, it->second, optBind);
833 if (!v.isEmpty()) {
834 Array::Ptr opts = new Array();
835 for (const auto& s : varStrings(v)) opts->add(s);
836 w->set("options", opts);
837 }
838 }
839}
840
841void syncVMToView(EditorHost::Impl& I, Editor& ed) {
842 if (!I.vm || ed.vmName.empty()) return;
843 auto it = I.vms.find(ed.vmName);
844 if (it == I.vms.end()) return;
845 forEachWidget(ed.view, [&](Object::Ptr w) {
846 const std::string bind = strOf(w, "bind");
847 const std::string id = strOf(w, "id");
848 if (bind.empty() || id.empty()) return;
849 const Var v = readVMVar(*I.vm, it->second, bind);
850 if (v.isEmpty()) return;
851 auto cur = ed.values.find(id);
852 if (cur == ed.values.end() || jsonStringify(cur->second) != jsonStringify(v))
853 ed.values[id] = v;
854 });
855}
856
857void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr w);
858
859void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids, const std::string& layout) {
860 if (!kids) return;
861 for (size_t i = 0; i < kids->size(); ++i) {
862 Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
863 if (!w) continue;
864 resolveOneWay(I, ed, w);
865 renderWidget(I, ed, w);
866 if (layout == "horizontal" && i + 1 < kids->size()) ImGui::SameLine();
867 }
868}
869
870void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr w) {
871 if (!w) return;
872 if (!boolOf(w, "visible", true)) return;
873 const std::string type = strOf(w, "type", "label");
874 const std::string id = strOf(w, "id");
875 const std::string label = strOf(w, "label");
876 const bool enabled = boolOf(w, "enabled", true);
877 if (!enabled) {
878 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
879 ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
880 }
881
882 const auto cur = [&](const char* key, const Var& def) -> Var {
883 if (!id.empty()) {
884 auto it = ed.values.find(id);
885 if (it != ed.values.end()) return it->second;
886 }
887 if (w->has(key)) return w->get(key);
888 return def;
889 };
890
891 if (type == "label" || type == "text") {
892 const std::string text = strOf(w, "text", label);
893 if (type == "text") ImGui::TextWrapped("%s", text.c_str());
894 else ImGui::TextUnformatted(text.c_str());
895 recordRect(ed, id);
896 } else if (type == "separator") {
897 ImGui::Separator();
898 } else if (type == "spacer") {
899 ImGui::Dummy(ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 8.f)));
900 } else if (type == "progress") {
901 float v = std::clamp(varFloat(cur("value", 0.0), 0.f), 0.f, 1.f);
902 const float pw = floatOf(w, "width", 0.f);
903 ImGui::ProgressBar(v, ImVec2(pw > 0.f ? pw : -FLT_MIN, 0.f),
904 label.empty() ? nullptr : label.c_str());
905 recordRect(ed, id);
906 } else if (type == "plot") {
907 const std::vector<float> data = varFloats(cur("data", Var()));
908 if (!data.empty()) {
909 ImGui::PlotLines(widgetIdLabel(w).c_str(), data.data(), static_cast<int>(data.size()), 0,
910 nullptr, floatOf(w, "min", 0.f), floatOf(w, "max", 1.f),
911 ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 80.f)));
912 recordRect(ed, id);
913 }
914 } else if (type == "input") {
915 std::string buf = varString(cur("value", strOf(w, "value")));
916 const bool multi = boolOf(w, "multiline", false);
917 const bool changed =
918 controls::inputText(widgetIdLabel(w).c_str(), buf, multi,
919 ImVec2(floatOf(w, "width", 0.f),
920 floatOf(w, "height", 96.f))) ==
922 if (enabled && changed && !id.empty()) {
923 setWidgetValue(I, ed, id, Var(buf));
924 }
925 recordRect(ed, id);
926 } else if (type == "slider" || type == "slider2" || type == "slider3") {
927 const int n = type == "slider" ? 1 : (type == "slider2" ? 2 : 3);
928 float v[3] = {0.f, 0.f, 0.f};
929 const std::vector<float> init = widgetFloatInit(cur("value", Var()));
930 for (int i = 0; i < n; ++i) v[i] = i < static_cast<int>(init.size()) ? init[i] : 0.f;
931 const float lo = floatOf(w, "min", 0.f), hi = floatOf(w, "max", 1.f);
932 const std::string fmt = strOf(w, "format", "%.2f");
933 bool changed = false;
934 if (n == 1)
935 changed = controls::slider(widgetIdLabel(w).c_str(), v[0], lo, hi, fmt.c_str()) ==
937 else if (n == 2) changed = ImGui::SliderFloat2(widgetIdLabel(w).c_str(), v, lo, hi, fmt.c_str());
938 else changed = ImGui::SliderFloat3(widgetIdLabel(w).c_str(), v, lo, hi, fmt.c_str());
939 if (enabled && changed && !id.empty()) {
940 Array::Ptr arr = new Array();
941 for (int i = 0; i < n; ++i) arr->add(static_cast<double>(v[i]));
942 setWidgetValue(I, ed, id, Var(arr));
943 }
944 recordRect(ed, id);
945 } else if (type == "color") {
946 const bool alpha = boolOf(w, "alpha", false);
947 const std::vector<float> init = varFloats(cur("value", Var()));
948 float c[4] = {1.f, 1.f, 1.f, 1.f};
949 for (size_t i = 0; i < std::min<size_t>(init.size(), 4); ++i) c[i] = init[i];
950 const bool changed = alpha ? ImGui::ColorEdit4(widgetIdLabel(w).c_str(), c)
951 : ImGui::ColorEdit3(widgetIdLabel(w).c_str(), c);
952 if (enabled && changed && !id.empty()) {
953 Array::Ptr arr = new Array();
954 const int n = alpha ? 4 : 3;
955 for (int i = 0; i < n; ++i) arr->add(static_cast<double>(c[i]));
956 setWidgetValue(I, ed, id, Var(arr));
957 }
958 recordRect(ed, id);
959 } else if (type == "checkbox") {
960 bool b = varBool(cur("value", false), boolOf(w, "value", false));
961 if (enabled &&
962 controls::checkbox(widgetIdLabel(w).c_str(), b) == controls::ControlEdit::Changed &&
963 !id.empty())
964 setWidgetValue(I, ed, id, Var(b));
965 recordRect(ed, id);
966 } else if (type == "dropdown" || type == "listbox") {
967 const std::vector<std::string> opts = varStrings(cur("options", Var()));
968 std::vector<const char*> items;
969 for (const auto& s : opts) items.push_back(s.c_str());
970 std::string sel = varString(cur("value", strOf(w, "value")));
971 int idx = 0;
972 for (size_t i = 0; i < opts.size(); ++i)
973 if (opts[i] == sel) idx = static_cast<int>(i);
974 bool changed = false;
975 if (type == "dropdown")
976 changed = controls::combo(widgetIdLabel(w).c_str(), idx, items) ==
978 else
979 changed = !opts.empty() && ImGui::ListBox(widgetIdLabel(w).c_str(), &idx, items.data(),
980 static_cast<int>(items.size()),
981 intOf(w, "heightItems", -1));
982 if (enabled && changed && !id.empty() && idx >= 0 && idx < static_cast<int>(opts.size()))
983 setWidgetValue(I, ed, id, Var(opts[static_cast<size_t>(idx)]));
984 recordRect(ed, id);
985 } else if (type == "button") {
986 if (enabled &&
987 controls::button(widgetIdLabel(w).c_str(), floatOf(w, "width", 0.f)) ==
989 emitEvent(ed, "click", id, Var());
990 const std::string cmd = strOf(w, "command", strOf(w, "action"));
991 if (!cmd.empty() && !ed.vmName.empty() && I.vm) {
992 auto it = I.vms.find(ed.vmName);
993 if (it != I.vms.end()) {
994 std::string err;
995 callTableFunc(I.vm->getHandle(), it->second, vmFuncName(cmd),
996 {Var(ed.id), Var(id)}, &err);
997 if (!err.empty())
998 fprintf(stderr, "eve.host: command %s: %s\n", cmd.c_str(), err.c_str());
999 }
1000 }
1001 }
1002 recordRect(ed, id);
1003 } else if (type == "tree") {
1004 const bool open = boolOf(w, "open", true);
1005 if (ImGui::TreeNodeEx(widgetIdLabel(w).c_str(),
1006 open ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
1007 renderChildren(I, ed,
1008 w->has("children") ? w->getArray("children") : nullptr, ed.layout);
1009 ImGui::TreePop();
1010 }
1011 recordRect(ed, id);
1012 } else if (type == "group") {
1013 const bool border = boolOf(w, "border", true);
1014 if (border) {
1015 if (ImGui::BeginChild(widgetIdLabel(w).c_str(),
1016 ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 0.f)), true)) {
1017 if (!label.empty()) ImGui::TextUnformatted(label.c_str());
1018 renderChildren(I, ed,
1019 w->has("children") ? w->getArray("children") : nullptr,
1020 strOf(w, "layout", "vertical"));
1021 }
1022 ImGui::EndChild();
1023 } else {
1024 ImGui::BeginGroup();
1025 if (!label.empty()) ImGui::TextUnformatted(label.c_str());
1026 renderChildren(I, ed,
1027 w->has("children") ? w->getArray("children") : nullptr,
1028 strOf(w, "layout", "vertical"));
1029 ImGui::EndGroup();
1030 }
1031 recordRect(ed, id);
1032 } else if (type == "tabs") {
1033 Array::Ptr tabs = w->has("children") ? w->getArray("children") : nullptr;
1034 if (tabs && ImGui::BeginTabBar((id.empty() ? "tabs" : id).c_str())) {
1035 for (size_t i = 0; i < tabs->size(); ++i) {
1036 Object::Ptr tab = tabs->getObject(static_cast<unsigned>(i));
1037 if (!tab) continue;
1038 if (ImGui::BeginTabItem(strOf(tab, "label", "Tab").c_str())) {
1039 renderChildren(I, ed,
1040 tab->has("children") ? tab->getArray("children") : nullptr,
1041 strOf(tab, "layout", "vertical"));
1042 ImGui::EndTabItem();
1043 }
1044 }
1045 ImGui::EndTabBar();
1046 }
1047 } else if (type == "tab") {
1048 renderChildren(I, ed, w->has("children") ? w->getArray("children") : nullptr,
1049 strOf(w, "layout", "vertical"));
1050 } else if (type == "table") {
1051 const std::vector<std::string> cols =
1052 w->has("columns") ? varStrings(w->get("columns")) : std::vector<std::string>();
1053 Array::Ptr rows = w->has("rows") ? w->getArray("rows") : nullptr;
1054 if (!cols.empty() && ImGui::BeginTable((id.empty() ? "table" : id).c_str(),
1055 static_cast<int>(cols.size()),
1056 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
1057 for (const auto& c : cols) ImGui::TableSetupColumn(c.c_str());
1058 ImGui::TableHeadersRow();
1059 if (rows) {
1060 for (size_t r = 0; r < rows->size(); ++r) {
1061 ImGui::TableNextRow();
1062 Array::Ptr cells = rows->getArray(static_cast<unsigned>(r));
1063 if (!cells) continue;
1064 for (size_t c = 0; c < cells->size(); ++c) {
1065 ImGui::TableSetColumnIndex(static_cast<int>(c));
1066 ImGui::TextUnformatted(
1067 varString(cells->get(static_cast<unsigned int>(c))).c_str());
1068 }
1069 }
1070 }
1071 ImGui::EndTable();
1072 }
1073 recordRect(ed, id);
1074 } else if (type == "viewport") {
1075 const float vw = floatOf(w, "width", 0.f);
1076 const float vh = floatOf(w, "height", 120.f);
1077 const std::vector<float> bg =
1078 w->has("bg") ? varFloats(w->get("bg")) : std::vector<float>();
1079 const ImVec2 pos = ImGui::GetCursorScreenPos();
1080 ImVec2 size(vw > 0.f ? vw : ImGui::GetContentRegionAvail().x, vh);
1081 ImU32 col = IM_COL32(10, 12, 16, 255);
1082 if (bg.size() >= 3)
1083 col = IM_COL32(static_cast<int>(bg[0] * 255.f), static_cast<int>(bg[1] * 255.f),
1084 static_cast<int>(bg[2] * 255.f), 255);
1085 ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), col);
1086 if (ImGui::BeginChild(widgetIdLabel(w).c_str(), size, true)) {
1087 if (!label.empty()) ImGui::TextUnformatted(label.c_str());
1088 // AI scripts draw inside here using eve.host.widgetRect().
1089 }
1090 ImGui::EndChild();
1091 if (!id.empty()) {
1092 ed.rectMin[id] = ImGui::GetItemRectMin();
1093 ed.rectMax[id] = ImGui::GetItemRectMax();
1094 }
1095 }
1096
1097 const std::string tip = strOf(w, "tooltip");
1098 if (!tip.empty() && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip.c_str());
1099 if (!enabled) {
1100 ImGui::PopItemFlag();
1101 ImGui::PopStyleVar();
1102 }
1103}
1104
1105void renderEditor(EditorHost::Impl& I, Editor& ed) {
1106 if (ed.removed) return;
1107 if (ed.width > 0.f)
1108 ImGui::SetNextWindowSize(ImVec2(ed.width, ed.height), ImGuiCond_FirstUseEver);
1109 if (ed.x != 0.f || ed.y != 0.f)
1110 ImGui::SetNextWindowPos(ImVec2(ed.x, ed.y), ImGuiCond_FirstUseEver);
1111
1112 ImVec4 accent(0.35f, 0.60f, 1.00f, 1.f), bg(0.10f, 0.11f, 0.13f, 1.f);
1113 ImVec4 panel(0.14f, 0.15f, 0.18f, 1.f), text(0.92f, 0.93f, 0.95f, 1.f);
1114 float radius = 4.f, fontScale = 1.f;
1115 if (ed.theme) {
1116 if (ed.theme->has("accent")) {
1117 const auto c = varFloats(ed.theme->get("accent"));
1118 if (c.size() >= 3) accent = ImVec4(c[0], c[1], c[2], 1.f);
1119 }
1120 if (ed.theme->has("bg")) {
1121 const auto c = varFloats(ed.theme->get("bg"));
1122 if (c.size() >= 3) bg = ImVec4(c[0], c[1], c[2], 1.f);
1123 }
1124 if (ed.theme->has("panel")) {
1125 const auto c = varFloats(ed.theme->get("panel"));
1126 if (c.size() >= 3) panel = ImVec4(c[0], c[1], c[2], 1.f);
1127 }
1128 if (ed.theme->has("text")) {
1129 const auto c = varFloats(ed.theme->get("text"));
1130 if (c.size() >= 3) text = ImVec4(c[0], c[1], c[2], 1.f);
1131 }
1132 radius = floatOf(ed.theme, "radius", 4.f);
1133 fontScale = floatOf(ed.theme, "fontScale", 1.f);
1134 if (strOf(ed.theme, "preset") == "light") {
1135 bg = ImVec4(0.93f, 0.93f, 0.93f, 1.f);
1136 panel = ImVec4(0.84f, 0.84f, 0.86f, 1.f);
1137 text = ImVec4(0.10f, 0.10f, 0.12f, 1.f);
1138 }
1139 }
1140
1141 ImGui::PushStyleColor(ImGuiCol_WindowBg, bg);
1142 ImGui::PushStyleColor(ImGuiCol_ChildBg, panel);
1143 ImGui::PushStyleColor(ImGuiCol_Text, text);
1144 ImGui::PushStyleColor(ImGuiCol_Button, accent);
1145 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1146 ImVec4(accent.x + 0.08f, accent.y + 0.08f, accent.z + 0.08f, 1.f));
1147 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
1148 ImVec4(accent.x - 0.05f, accent.y - 0.05f, accent.z - 0.05f, 1.f));
1149 ImGui::PushStyleColor(ImGuiCol_Header, accent);
1150 ImGui::PushStyleColor(ImGuiCol_SliderGrab, accent);
1151 ImGui::PushStyleColor(ImGuiCol_CheckMark, accent);
1152 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius);
1153 ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, radius);
1154 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, radius);
1155
1156 bool open = true;
1157 ImGuiWindowFlags flags = ImGuiWindowFlags_NoSavedSettings;
1158 if (!ed.resizable) flags |= ImGuiWindowFlags_NoResize;
1159 if (!ed.collapsible) flags |= ImGuiWindowFlags_NoCollapse;
1160 if (ImGui::Begin((ed.title + "##" + ed.id).c_str(), &open, flags)) {
1161 if (fontScale != 1.f) ImGui::SetWindowFontScale(fontScale);
1162 renderChildren(I, ed,
1163 (ed.view && ed.view->has("children")) ? ed.view->getArray("children")
1164 : nullptr,
1165 ed.layout);
1166 }
1167 if (!open) {
1168 emitEvent(ed, "closed", "", Var());
1169 ed.removed = true;
1170 }
1171 ImGui::End();
1172 if (fontScale != 1.f) ImGui::SetWindowFontScale(1.f);
1173 ImGui::PopStyleVar(3);
1174 ImGui::PopStyleColor(9);
1175}
1176
1177} // namespace
1178
1180 if (!impl_ || !impl_->windowOpen || !impl_->ui) return;
1181 auto& I = *impl_;
1182 for (auto& [id, ed] : I.editors) {
1183 syncVMToView(I, ed);
1184 renderEditor(I, ed);
1185 }
1186 for (auto it = I.editors.begin(); it != I.editors.end();) {
1187 if (it->second.removed) it = I.editors.erase(it);
1188 else ++it;
1189 }
1190}
1191
1193 if (!impl_ || !impl_->windowOpen || !impl_->vm) return;
1194 auto& I = *impl_;
1195 I.inFrame = true;
1196 if (!I.event) I.event = ModuleManager::requireInstance<eve::platform_event::PlatformEvent>("PlatformEvent");
1197 if (!I.timer) I.timer = ModuleManager::requireInstance<eve::timer::Timer>("Timer");
1198 if (!I.gfx) {
1199 I.inFrame = false;
1200 return;
1201 }
1202
1203 I.event->pump();
1204 while (auto m = I.event->pollOwned()) {
1205 const std::string name = m->name;
1206 if (name == "quit") {
1207 closeWindow();
1208 requestExit();
1209 I.inFrame = false;
1210 return;
1211 }
1212 }
1213
1214 const float dt = I.timer ? I.timer->step() : 0.f;
1215 callRootFunc(I.vm->getHandle(), "eve_host_update", {Var(static_cast<double>(dt))});
1216 I.gfx->clearScreen();
1217 callRootFunc(I.vm->getHandle(), "eve_host_render", {});
1218 if (I.ui) {
1219 I.ui->beginFrameAndRender();
1220 renderImGui();
1221 }
1222 I.gfx->present();
1223 if (I.ui) I.ui->dispatchEvents();
1224 I.inFrame = false;
1225}
1226
1227namespace {
1228
1229Var parseEditorValue(const std::string& jsonValue) {
1230 Var v;
1231 try {
1232 Poco::JSON::Parser p;
1233 v = p.parse(jsonValue);
1234 } catch (...) {
1235 // Poco's parser wants a top-level JSON value; some clients pass bare
1236 // numbers/booleans without JSON quoting. Handle those explicitly.
1237 if (jsonValue == "true")
1238 v = Var(true);
1239 else if (jsonValue == "false")
1240 v = Var(false);
1241 else if (jsonValue == "null")
1242 v = Var();
1243 else {
1244 char* end = nullptr;
1245 const double d = std::strtod(jsonValue.c_str(), &end);
1246 if (end && end != jsonValue.c_str() && *end == '\0')
1247 v = Var(d);
1248 else
1249 v = Var(jsonValue); // plain text
1250 }
1251 }
1252 return v;
1253}
1254
1255} // namespace
1256
1257std::string EditorHost::setEditorValue(const std::string& editorId, const std::string& widgetId,
1258 const std::string& jsonValue) {
1259 if (!impl_) return "error: host not started";
1260 auto it = impl_->editors.find(editorId);
1261 if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
1262 if (widgetId.empty()) return "error: missing widget";
1263 const Var v = parseEditorValue(jsonValue);
1264 setWidgetValue(*impl_, it->second, widgetId, v);
1265 return "ok";
1266}
1267
1268std::string EditorHost::restoreEditorValue(const std::string& editorId, const std::string& widgetId,
1269 const std::string& jsonValue) {
1270 if (!impl_ || !impl_->vm) return "error: host not started";
1271 auto it = impl_->editors.find(editorId);
1272 if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
1273 if (widgetId.empty()) return "error: missing widget";
1274 const Var v = parseEditorValue(jsonValue);
1275 it->second.values[widgetId] = v;
1276 auto vmIt = impl_->vms.find(it->second.vmName);
1277 const auto widget = findWidget(it->second.view, widgetId);
1278 if (vmIt != impl_->vms.end() && widget) {
1279 const std::string bind = strOf(widget, "bind");
1280 if (!bind.empty()) writeVMVar(*impl_->vm, vmIt->second, bind, v);
1281 }
1282 return "ok";
1283}
1284
1285std::string EditorHost::consumeEvents(const std::string& editorId) {
1286 if (!impl_) return "[]";
1287 Array::Ptr arr = new Array();
1288 auto drain = [&](Editor& ed) {
1289 for (const auto& e : ed.events) {
1290 Object::Ptr eo = new Object();
1291 eo->set("editor", ed.id);
1292 eo->set("widget", e.widget);
1293 eo->set("type", e.type);
1294 if (!e.value.isEmpty()) eo->set("value", e.value);
1295 arr->add(eo);
1296 }
1297 ed.events.clear();
1298 };
1299 if (!editorId.empty()) {
1300 auto it = impl_->editors.find(editorId);
1301 if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
1302 drain(it->second);
1303 } else {
1304 for (auto& [k, ed] : impl_->editors) drain(ed);
1305 }
1306 return jsonStringify(Var(arr));
1307}
1308
1309std::string EditorHost::widgetRect(const std::string& editorId, const std::string& widgetId) const {
1310 if (!impl_) return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
1311 auto it = impl_->editors.find(editorId);
1312 if (it == impl_->editors.end()) return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
1313 const Editor& ed = it->second;
1314 auto mn = ed.rectMin.find(widgetId), mx = ed.rectMax.find(widgetId);
1315 Object::Ptr o = new Object();
1316 if (mn != ed.rectMin.end() && mx != ed.rectMax.end()) {
1317 o->set("x", static_cast<double>(mn->second.x));
1318 o->set("y", static_cast<double>(mn->second.y));
1319 o->set("width", static_cast<double>(mx->second.x - mn->second.x));
1320 o->set("height", static_cast<double>(mx->second.y - mn->second.y));
1321 } else {
1322 o->set("x", 0);
1323 o->set("y", 0);
1324 o->set("width", 0);
1325 o->set("height", 0);
1326 }
1327 return jsonStringify(Var(o));
1328}
1329
1330std::string EditorHost::registerVM(const std::string& name, const std::string& source,
1331 const std::string& sourceName) {
1332 if (!impl_ || !impl_->vm) return "error: host not started";
1333 if (name.empty()) return "error: missing name";
1334 const std::string uri = sourceName.empty() ? std::string("host_vm.nut") : sourceName;
1335 const std::string err = runSquirrel(impl_->vm->getHandle(), source, uri.c_str());
1336 if (!err.empty()) return "error: " + err;
1337 try {
1338 ssq::Table tbl = impl_->vm->find(name.c_str()).toTable();
1339 impl_->vms[name] = tbl;
1340 impl_->vmSources[name] = source;
1341 return "ok";
1342 } catch (const std::exception& e) {
1343 return std::string("error: table '") + name + "' not found after run: " + e.what();
1344 }
1345}
1346
1347std::string EditorHost::unregisterVM(const std::string& name) {
1348 if (!impl_) return "error: host not started";
1349 if (name.empty()) return "error: missing name";
1350 impl_->vms.erase(name);
1351 impl_->vmSources.erase(name);
1352 if (impl_->vm) {
1353 HSQUIRRELVM v = impl_->vm->getHandle();
1354 const SQInteger top = sq_gettop(v);
1355 sq_pushroottable(v);
1356 sq_pushstring(v, name.c_str(), static_cast<SQInteger>(name.size()));
1357 sq_deleteslot(v, -2, SQFalse);
1358 sq_pop(v, 1);
1359 sq_settop(v, top);
1360 }
1361 return "ok";
1362}
1363
1364std::string EditorHost::saveEditor(const std::string& id) {
1365 if (!impl_) return "error: host not started";
1366 auto it = impl_->editors.find(id);
1367 if (it == impl_->editors.end()) return "error: editor not found: " + id;
1368 try {
1369 std::error_code ec;
1370 std::filesystem::path dir = std::filesystem::path(impl_->rootDir) / "editors";
1371 std::filesystem::create_directories(dir, ec);
1372 std::filesystem::path vp = dir / (id + ".editor.json");
1373 {
1374 std::ofstream ofs(vp, std::ios::trunc);
1375 if (!ofs) return "error: cannot write " + vp.string();
1376 ofs << jsonStringify(Var(it->second.view));
1377 }
1378 if (!it->second.vmName.empty()) {
1379 auto vs = impl_->vmSources.find(it->second.vmName);
1380 if (vs != impl_->vmSources.end()) {
1381 std::filesystem::path sp = dir / (id + ".vm.nut");
1382 std::ofstream ofs2(sp, std::ios::trunc);
1383 if (!ofs2) return "error: cannot write " + sp.string();
1384 ofs2 << vs->second;
1385 }
1386 }
1387 return "ok";
1388 } catch (const std::exception& e) {
1389 return std::string("error: ") + e.what();
1390 }
1391}
1392
1393std::string EditorHost::unloadEditor(const std::string& id) {
1394 if (!impl_) return "error: host not started";
1395 auto it = impl_->editors.find(id);
1396 if (it == impl_->editors.end()) return "error: editor not found: " + id;
1397 impl_->editors.erase(it);
1398 return "ok";
1399}
1400
1402 if (!impl_) return;
1403 std::error_code ec;
1404 std::filesystem::path dir = std::filesystem::path(impl_->rootDir) / "editors";
1405 if (!std::filesystem::is_directory(dir, ec)) return;
1406 std::vector<std::pair<std::string, std::string>> pending;
1407 for (auto& entry : std::filesystem::directory_iterator(dir, ec)) {
1408 if (ec) break;
1409 const std::filesystem::path p = entry.path();
1410 const std::string name = p.filename().string();
1411 const std::string suffix = ".editor.json";
1412 if (p.extension() != ".json" || name.size() <= suffix.size() ||
1413 name.compare(name.size() - suffix.size(), suffix.size(), suffix) != 0)
1414 continue;
1415 const std::string id = name.substr(0, name.size() - suffix.size());
1416 std::ifstream ifs(p);
1417 std::string json((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
1418 if (!json.empty()) pending.emplace_back(id, json);
1419 }
1420 for (const auto& [id, json] : pending) {
1421 try {
1422 Object::Ptr root = parseObject(json);
1423 const std::string vmName = strOf(root, "vm");
1424 if (!vmName.empty()) {
1425 std::ifstream ifs2(dir / (id + ".vm.nut"));
1426 std::string src((std::istreambuf_iterator<char>(ifs2)),
1427 std::istreambuf_iterator<char>());
1428 if (!src.empty()) registerVM(vmName, src, "game:/editors/" + id + ".vm.nut");
1429 }
1430 } catch (...) {
1431 }
1432 applyEditor(json);
1433 }
1434}
1435
1436std::string EditorHost::runScript(const std::string& source, const std::string& sourceName) {
1437 if (!impl_ || !impl_->vm) return "error: host not started";
1438 if (source.empty()) return "error: missing source";
1439 const std::string uri = sourceName.empty() ? std::string("host_snippet.nut") : sourceName;
1440 const std::string err = runSquirrel(impl_->vm->getHandle(), source, uri.c_str());
1441 return err.empty() ? "ok" : ("error: " + err);
1442}
1443
1444std::string EditorHost::capture(const std::string& path) {
1445 if (!impl_ || !impl_->gfx) return "error: no window";
1446 auto& I = *impl_;
1447 try {
1448 std::string p = path.empty() ? "eve_host_capture.png" : path;
1449 I.gfx->setScreenReadbackEnabled(true);
1450 eve::image::ImageData* img = nullptr;
1451 try {
1452 img = I.gfx->newImageData();
1453 } catch (...) {
1454 // No presented frame yet (e.g. capture right after the window was
1455 // opened). Render one frame with readback enabled, then retry.
1456 if (I.inFrame) throw; // cannot present reentrantly from a script hook
1457 frame();
1458 img = I.gfx->newImageData();
1459 }
1460 if (!img) return "error: readback returned no image";
1461 const int w = I.gfx->getPixelWidth();
1462 const int h = I.gfx->getPixelHeight();
1464 img->encode(eve::image::ImageData::FormatHandler::ENCODED_PNG, p.c_str(), false);
1465 delete img;
1466 if (!png) return "error: PNG encode failed";
1467 std::error_code ec;
1468 std::filesystem::create_directories(std::filesystem::path(p).parent_path(), ec);
1469 std::ofstream out(p, std::ios::binary | std::ios::trunc);
1470 if (!out.good()) {
1471 delete png;
1472 return "error: cannot write " + p;
1473 }
1474 out.write(static_cast<const char*>(png->getData()),
1475 static_cast<std::streamsize>(png->getSize()));
1476 out.close();
1477 const bool wrote = out.good();
1478 delete png;
1479 if (!wrote) return "error: failed writing " + p;
1480 Object::Ptr o = new Object();
1481 o->set("path", p);
1482 o->set("width", w);
1483 o->set("height", h);
1484 return jsonStringify(Var(o));
1485 } catch (const std::exception& e) {
1486 return std::string("error: ") + e.what();
1487 }
1488}
1489
1490std::string EditorHost::status() const {
1491 if (!impl_) return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
1492 Object::Ptr o = new Object();
1493 o->set("running", running_);
1494 o->set("windowOpen", impl_->windowOpen);
1495 o->set("windowTitle", impl_->windowTitle);
1496 o->set("gameRoot", gameRoot_);
1497 Array::Ptr eds = new Array();
1498 for (const auto& [k, ed] : impl_->editors) {
1499 Object::Ptr e = new Object();
1500 e->set("id", k);
1501 e->set("title", ed.title);
1502 eds->add(e);
1503 }
1504 o->set("editors", eds);
1505 Array::Ptr vms = new Array();
1506 for (const auto& [k, v] : impl_->vms) vms->add(k);
1507 o->set("viewModels", vms);
1508 return jsonStringify(Var(o));
1509}
1510
1512 try {
1513 ssq::Table eveTbl = vm.find("eve").toTable();
1514 ssq::Table host = eveTbl.addTable("host");
1515 host.addFunc("status", []() { return EditorHost::instance().status(); });
1516 host.addFunc("openWindow", [](std::string t, int w, int h) {
1517 return EditorHost::instance().openWindow(t, w, h);
1518 });
1519 host.addFunc("closeWindow", []() { return EditorHost::instance().closeWindow(); });
1520 host.addFunc("windowState", []() { return EditorHost::instance().windowState(); });
1521 host.addFunc("applyEditor", [](std::string json) {
1522 return EditorHost::instance().applyEditor(json);
1523 });
1524 host.addFunc("removeEditor", [](std::string id) {
1526 });
1527 host.addFunc("setValue", [](std::string editor, std::string widget, std::string value) {
1529 });
1530 host.addFunc("events", [](std::string editor) {
1531 return EditorHost::instance().consumeEvents(editor);
1532 });
1533 host.addFunc("registerVM", [](std::string name, std::string source, std::string uri) {
1535 });
1536 host.addFunc("unregisterVM", [](std::string name) {
1538 });
1539 host.addFunc("widgetRect", [](std::string editor, std::string widget) {
1540 return EditorHost::instance().widgetRect(editor, widget);
1541 });
1542 host.addFunc("capture", [](std::string path) {
1544 });
1545 host.addFunc("save", [](std::string id) {
1546 return EditorHost::instance().saveEditor(id);
1547 });
1548 host.addFunc("runScript", [](std::string source, std::string uri) {
1550 });
1551 host.addFunc("reloadResource", [](std::string path) {
1553 });
1554 host.addFunc("hotReloadStatus", []() {
1556 });
1557 } catch (...) {
1558 // eve table missing — the host surface is optional.
1559 }
1560}
1561
1562} // namespace eve::ui
1563
1564#endif // EVENGINE_WEBGPU && __EMSCRIPTEN__
double value
SQInteger top
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int tip
Definition AnimSmr.cpp:121
int root
Definition AnimSmr.cpp:119
const std::string & s
std::vector< QuestEvent > pending
struct SQVM * HSQUIRRELVM
std::string title
glm::vec4 p[6]
std::string label
HSQUIRRELVM vm
Definition ECS.cpp:20
std::map< std::string, Var > values
std::map< std::string, ImVec2 > rectMin
bool collapsible
std::string widget
Object::Ptr theme
std::map< std::string, ImVec2 > rectMax
std::string vmName
bool resizable
int rows
int cols
std::string layout
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
double r
bool border
float v
std::int32_t c
int h
std::uint32_t height
std::uint32_t width
std::string text
std::int32_t parent
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string error
Definition Package.cpp:60
int idx
float f
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::string uri
float d
float t
glm::mat4 view
int removed
std::string string
Battle::Events events
float size
Definition TreeMesh.cpp:156
V3 dir
Definition TreeMesh.cpp:150
UIHostHandle host
uint32_t index
const UnitySourceAsset & source
float m[16]
static Runtime * runtime()
Active runtime associated with the last expose() call, or nullptr. @ownership Borrowed; the embedding...
Definition Module.cpp:292
Data buffer paired with a filename (used for type identification).
Definition FileData.h:14
size_t getSize() const
Byte length of the owned buffer.
Definition FileData.h:29
void * getData() const
Pointer to the owned byte buffer.
Definition FileData.h:27
Represents raw pixel data.
Definition ImageData.h:38
filesystem::FileData * encode(FormatHandler::EncodedFormat format, const char *filename, bool writefile) const
Encodes raw pixel data into a given format.
EVENGINE_API_FOUNDATION public API.
static SQRESULT compileBuffer(HSQUIRRELVM vm, const SQChar *source, SQInteger size, const SQChar *sourceName, SQBool raiseError)
Unified raw-VM entry used by REPL, debugger, MCP, and editor adapters.
High-resolution frame/elapsed timer backed by SDL_GetPerformanceCounter(). Script: timer <- eve....
Definition Timer.h:20
Headless MCP editor host (MVVM).
Definition EditorHost.h:34
void stop()
Stops .
std::string status() const
Status.
std::string consumeEvents(const std::string &editorId)
Read and clear pending interaction events for one editor ("" = all).
void requestExit()
Request exit.
Definition EditorHost.h:122
std::string closeWindow()
Closes window.
std::string listEditors() const
List editors.
std::string unregisterVM(const std::string &name)
Unregisters vm.
EditorHost(const EditorHost &)=delete
void syncBindings()
Pull bound ViewModel values into widget state (per-frame + on read).
void renderImGui()
Draw editor ImGui windows; call between beginFrameAndRender/present.
bool isWindowOpen() const
True when window open.
void loadEditorsFromDisk()
Load editors/<id>.editor.json (+ matching .vm.nut) on host startup.
std::string setEditorValue(const std::string &editorId, const std::string &widgetId, const std::string &jsonValue)
Sets the editor value.
std::string capture(const std::string &path)
Capture.
std::string applyEditor(const std::string &json)
JSON object/string: {id,title,vm,x,y,width,height,theme,children[]}.
std::string unloadEditor(const std::string &id)
Remove an editor from the session (files stay on disk).
std::string windowState() const
Window state.
const std::string & gameRoot() const
Game root.
Definition EditorHost.h:53
std::string editorState(const std::string &id) const
Full state JSON: editors + values + (non-destructive) events.
void frame()
Pump events + update/render hooks + present (no-op without window).
std::string removeEditor(const std::string &id)
Removes editor.
std::string hotReloadStatus() const
Watch count, reload counters and latest diagnostic as compact JSON.
static EditorHost & instance()
Instance.
std::string openWindow(const std::string &title, int width, int height)
Opens window.
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 widgetRect(const std::string &editorId, const std::string &widgetId) const
Last-frame widget rect JSON (for script drawing inside viewports).
std::string reloadResource(const std::string &path)
Reload mcp.nut, scripts under mcp, or editor VM and manifest files.
std::string restoreEditorValue(const std::string &editorId, const std::string &widgetId, const std::string &jsonValue)
Restore a widget and its binding without emitting events or onChange.
std::string saveEditor(const std::string &id)
Write editors/<id>.editor.json + editors/<id>.vm.nut under gameRoot.
~EditorHost()
Editor host.
void start(ssq::VM &vm, const std::string &gameRoot={}, bool allowWindow=true)
Attach to the host VM. gameRoot is the project dir for editors/.
void exposeScriptApi(ssq::VM &vm)
Expose eve.host table (registerVM/unregisterVM/widgetRect/...).
std::string runScript(const std::string &source, const std::string &sourceName={})
Compile and run Squirrel source in the host VM.
Declarative UI module (eve.UI).
Definition UI.h:41
Platform window interface (SDL implementation on desktop/mobile). Script: win <- eve....
Definition Window.h:47
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
std::string formatLastScriptError(HSQUIRRELVM vm)
Formats the last captured error for a VM without consuming it.
ControlEdit slider(const char *label, float &value, float minimum, float maximum, const char *format="%.3f")
Shared immediate renderer for scalar sliders.
ControlEdit button(const char *label, float width=0.f, float height=0.f)
Shared immediate renderer for retained and EditorHost buttons.
ControlEdit combo(const char *label, int &selected, const std::vector< const char * > &items)
Shared immediate renderer for indexed combo boxes.
ControlEdit inputText(const char *label, std::string &value, bool multiline=false, ImVec2 multilineSize=ImVec2(0.f, 96.f))
UTF-8 owning text editor with automatic buffer growth.
ControlEdit checkbox(const char *label, bool &value)
Shared immediate renderer for retained and EditorHost checkboxes.
enum EVENGINE_API_FOUNDATION Array
OT_ARRAY (editable via Runtime array APIs / ReflectedPropertyModel).
Definition Runtime.h:120
SettlementPipeline::Stage fn
bool enabled
eve::window::Window * win
eve::graphics::Graphics * gfx
std::map< std::string, ssq::Table > vms
std::map< std::string, Editor > editors
eve::timer::Timer * timer
std::map< std::string, std::string > vmSources
Display settings for window creation/resize. width/height == 0 selects the desktop display mode size.
Definition Window.h:18