Files

374 lines
12 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/ECS.h"
#include "common/Module.h"
#include <simplesquirrel/simplesquirrel.hpp>
#include <cmath>
#include <functional>
#include <vector>
class Node : public ecs::Entity {
public:
ENTITY(Node, ecs::Entity)
void release() override { ecs::DestroyEntity(this); }
struct Position {
float x = 0;
float y = 0;
};
COMPONENT(Position, position)
};
class Movable : public Node {
public:
ENTITY(Movable, Node)
void release() override { ecs::DestroyEntity(this); }
struct Velocity {
float dx = 1;
float dy = 1;
};
COMPONENT(Velocity, velocity)
};
static int countNodes() {
int n = 0;
auto view = ecs::View<Node, Node::Position>();
for (auto it = view.begin(); it != view.end(); ++it)
++n;
return n;
}
TEST_CASE("ECS.basicCreateAndComponent") {
ecs::Table world;
ecs::ScopedTable guard(world);
Node* a = Node::create();
Movable* b = Movable::create();
a->position()->x = 10;
a->position()->y = 0;
b->position()->x = 0;
b->position()->y = 10;
b->velocity()->dx = 2;
b->velocity()->dy = 2;
CHECK(std::abs(a->position()->x - 10.f) < 1e-5f);
CHECK(std::abs(b->position()->y - 10.f) < 1e-5f);
CHECK(std::abs(b->velocity()->dx - 2.f) < 1e-5f);
a->release();
b->release();
}
TEST_CASE("ECS.viewIncludesSubclass") {
ecs::Table world;
ecs::ScopedTable guard(world);
Node::create();
Movable::create();
int count = 0;
auto view = ecs::View<Node, Node::Position>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [pos] = *it;
pos->x = 0;
pos->y = 0;
++count;
}
CHECK_EQ(count, 2);
}
TEST_CASE("ECS.dynamicCreateDestroy") {
ecs::Table world;
ecs::ScopedTable guard(world);
Node* a = Node::create();
Movable* b = Movable::create();
a->position()->x = 3.f;
b->position()->x = 7.f;
CHECK(ecs::is_entity_visible(a));
CHECK(ecs::is_entity_visible(b));
CHECK_EQ(countNodes(), 2);
ecs::EntityHandle ha = ecs::handle_of(a);
const uint32_t oldGen = a->generation;
a->release();
CHECK(!ecs::is_entity_visible(a));
CHECK(ecs::try_get(ha) == nullptr);
CHECK_EQ(countNodes(), 1);
Node* c = Node::create();
c->position()->x = 11.f;
CHECK(ecs::is_entity_visible(c));
CHECK(ecs::try_get(ha) == nullptr);
CHECK(ecs::try_get(ecs::handle_of(c)) == c);
CHECK_EQ(countNodes(), 2);
if (c == a)
CHECK(c->generation != oldGen);
b->release();
CHECK_EQ(countNodes(), 1);
c->release();
CHECK_EQ(countNodes(), 0);
CHECK(ecs::try_get(ecs::handle_of(c)) == nullptr);
}
TEST_CASE("ECS.deferredCreateDestroyDuringView") {
ecs::Table world;
ecs::ScopedTable guard(world);
Node* a = Node::create();
a->position()->x = 1.f;
CHECK_EQ(countNodes(), 1);
{
ecs::ScopedDefer defer;
Node* spawned = Node::create();
CHECK((spawned->flags & ecs::kEntityStaging) != 0);
CHECK_EQ(countNodes(), 1);
a->release();
CHECK((a->flags & ecs::kEntityDead) != 0);
CHECK_EQ(countNodes(), 0);
// spawned is in the staging table; do not keep the pointer after commit.
}
CHECK_EQ(countNodes(), 1);
}
// ---------------------------------------------------------------------------
// Script side: extend C++ entity classes from Squirrel, drive them with a
// script System, and have eve.view() return the C++ entities.
// ---------------------------------------------------------------------------
static const char* kEcsScriptBridgeContent = R"SQ(
function testInheritCppEntityAndAccess() {
class Enemy extends eve.Node {}
local e = Enemy()
if (e == null) return false
if (!e.isAlive()) return false
// ids are 0-based registry indices; every live entity has a stable id.
if (e.getId() < 0) return false
e.getPos().setX(10.0)
e.getPos().setY(20.0)
if (e.getPosX() != 10.0) return false
if (e.getPosY() != 20.0) return false
e.setPosX(30.0)
if (e.getPosX() != 30.0) return false
return true
}
function testSystemOverCppEntities() {
class Enemy extends eve.Movable {}
class MoveSys extends eve.System {
constructor() { base.constructor(Enemy) }
function update(dt) {
foreach (e in entities()) {
e.setPosX(e.getPosX() + e.getVelX() * dt)
e.setPosY(e.getPosY() + e.getVelY() * dt)
}
}
}
local a = Enemy()
local b = Enemy()
a.setPosX(0.0)
a.setVelX(1.0)
b.setPosX(10.0)
b.setVelX(2.0)
local sys = MoveSys()
sys.update(2.0)
if (a.getPosX() != 2.0) return false
if (b.getPosX() != 14.0) return false
return true
}
function testCppCreatedEntitiesVisible() {
class Enemy extends eve.Node {}
local all = eve.view(Enemy)
if (all.len() < 2) return false
local found42 = false
foreach (e in all) {
if (e.getPosX() == 42.0) found42 = true
e.setPosX(e.getPosX() + 1.0)
}
if (!found42) return false
return true
}
function testDestroyFromScript() {
class Enemy extends eve.Node {}
local e = Enemy()
if (!e.isAlive()) return false
e.destroy()
if (e.isAlive()) return false
return true
}
)SQ";
namespace {
// Register a C++ ECS entity type so eve.view() (and script Systems built on it)
// can enumerate live instances and hand them to scripts as wrapped instances.
template <typename T>
void registerCppEntityClassForScript(const ssq::Class& cls) {
eve::registerCppEntityView(cls, [](ssq::Array& out) {
HSQUIRRELVM vm = out.getHandle();
sq_pushobject(vm, out.getRaw());
ecs::Table* table = ecs::current();
if (table == nullptr) {
sq_pop(vm, 1);
return;
}
ecs::IComponentManager& cm = table->getOrCreateManager<T>();
auto* reg = cm.getOrCreateRegistryComponentBuffer<T>();
// Depth-first walk of the registry and its subclass/sibling registries,
// matching ecs::View<T> semantics.
std::vector<ecs::IComponentBuffer*> stack;
stack.push_back(reg);
while (!stack.empty()) {
ecs::IComponentBuffer* buf = stack.back();
stack.pop_back();
auto* r = dynamic_cast<ecs::IRegistryComponentBuffer*>(buf);
if (r != nullptr) {
for (uint32_t i = 0; i < r->entity_count(); ++i) {
ecs::Entity* ent = r->entity_at(i);
if (ent != nullptr && ecs::is_entity_visible(ent)) {
ssq::detail::pushByPtr<T>(vm, static_cast<T*>(ent));
sq_arrayappend(vm, -2);
}
}
}
if (buf->children != nullptr)
stack.push_back(buf->children);
if (buf->next != nullptr)
stack.push_back(buf->next);
}
sq_pop(vm, 1);
});
}
class ScriptEcsBridgeFixture {
public:
ScriptEcsBridgeFixture() : vm(2048, ssq::Libs::ALL) {
eve::ModuleManager::expose(vm);
registerCppBindings();
ssq::Script s = vm.compileSource(kEcsScriptBridgeContent);
vm.run(s);
}
protected:
// Register C++ Node/Movable as inheritable script classes and expose
// component accessors so scripts can read/write the C++ entities.
void registerCppBindings() {
ssq::Table eve(vm.find("eve"));
auto nodeCls = eve.addClass("Node", std::function<Node*()>([]() -> Node* { return Node::create(); }), false);
registerCppEntityClassForScript<Node>(nodeCls);
nodeCls.addFunc("getId", [](Node* self) { return int(self->id); });
nodeCls.addFunc("isAlive", [](Node* self) { return ecs::is_entity_visible(self); });
nodeCls.addFunc("destroy", [](Node* self) { self->release(); });
nodeCls.addFunc("getPosX", [](Node* self) { return self->position()->x; });
nodeCls.addFunc("getPosY", [](Node* self) { return self->position()->y; });
nodeCls.addFunc("setPosX", [](Node* self, float v) { self->position()->x = v; });
nodeCls.addFunc("setPosY", [](Node* self, float v) { self->position()->y = v; });
nodeCls.addFunc("getPos", [](Node* self) -> Node::Position* { return &*self->position(); });
auto movableCls = eve.addClass("Movable", std::function<Movable*()>([]() -> Movable* { return Movable::create(); }), false);
registerCppEntityClassForScript<Movable>(movableCls);
movableCls.addFunc("getId", [](Movable* self) { return int(self->id); });
movableCls.addFunc("isAlive", [](Movable* self) { return ecs::is_entity_visible(self); });
movableCls.addFunc("destroy", [](Movable* self) { self->release(); });
movableCls.addFunc("getPosX", [](Movable* self) { return self->position()->x; });
movableCls.addFunc("getPosY", [](Movable* self) { return self->position()->y; });
movableCls.addFunc("setPosX", [](Movable* self, float v) { self->position()->x = v; });
movableCls.addFunc("setPosY", [](Movable* self, float v) { self->position()->y = v; });
movableCls.addFunc("getVelX", [](Movable* self) { return self->velocity()->dx; });
movableCls.addFunc("getVelY", [](Movable* self) { return self->velocity()->dy; });
movableCls.addFunc("setVelX", [](Movable* self, float v) { self->velocity()->dx = v; });
movableCls.addFunc("setVelY", [](Movable* self, float v) { self->velocity()->dy = v; });
// Component structs exposed as script classes so getPos() returns a
// live view of the C++ component storage.
auto posCls = eve.addClass("Position",
std::function<Node::Position*()>([]() -> Node::Position* { return nullptr; }), false);
posCls.addFunc("getX", [](Node::Position* self) { return self->x; });
posCls.addFunc("getY", [](Node::Position* self) { return self->y; });
posCls.addFunc("setX", [](Node::Position* self, float v) { self->x = v; });
posCls.addFunc("setY", [](Node::Position* self, float v) { self->y = v; });
}
ssq::VM vm;
};
} // namespace
TEST_CASE_FIXTURE(ScriptEcsBridgeFixture, "ECS.script.inheritCppEntityAndAccess") {
ecs::Table world;
ecs::ScopedTable guard(world);
CHECK(vm.callFunc(vm.findFunc("testInheritCppEntityAndAccess"), vm).toBool());
int found = 0;
auto view = ecs::View<Node, Node::Position>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [pos] = *it;
if (std::abs(pos->x - 30.f) < 1e-5f)
++found;
}
CHECK_EQ(found, 1);
}
TEST_CASE_FIXTURE(ScriptEcsBridgeFixture, "ECS.script.systemOverCppEntities") {
ecs::Table world;
ecs::ScopedTable guard(world);
CHECK(vm.callFunc(vm.findFunc("testSystemOverCppEntities"), vm).toBool());
int atTwo = 0, atFourteen = 0;
auto view = ecs::View<Movable, Node::Position>();
for (auto it = view.begin(); it != view.end(); ++it) {
auto [pos] = *it;
if (std::abs(pos->x - 2.f) < 1e-5f)
++atTwo;
if (std::abs(pos->x - 14.f) < 1e-5f)
++atFourteen;
}
CHECK_EQ(atTwo, 1);
CHECK_EQ(atFourteen, 1);
}
TEST_CASE_FIXTURE(ScriptEcsBridgeFixture, "ECS.script.cppCreatedEntitiesVisible") {
ecs::Table world;
ecs::ScopedTable guard(world);
Node* n1 = Node::create();
Node* n2 = Node::create();
n1->position()->x = 42.f;
CHECK(vm.callFunc(vm.findFunc("testCppCreatedEntitiesVisible"), vm).toBool());
// Script view() wrapped the C++ entities and mutated their components.
CHECK(std::abs(n1->position()->x - 43.f) < 1e-5f);
CHECK(std::abs(n2->position()->x - 1.f) < 1e-5f);
}
TEST_CASE_FIXTURE(ScriptEcsBridgeFixture, "ECS.script.destroyFromScript") {
ecs::Table world;
ecs::ScopedTable guard(world);
CHECK(vm.callFunc(vm.findFunc("testDestroyFromScript"), vm).toBool());
int live = 0;
auto view = ecs::View<Node, Node::Position>();
for (auto it = view.begin(); it != view.end(); ++it)
++live;
CHECK_EQ(live, 0);
}