载入中...
搜索中...
未找到
HotReload.cpp
浏览该文件的文档.
2
4#include "common/Capability.h"
5#include "common/Module.h"
8
9#ifndef EVENGINE_WEBGPU
10#include <Poco/Exception.h>
11#include <Poco/JSON/Array.h>
12#include <Poco/JSON/Object.h>
13#include <Poco/JSON/Parser.h>
14#include <Poco/JSON/Stringifier.h>
15#include <Poco/Net/HTTPClientSession.h>
16#include <Poco/Net/HTTPRequest.h>
17#include <Poco/Net/HTTPResponse.h>
18#include <Poco/StreamCopier.h>
19#include <Poco/Timespan.h>
20#include <Poco/URI.h>
21#endif
22
23#include <simplesquirrel/simplesquirrel.hpp>
24
25#if defined(EVENGINE_ANDROID)
26#include "android/android.h"
27#include <SDL2/SDL.h>
28#elif defined(EVENGINE_IOS)
29#include "ios/ios.h"
30#endif
31
32#include <cctype>
33#include <chrono>
34#include <cstdio>
35#include <filesystem>
36#include <fstream>
37#include <sstream>
38#include <string>
39#include <system_error>
40#include <vector>
41
42namespace eve::filesystem {
43namespace {
44
45std::string joinDir(const std::string &dir, const std::string &name) {
46 if (dir.empty() || dir == ".") return name;
47 if (dir.back() == '/' || dir.back() == '\\') return dir + name;
48 return dir + "/" + name;
49}
50
51std::filesystem::path pathFromUtf8(const std::string &text) {
52 const auto *data = reinterpret_cast<const char8_t *>(text.data());
53 return std::filesystem::path(std::u8string_view(data, text.size()));
54}
55
56std::string pathToUtf8(const std::filesystem::path &p) {
57 auto u8 = p.u8string();
58 return std::string(reinterpret_cast<const char *>(u8.data()), u8.size());
59}
60
61bool looksAbsoluteOs(const std::string &path) {
62 if (path.empty()) return false;
63 if (path[0] == '/' || path[0] == '\\') return true;
64#ifdef _WIN32
65 if (path.size() >= 2 && path[1] == ':') return true;
66#endif
67 return false;
68}
69
70bool hasParentSegment(const std::string &path) {
71 if (path == "..") return true;
72 if (path.size() >= 3 && (path.compare(0, 3, "../") == 0 || path.compare(0, 3, "..\\") == 0))
73 return true;
74 return path.find("/..") != std::string::npos || path.find("\\..") != std::string::npos;
75}
76
77bool skipWatchDirName(const std::string &name) {
78 return name == ".git" || name == "build" || name == "third-party" || name == "node_modules" ||
79 name == ".cursor" || name == ".local-debug" || name == "CMakeFiles";
80}
81
82std::string posixRelative(const std::filesystem::path &dir, const std::filesystem::path &root) {
83 std::error_code ec;
84 auto rel = std::filesystem::relative(dir, root, ec);
85 if (ec) return {};
86 std::string s = rel.generic_string();
87 if (s.empty()) return ".";
88 return s;
89}
90
91bool isPathPrefix(const std::filesystem::path &prefix, const std::filesystem::path &full) {
92 std::error_code ec;
93 auto a = std::filesystem::weakly_canonical(prefix, ec);
94 if (ec) a = prefix.lexically_normal();
95 auto b = std::filesystem::weakly_canonical(full, ec);
96 if (ec) b = full.lexically_normal();
97 std::string as = a.generic_string();
98 std::string bs = b.generic_string();
99 if (as.size() > bs.size()) return false;
100#ifdef _WIN32
101 for (char &ch : as) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
102 for (char &ch : bs) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
103#endif
104 if (bs.compare(0, as.size(), as) != 0) return false;
105 return bs.size() == as.size() || bs[as.size()] == '/';
106}
107
108int watchTreeVfs(Filesystem *fs, const std::string &root) {
109 int added = 0;
110 std::vector<std::string> stack;
111 stack.push_back(root == "." ? std::string(".") : root);
112
113 while (!stack.empty()) {
114 std::string dir = stack.back();
115 stack.pop_back();
116 if (fs->watch(dir)) ++added;
117
118 std::vector<std::string> items;
119 try {
120 if (dir == "." || dir.empty())
121 items = fs->getDirectoryItems("");
122 else
123 items = fs->getDirectoryItems(dir);
124 } catch (...) {
125 continue;
126 }
127
128 for (const auto &item : items) {
129 if (item.empty() || item == "." || item == "..") continue;
130 const std::string child = (dir == "." || dir.empty()) ? item : joinDir(dir, item);
131 Filesystem::Info info{};
132 if (!fs->getInfo(child, info)) continue;
133 if (info.type == "directory") stack.push_back(child);
134 }
135 }
136 return added;
137}
138
139int watchTreeOs(Filesystem *fs, const std::filesystem::path &realRoot) {
140 std::error_code ec;
141 if (!std::filesystem::is_directory(realRoot, ec)) return 0;
142
143 const auto cwd = pathFromUtf8(fs->getWorkingDirectory());
144 auto reportFor = [&](const std::filesystem::path &dir) -> std::string {
145 std::error_code canonEc;
146 auto d = std::filesystem::weakly_canonical(dir, canonEc);
147 if (canonEc) d = dir;
148 auto c = std::filesystem::weakly_canonical(cwd, canonEc);
149 if (canonEc) c = cwd;
150 if (isPathPrefix(c, d)) {
151 const std::string rel = posixRelative(d, c);
152 return rel.empty() ? "." : rel;
153 }
154 const std::string rel = posixRelative(d, realRoot);
155 return rel.empty() ? "." : rel;
156 };
157
158 int added = 0;
159 if (fs->watchRealDirectory(pathToUtf8(realRoot), reportFor(realRoot)).ok()) ++added;
160
161 auto it = std::filesystem::recursive_directory_iterator(
162 realRoot, std::filesystem::directory_options::skip_permission_denied, ec);
163 const auto end = std::filesystem::recursive_directory_iterator();
164 for (; it != end; it.increment(ec)) {
165 if (ec) {
166 ec.clear();
167 continue;
168 }
169 std::error_code dirEc;
170 if (!it->is_directory(dirEc)) continue;
171 const auto name = it->path().filename().string();
172 if (name.empty() || name == "." || name == ".." || skipWatchDirName(name)) {
173 it.disable_recursion_pending();
174 continue;
175 }
176 const auto &dir = it->path();
177 // When watching a parent workflow, skip sibling apps (apps/battle while
178 // running apps/skill) but keep the current working directory.
179 if (isPathPrefix(realRoot, cwd)) {
180 const auto appsDir = cwd.parent_path();
181 if (isPathPrefix(appsDir, dir) && !isPathPrefix(dir, appsDir) && !isPathPrefix(cwd, dir)) {
182 it.disable_recursion_pending();
183 continue;
184 }
185 }
186 const std::string report = reportFor(dir);
187 if (report.empty() || report.find("..") != std::string::npos) continue;
188 if (fs->watchRealDirectory(pathToUtf8(dir), report).ok()) ++added;
189 }
190 return added;
191}
192
193bool resolveOsWatchRoot(Filesystem *fs, const std::string &root, std::filesystem::path &out) {
194 std::error_code ec;
195 std::filesystem::path candidate;
196 if (root == "." || root == "./") {
197 candidate = pathFromUtf8(fs->getWorkingDirectory());
198 } else if (looksAbsoluteOs(root)) {
199 candidate = pathFromUtf8(root);
200 } else {
201 candidate = pathFromUtf8(fs->getWorkingDirectory()) / pathFromUtf8(root);
202 }
203 auto canonical = std::filesystem::weakly_canonical(candidate, ec);
204 if (ec) return false;
205 if (!std::filesystem::is_directory(canonical, ec)) return false;
206 out = canonical;
207 return true;
208}
209
210// --- Remote sync helpers ---
211
212#ifndef EVENGINE_WEBGPU
213std::string stripTrailingSlash(std::string s) {
214 while (s.size() > 1 && s.back() == '/') s.pop_back();
215 return s;
216}
217
218// Path-escape for use inside a URL path segment (spaces, '#', '?', etc.).
219std::string urlPathEscape(const std::string &s) {
220 std::string out;
221 out.reserve(s.size() + 8);
222 const char hex[] = "0123456789ABCDEF";
223 for (unsigned char c : s) {
224 if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') {
225 out += static_cast<char>(c);
226 } else {
227 out += '%';
228 out += hex[c >> 4];
229 out += hex[c & 0xF];
230 }
231 }
232 return out;
233}
234
235// HTTP GET; returns 200 response body or empty on failure.
236std::string httpGet(const std::string &url, int timeoutMs) {
237 try {
238 Poco::URI uri(url);
239 Poco::Net::HTTPClientSession session(uri.getHost(), uri.getPort());
240 session.setTimeout(Poco::Timespan(timeoutMs / 1000, (timeoutMs % 1000) * 1000));
241 std::string path = uri.getPathAndQuery();
242 if (path.empty()) path = "/";
243 Poco::Net::HTTPRequest req(Poco::Net::HTTPRequest::HTTP_GET, path,
244 Poco::Net::HTTPMessage::HTTP_1_1);
245 req.setHost(uri.getHost());
246 std::ostream &os = session.sendRequest(req);
247 (void)os;
248 Poco::Net::HTTPResponse resp;
249 std::istream &is = session.receiveResponse(resp);
250 if (resp.getStatus() != Poco::Net::HTTPResponse::HTTP_OK) return {};
251 std::ostringstream oss;
252 Poco::StreamCopier::copyStream(is, oss);
253 return oss.str();
254 } catch (...) {
255 return {};
256 }
257}
258
259bool writeFileBytes(const std::string &realPath, const std::string &data) {
260 std::error_code ec;
261 std::filesystem::path p(realPath);
262 std::filesystem::create_directories(p.parent_path(), ec);
263 std::ofstream ofs(realPath, std::ios::binary | std::ios::trunc);
264 if (!ofs) return false;
265 ofs.write(data.data(), static_cast<std::streamsize>(data.size()));
266 return ofs.good();
267}
268#endif // !EVENGINE_WEBGPU
269
270} // namespace
271
273
275
276std::string HotReload::normalizePath(std::string path) {
277 for (char &c : path) {
278 if (c == '\\') c = '/';
279 }
280 while (path.size() >= 2 && path[0] == '.' && path[1] == '/') path.erase(0, 2);
281 while (path.size() > 1 && path.back() == '/') path.pop_back();
282 return path;
283}
284
285void HotReload::bind(std::string path, std::string kind) {
286 path = normalizePath(std::move(path));
287 if (path.empty()) return;
288 if (kind.empty()) kind = "auto";
289 bindings_[path] = kind;
290}
291
292void HotReload::unbind(std::string path) {
293 bindings_.erase(normalizePath(std::move(path)));
294}
295
296bool HotReload::tryReload(std::string path) {
297 const std::string norm = normalizePath(std::move(path));
298 if (norm.empty()) return false;
299
300 std::string kind = "auto";
301 auto bit = bindings_.find(norm);
302 if (bit != bindings_.end()) kind = bit->second;
303
304 bool any = false;
305 eve::cap::forEach<eve::caps::IAssetReloader>([&](eve::caps::IAssetReloader *r) {
306 // "auto" lets each reloader claim the path by extension. An explicit
307 // kind targets exactly one reloader and ignores the extension, so a
308 // config file with an unexpected suffix can still be bound by hand.
309 const bool wanted = (kind == "auto") ? r->handlesPath(norm) : (kind == r->reloadKind());
310 if (!wanted) return;
311 auto result = r->reload(norm);
312 if (result.ok() && result.value()) any = true;
313 });
314 return any;
315}
316
317int HotReload::watchTree(std::string root) {
318 StartupStage stage("hotreload: watchTree (recursive walk + register watches)");
319 auto *fs = Filesystem::create();
320 if (!fs) return 0;
321 root = normalizePath(std::move(root));
322 if (root.empty()) root = ".";
323
325 const bool vfsDir = (root == "." || root == "./") ||
326 (!looksAbsoluteOs(root) && !hasParentSegment(root) && fs->getInfo(root, info) &&
327 info.type == "directory");
328 if (vfsDir) return watchTreeVfs(fs, root);
329
330 std::filesystem::path realRoot;
331 if (!resolveOsWatchRoot(fs, root, realRoot)) return 0;
332 return watchTreeOs(fs, realRoot);
333}
334
336 auto *fs = Filesystem::create();
337 if (!fs) return false;
338 path = normalizePath(std::move(path));
339 if (path.empty()) return false;
340
342 if (fs->getInfo(path, info) && info.type == "directory") {
344 return true;
345 }
346 std::filesystem::path real;
347 if (!resolveOsWatchRoot(fs, path, real)) return false;
348 watchTreeOs(fs, real);
349 return true;
350}
351
352// --- Remote hot reload (dev-server sync) ---
353// Poco (HTTP) is not available in the Emscripten/WebGPU build; the API is
354// stubbed there so scripts still compile, and startRemoteSync just fails.
355
356#ifndef EVENGINE_WEBGPU
357
358std::string HotReload::ensureHotDir() {
359 if (!hotDir_.empty()) return hotDir_;
360
361 std::string base;
362#if defined(EVENGINE_ANDROID)
363 base = eve::android::getHotReloadDirectory();
364#elif defined(EVENGINE_IOS)
365 base = eve::ios::getHotReloadDirectory();
366#else
367 {
368 auto *fs = Filesystem::create();
369 base = fs ? fs->getAppdataDirectory() : std::string(".");
370 if (!base.empty() && base.back() == '/') base.pop_back();
371 base += "/EVE/hotreload";
372 }
373#endif
374 if (base.empty()) base = ".";
375 std::error_code ec;
376 std::filesystem::create_directories(base, ec);
377 hotDir_ = base;
378 return hotDir_;
379}
380
382 if (syncStarted_) return;
383 std::error_code ec;
384 std::filesystem::create_directories(dir, ec);
385 hotDir_ = std::move(dir);
386}
387
388bool HotReload::mountHotDir() {
389 if (hotDirMounted_) return true;
390 auto *fs = Filesystem::create();
391 if (!fs) return false;
392 const std::string dir = ensureHotDir();
393 if (!fs->mountRealDirectory(dir, "/", /*appendToPath=*/false)) return false;
394 hotDirMounted_ = true;
395 return true;
396}
397
398bool HotReload::fetchManifest(std::vector<RemoteFile> &out) {
399 const std::string url = stripTrailingSlash(syncUrl_) + "/manifest";
400 const std::string body = httpGet(url, 2000);
401 if (body.empty()) return false;
402
403 try {
404 Poco::JSON::Parser parser;
405 Poco::Dynamic::Var result = parser.parse(body);
406 Poco::JSON::Array::Ptr arr = result.extract<Poco::JSON::Array::Ptr>();
407 if (!arr) return false;
408 for (size_t i = 0; i < arr->size(); ++i) {
409 Poco::JSON::Object::Ptr o = arr->getObject(static_cast<unsigned int>(i));
410 if (!o) continue;
411 RemoteFile f;
412 f.path = o->optValue<std::string>("path", "");
413 if (f.path.empty()) continue;
414 f.size = o->optValue<int64_t>("size", -1);
415 f.mtime = o->optValue<int64_t>("mtime", -1);
416 f.path = normalizePath(std::move(f.path));
417 if (!f.path.empty()) out.push_back(std::move(f));
418 }
419 return true;
420 } catch (...) {
421 return false;
422 }
423}
424
425bool HotReload::downloadFile(const std::string &relPath) {
426 const std::string url = stripTrailingSlash(syncUrl_) + "/raw/" + urlPathEscape(relPath);
427 const std::string body = httpGet(url, 5000);
428 if (body.empty()) return false;
429 const std::string real = joinDir(ensureHotDir(), relPath);
430 return writeFileBytes(real, body);
431}
432
433std::map<std::string, std::pair<int64_t, int64_t>> HotReload::loadRecord() const {
434 std::map<std::string, std::pair<int64_t, int64_t>> record;
435 const std::string real = joinDir(hotDir_, ".eve-manifest.json");
436 std::ifstream ifs(real, std::ios::binary);
437 if (!ifs) return record;
438 std::ostringstream oss;
439 oss << ifs.rdbuf();
440 try {
441 Poco::JSON::Parser parser;
442 Poco::Dynamic::Var result = parser.parse(oss.str());
443 Poco::JSON::Array::Ptr arr = result.extract<Poco::JSON::Array::Ptr>();
444 if (!arr) return record;
445 for (size_t i = 0; i < arr->size(); ++i) {
446 Poco::JSON::Object::Ptr o = arr->getObject(static_cast<unsigned int>(i));
447 if (!o) continue;
448 const std::string path = o->optValue<std::string>("path", "");
449 if (path.empty()) continue;
450 record[path] = {o->optValue<int64_t>("size", -1), o->optValue<int64_t>("mtime", -1)};
451 }
452 } catch (...) {
453 }
454 return record;
455}
456
457void HotReload::saveRecord(const std::map<std::string, std::pair<int64_t, int64_t>> &record) const {
458 Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
459 for (const auto &kv : record) {
460 Poco::JSON::Object::Ptr o = new Poco::JSON::Object();
461 o->set("path", kv.first);
462 o->set("size", kv.second.first);
463 o->set("mtime", kv.second.second);
464 arr->add(o);
465 }
466 std::ostringstream oss;
467 Poco::JSON::Stringifier::stringify(arr, oss, 0, 0);
468 const std::string real = joinDir(hotDir_, ".eve-manifest.json");
469 writeFileBytes(real, oss.str());
470}
471
472void HotReload::queueChange(std::string path) {
473 std::lock_guard<std::mutex> lock(syncMu_);
474 changedQueue_.push_back(std::move(path));
475}
476
477void HotReload::applyManifest(const std::vector<RemoteFile> &manifest) {
478 auto record = loadRecord();
479 std::map<std::string, bool> seen;
480 for (const auto &file : manifest) {
481 seen[file.path] = true;
482 auto it = record.find(file.path);
483 if (it != record.end() && it->second.first == file.size && it->second.second == file.mtime)
484 continue; // unchanged
485 if (downloadFile(file.path)) {
486 record[file.path] = {file.size, file.mtime};
487 queueChange(file.path);
488 }
489 }
490 // Remove files that disappeared from the server.
491 for (auto it = record.begin(); it != record.end();) {
492 if (seen.find(it->first) != seen.end()) {
493 ++it;
494 continue;
495 }
496 const std::string real = joinDir(hotDir_, it->first);
497 std::error_code ec;
498 std::filesystem::remove(real, ec);
499 queueChange(it->first);
500 it = record.erase(it);
501 }
502 saveRecord(record);
503}
504
505void HotReload::syncLoop(int pollMs) {
506 int failStreak = 0;
507 while (syncRunning_.load()) {
508 std::vector<RemoteFile> manifest;
509 if (fetchManifest(manifest)) {
510 failStreak = 0;
511 {
512 std::lock_guard<std::mutex> l(syncMu_);
513 syncStatus_ = "syncing";
514 }
515 applyManifest(manifest);
516 {
517 std::lock_guard<std::mutex> l(syncMu_);
518 syncStatus_ = "synced";
519 }
520 } else {
521 ++failStreak;
522 {
523 std::lock_guard<std::mutex> l(syncMu_);
524 syncStatus_ = failStreak > 3 ? "error:unreachable" : "idle";
525 }
526 }
527
528 std::unique_lock<std::mutex> l(syncMu_);
529 syncCv_.wait_for(l, std::chrono::milliseconds(pollMs), [this]() { return !syncRunning_.load(); });
530 }
531 {
532 std::lock_guard<std::mutex> l(syncMu_);
533 syncStatus_ = "idle";
534 }
535}
536
537bool HotReload::startRemoteSync(std::string url, int pollMs) {
538 std::lock_guard<std::mutex> lock(syncMu_);
539 if (syncStarted_) return false;
540 url = stripTrailingSlash(std::move(url));
541 if (url.empty()) return false;
542 if (url.rfind("http://", 0) != 0 && url.rfind("https://", 0) != 0) url = "http://" + url;
543
544 syncUrl_ = std::move(url);
545 syncStatus_ = "syncing";
546 syncStarted_ = true;
547 hotDirMounted_ = false;
548 // Mount the overlay dir from the calling (main) thread; PhysFS mount is not
549 // guaranteed thread-safe, so never touch it from the sync thread.
550 if (!mountHotDir()) {
551 syncStarted_ = false;
552 syncStatus_ = "error:mount";
553 return false;
554 }
555 syncRunning_.store(true);
556 syncThread_ = std::thread([this, pollMs]() { syncLoop(pollMs); });
557 return true;
558}
559
561 {
562 std::lock_guard<std::mutex> lock(syncMu_);
563 if (!syncStarted_) return;
564 syncRunning_.store(false);
565 }
566 syncCv_.notify_all();
567 if (syncThread_.joinable()) syncThread_.join();
568 std::lock_guard<std::mutex> lock(syncMu_);
569 syncStarted_ = false;
570 changedQueue_.clear();
571}
572
573bool HotReload::isRemoteSyncing() const { return syncRunning_.load(); }
574
575std::string HotReload::remoteSyncStatus() const {
576 std::lock_guard<std::mutex> lock(syncMu_);
577 return syncStatus_;
578}
579
581 std::lock_guard<std::mutex> lock(syncMu_);
582 if (changedQueue_.empty()) return {};
583 std::string p = std::move(changedQueue_.front());
584 changedQueue_.pop_front();
585 return p;
586}
587
588#endif // !EVENGINE_WEBGPU
589
590#ifdef EVENGINE_WEBGPU
591// WebGPU (browser) build: no Poco HTTP client / threads for remote sync.
592std::string HotReload::ensureHotDir() { return {}; }
593bool HotReload::mountHotDir() { return false; }
594bool HotReload::fetchManifest(std::vector<RemoteFile> &) { return false; }
595bool HotReload::downloadFile(const std::string &) { return false; }
596void HotReload::applyManifest(const std::vector<RemoteFile> &) {}
597std::map<std::string, std::pair<int64_t, int64_t>> HotReload::loadRecord() const { return {}; }
598void HotReload::saveRecord(const std::map<std::string, std::pair<int64_t, int64_t>> &) const {}
599void HotReload::syncLoop(int) {}
600void HotReload::queueChange(std::string) {}
601bool HotReload::startRemoteSync(std::string, int) { return false; }
603void HotReload::setRemoteHotDir(std::string) {}
604bool HotReload::isRemoteSyncing() const { return false; }
605std::string HotReload::remoteSyncStatus() const { return "idle"; }
606std::string HotReload::pollRemoteChange() { return {}; }
607#endif // EVENGINE_WEBGPU
608
609void HotReload::expose(ssq::Table &table) {
610 auto cls = table.addClass(name, HotReload::create, false);
611 expose(cls);
612}
613
614void HotReload::expose(ssq::Class &cls) {
615 cls.addFunc("getName", &HotReload::getName);
616 cls.addFunc("bind", &HotReload::bind);
617 cls.addFunc("unbind", &HotReload::unbind);
618 cls.addFunc("tryReload", &HotReload::tryReload);
619 cls.addFunc("watchTree", &HotReload::watchTree);
620 cls.addFunc("watchNewDirectory", &HotReload::watchNewDirectory);
621 cls.addFunc("startRemoteSync", &HotReload::startRemoteSync);
622 cls.addFunc("stopRemoteSync", &HotReload::stopRemoteSync);
623 cls.addFunc("isRemoteSyncing", &HotReload::isRemoteSyncing);
624 cls.addFunc("remoteSyncStatus", &HotReload::remoteSyncStatus);
625 cls.addFunc("pollRemoteChange", &HotReload::pollRemoteChange);
626}
627
628} // namespace eve::filesystem
int root
Definition AnimSmr.cpp:119
const std::string & s
glm::vec4 p[6]
HSQOBJECT cls
Definition ECS.cpp:21
filesystem::File * file
double r
std::int32_t c
std::string text
TokenKind kind
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
float f
std::string path
Definition PlayHost.cpp:110
std::string uri
float d
std::optional< InteractionSession > session
Definition Tactics.cpp:39
V3 dir
Definition TreeMesh.cpp:150
std::string body
virtual std::string getName() const =0
Returns the name.
StartupStage public API.
EVENGINE_API_FOUNDATION public API.
Path→reload dispatcher for soft hot reload. Driven from load.nut via pollWatch → tryReload; also used...
Definition HotReload.h:36
void setRemoteHotDir(std::string dir)
Sets the remote hot dir.
int watchTree(std::string root=".")
Recursively watch root and all subdirectories. Virtual paths walk the VFS. .., ../…,...
std::string pollRemoteChange()
Polls remote change.
bool isRemoteSyncing() const
True when remote syncing.
bool tryReload(std::string path)
Offer a (normalized) path to the registered reloaders; true if any reloaded.
~HotReload() override
Hot reload.
bool watchNewDirectory(std::string path)
If path is a directory, recursively add watches for it and its current descendants.
void unbind(std::string path)
Unbinds unbind.
void stopRemoteSync()
Stops remote sync.
bool startRemoteSync(std::string url, int pollMs=1000)
Starts remote sync.
std::string remoteSyncStatus() const
Remote sync status.
void bind(std::string path, std::string kind="auto")
Pin a path to one reloader kind ("particle" / "tilemap" / "texture" / whatever a linked module regist...
static std::string normalizePath(std::string path)
Normalize path.
std::unordered_map< std::string, SkillDefinition > & table()
Definition Skill.cpp:65
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
glm::uvec4 info