6#include <Poco/Dynamic/Var.h>
7#include <Poco/JSON/Array.h>
8#include <Poco/JSON/Object.h>
9#include <Poco/JSON/Parser.h>
10#include <Poco/JSON/Stringifier.h>
19#include <unordered_map>
20#include <unordered_set>
26bool isEngineName(
const std::string&
name) {
27 static const std::unordered_set<std::string> kSkip = {
72 "soft_reload_scripts",
75 "format_script_error",
82 "async_dispatch_event",
91 "dev_notify_frame_done",
94 return kSkip.count(
name) > 0;
97using SeenSet = std::unordered_set<const void*>;
101 sq_resetobject(&obj);
102 sq_getstackobj(
vm,
idx, &obj);
103 return reinterpret_cast<const void*
>(obj._unVal.pRefCounted);
106const char* sqTypeName(SQObjectType
t) {
108 case OT_CLOSURE:
return "function";
109 case OT_NATIVECLOSURE:
return "native function";
110 case OT_CLASS:
return "class";
111 case OT_INSTANCE:
return "class instance";
112 case OT_USERDATA:
return "userdata";
113 case OT_USERPOINTER:
return "userpointer";
114 case OT_THREAD:
return "thread";
115 case OT_GENERATOR:
return "generator";
116 case OT_WEAKREF:
return "weakref";
117 default:
return "value";
121StateValue sqToStateValue(
HSQUIRRELVM vm, SQInteger
idx, SeenSet& seen,
int depth, std::string* firstError) {
125 const SQObjectType
t = sq_gettype(
vm,
idx);
130 sq_getinteger(
vm,
idx, &
v);
144 const SQChar*
s =
nullptr;
145 sq_getstring(
vm,
idx, &
s);
149 const void*
id = objectId(
vm,
idx);
152 const SQInteger
top = sq_gettop(
vm);
153 const SQInteger
size = sq_getsize(
vm,
idx);
154 for (SQInteger i = 0; i <
size; ++i) {
155 sq_pushinteger(
vm, i);
156 if (SQ_SUCCEEDED(sq_get(
vm,
idx))) {
157 arr.pushBack(sqToStateValue(
vm, -1, seen,
depth + 1, firstError));
168 const void*
id = objectId(
vm,
idx);
171 const SQInteger
top = sq_gettop(
vm);
173 while (SQ_SUCCEEDED(sq_next(
vm,
idx))) {
174 if (sq_gettype(
vm, -2) == OT_STRING) {
175 const SQChar*
key =
nullptr;
176 sq_getstring(
vm, -2, &
key);
178 const SQObjectType vt = sq_gettype(
vm, -1);
179 if (vt == OT_CLOSURE || vt == OT_NATIVECLOSURE || vt == OT_CLASS || vt == OT_INSTANCE ||
180 vt == OT_USERDATA || vt == OT_USERPOINTER || vt == OT_THREAD || vt == OT_GENERATOR ||
182 if (firstError && firstError->empty()) {
183 *firstError = std::string(
"'") +
key +
"' (" + sqTypeName(vt) +
")";
186 obj.set(std::string(
key), sqToStateValue(
vm, -1, seen,
depth + 1, firstError));
204 switch (var.kind()) {
210 const std::string&
s = var.asString();
211 sq_pushstring(
vm,
s.c_str(),
static_cast<SQInteger
>(
s.size()));
216 for (
size_t i = 0; i < var.arraySize(); ++i) {
217 if (!pushStateValue(
vm, var.at(i),
depth + 1))
return false;
218 sq_arrayappend(
vm, -2);
223 for (
const auto&
key : var.keys()) {
224 sq_pushstring(
vm,
key.c_str(),
static_cast<SQInteger
>(
key.size()));
225 if (!pushStateValue(
vm, *var.find(
key),
depth + 1))
return false;
226 sq_newslot(
vm, -3, SQFalse);
233Poco::Dynamic::Var stateToVar(
const StateValue& var) {
234 switch (var.kind()) {
241 Poco::JSON::Array::Ptr arr(
new Poco::JSON::Array());
242 for (
size_t i = 0; i < var.arraySize(); ++i) arr->add(stateToVar(var.at(i)));
243 return Poco::Dynamic::Var(arr);
246 Poco::JSON::Object::Ptr obj(
new Poco::JSON::Object());
247 for (
const auto&
key : var.keys()) obj->set(
key, stateToVar(*var.find(
key)));
248 return Poco::Dynamic::Var(obj);
251 return Poco::Dynamic::Var();
254StateValue varToState(
const Poco::Dynamic::Var& var) {
257 if (var.isInteger())
return StateValue::integer(
static_cast<int64_t
>(var.convert<Poco::Int64>()));
262 if (var.type() ==
typeid(Poco::JSON::Array::Ptr)) {
264 Poco::JSON::Array::Ptr
a = var.extract<Poco::JSON::Array::Ptr>();
266 for (
size_t i = 0; i <
a->size(); ++i)
267 arr.pushBack(varToState(
a->get(
static_cast<unsigned int>(i))));
270 }
catch (
const Poco::BadCastException&) {
275 if (var.type() ==
typeid(Poco::JSON::Object::Ptr)) {
277 Poco::JSON::Object::Ptr o = var.extract<Poco::JSON::Object::Ptr>();
279 for (
const auto&
name : o->getNames()) obj.set(
name, varToState(o->get(
name)));
282 }
catch (
const Poco::BadCastException&) {
298 if (
name.empty())
return;
300 for (
const auto&
r : marked_) {
301 if (
r ==
name)
return;
303 marked_.push_back(std::move(
name));
307 marked_.erase(std::remove(marked_.begin(), marked_.end(),
name), marked_.end());
311 if (
name.empty())
return;
313 for (
const auto&
r : transient_) {
314 if (
r ==
name)
return;
316 transient_.push_back(std::move(
name));
320 transient_.erase(std::remove(transient_.begin(), transient_.end(),
name), transient_.end());
339std::vector<std::string> Snapshot::resolveRoots(
HSQUIRRELVM vm)
const {
340 if (!marked_.empty())
return marked_;
341 std::vector<std::string> out;
344 const auto isTransient = [
this](
const std::string&
name) {
345 return std::find(transient_.begin(), transient_.end(),
name) != transient_.end();
348 for (
const char* pref : {
"eve_state",
"gameState",
"state"}) {
349 if (isTransient(pref))
continue;
350 const SQInteger
top = sq_gettop(
vm);
351 sq_pushroottable(
vm);
352 sq_pushstring(
vm, pref, -1);
353 if (SQ_SUCCEEDED(sq_get(
vm, -2))) {
354 const SQObjectType
t = sq_gettype(
vm, -1);
355 if (
t == OT_TABLE ||
t == OT_INSTANCE ||
t == OT_ARRAY) out.emplace_back(pref);
359 if (!out.empty())
return out;
361 const SQInteger
top = sq_gettop(
vm);
362 sq_pushroottable(
vm);
363 const SQInteger rootIdx = sq_gettop(
vm);
365 while (SQ_SUCCEEDED(sq_next(
vm, rootIdx))) {
366 if (sq_gettype(
vm, -2) == OT_STRING) {
367 const SQChar*
key =
nullptr;
368 sq_getstring(
vm, -2, &
key);
369 if (
key && !isEngineName(
key) && !isTransient(
key)) {
370 const SQObjectType vt = sq_gettype(
vm, -1);
371 if (vt == OT_INTEGER || vt == OT_FLOAT || vt == OT_BOOL || vt == OT_STRING || vt == OT_TABLE ||
373 out.emplace_back(
key);
389 auto rootNames = resolveRoots(
vm);
391 for (
const auto&
name : rootNames) {
393 const SQInteger
top = sq_gettop(
vm);
394 sq_pushroottable(
vm);
395 sq_pushstring(
vm,
name.c_str(), -1);
396 if (SQ_SUCCEEDED(sq_get(
vm, -2))) {
397 const SQObjectType rt = sq_gettype(
vm, -1);
398 if (rt == OT_CLOSURE || rt == OT_NATIVECLOSURE || rt == OT_CLASS || rt == OT_INSTANCE ||
399 rt == OT_USERDATA || rt == OT_USERPOINTER || rt == OT_THREAD || rt == OT_GENERATOR ||
403 *
error =
"root '" +
name +
"' is a " + sqTypeName(rt) +
" (state roots must be plain data)";
407 std::string firstError;
408 roots.set(
name, sqToStateValue(
vm, -1, seen, 0, &firstError));
409 if (!firstError.empty()) {
412 *
error =
"root '" +
name +
"' contains non-serializable value: " + firstError;
424 if (
p->captureState(captured)) native.
set(
p->stateKind(), std::move(captured));
429 out.set(
"roots", std::move(
roots));
430 out.set(
"native", std::move(native));
432 }
catch (
const std::exception& e) {
442 Poco::JSON::Object::Ptr doc = stateToVar(
state).extract<Poco::JSON::Object::Ptr>();
443 std::ostringstream oss;
446 }
catch (
const Poco::Exception& e) {
449 }
catch (
const std::exception& e) {
463 if (
error) *
error =
"invalid snapshot: missing roots";
467 const SQInteger
top = sq_gettop(
vm);
468 sq_pushroottable(
vm);
469 sq_pushstring(
vm,
name.c_str(),
static_cast<SQInteger
>(
name.size()));
475 sq_newslot(
vm, -3, SQFalse);
481 std::string nativeErr;
484 if (!p->resetToDefaults()) {
486 nativeErr += std::string(p->stateKind()) +
": reset failed; ";
494 if (!
p->restoreState(*
v, &perr)) {
496 nativeErr += std::string(
p->stateKind()) +
": " + perr +
"; ";
497 p->resetToDefaults();
506 }
catch (
const std::exception& e) {
514 Poco::JSON::Parser parser;
515 Poco::Dynamic::Var result = parser.parse(json);
516 return restoreState(
vm, varToState(result),
error);
517 }
catch (
const Poco::Exception& e) {
520 }
catch (
const std::exception& e) {
527 const std::string json = capture(
vm,
error);
528 if (json.empty())
return false;
529 std::ofstream ofs(
path, std::ios::binary);
535 return static_cast<bool>(ofs);
539 std::ifstream ifs(
path, std::ios::binary);
544 std::ostringstream oss;
546 return restore(
vm, oss.str(),
error);
struct SQVM * HSQUIRRELVM
JSON-compatible state value tree used by state hot reload.
static StateValue object()
Empty object.
static StateValue boolean(bool v)
Boolean value.
static StateValue array()
Empty array.
static StateValue integer(int64_t v)
Integer value.
static StateValue number(double v)
Floating-point value.
const StateValue * find(const std::string &key) const
Look up key; nullptr when absent.
static StateValue null()
Null value.
static StateValue string(std::string v)
String value.
void set(const std::string &key, StateValue v)
Insert or replace key; only valid on objects.
Runtime-state serialization for state hot reload.
Script + native state snapshot for state hot reload.
bool captureState(HSQUIRRELVM vm, StateValue &out, std::string *error=nullptr) const
Capture script roots plus every Preserve IStateProvider.
bool restoreState(HSQUIRRELVM vm, const StateValue &state, std::string *error=nullptr) const
Restore a captured state; v1-shaped values restore roots only.
void unmarkTransientRoot(const std::string &name)
Remove an explicit transient-root declaration.
std::string capture(HSQUIRRELVM vm, std::string *error=nullptr) const
Capture marked roots, or heuristic roots when none marked.
std::vector< std::string > roots() const
Return the explicit persistent roots.
std::vector< std::string > transientRoots() const
Return the explicit transient roots.
void setRootPolicies(std::vector< std::string > persistent, std::vector< std::string > transient)
Atomically replace explicit persistent/transient root policy.
static bool isEngineBinding(const std::string &name)
Built-in names never auto-captured (modules / boot bindings).
void markTransientRoot(std::string name)
Mark a root for reconstruction from new script definitions.
void markRoot(std::string name)
Mark a root as persistent, replacing a transient declaration.
static Snapshot & instance()
void unmarkRoot(const std::string &name)
Remove an explicit persistent-root declaration.
void clearRoots()
Clear every explicit persistent and transient declaration.