Files
EVEngine/src/engine/common/ECS.cpp
T

892 lines
32 KiB
C++

#include "common/ECS.h"
#include "common/ScriptError.h"
#include <simplesquirrel/simplesquirrel.hpp>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
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<std::string, ScriptComponentReg>& scriptComponents() {
static std::unordered_map<std::string, ScriptComponentReg> m;
return m;
}
int& nextEntityId() {
static int id = 1;
return id;
}
int allocEntityId() { return nextEntityId()++; }
// 当前 VM 的 Squirrel 类对象 → 脚本 view() 收集函数。
std::unordered_map<const void*, CppEntityViewFn>& cppEntityViews() {
static std::unordered_map<const void*, CppEntityViewFn> views;
return views;
}
std::vector<std::function<void(ssq::Table&)>>& postEcsHooks() {
static std::vector<std::function<void(ssq::Table&)>> 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<storage, " + access +
"> ecs" + b.binding + " : array<f32>;\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<SQInteger>(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<void(ssq::Class, ssq::Array)>(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