#include "common/ECS.h" #include "common/ScriptError.h" #include #include #include #include #include #include namespace eve { namespace { // --------------------------------------------------------------------------- // Script component registry (name → Squirrel class) // --------------------------------------------------------------------------- struct ScriptComponentReg { HSQUIRRELVM vm = nullptr; HSQOBJECT cls; ScriptComponentReg() { sq_resetobject(&cls); } ScriptComponentReg(HSQUIRRELVM v, HSQOBJECT o) : vm(v), cls(o) {} }; std::unordered_map& scriptComponents() { static std::unordered_map m; return m; } int& nextEntityId() { static int id = 1; return id; } int allocEntityId() { return nextEntityId()++; } // 当前 VM 的 Squirrel 类对象 → 脚本 view() 收集函数。 std::unordered_map& cppEntityViews() { static std::unordered_map views; return views; } std::vector>& postEcsHooks() { static std::vector> hooks; return hooks; } bool& ecsScriptInjected() { static bool injected = false; return injected; } size_t& postEcsHooksFlushed() { static size_t n = 0; return n; } // --------------------------------------------------------------------------- // Class helpers // --------------------------------------------------------------------------- bool isSubclassOf(HSQUIRRELVM vm, HSQOBJECT child, HSQOBJECT base) { if (child._type != OT_CLASS || base._type != OT_CLASS) return false; const SQInteger top = sq_gettop(vm); sq_pushobject(vm, child); for (int guard = 0; guard < 64; ++guard) { if (sq_gettype(vm, -1) != OT_CLASS) break; HSQOBJECT cur; sq_getstackobj(vm, -1, &cur); if (cur._unVal.pClass == base._unVal.pClass) { sq_settop(vm, top); return true; } if (SQ_FAILED(sq_getbase(vm, -1))) break; sq_remove(vm, -2); // drop current, keep base if (sq_gettype(vm, -1) != OT_CLASS) break; } sq_settop(vm, top); return false; } /** Collect non-method members of a class into `out` (field defaults / statics). */ void collectClassFields(HSQUIRRELVM vm, HSQOBJECT clsObj, HSQOBJECT outTable) { const SQInteger top = sq_gettop(vm); sq_pushobject(vm, outTable); // out sq_pushobject(vm, clsObj); // out, cls sq_pushnull(vm); // out, cls, iter while (SQ_SUCCEEDED(sq_next(vm, -2))) { // out, cls, iter, key, value if (sq_gettype(vm, -2) == OT_STRING) { const SQChar* key = nullptr; sq_getstring(vm, -2, &key); if (key && key[0] != '_') { const SQObjectType vt = sq_gettype(vm, -1); if (vt != OT_CLOSURE && vt != OT_NATIVECLOSURE && vt != OT_GENERATOR) { sq_push(vm, -5); // out sq_push(vm, -3); // key sq_push(vm, -3); // value sq_rawset(vm, -3); sq_pop(vm, 1); // pop out } } } sq_pop(vm, 2); // key, value } sq_settop(vm, top); } ssq::Table inspectClassFields(ssq::Object clsObj) { HSQUIRRELVM vm = clsObj.getHandle(); ssq::Table out(vm); if (clsObj.getType() != ssq::Type::CLASS) return out; collectClassFields(vm, clsObj.getRaw(), out.getRaw()); return out; } bool scriptIsSubclass(ssq::Object child, ssq::Object base) { if (child.getType() != ssq::Type::CLASS || base.getType() != ssq::Type::CLASS) return false; return isSubclassOf(child.getHandle(), child.getRaw(), base.getRaw()); } ssq::Object getClassBase(ssq::Object cls) { HSQUIRRELVM vm = cls.getHandle(); ssq::Object ret(vm); if (cls.getType() != ssq::Type::CLASS) return ret; const SQInteger top = sq_gettop(vm); sq_pushobject(vm, cls.getRaw()); if (SQ_FAILED(sq_getbase(vm, -1)) || sq_gettype(vm, -1) != OT_CLASS) { sq_settop(vm, top); return ret; } sq_getstackobj(vm, -1, &ret.getRaw()); sq_addref(vm, &ret.getRaw()); sq_settop(vm, top); return ret; } void registerScriptComponent(const std::string& name, ssq::Object cls) { if (cls.getType() != ssq::Type::CLASS) return; HSQUIRRELVM vm = cls.getHandle(); HSQOBJECT o = cls.getRaw(); sq_addref(vm, &o); auto& m = scriptComponents(); auto it = m.find(name); if (it != m.end()) { if (it->second.vm) sq_release(it->second.vm, &it->second.cls); it->second = ScriptComponentReg(vm, o); } else { m.emplace(name, ScriptComponentReg(vm, o)); } } // --------------------------------------------------------------------------- // Injected Squirrel runtime // --------------------------------------------------------------------------- const char* kEcsScript = R"SQ( // Script ECS: define Component / Entity classes, create instances, view & System. // Mirrors ECS.hpp's class + component model for Squirrel game logic. if (!("Number" in eve)) eve.Number <- "number" if (!("Boolean" in eve)) eve.Boolean <- "boolean" if (!("String" in eve)) eve.String <- "string" eve._ecsTypes <- {} // EntityClass -> { cls, instances } // --- 性能缓存(万级实体稳态零分配) --- // view 结果缓存:create/destroy 时按类链失效;C++ 桥接类(_cppHasView=true)不缓存。 eve._ecsViewCache <- {} // EntityClass -> array eve._ecsSlotsCache <- {} // EntityClass -> { fieldName = ComponentClass } eve._ecsCompDefaults <- {} // ComponentClass -> { fieldName = resolvedDefault } eve._ecsRevision <- 0 // increments on script entity structural changes function eve::_ecsResolveMarker(v) { if (v == this.Number || v == "number") return 0.0 if (v == this.Boolean || v == "boolean") return false if (v == this.String || v == "string") return "" return v } function eve::_ecsIsComponentClass(obj) { if (typeof obj != "class") return false return this.isSubclass(obj, this.Component) } function eve::_ecsInstantiateComponent(compClass) { local c = compClass() if (compClass in this._ecsCompDefaults) { foreach (name, val in this._ecsCompDefaults[compClass]) c[name] = val } else { local fields = this.inspectClassFields(compClass) local resolved = {} foreach (name, val in fields) { local cur = null try { cur = c[name] } catch (e) { cur = null } if (cur == this.Number || cur == this.Boolean || cur == this.String || cur == "number" || cur == "boolean" || cur == "string") { c[name] = this._ecsResolveMarker(cur) resolved[name] <- c[name] } } this._ecsCompDefaults[compClass] <- resolved } return c } function eve::_ecsCollectSlots(cls) { if (cls in this._ecsSlotsCache) return this._ecsSlotsCache[cls] // { fieldName = ComponentClass } walking base classes (Entity → … → cls). local slots = {} local cur = cls local guard = 0 while (cur != null && guard < 64) { if (!this.isSubclass(cur, this.Entity)) break local declared = null try { declared = cur.components } catch (e) { declared = null } if (typeof declared == "table") { foreach (k, v in declared) { if (!(k in slots) && this._ecsIsComponentClass(v)) slots[k] <- v } } local fields = this.inspectClassFields(cur) foreach (k, v in fields) { if (k == "components") continue if (!(k in slots) && this._ecsIsComponentClass(v)) slots[k] <- v } cur = this.getClassBase(cur) guard += 1 } this._ecsSlotsCache[cls] <- slots return slots } function eve::_ecsInvalidateViews(cls) { local cur = cls local guard = 0 while (cur != null && guard < 64) { if (cur in this._ecsViewCache) delete this._ecsViewCache[cur] if (!this.isSubclass(cur, this.Entity)) break cur = this.getClassBase(cur) guard += 1 } } function eve::_ecsEnsureType(cls) { if (cls in this._ecsTypes) return this._ecsTypes[cls] local info = { cls = cls, instances = [] } this._ecsTypes[cls] <- info return info } function eve::_ecsRegisterInstance(entity, cls) { this._ecsEnsureType(cls).instances.push(entity) this._ecsRevision += 1 this._ecsInvalidateViews(cls) } function eve::_ecsUnregisterInstance(entity, cls) { if (!(cls in this._ecsTypes)) return local arr = this._ecsTypes[cls].instances for (local i = arr.len() - 1; i >= 0; --i) { if (arr[i] == entity) { // O(1) swap-remove(顺序不保证);视图缓存随后失效重建 local last = arr.len() - 1 if (i != last) arr[i] = arr[last] arr.pop() this._ecsRevision += 1 break } } this._ecsInvalidateViews(cls) } function eve::_ecsCollectInstances(cls, out) { foreach (key, info in this._ecsTypes) { if (info.cls == cls || this.isSubclass(info.cls, cls)) { foreach (e in info.instances) { if (e != null && e.isAlive()) out.push(e) } } } } eve.Component <- class { constructor() {} } eve.Entity <- class { _eid = 0 _alive = false _etype = null _slots = null _comps = null constructor() { _eid = 0 _alive = false _etype = null _slots = null _comps = null } static function create() { local e = this() e._etype = this e._eid = eve.allocEntityId() e._alive = true e._slots = eve._ecsCollectSlots(this) e._comps = {} foreach (name, compClass in e._slots) { local c = eve._ecsInstantiateComponent(compClass) e._comps[name] <- c try { e[name] = c } catch (ex) {} } eve._ecsRegisterInstance(e, this) return e } // Declare a component slot on this entity class (optional sugar). static function component(name, compClass) { if (!("components" in this) || typeof this.components != "table") this.components <- {} this.components[name] <- compClass return this } function getId() { return _eid } function isAlive() { return _alive } function getComponent(compClass) { if (_slots == null || _comps == null) return null foreach (name, cls in _slots) { if (cls == compClass) { if (name in _comps) return _comps[name] return null } } return null } function hasComponent(compClass) { return getComponent(compClass) != null } function destroy() { if (!_alive) return _alive = false if (_etype != null) eve._ecsUnregisterInstance(this, _etype) } } // Historical name from early game examples eve.EntityContainer <- eve.Entity eve.System <- class { _query = null constructor(query = null) { _query = query } function setQuery(query) { _query = query } function entities() { if (_query == null) return [] if (typeof _query == "array") { local out = [] foreach (q in _query) { foreach (e in eve.view(q)) out.push(e) } return out } return eve.view(_query) } function update(dt) {} } // GPU-backed System: pack float component fields → SSBO → compute shader → unpack. // Requires eve.Gpgpu / eve.EcsShaderSystem (gpgpu module). Push constants: // pc.data[0] = dt, pc.data[1] = entityCount. Each bindFields() maps one binding. eve.ShaderSystem <- class extends eve.System { _gpu = null _backend = null _bindings = null _readback = true _localSize = 64 constructor(query = null, gpu = null, glsl = null, localSize = 64) { base.constructor(query) _bindings = [] _readback = true _localSize = localSize if (gpu != null) setGpgpu(gpu) if (gpu != null && glsl != null) setShaderSource(glsl) } function setGpgpu(gpu) { _gpu = gpu if (_backend == null) { if (!("EcsShaderSystem" in eve)) throw "eve.ShaderSystem requires gpgpu module (eve.EcsShaderSystem)" _backend = eve.EcsShaderSystem() } _backend.setGpgpu(gpu) _backend.setLocalSize(_localSize) return this } function setShaderSource(glsl) { if (_backend == null) throw "eve.ShaderSystem.setShaderSource: call setGpgpu first" _backend.setShaderSource(glsl) return this } function setLocalSize(n) { _localSize = n if (_backend != null) _backend.setLocalSize(n) return this } function setReadback(enabled) { _readback = enabled return this } // binding: SSBO index; slot: entity component field name; fields: ["x","y",...] // uploadEachUpdate=false keeps the buffer GPU-resident after its initial upload. // readback=false leaves results resident until requestReadback() is called. function bindFields(binding, slot, fields, uploadEachUpdate = true, readback = null) { _bindings.push({ binding = binding, slot = slot, fields = fields, uploadEachUpdate = uploadEachUpdate, readback = readback, uploadedRevision = -1, uploadRequested = false, readbackRequested = false, uploadFirst = 0, uploadCount = 0, readbackFirst = 0, readbackCount = 0, source = null, sourceBinding = -1, initializeFromCpu = true, schema = null }) return this } // Typed binding schema: { slot, fields, scalar="f32", access="read_write" }. // write-only bindings allocate storage without an unnecessary CPU upload. function bindSchema(binding, schema, uploadEachUpdate = false, readback = false) { if (typeof schema != "table" || !("slot" in schema) || !("fields" in schema)) throw "eve.ShaderSystem.bindSchema: schema requires slot and fields" if (typeof schema.slot != "string" || typeof schema.fields != "array" || schema.fields.len() <= 0) throw "eve.ShaderSystem.bindSchema: invalid slot or fields" foreach (field in schema.fields) if (typeof field != "string") throw "eve.ShaderSystem.bindSchema: every field must be a string" local scalar = "scalar" in schema ? schema.scalar : "f32" if (scalar != "f32") throw "eve.ShaderSystem.bindSchema: only f32 is currently supported" local access = "access" in schema ? schema.access : "read_write" if (access != "read" && access != "write" && access != "read_write") throw "eve.ShaderSystem.bindSchema: access must be read, write, or read_write" local normalized = { slot = schema.slot, fields = schema.fields, scalar = scalar, access = access, strideFloats = schema.fields.len() } _bindings.push({ binding = binding, slot = schema.slot, fields = schema.fields, uploadEachUpdate = access == "write" ? false : uploadEachUpdate, readback = readback, uploadedRevision = -1, uploadRequested = false, readbackRequested = false, uploadFirst = 0, uploadCount = 0, readbackFirst = 0, readbackCount = 0, source = null, sourceBinding = -1, initializeFromCpu = access != "write", schema = normalized }) return this } function getBindingSchema(binding) { foreach (b in _bindings) if (b.binding == binding) return b.schema return null } // Generate backend declaration boilerplate; the kernel body remains explicit. function shaderDeclarations(language = "glsl") { local out = "" foreach (b in _bindings) { if (b.schema == null) continue if (language == "glsl") { local qualifier = b.schema.access == "read" ? "readonly " : b.schema.access == "write" ? "writeonly " : "" out += "layout(set = 0, binding = " + b.binding + ") " + qualifier + "buffer EcsBinding" + b.binding + " { float data[]; } ecs" + b.binding + ";\n" } else if (language == "wgsl") { local access = b.schema.access == "read" ? "read" : "read_write" out += "@group(0) @binding(" + b.binding + ") var ecs" + b.binding + " : array;\n" } else { throw "eve.ShaderSystem.shaderDeclarations: language must be glsl or wgsl" } } return out } // Reuse another ShaderSystem's resident buffer without a CPU round trip. // The source system is retained and must be prepared before this consumer. function bindSharedFields(binding, sourceSystem, sourceBinding, slot, fields, readback = false) { if (sourceSystem == null) throw "eve.ShaderSystem.bindSharedFields: source system is null" _bindings.push({ binding = binding, slot = slot, fields = fields, uploadEachUpdate = false, readback = readback, uploadedRevision = -1, uploadRequested = false, readbackRequested = false, uploadFirst = 0, uploadCount = 0, readbackFirst = 0, readbackCount = 0, source = sourceSystem, sourceBinding = sourceBinding, initializeFromCpu = false, schema = null }) return this } function setBindingUpload(binding, enabled) { foreach (b in _bindings) if (b.binding == binding) b.uploadEachUpdate = enabled return this } function setBindingReadback(binding, enabled) { foreach (b in _bindings) if (b.binding == binding) b.readback = enabled return this } // Re-upload CPU component values on the next update. binding < 0 selects all. function requestUpload(binding = -1) { foreach (b in _bindings) { if ((binding < 0 || b.binding == binding) && b.source == null) { b.uploadRequested = true b.uploadFirst = 0 b.uploadCount = -1 } } return this } // Re-upload a stable contiguous entity range. Repeated requests are coalesced. function requestUploadRange(binding, firstEntity, entityCount) { if (firstEntity < 0 || entityCount <= 0) throw "eve.ShaderSystem.requestUploadRange: invalid entity range" foreach (b in _bindings) { if (b.binding != binding) continue if (b.source != null) throw "eve.ShaderSystem.requestUploadRange: shared binding is GPU-owned" if (!b.uploadRequested || b.uploadCount == 0) { b.uploadFirst = firstEntity b.uploadCount = entityCount } else if (b.uploadCount >= 0) { local oldEnd = b.uploadFirst + b.uploadCount local newEnd = firstEntity + entityCount local end = oldEnd > newEnd ? oldEnd : newEnd b.uploadFirst = b.uploadFirst < firstEntity ? b.uploadFirst : firstEntity b.uploadCount = end - b.uploadFirst } b.uploadRequested = true } return this } // Read GPU values back after the next update. binding < 0 selects all. function requestReadback(binding = -1) { foreach (b in _bindings) { if (binding < 0 || b.binding == binding) { b.readbackRequested = true b.readbackFirst = 0 b.readbackCount = -1 } } return this } // Read back a stable contiguous entity range. Repeated requests are coalesced. function requestReadbackRange(binding, firstEntity, entityCount) { if (firstEntity < 0 || entityCount <= 0) throw "eve.ShaderSystem.requestReadbackRange: invalid entity range" foreach (b in _bindings) { if (b.binding != binding) continue if (!b.readbackRequested || b.readbackCount == 0) { b.readbackFirst = firstEntity b.readbackCount = entityCount } else if (b.readbackCount >= 0) { local oldEnd = b.readbackFirst + b.readbackCount local newEnd = firstEntity + entityCount local end = oldEnd > newEnd ? oldEnd : newEnd b.readbackFirst = b.readbackFirst < firstEntity ? b.readbackFirst : firstEntity b.readbackCount = end - b.readbackFirst } b.readbackRequested = true } return this } function setFloat(index, value) { if (_backend != null) _backend.setFloat(index, value) return this } function getBackend() { return _backend } // Register renderer resources and submit a standard 80-byte GpuInstance binding // without copying the instance stream back through the CPU. function setGpuDrivenEnabled(enabled) { if (_gpu == null) throw "eve.ShaderSystem.setGpuDrivenEnabled: call setGpgpu first" _gpu.setGpuDrivenEnabled(enabled) return this } function gpuDrivenMeshRecord(mesh) { if (_gpu == null) throw "eve.ShaderSystem.gpuDrivenMeshRecord: call setGpgpu first" return _gpu.gpuDrivenMeshRecord(mesh) } function gpuDrivenMaterialRecord(material) { if (_gpu == null) throw "eve.ShaderSystem.gpuDrivenMaterialRecord: call setGpgpu first" return _gpu.gpuDrivenMaterialRecord(material) } function submitResidentInstances(binding, buckets, instanceCount = -1, offsetBytes = 0) { if (_gpu == null || _backend == null) throw "eve.ShaderSystem.submitResidentInstances: call setGpgpu first" local buffer = _backend.getBuffer(binding) if (buffer == null) throw "eve.ShaderSystem.submitResidentInstances: binding has no buffer" if (instanceCount < 0) instanceCount = entities().len() return _gpu.submitResidentInstances(buffer, buckets, instanceCount, offsetBytes) } function _prepareGpuBatch() { if (_backend == null || _gpu == null) return if (!("packEcsFloats" in eve) || !("unpackEcsFloats" in eve)) return local ents = entities() local n = ents.len() if (n <= 0) return null foreach (b in _bindings) { local floatsPer = b.fields.len() if (floatsPer <= 0) continue if (b.source != null) { local sourceBackend = b.source.getBackend() local sourceBuffer = sourceBackend == null ? null : sourceBackend.getBuffer(b.sourceBinding) if (sourceBuffer == null) throw "eve.ShaderSystem shared source must be prepared before consumer" if (sourceBuffer.getSize() < n * floatsPer * 4) throw "eve.ShaderSystem shared buffer is smaller than consumer view" _backend.attachBuffer(b.binding, sourceBuffer) continue } local buf = _backend.ensureBuffer(b.binding, n * floatsPer) local structureChanged = b.uploadedRevision != eve._ecsRevision local fullUpload = (structureChanged && (b.initializeFromCpu || b.uploadRequested)) || b.uploadEachUpdate || (b.uploadRequested && b.uploadCount < 0) if (fullUpload) { eve.packEcsFloats(ents, b.slot, b.fields, buf) b.uploadedRevision = eve._ecsRevision b.uploadRequested = false b.uploadCount = 0 } else if (structureChanged) { // Output-only schema: capacity/order changed, but the shader is // authoritative and promises to initialize every accessed record. b.uploadedRevision = eve._ecsRevision } else if (b.uploadRequested) { if (!("packEcsFloatsRange" in eve)) throw "eve.ShaderSystem.requestUploadRange requires gpgpu range packing" eve.packEcsFloatsRange(ents, b.slot, b.fields, buf, b.uploadFirst, b.uploadCount) b.uploadRequested = false b.uploadCount = 0 } } return { entities = ents, count = n } } function _completeGpuBatch(batch) { if (batch == null) return local ents = batch.entities local n = batch.count foreach (b in _bindings) { local shouldReadback = b.readback == null ? _readback : b.readback if (shouldReadback || (b.readbackRequested && b.readbackCount < 0)) { local floatsPer = b.fields.len() if (floatsPer <= 0) continue local buf = _backend.getBuffer(b.binding) eve.unpackEcsFloats(ents, b.slot, b.fields, buf, n) b.readbackRequested = false b.readbackCount = 0 } else if (b.readbackRequested) { if (!("unpackEcsFloatsRange" in eve)) throw "eve.ShaderSystem.requestReadbackRange requires gpgpu range unpacking" local buf = _backend.getBuffer(b.binding) eve.unpackEcsFloatsRange(ents, b.slot, b.fields, buf, b.readbackFirst, b.readbackCount) b.readbackRequested = false b.readbackCount = 0 } } } // Record into a shared GpuSequence. Call completeRecorded(batch) after submit(). function record(sequence, dt) { if (sequence == null) throw "eve.ShaderSystem.record: sequence is null" local batch = _prepareGpuBatch() if (batch != null) _backend.recordDispatch(sequence, batch.count, dt) return batch } function completeRecorded(batch) { _completeGpuBatch(batch) return this } function update(dt) { local batch = _prepareGpuBatch() if (batch == null) return _backend.dispatch(batch.count, dt) _completeGpuBatch(batch) } } function eve::view(entityClass) { if (entityClass == null) return [] // C++ 桥接类的实体在脚本外增删(无法通知脚本),不缓存,每次新鲜收集 local cppBacked = false if ("_cppHasView" in this) cppBacked = this._cppHasView(entityClass) if (!cppBacked && (entityClass in this._ecsViewCache)) return this._ecsViewCache[entityClass] local out = [] this._ecsCollectInstances(entityClass, out) if ("_cppCollect" in this) this._cppCollect(entityClass, out) if (!cppBacked) this._ecsViewCache[entityClass] <- out return out } function eve::ecsReady() { // This function is installed only after the complete ECS script has run. // Avoid probing the delegated eve table here: Squirrel's `in` lookup may // invoke its lazy _get handler and turn a readiness query into an error. return true } )SQ"; void injectEcsScript(ssq::Table& eveTable) { HSQUIRRELVM vm = eveTable.getHandle(); const SQInteger top = sq_gettop(vm); if (SQ_FAILED(sq_compilebuffer(vm, kEcsScript, static_cast(std::strlen(kEcsScript)), "ecs.nut", SQTrue))) { sq_settop(vm, top); const script::ScriptErrorContext ctx = script::captureCompileError(vm); std::fprintf(stderr, "EVEngine: ECS bootstrap script failed to compile: %s\n", ctx.empty() ? "unknown error" : ctx.message.c_str()); return; } sq_pushroottable(vm); if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) { const script::ScriptErrorContext ctx = script::takeLastScriptError(vm); std::fprintf(stderr, "EVEngine: ECS bootstrap script failed to run: %s\n", ctx.empty() ? "unknown error" : script::formatScriptError(ctx).c_str()); } sq_settop(vm, top); } /** * Script hook for eve.view(): walk the script class chain (cls → base → …) and, * at the first class registered via registerCppEntityView(), invoke its collector * to append the matching C++ entities to `out`. */ void cppCollect(ssq::Class cls, ssq::Array out) { HSQUIRRELVM vm = cls.getHandle(); const SQInteger top = sq_gettop(vm); sq_pushobject(vm, cls.getRaw()); for (int guard = 0; guard < 64; ++guard) { if (sq_gettype(vm, -1) != OT_CLASS) break; HSQOBJECT cur; sq_getstackobj(vm, -1, &cur); auto it = cppEntityViews().find(cur._unVal.pClass); if (it != cppEntityViews().end()) { sq_settop(vm, top); it->second(out); return; } if (SQ_FAILED(sq_getbase(vm, -1))) break; sq_remove(vm, -2); // drop current class, keep base } sq_settop(vm, top); } /** * Script hook for view-cache policy: returns true when any class on the chain * (cls → base → …) is registered via registerCppEntityView(). C++ entities can * appear/disappear outside script knowledge, so such views must not be cached. */ bool cppHasView(ssq::Object cls) { if (cls.getType() != ssq::Type::CLASS) return false; HSQUIRRELVM vm = cls.getHandle(); const SQInteger top = sq_gettop(vm); sq_pushobject(vm, cls.getRaw()); for (int guard = 0; guard < 64; ++guard) { if (sq_gettype(vm, -1) != OT_CLASS) break; HSQOBJECT cur; sq_getstackobj(vm, -1, &cur); if (cppEntityViews().find(cur._unVal.pClass) != cppEntityViews().end()) { sq_settop(vm, top); return true; } if (SQ_FAILED(sq_getbase(vm, -1))) break; sq_remove(vm, -2); // drop current class, keep base } sq_settop(vm, top); return false; } } // namespace void exposeECS(ssq::Table& table) { table.set("Number", std::string("number")); table.set("Boolean", std::string("boolean")); table.set("String", std::string("string")); // ecsReady / view / Entity / Component are provided by the injected script. table.addFunc("inspectClassFields", inspectClassFields); table.addFunc("isSubclass", scriptIsSubclass); table.addFunc("getClassBase", getClassBase); table.addFunc("allocEntityId", allocEntityId); table.addFunc("_cppHasView", cppHasView); table.addFunc("component", [](std::string name, ssq::Object cls) { registerScriptComponent(name, cls); }); table.addFunc("_cppCollect", std::function(cppCollect)); injectEcsScript(table); ecsScriptInjected() = true; // A new VM re-runs every hook (SceneEntity class, …) against this table. postEcsHooksFlushed() = 0; flushPostEcsHooks(table); } void prepareEcsScriptInjection() { // Class identities belong to one VM. Clear them when a new eve table is // prepared, before any lazy/native exposer registers the next VM's classes. cppEntityViews().clear(); ecsScriptInjected() = false; postEcsHooksFlushed() = 0; } void registerCppEntityView(const ssq::Class& cls, CppEntityViewFn fn) { cppEntityViews()[cls.getRaw()._unVal.pClass] = std::move(fn); } void registerPostEcsHook(PostEcsHook fn) { postEcsHooks().push_back(std::move(fn)); } void flushPostEcsHooks(ssq::Table& table) { if (!ecsScriptInjected()) return; auto& hooks = postEcsHooks(); auto& n = postEcsHooksFlushed(); while (n < hooks.size()) { try { hooks[n](table); } catch (...) { // A failing module hook must not break VM exposure. } ++n; } } void exposeECSToVM(ssq::VM& vm) { try { ssq::Table eveTable(vm.find("eve")); exposeECS(eveTable); } catch (...) { } } } // namespace eve