4#include <simplesquirrel/simplesquirrel.hpp>
9#include <unordered_map>
19struct ScriptComponentReg {
22 ScriptComponentReg() { sq_resetobject(&
cls); }
26std::unordered_map<std::string, ScriptComponentReg>& scriptComponents() {
27 static std::unordered_map<std::string, ScriptComponentReg>
m;
36int allocEntityId() {
return nextEntityId()++; }
39std::unordered_map<size_t, CppEntityViewFn>& cppEntityViews() {
40 static std::unordered_map<size_t, CppEntityViewFn> views;
44std::vector<std::function<void(ssq::Table&)>>& postEcsHooks() {
45 static std::vector<std::function<void(ssq::Table&)>> hooks;
53bool isSubclassOf(
HSQUIRRELVM vm, HSQOBJECT child, HSQOBJECT base) {
54 if (
child._type != OT_CLASS || base._type != OT_CLASS)
return false;
55 const SQInteger top = sq_gettop(
vm);
56 sq_pushobject(
vm, child);
57 for (
int guard = 0; guard < 64; ++guard) {
58 if (sq_gettype(
vm, -1) != OT_CLASS)
break;
60 sq_getstackobj(
vm, -1, &cur);
61 if (cur._unVal.pClass == base._unVal.pClass) {
65 if (SQ_FAILED(sq_getbase(
vm, -1)))
break;
67 if (sq_gettype(
vm, -1) != OT_CLASS)
break;
74void collectClassFields(
HSQUIRRELVM vm, HSQOBJECT clsObj, HSQOBJECT outTable) {
75 const SQInteger top = sq_gettop(
vm);
76 sq_pushobject(
vm, outTable);
77 sq_pushobject(
vm, clsObj);
79 while (SQ_SUCCEEDED(sq_next(
vm, -2))) {
81 if (sq_gettype(
vm, -2) == OT_STRING) {
82 const SQChar* key =
nullptr;
83 sq_getstring(
vm, -2, &key);
84 if (key && key[0] !=
'_') {
85 const SQObjectType vt = sq_gettype(
vm, -1);
86 if (vt != OT_CLOSURE && vt != OT_NATIVECLOSURE && vt != OT_GENERATOR) {
100ssq::Table inspectClassFields(ssq::Object clsObj) {
103 if (clsObj.getType() != ssq::Type::CLASS)
return out;
104 collectClassFields(
vm, clsObj.getRaw(), out.getRaw());
108bool scriptIsSubclass(ssq::Object child, ssq::Object base) {
109 if (
child.getType() != ssq::Type::CLASS || base.getType() != ssq::Type::CLASS)
111 return isSubclassOf(
child.getHandle(),
child.getRaw(), base.getRaw());
114ssq::Object getClassBase(ssq::Object
cls) {
117 if (
cls.getType() != ssq::Type::CLASS)
return ret;
118 const SQInteger top = sq_gettop(
vm);
119 sq_pushobject(
vm,
cls.getRaw());
120 if (SQ_FAILED(sq_getbase(
vm, -1)) || sq_gettype(
vm, -1) != OT_CLASS) {
124 sq_getstackobj(
vm, -1, &ret.getRaw());
125 sq_addref(
vm, &ret.getRaw());
130void registerScriptComponent(
const std::string&
name, ssq::Object
cls) {
131 if (
cls.getType() != ssq::Type::CLASS)
return;
133 HSQOBJECT o =
cls.getRaw();
135 auto&
m = scriptComponents();
136 auto it =
m.find(
name);
138 if (it->second.vm) sq_release(it->second.vm, &it->second.cls);
139 it->second = ScriptComponentReg(
vm, o);
141 m.emplace(
name, ScriptComponentReg(
vm, o));
149const char* kEcsScript = R
"SQ(
150// Script ECS: define Component / Entity classes, create instances, view & System.
151// Mirrors ECS.hpp's class + component model for Squirrel game logic.
153if (!("Number" in eve)) eve.Number <- "number"
154if (!("Boolean" in eve)) eve.Boolean <- "boolean"
155if (!("String" in eve)) eve.String <- "string"
157eve._ecsTypes <- {} // EntityClass -> { cls, instances }
159// --- 性能缓存(万级实体稳态零分配) ---
160// view 结果缓存:create/destroy 时按类链失效;C++ 桥接类(_cppHasView=true)不缓存。
161eve._ecsViewCache <- {} // EntityClass -> array
162eve._ecsSlotsCache <- {} // EntityClass -> { fieldName = ComponentClass }
163eve._ecsCompDefaults <- {} // ComponentClass -> { fieldName = resolvedDefault }
165function eve::_ecsResolveMarker(v) {
166 if (v == eve.Number || v == "number") return 0.0
167 if (v == eve.Boolean || v == "boolean") return false
168 if (v == eve.String || v == "string") return ""
172function eve::_ecsIsComponentClass(obj) {
173 if (typeof obj != "class") return false
174 try { return eve.isSubclass(obj, eve.Component) } catch (e) { return false }
177function eve::_ecsInstantiateComponent(compClass) {
178 local c = compClass()
179 if (compClass in eve._ecsCompDefaults) {
180 foreach (name, val in eve._ecsCompDefaults[compClass]) c[name] = val
182 local fields = eve.inspectClassFields(compClass)
184 foreach (name, val in fields) {
186 try { cur = c[name] } catch (e) { cur = null }
187 if (cur == eve.Number || cur == eve.Boolean || cur == eve.String
188 || cur == "number" || cur == "boolean" || cur == "string") {
189 c[name] = eve._ecsResolveMarker(cur)
190 resolved[name] <- c[name]
193 eve._ecsCompDefaults[compClass] <- resolved
198function eve::_ecsCollectSlots(cls) {
199 if (cls in eve._ecsSlotsCache) return eve._ecsSlotsCache[cls]
200 // { fieldName = ComponentClass } walking base classes (Entity → … → cls).
204 while (cur != null && guard < 64) {
205 if (!eve.isSubclass(cur, eve.Entity)) break
207 local declared = null
208 try { declared = cur.components } catch (e) { declared = null }
209 if (typeof declared == "table") {
210 foreach (k, v in declared) {
211 if (!(k in slots) && eve._ecsIsComponentClass(v))
216 local fields = eve.inspectClassFields(cur)
217 foreach (k, v in fields) {
218 if (k == "components") continue
219 if (!(k in slots) && eve._ecsIsComponentClass(v))
223 cur = eve.getClassBase(cur)
226 eve._ecsSlotsCache[cls] <- slots
230function eve::_ecsInvalidateViews(cls) {
233 while (cur != null && guard < 64) {
234 if (cur in eve._ecsViewCache) delete eve._ecsViewCache[cur]
235 if (!eve.isSubclass(cur, eve.Entity)) break
236 cur = eve.getClassBase(cur)
241function eve::_ecsEnsureType(cls) {
242 if (cls in eve._ecsTypes) return eve._ecsTypes[cls]
243 local info = { cls = cls, instances = [] }
244 eve._ecsTypes[cls] <- info
248function eve::_ecsRegisterInstance(entity, cls) {
249 eve._ecsEnsureType(cls).instances.push(entity)
250 eve._ecsInvalidateViews(cls)
253function eve::_ecsUnregisterInstance(entity, cls) {
254 if (!(cls in eve._ecsTypes)) return
255 local arr = eve._ecsTypes[cls].instances
256 for (local i = arr.len() - 1; i >= 0; --i) {
257 if (arr[i] == entity) {
258 // O(1) swap-remove(顺序不保证);视图缓存随后失效重建
259 local last = arr.len() - 1
260 if (i != last) arr[i] = arr[last]
265 eve._ecsInvalidateViews(cls)
268function eve::_ecsCollectInstances(cls, out) {
269 foreach (key, info in eve._ecsTypes) {
270 if (info.cls == cls || eve.isSubclass(info.cls, cls)) {
271 foreach (e in info.instances) {
272 if (e != null && e.isAlive()) out.push(e)
278eve.Component <- class {
297 static function create() {
300 e._eid = eve.allocEntityId()
302 e._slots = eve._ecsCollectSlots(this)
304 foreach (name, compClass in e._slots) {
305 local c = eve._ecsInstantiateComponent(compClass)
307 try { e[name] = c } catch (ex) {}
309 eve._ecsRegisterInstance(e, this)
313 // Declare a component slot on this entity class (optional sugar).
314 static function component(name, compClass) {
315 if (!("components" in this) || typeof this.components != "table")
316 this.components <- {}
317 this.components[name] <- compClass
321 function getId() { return _eid }
322 function isAlive() { return _alive }
324 function getComponent(compClass) {
325 if (_slots == null || _comps == null) return null
326 foreach (name, cls in _slots) {
327 if (cls == compClass) {
328 if (name in _comps) return _comps[name]
335 function hasComponent(compClass) {
336 return getComponent(compClass) != null
342 if (_etype != null) eve._ecsUnregisterInstance(this, _etype)
346// Historical name from early game examples
347eve.EntityContainer <- eve.Entity
352 constructor(query = null) {
356 function setQuery(query) { _query = query }
358 function entities() {
359 if (_query == null) return []
360 if (typeof _query == "array") {
362 foreach (q in _query) {
363 foreach (e in eve.view(q)) out.push(e)
367 return eve.view(_query)
370 function update(dt) {}
373// GPU-backed System: pack float component fields → SSBO → compute shader → unpack.
374// Requires eve.Gpgpu / eve.EcsShaderSystem (gpgpu module). Push constants:
375// pc.data[0] = dt, pc.data[1] = entityCount. Each bindFields() maps one binding.
376eve.ShaderSystem <- class extends eve.System {
383 constructor(query = null, gpu = null, glsl = null, localSize = 64) {
384 base.constructor(query)
387 _localSize = localSize
388 if (gpu != null) setGpgpu(gpu)
389 if (gpu != null && glsl != null) setShaderSource(glsl)
392 function setGpgpu(gpu) {
394 if (_backend == null) {
395 if (!("EcsShaderSystem" in eve))
396 throw "eve.ShaderSystem requires gpgpu module (eve.EcsShaderSystem)"
397 _backend = eve.EcsShaderSystem()
399 _backend.setGpgpu(gpu)
400 _backend.setLocalSize(_localSize)
404 function setShaderSource(glsl) {
405 if (_backend == null) throw "eve.ShaderSystem.setShaderSource: call setGpgpu first"
406 _backend.setShaderSource(glsl)
410 function setLocalSize(n) {
412 if (_backend != null) _backend.setLocalSize(n)
416 function setReadback(enabled) {
421 // binding: SSBO index; slot: entity component field name; fields: ["x","y",...]
422 function bindFields(binding, slot, fields) {
423 _bindings.push({ binding = binding, slot = slot, fields = fields })
427 function setFloat(index, value) {
428 if (_backend != null) _backend.setFloat(index, value)
432 function getBackend() { return _backend }
434 function update(dt) {
435 if (_backend == null || _gpu == null) return
436 if (!("packEcsFloats" in eve) || !("unpackEcsFloats" in eve)) return
438 local ents = entities()
442 foreach (b in _bindings) {
443 local floatsPer = b.fields.len()
444 if (floatsPer <= 0) continue
445 local buf = _backend.ensureBuffer(b.binding, n * floatsPer)
446 eve.packEcsFloats(ents, b.slot, b.fields, buf)
449 _backend.dispatch(n, dt)
452 foreach (b in _bindings) {
453 local floatsPer = b.fields.len()
454 if (floatsPer <= 0) continue
455 local buf = _backend.getBuffer(b.binding)
456 eve.unpackEcsFloats(ents, b.slot, b.fields, buf, n)
462function eve::view(entityClass) {
463 if (entityClass == null) return []
464 // C++ 桥接类的实体在脚本外增删(无法通知脚本),不缓存,每次新鲜收集
465 local cppBacked = false
466 if ("_cppHasView" in eve) cppBacked = eve._cppHasView(entityClass)
467 if (!cppBacked && (entityClass in eve._ecsViewCache)) return eve._ecsViewCache[entityClass]
469 eve._ecsCollectInstances(entityClass, out)
470 if ("_cppCollect" in eve) eve._cppCollect(entityClass, out)
471 if (!cppBacked) eve._ecsViewCache[entityClass] <- out
475function eve::ecsReady() {
476 return ("Entity" in eve) && ("Component" in eve) && ("System" in eve)
480void injectEcsScript(ssq::Table& eveTable) {
482 const SQInteger top = sq_gettop(
vm);
483 if (SQ_FAILED(sq_compilebuffer(
vm, kEcsScript,
484 static_cast<SQInteger
>(std::strlen(kEcsScript)),
485 "ecs.nut", SQTrue))) {
488 std::fprintf(stderr,
"EVEngine: ECS bootstrap script failed to compile: %s\n",
489 ctx.empty() ?
"unknown error" : ctx.message.c_str());
492 sq_pushroottable(
vm);
493 if (SQ_FAILED(sq_call(
vm, 1, SQFalse, SQTrue))) {
495 std::fprintf(stderr,
"EVEngine: ECS bootstrap script failed to run: %s\n",
496 ctx.empty() ?
"unknown error"
507void cppCollect(ssq::Class
cls, ssq::Array out) {
509 const SQInteger top = sq_gettop(
vm);
510 sq_pushobject(
vm,
cls.getRaw());
511 for (
int guard = 0; guard < 64; ++guard) {
512 if (sq_gettype(
vm, -1) != OT_CLASS)
515 sq_getstackobj(
vm, -1, &cur);
516 SQUserPointer tag =
nullptr;
517 if (SQ_SUCCEEDED(sq_getobjtypetag(&cur, &tag))) {
518 auto it = cppEntityViews().find(
reinterpret_cast<size_t>(tag));
519 if (it != cppEntityViews().end()) {
525 if (SQ_FAILED(sq_getbase(
vm, -1)))
537bool cppHasView(ssq::Object
cls) {
538 if (
cls.getType() != ssq::Type::CLASS)
return false;
540 const SQInteger top = sq_gettop(
vm);
541 sq_pushobject(
vm,
cls.getRaw());
542 for (
int guard = 0; guard < 64; ++guard) {
543 if (sq_gettype(
vm, -1) != OT_CLASS)
break;
545 sq_getstackobj(
vm, -1, &cur);
546 SQUserPointer tag =
nullptr;
547 if (SQ_SUCCEEDED(sq_getobjtypetag(&cur, &tag))) {
548 if (cppEntityViews().find(
reinterpret_cast<size_t>(tag)) != cppEntityViews().end()) {
553 if (SQ_FAILED(sq_getbase(
vm, -1)))
break;
563 table.set(
"Number", std::string(
"number"));
564 table.set(
"Boolean", std::string(
"boolean"));
565 table.set(
"String", std::string(
"string"));
568 table.addFunc(
"inspectClassFields", inspectClassFields);
569 table.addFunc(
"isSubclass", scriptIsSubclass);
570 table.addFunc(
"getClassBase", getClassBase);
571 table.addFunc(
"allocEntityId", allocEntityId);
572 table.addFunc(
"_cppHasView", cppHasView);
573 table.addFunc(
"component", [](std::string
name, ssq::Object
cls) {
574 registerScriptComponent(
name,
cls);
576 table.addFunc(
"_cppCollect", std::function<
void(ssq::Class, ssq::Array)>(cppCollect));
578 injectEcsScript(table);
581 for (
auto &hook : postEcsHooks()) {
591 cppEntityViews()[typeHash] = std::move(
fn);
595 postEcsHooks().push_back(std::move(
fn));
600 ssq::Table eveTable(
vm.find(
"eve"));
struct SQVM * HSQUIRRELVM
SettlementPipeline::Stage fn
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
std::string formatScriptError(const ScriptErrorContext &ctx)
Formats a context into a human-readable multi-line report.
ScriptErrorContext takeLastScriptError(HSQUIRRELVM vm)
Consumes and clears the last recorded error for a VM.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
void registerPostEcsHook(PostEcsHook fn)
void exposeECS(ssq::Table &table)
脚本侧 ECS:Entity / Component / System / ShaderSystem / view(见 ECS.cpp)。 C++ 游戏实体仍直接用 ECS....
std::function< void(ssq::Table &table)> PostEcsHook
在脚本 ECS 基类(eve.Component / eve.Entity / eve.System)注入之后执行的回调。 模块用它注入"extends eve.Entity"的脚本基类(例如 eve....
std::function< void(ssq::Array &out)> CppEntityViewFn
C++ 实体 → 脚本 eve.view() 桥接。 用 registerCppEntityView(typeid(T*).hash_code(), fn) 登记 T 的收集函数; 脚本 eve....
void exposeECSToVM(ssq::VM &vm)
在 ModuleManager::expose 之后调用;保证 eve.Component 等不被其它模块覆盖。
void registerCppEntityView(size_t typeHash, CppEntityViewFn fn)