From 614a4ec58462d40082ce68374495ef71b4763e6d Mon Sep 17 00:00:00 2001 From: Leo Date: Wed, 19 Mar 2025 11:40:14 +0100 Subject: [PATCH] feat(ecs/zipper): add indexed zipper and zipper with tests --- packages/ecs/Makefile | 4 +- packages/ecs/test/wasm/IndexedZipper.spec.ts | 135 ++++++++++++++++++ packages/ecs/test/wasm/Registry.spec.ts | 132 ++++++++++-------- packages/ecs/test/wasm/SparseArray.spec.ts | 2 +- packages/ecs/test/wasm/Zipper.spec.ts | 137 +++++++++++++++++++ packages/ecs/wasm/Entity.cpp | 2 +- packages/ecs/wasm/IndexedZipper.cpp | 27 ++++ packages/ecs/wasm/IndexedZipper.hpp | 76 ++++++++++ packages/ecs/wasm/Registry.cpp | 46 ++++--- packages/ecs/wasm/Registry.hpp | 32 ++++- packages/ecs/wasm/SparseArray.cpp | 18 ++- packages/ecs/wasm/SparseArray.hpp | 8 +- packages/ecs/wasm/Utils.hpp | 2 +- packages/ecs/wasm/Zipper.cpp | 29 ++++ packages/ecs/wasm/Zipper.hpp | 77 +++++++++++ 15 files changed, 633 insertions(+), 94 deletions(-) create mode 100644 packages/ecs/test/wasm/IndexedZipper.spec.ts create mode 100644 packages/ecs/test/wasm/Zipper.spec.ts create mode 100644 packages/ecs/wasm/IndexedZipper.cpp create mode 100644 packages/ecs/wasm/IndexedZipper.hpp create mode 100644 packages/ecs/wasm/Zipper.cpp create mode 100644 packages/ecs/wasm/Zipper.hpp diff --git a/packages/ecs/Makefile b/packages/ecs/Makefile index e253e447..fa4d5482 100644 --- a/packages/ecs/Makefile +++ b/packages/ecs/Makefile @@ -1,6 +1,8 @@ SRC = wasm/SparseArray.cpp\ wasm/Entity.cpp\ wasm/Utils.cpp\ + wasm/Zipper.cpp\ + wasm/IndexedZipper.cpp\ wasm/Registry.cpp NAME := libecs @@ -12,7 +14,7 @@ WASM_NAME = $(OUT_DIR)/$(NAME).wasm TS_NAME = $(NAME).d.ts CFLAGS = -std=c++20 -LDFLAGS = -O3 --no-entry --bind +LDFLAGS = -O3 --no-entry --bind -sNO_DISABLE_EXCEPTION_CATCHING -sEXPORT_EXCEPTION_HANDLING_HELPERS CC = em++ diff --git a/packages/ecs/test/wasm/IndexedZipper.spec.ts b/packages/ecs/test/wasm/IndexedZipper.spec.ts new file mode 100644 index 00000000..8551203e --- /dev/null +++ b/packages/ecs/test/wasm/IndexedZipper.spec.ts @@ -0,0 +1,135 @@ +import Module from "../../public/libecs"; + +class Velocity { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +class Position { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +describe("IndexedZipper", () => { + test("basic instantation", async () => { + const m = await Module(); + const v = new m.MapStringSparseArray(); + + const zip = new m.IndexedZipper(v); + + expect(zip).toBeDefined(); + expect(zip.getValue()).toBeUndefined(); + }); + + test("single simple sparse array instantation", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 5; i++) { + const e = r.spawnEntity(); + r.addComponent(e, new Velocity(i, i)); + } + + const zip = r.getIndexedZipper([Velocity]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 5; i++, zip.next()) { + expect(zip.getValue()).toStrictEqual({ entity: i, Velocity: new Velocity(i, i) }); + } + expect(zip.getValue()).toBeUndefined(); + }); + + test("single complex sparse array instantation", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 5; i++) { + const e = new m.Entity(i * 5); + r.addComponent(e, new Velocity(i, i)); + } + + const zip = r.getIndexedZipper([Velocity]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 5; i++) { + expect(zip.getValue()).toStrictEqual({ entity: i * 5, Velocity: new Velocity(i, i) }); + zip.next(); + } + expect(zip.getValue()).toBeUndefined(); + }); + + test("multiple complex sparse array instantation", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 20; i++) { + const e = r.spawnEntity(i); + if (i % 5 === 0) r.addComponent(e, new Velocity(0, i)); + if (i % 3 === 0) r.addComponent(e, new Position(i, 0)); + } + + const zip = r.getIndexedZipper([Velocity, Position]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 20; i++) { + if (i % 3 === 0 && i % 5 === 0) { + expect(zip.getValue()).toStrictEqual({ + entity: i, + Velocity: new Velocity(0, i), + Position: new Position(i, 0), + }); + zip.next(); + } + } + expect(zip.getValue()).toBeUndefined(); + }); + + test("simple indexed zipper modification", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 20; i++) { + const e = r.spawnEntity(i); + if (i % 5 === 0) { + r.addComponent(e, new Velocity(0, i)); + } + } + + let zip = r.getIndexedZipper([Velocity]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 20; i++) { + if (i % 5 === 0) { + const vel = zip.getValue()["Velocity"]; + vel.y *= 2; + zip.next(); + } + } + + zip = r.getIndexedZipper([Velocity]); + for (let i = 0; i < 20; i++) { + if (i % 5 === 0) { + expect(zip.getValue()).toStrictEqual({ + entity: i, + Velocity: new Velocity(0, i * 2), + }); + zip.next(); + } + } + expect(zip.getValue()).toBeUndefined(); + }); +}); diff --git a/packages/ecs/test/wasm/Registry.spec.ts b/packages/ecs/test/wasm/Registry.spec.ts index e705f8f7..72a98c14 100644 --- a/packages/ecs/test/wasm/Registry.spec.ts +++ b/packages/ecs/test/wasm/Registry.spec.ts @@ -29,17 +29,17 @@ describe("Registry", () => { const vel = new Velocity(1, 2); const pos = new Position(4, 5); - const e = r.spawn_entity(); - expect(e.get_id()).toBe(0); + const e = r.spawnEntity(); + expect(e.getId()).toBe(0); - r.add_component(e, vel); - r.add_component(e, pos); + r.addComponent(e, vel); + r.addComponent(e, pos); - const velocities = r.get_components(Velocity); - const positions = r.get_components(Position); + const velocities = r.getComponents(Velocity); + const positions = r.getComponents(Position); - expect(velocities.get(e.get_id())).toStrictEqual(new Velocity(1, 2)); - expect(positions.get(e.get_id())).toStrictEqual(new Position(4, 5)); + expect(velocities.get(e.getId())).toStrictEqual(new Velocity(1, 2)); + expect(positions.get(e.getId())).toStrictEqual(new Position(4, 5)); }); test("override components", async () => { @@ -49,13 +49,13 @@ describe("Registry", () => { const vel = new Velocity(1, 2); const vel2 = new Velocity(4, 5); - const e = r.spawn_entity(); + const e = r.spawnEntity(); - r.add_component(e, vel); - expect(r.get_components(Velocity).get(e.get_id())).toStrictEqual(new Velocity(1, 2)); + r.addComponent(e, vel); + expect(r.getComponents(Velocity).get(e.getId())).toStrictEqual(new Velocity(1, 2)); - r.add_component(e, vel2); - expect(r.get_components(Velocity).get(e.get_id())).toStrictEqual(new Velocity(4, 5)); + r.addComponent(e, vel2); + expect(r.getComponents(Velocity).get(e.getId())).toStrictEqual(new Velocity(4, 5)); }); test("basic remove", async () => { @@ -63,14 +63,14 @@ describe("Registry", () => { const r = new m.Registry(); const vel = new Velocity(1, 2); - const e = r.spawn_entity(); + const e = r.spawnEntity(); - r.add_component(e, vel); - expect(r.get_components(Velocity).get(e.get_id())).toStrictEqual(new Velocity(1, 2)); + r.addComponent(e, vel); + expect(r.getComponents(Velocity).get(e.getId())).toStrictEqual(new Velocity(1, 2)); - r.remove_component(e, Velocity); - expect(r.get_components(Velocity).size()).toEqual(1); - expect(r.get_components(Velocity).get(e.get_id())).toBeUndefined(); + r.removeComponent(e, Velocity); + expect(r.getComponents(Velocity).size()).toEqual(1); + expect(r.getComponents(Velocity).get(e.getId())).toBeUndefined(); }); test("basic kill", async () => { @@ -81,23 +81,23 @@ describe("Registry", () => { const vel = new Velocity(1, 2); const pos = new Position(4, 5); - r.spawn_entity(); - const e = r.spawn_entity(); - expect(e.get_id()).toBe(1); + r.spawnEntity(); + const e = r.spawnEntity(); + expect(e.getId()).toBe(1); - r.add_component(e, vel); - r.add_component(e, pos); + r.addComponent(e, vel); + r.addComponent(e, pos); - const velocities = r.get_components(Velocity); - const positions = r.get_components(Position); + const velocities = r.getComponents(Velocity); + const positions = r.getComponents(Position); expect(positions.size()).toEqual(2); - expect(velocities.get(e.get_id())).toStrictEqual(new Velocity(1, 2)); - expect(positions.get(e.get_id())).toStrictEqual(new Position(4, 5)); + expect(velocities.get(e.getId())).toStrictEqual(new Velocity(1, 2)); + expect(positions.get(e.getId())).toStrictEqual(new Position(4, 5)); - r.kill_entity(e); - expect(r.get_components(Velocity).get(e.get_id())).toBeUndefined(); - expect(r.get_components(Position).get(e.get_id())).toBeUndefined(); + r.killEntity(e); + expect(r.getComponents(Velocity).get(e.getId())).toBeUndefined(); + expect(r.getComponents(Position).get(e.getId())).toBeUndefined(); }); test("system incrementing a variable", async () => { @@ -106,13 +106,13 @@ describe("Registry", () => { let counter = 0; - r.add_system(() => { + r.addSystem(() => { counter += 1; }); for (let i = 0; i <= 15; i++) { expect(counter).toBe(i); - r.run_systems(); + r.runSystems(); } expect(counter).toBe(16); }); @@ -122,22 +122,22 @@ describe("Registry", () => { const r = new m.Registry(); expect(r).toBeDefined(); - const e = r.spawn_entity(); - const e2 = r.spawn_entity(); - expect(e2.get_id()).toBe(1); - const e3 = r.spawn_entity(); + const e = r.spawnEntity(); + const e2 = r.spawnEntity(); + expect(e2.getId()).toBe(1); + const e3 = r.spawnEntity(); - r.add_component(e, new Velocity(1, 1)); - r.add_component(e, new Position(-2, -2)); + r.addComponent(e, new Velocity(1, 1)); + r.addComponent(e, new Position(-2, -2)); - r.add_component(e2, new Velocity(-1, -1)); - r.add_component(e2, new Position(2, 2)); + r.addComponent(e2, new Velocity(-1, -1)); + r.addComponent(e2, new Position(2, 2)); - r.add_component(e3, new Position(0, 0)); + r.addComponent(e3, new Position(0, 0)); - r.add_system(() => { - const velocities = r.get_components(Velocity); - const positions = r.get_components(Position); + r.addSystem(() => { + const velocities = r.getComponents(Velocity); + const positions = r.getComponents(Position); for (let i = 0; i < velocities.size() && i < positions.size(); i++) { if (velocities.get(i) === undefined || positions.get(i) === undefined) { continue; @@ -147,20 +147,38 @@ describe("Registry", () => { } }); - expect(r.get_components(Position).size()).toEqual(3); + expect(r.getComponents(Position).size()).toEqual(3); - expect(r.get_components(Position).get(e.get_id())).toStrictEqual(new Position(-2, -2)); - expect(r.get_components(Position).get(e2.get_id())).toStrictEqual(new Position(2, 2)); - expect(r.get_components(Position).get(e3.get_id())).toStrictEqual(new Position(0, 0)); - r.run_systems(); + expect(r.getComponents(Position).get(e.getId())).toStrictEqual(new Position(-2, -2)); + expect(r.getComponents(Position).get(e2.getId())).toStrictEqual(new Position(2, 2)); + expect(r.getComponents(Position).get(e3.getId())).toStrictEqual(new Position(0, 0)); + r.runSystems(); - expect(r.get_components(Position).get(e.get_id())).toStrictEqual(new Position(-1, -1)); - expect(r.get_components(Position).get(e2.get_id())).toStrictEqual(new Position(1, 1)); - expect(r.get_components(Position).get(e3.get_id())).toStrictEqual(new Position(0, 0)); - r.run_systems(); + expect(r.getComponents(Position).get(e.getId())).toStrictEqual(new Position(-1, -1)); + expect(r.getComponents(Position).get(e2.getId())).toStrictEqual(new Position(1, 1)); + expect(r.getComponents(Position).get(e3.getId())).toStrictEqual(new Position(0, 0)); + r.runSystems(); - expect(r.get_components(Position).get(e.get_id())).toStrictEqual(new Position(0, 0)); - expect(r.get_components(Position).get(e2.get_id())).toStrictEqual(new Position(0, 0)); - expect(r.get_components(Position).get(e3.get_id())).toStrictEqual(new Position(0, 0)); + expect(r.getComponents(Position).get(e.getId())).toStrictEqual(new Position(0, 0)); + expect(r.getComponents(Position).get(e2.getId())).toStrictEqual(new Position(0, 0)); + expect(r.getComponents(Position).get(e3.getId())).toStrictEqual(new Position(0, 0)); + }); + + test("Try unallowed component name", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + const entityComp = { name: "entity" }; + + const e = r.spawnEntity(); + expect(e.getId()).toBe(0); + + try { + r.addComponent(e, entityComp); + fail(); + } catch (e) { + expect(m.getExceptionMessage(e)[1].toString()).toBeDefined(); + } }); }); diff --git a/packages/ecs/test/wasm/SparseArray.spec.ts b/packages/ecs/test/wasm/SparseArray.spec.ts index ac115a3e..095a0445 100644 --- a/packages/ecs/test/wasm/SparseArray.spec.ts +++ b/packages/ecs/test/wasm/SparseArray.spec.ts @@ -25,7 +25,7 @@ describe("SparseArray", () => { expect(sa.get(0)).toBe(undefined); }); - test("basic iteration", async () => { + test("basic iteration with get and size", async () => { const m = await Module(); const sa = new m.SparseArray(); sa.set(0, 1); diff --git a/packages/ecs/test/wasm/Zipper.spec.ts b/packages/ecs/test/wasm/Zipper.spec.ts new file mode 100644 index 00000000..ab1b7b12 --- /dev/null +++ b/packages/ecs/test/wasm/Zipper.spec.ts @@ -0,0 +1,137 @@ +import Module from "../../public/libecs"; + +class Velocity { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +class Position { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +describe("Zipper", () => { + test("basic instantation", async () => { + const m = await Module(); + const v = new m.MapStringSparseArray(); + + const zip = new m.Zipper(v); + + expect(zip).toBeDefined(); + expect(zip.getValue()).toBeUndefined(); + }); + + test("single simple sparse array instantation", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 5; i++) { + const e = r.spawnEntity(); + r.addComponent(e, new Velocity(i, i)); + } + + const zip = r.getZipper([Velocity]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 5; i++, zip.next()) { + expect(zip.getValue()).toStrictEqual({ Velocity: new Velocity(i, i) }); + } + expect(zip.getValue()).toBeUndefined(); + }); + + test("single complex sparse array instantation", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 5; i++) { + const e = new m.Entity(i * 5); + r.addComponent(e, new Velocity(i, i)); + } + + const zip = r.getZipper([Velocity]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 5; i++) { + expect(zip.getValue()).toStrictEqual({ Velocity: new Velocity(i, i) }); + zip.next(); + } + expect(zip.getValue()).toBeUndefined(); + }); + + test("multiple complex sparse array instantation", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 20; i++) { + const e = r.spawnEntity(i); + if (i % 5 === 0) { + r.addComponent(e, new Velocity(0, i)); + } + if (i % 3 === 0) { + r.addComponent(e, new Position(i, 0)); + } + } + + const zip = r.getZipper([Velocity, Position]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 20; i++) { + if (i % 3 === 0 && i % 5 === 0) { + expect(zip.getValue()).toStrictEqual({ + Velocity: new Velocity(0, i), + Position: new Position(i, 0), + }); + zip.next(); + } + } + expect(zip.getValue()).toBeUndefined(); + }); + + test("simple zipper modification", async () => { + const m = await Module(); + const r = new m.Registry(); + expect(r).toBeDefined(); + + for (let i = 0; i < 20; i++) { + const e = r.spawnEntity(i); + if (i % 5 === 0) { + r.addComponent(e, new Velocity(0, i)); + } + } + + let zip = r.getZipper([Velocity]); + expect(zip).toBeDefined(); + + for (let i = 0; i < 20; i++) { + if (i % 5 === 0) { + const vel = zip.getValue()["Velocity"]; + vel.y *= 2; + zip.next(); + } + } + + zip = r.getZipper([Velocity]); + for (let i = 0; i < 20; i++) { + if (i % 5 === 0) { + expect(zip.getValue()).toStrictEqual({ + Velocity: new Velocity(0, i * 2), + }); + zip.next(); + } + } + expect(zip.getValue()).toBeUndefined(); + }); +}); diff --git a/packages/ecs/wasm/Entity.cpp b/packages/ecs/wasm/Entity.cpp index 039da9dc..30064bb8 100644 --- a/packages/ecs/wasm/Entity.cpp +++ b/packages/ecs/wasm/Entity.cpp @@ -18,6 +18,6 @@ namespace nfo { EMSCRIPTEN_BINDINGS(Entity) { - emscripten::class_("Entity").constructor().function("get_id", &Entity::get_id); + emscripten::class_("Entity").constructor().function("getId", &Entity::get_id); } } // namespace nfo diff --git a/packages/ecs/wasm/IndexedZipper.cpp b/packages/ecs/wasm/IndexedZipper.cpp new file mode 100644 index 00000000..48729991 --- /dev/null +++ b/packages/ecs/wasm/IndexedZipper.cpp @@ -0,0 +1,27 @@ +/*⠀ ⠀⠀ ⠀⠀⠀⠀⢀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀ +**⠀⠀ ⠀⢀⣠⣾⡿⠿⠛⠛⠛⠛⠿⢿⣷⣄⡀⠀⠀⠀ +** _ __ ______ ⠀ ⠀ ⣰⣿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣆⠀⠀ +** / | / /___ _____ ____ / ____/___ _________ ____ ⠀ ⣾⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣷⠀ +** / |/ / __ `/ __ \/ __ \/ /_ / __ \/ ___/ __ `/ _ \ ⢰⣿⠃⠀⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⠤⠀⠀⠘⣿⡆ +** / /| / /_/ / / / / /_/ / __/ / /_/ / / / /_/ / __/ ⢸⣿⠀⠀⠀⠉⠛⠛⢻⣿⣿⣿⠉⠀⠀⠀⠀⠀⣿⡇ +** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇ +** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀ +** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀ +** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀ +**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀ +*/ + +#include +#include + +#include "IndexedZipper.hpp" + +namespace nfo { + EMSCRIPTEN_BINDINGS(IndexedZipper) + { + emscripten::class_("IndexedZipper") + .constructor *> &>() + .function("next", &IndexedZipper::next) + .function("getValue", &IndexedZipper::get_value); + } +} // namespace nfo diff --git a/packages/ecs/wasm/IndexedZipper.hpp b/packages/ecs/wasm/IndexedZipper.hpp new file mode 100644 index 00000000..2fc9cea8 --- /dev/null +++ b/packages/ecs/wasm/IndexedZipper.hpp @@ -0,0 +1,76 @@ +/*⠀ ⠀⠀ ⠀⠀⠀⠀⢀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀ +**⠀⠀ ⠀⢀⣠⣾⡿⠿⠛⠛⠛⠛⠿⢿⣷⣄⡀⠀⠀⠀ +** _ __ ______ ⠀ ⠀ ⣰⣿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣆⠀⠀ +** / | / /___ _____ ____ / ____/___ _________ ____ ⠀ ⣾⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣷⠀ +** / |/ / __ `/ __ \/ __ \/ /_ / __ \/ ___/ __ `/ _ \ ⢰⣿⠃⠀⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⠤⠀⠀⠘⣿⡆ +** / /| / /_/ / / / / /_/ / __/ / /_/ / / / /_/ / __/ ⢸⣿⠀⠀⠀⠉⠛⠛⢻⣿⣿⣿⠉⠀⠀⠀⠀⠀⣿⡇ +** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇ +** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀ +** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀ +** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀ +**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀ +*/ + +#pragma once + +#include +#include + +#include "SparseArray.hpp" +#include "Utils.hpp" + +namespace nfo { + class IndexedZipper { + public: + explicit IndexedZipper(const std::map *> &arrays) : _arrays(arrays), _max(0), _idx(0) + { + _max = arrays.empty() ? 0 : arrays.begin()->second->size(); + for (SparseArray *&arr : _arrays | std::views::values) { + _max = (std::min)(arr->size(), _max); + } + if (_idx < _max && !all_set()) + incr(); + } + + [[nodiscard]] emscripten::val get_value() const + { + if (_idx >= _max) + return emscripten::val::undefined(); + + emscripten::val res = emscripten::val::object(); + for (SparseArray *const &arr : _arrays | std::views::values) { + res.set(get_js_class_name((*arr)[_idx].value()), (*arr)[_idx].value_or(emscripten::val::undefined())); + } + res.set("entity", _idx); + return res; + } + + emscripten::val next() + { + incr(); + return get_value(); + } + + private: + void incr() + { + if (_idx >= _max) + return; + do { + _idx++; + } while (_idx < _max && !all_set()); + } + + [[nodiscard]] bool all_set() const + { + if (_idx >= _max) + return false; + + return std::ranges::all_of(_arrays | std::views::values, [this](SparseArray *const &arr) { return (*arr)[_idx].has_value(); }); + } + + std::map *> _arrays; + std::size_t _max; + std::size_t _idx; + }; +} // namespace nfo diff --git a/packages/ecs/wasm/Registry.cpp b/packages/ecs/wasm/Registry.cpp index 93d9f1a2..41d6b7e0 100644 --- a/packages/ecs/wasm/Registry.cpp +++ b/packages/ecs/wasm/Registry.cpp @@ -21,35 +21,41 @@ namespace nfo { { emscripten::class_("Registry") .constructor() - .function("register_component", &Registry::register_component) + .function("registerComponent", &Registry::register_component) .function( - "get_components_const", + "getComponentsConst", emscripten::select_overload const &(const emscripten::val &) const, Registry>(&Registry::get_components) ) - .function("get_components", emscripten::select_overload &(const emscripten::val &), Registry>(&Registry::get_components)) .function( - "get_entity_component_const", - emscripten::select_overload const &(Entity, const emscripten::val &) const, Registry>(&Registry::get_entity_component) + "getComponents", emscripten::select_overload &(const emscripten::val &), Registry>(&Registry::get_components) ) .function( - "get_entity_component", + "getEntityComponentConst", + emscripten::select_overload const &(Entity, const emscripten::val &) const, Registry>( + &Registry::get_entity_component + ) + ) + .function( + "getEntityComponent", emscripten::select_overload &(Entity, const emscripten::val &), Registry>(&Registry::get_entity_component) ) - .function("spawn_entity", &Registry::spawn_entity) - .function("entity_from_index", &Registry::entity_from_index) - .function("kill_entity", &Registry::kill_entity) - .function("clear_entities", &Registry::clear_entities) + .function("spawnEntity", &Registry::spawn_entity) + .function("entityFromIndex", &Registry::entity_from_index) + .function("killEntity", &Registry::kill_entity) + .function("clearEntities", &Registry::clear_entities) .function( - "add_component", - emscripten::select_overload< - SparseArray::reference_type &(const Entity &, emscripten::val &&), - Registry>(&Registry::add_component) + "addComponent", + emscripten::select_overload::reference_type &(const Entity &, emscripten::val &&), Registry>( + &Registry::add_component + ) ) - .function("remove_component", emscripten::select_overload(&Registry::remove_component)) - .function("add_system", emscripten::select_overload(&Registry::add_system)) - .function("run_systems", &Registry::run_systems) - .function("remove_system", &Registry::remove_system) - .function("clear_systems", &Registry::clear_systems) - .function("max_entities", &Registry::max_entities); + .function("removeComponent", emscripten::select_overload(&Registry::remove_component)) + .function("addSystem", emscripten::select_overload(&Registry::add_system)) + .function("runSystems", &Registry::run_systems) + .function("removeSystem", &Registry::remove_system) + .function("clearSystems", &Registry::clear_systems) + .function("getZipper", &Registry::get_zipper) + .function("getIndexedZipper", &Registry::get_indexed_zipper) + .function("maxEntities", &Registry::max_entities); } } // namespace nfo diff --git a/packages/ecs/wasm/Registry.hpp b/packages/ecs/wasm/Registry.hpp index 2883b329..1bd5601f 100644 --- a/packages/ecs/wasm/Registry.hpp +++ b/packages/ecs/wasm/Registry.hpp @@ -21,9 +21,10 @@ #include #include "Entity.hpp" +#include "IndexedZipper.hpp" #include "SparseArray.hpp" #include "Utils.hpp" - +#include "Zipper.hpp" namespace nfo { class Registry { @@ -31,9 +32,10 @@ namespace nfo { SparseArray ®ister_component(const emscripten::val &c) { std::string component_type(get_js_class_name(c)); - if (!_components_arrays.contains(component_type)) { + if (component_type == "entity" || component_type == "id") + throw std::runtime_error("Component type '" + component_type + "' not supported : you can't use : id, entity, " + UNKNOWN_COMPONENT_TYPE); + if (!_components_arrays.contains(component_type)) _components_arrays.emplace(component_type, SparseArray()); - } if (!_remove_functions.contains(component_type)) { _remove_functions.emplace(component_type, [c](Registry ®, Entity const &ent) { SparseArray &array = reg.get_components(c); @@ -179,6 +181,30 @@ namespace nfo { return _next_entity; } + Zipper get_zipper(const emscripten::val &comps) + { + if (!comps.isArray()) + throw std::runtime_error("get_zipper: comps is not an array"); + + std::map *> arrays; + for (int i = 0; i < comps["length"].as(); i++) { + arrays[get_js_class_name(comps[i])] = &get_components(comps[i]); + } + return Zipper(arrays); + } + + IndexedZipper get_indexed_zipper(const emscripten::val &comps) + { + if (!comps.isArray()) + throw std::runtime_error("get_zipper: comps is not an array"); + + std::map *> arrays; + for (int i = 0; i < comps["length"].as(); i++) { + arrays[get_js_class_name(comps[i])] = &get_components(comps[i]); + } + return IndexedZipper(arrays); + } + private: std::unordered_map _components_arrays; diff --git a/packages/ecs/wasm/SparseArray.cpp b/packages/ecs/wasm/SparseArray.cpp index d73b5f35..da9e1f02 100644 --- a/packages/ecs/wasm/SparseArray.cpp +++ b/packages/ecs/wasm/SparseArray.cpp @@ -27,13 +27,13 @@ namespace nfo { .function("erase", &SparseArray::erase) .function("size", &SparseArray::size) .function( - "get_index", + "getIndex", emscripten::select_overload::size_type(SparseArray::const_reference_type) const>( &SparseArray::get_index ) ) .function( - "get_const", + "getConst", emscripten::select_overload::const_reference_type(SparseArray::size_type) const>( &SparseArray::operator[] ) @@ -45,14 +45,14 @@ namespace nfo { ) ) .function( - "insert_at", + "insertAt", emscripten::select_overload< SparseArray::reference_type(SparseArray::size_type, SparseArray::const_reference_type)>( &SparseArray::insert_at ) ) .function( - "insert_at", + "insertAt", emscripten::select_overload< SparseArray::reference_type(SparseArray::size_type, SparseArray::value_type &&)>( &SparseArray::insert_at @@ -62,7 +62,13 @@ namespace nfo { .function("empty", &SparseArray::empty) .function("resize", &SparseArray::resize) .function("set", &SparseArray::set) - .function("setByCopy", emscripten::select_overload &(SparseArray const &)>(&SparseArray::operator=)) - .function("setByMove", emscripten::select_overload &(SparseArray &&)>(&SparseArray::operator=)); + .function( + "setByCopy", + emscripten::select_overload &(SparseArray const &)>(&SparseArray::operator=) + ) + .function( + "setByMove", + emscripten::select_overload &(SparseArray &&)>(&SparseArray::operator=) + ); } } // namespace nfo diff --git a/packages/ecs/wasm/SparseArray.hpp b/packages/ecs/wasm/SparseArray.hpp index 55f30079..1148bb1d 100644 --- a/packages/ecs/wasm/SparseArray.hpp +++ b/packages/ecs/wasm/SparseArray.hpp @@ -13,9 +13,11 @@ #pragma once +#include #include -#include #include +#include +#include <__ostream/basic_ostream.h> namespace nfo { template @@ -127,9 +129,7 @@ namespace nfo { [[nodiscard]] bool empty() const { - return _data.empty() || std::ranges::all_of(_data, [](const auto &v) { - return !v.has_value(); - }); + return _data.empty() || std::ranges::all_of(_data, [](const auto &v) { return !v.has_value(); }); } reference_type insert_at(size_type idx, const_reference_type value) diff --git a/packages/ecs/wasm/Utils.hpp b/packages/ecs/wasm/Utils.hpp index e9f0ce4d..3b45d3c2 100644 --- a/packages/ecs/wasm/Utils.hpp +++ b/packages/ecs/wasm/Utils.hpp @@ -19,7 +19,7 @@ #define UNKNOWN_COMPONENT_TYPE "__magic_unkown_component_type" -std::optional get_js_as_string(const emscripten::val &c); +std::optional json_to_str(const emscripten::val &c); std::optional get_js_member(const emscripten::val &c, const std::string &member); std::string get_js_class_name(const emscripten::val &c); std::optional get_js_class_type_name(const emscripten::val &c); diff --git a/packages/ecs/wasm/Zipper.cpp b/packages/ecs/wasm/Zipper.cpp new file mode 100644 index 00000000..119b07df --- /dev/null +++ b/packages/ecs/wasm/Zipper.cpp @@ -0,0 +1,29 @@ +/*⠀ ⠀⠀ ⠀⠀⠀⠀⢀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀ +**⠀⠀ ⠀⢀⣠⣾⡿⠿⠛⠛⠛⠛⠿⢿⣷⣄⡀⠀⠀⠀ +** _ __ ______ ⠀ ⠀ ⣰⣿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣆⠀⠀ +** / | / /___ _____ ____ / ____/___ _________ ____ ⠀ ⣾⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣷⠀ +** / |/ / __ `/ __ \/ __ \/ /_ / __ \/ ___/ __ `/ _ \ ⢰⣿⠃⠀⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⠤⠀⠀⠘⣿⡆ +** / /| / /_/ / / / / /_/ / __/ / /_/ / / / /_/ / __/ ⢸⣿⠀⠀⠀⠉⠛⠛⢻⣿⣿⣿⠉⠀⠀⠀⠀⠀⣿⡇ +** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇ +** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀ +** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀ +** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀ +**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀ +*/ + +#include +#include + +#include "Zipper.hpp" + +namespace nfo { + EMSCRIPTEN_BINDINGS(Zipper) + { + emscripten::register_map *>("MapStringSparseArray"); + + emscripten::class_("Zipper") + .constructor *> &>() + .function("next", &Zipper::next) + .function("getValue", &Zipper::get_value); + } +} // namespace nfo diff --git a/packages/ecs/wasm/Zipper.hpp b/packages/ecs/wasm/Zipper.hpp new file mode 100644 index 00000000..ec79c2b2 --- /dev/null +++ b/packages/ecs/wasm/Zipper.hpp @@ -0,0 +1,77 @@ +/*⠀ ⠀⠀ ⠀⠀⠀⠀⢀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀ +**⠀⠀ ⠀⢀⣠⣾⡿⠿⠛⠛⠛⠛⠿⢿⣷⣄⡀⠀⠀⠀ +** _ __ ______ ⠀ ⠀ ⣰⣿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣆⠀⠀ +** / | / /___ _____ ____ / ____/___ _________ ____ ⠀ ⣾⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣷⠀ +** / |/ / __ `/ __ \/ __ \/ /_ / __ \/ ___/ __ `/ _ \ ⢰⣿⠃⠀⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⠤⠀⠀⠘⣿⡆ +** / /| / /_/ / / / / /_/ / __/ / /_/ / / / /_/ / __/ ⢸⣿⠀⠀⠀⠉⠛⠛⢻⣿⣿⣿⠉⠀⠀⠀⠀⠀⣿⡇ +** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇ +** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀ +** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀ +** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀ +**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀ +*/ + +#pragma once + +#include +#include + +#include "SparseArray.hpp" +#include "Utils.hpp" + +namespace nfo { + class Zipper { + public: + explicit Zipper(const std::map *> &arrays) : _arrays(arrays), _max(0), _idx(0) + { + _max = arrays.empty() ? 0 : arrays.begin()->second->size(); + for (SparseArray *&arr : _arrays | std::views::values) { + _max = (std::min)(arr->size(), _max); + } + if (_idx < _max && !all_set()) + incr(); + } + + [[nodiscard]] emscripten::val get_value() const + { + if (_idx >= _max) + return emscripten::val::undefined(); + + emscripten::val res = emscripten::val::object(); + for (SparseArray *const &arr : _arrays | std::views::values) { + res.set(get_js_class_name((*arr)[_idx].value()), (*arr)[_idx].value_or(emscripten::val::undefined())); + } + return res; + } + + emscripten::val next() + { + incr(); + return get_value(); + } + + private: + void incr() + { + if (_idx >= _max) + return; + do { + _idx++; + } while (_idx < _max && !all_set()); + } + + [[nodiscard]] bool all_set() const + { + if (_idx >= _max) + return false; + + return std::ranges::all_of(_arrays | std::views::values, [this](SparseArray *const &arr) { + return (*arr)[_idx].has_value(); + }); + } + + std::map *> _arrays; + std::size_t _max; + std::size_t _idx; + }; +} // namespace nfo