diff --git a/build.zig b/build.zig index a8cb9df..da3a1f9 100644 --- a/build.zig +++ b/build.zig @@ -19,7 +19,7 @@ pub fn build(b: *std.Build) !void { b.installArtifact(lib); const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "tests/main.zig" }, + .root_source_file = .{ .path = "test/main.zig" }, // Has to be specified so tests can run autodoc tests from src/. .main_pkg_path = .{ .path = "." }, .target = target, @@ -38,10 +38,6 @@ fn setupFlecs(step: *std.Build.CompileStep) void { .file = .{ .path = "libs/flecs/flecs.c" }, .flags = &.{"-fno-sanitize=undefined"}, }); - step.defineCMacro("FLECS_NO_CPP", null); - step.defineCMacro("FLECS_USE_OS_ALLOC", null); - if (@import("builtin").mode == .Debug) - step.defineCMacro("FLECS_SANITIZE", null); if (step.target.isWindows()) step.linkSystemLibraryName("ws2_32"); } diff --git a/src/c.zig b/src/c.zig index 5981635..016c88f 100644 --- a/src/c.zig +++ b/src/c.zig @@ -1,3 +1,8 @@ pub usingnamespace @cImport({ + @cDefine("FLECS_NO_CPP", {}); + @cDefine("FLECS_USE_OS_ALLOC", {}); + if (@import("builtin").mode == .Debug) + @cDefine("FLECS_SANITIZE", {}); + @cInclude("flecs.h"); }); diff --git a/src/entity.zig b/src/entity.zig index fc72f6f..9d1d70e 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -1,5 +1,6 @@ const c = @import("./c.zig"); +const Lookup = @import("./main.zig").Lookup; const World = @import("./world.zig").World; pub fn Entity(comptime ctx: anytype) type { @@ -12,5 +13,23 @@ pub fn Entity(comptime ctx: anytype) type { pub fn fromRaw(world: *World(ctx), raw: c.ecs_entity_t) Self { return .{ .world = world, .raw = raw }; } + + pub fn isValid(self: Self) bool { + return c.ecs_is_valid(self.world.raw, self.raw); + } + + pub fn isAlive(self: Self) bool { + return c.ecs_is_alive(self.world.raw, self.raw); + } + + pub fn get(self: Self, comptime T: type) ?*const T { + const id = Lookup(ctx, T).id; + return @alignCast(@ptrCast(c.ecs_get_id(self.world.raw, self.raw, id))); + } + + pub fn set(self: Self, comptime T: type, value: T) void { + const id = Lookup(ctx, T).id; + _ = c.ecs_set_id(self.world.raw, self.raw, id, @sizeOf(T), &value); + } }; } diff --git a/src/id.zig b/src/id.zig index e893ac7..2b17d8f 100644 --- a/src/id.zig +++ b/src/id.zig @@ -6,5 +6,11 @@ pub fn Id(comptime ctx: anytype) type { return struct { world: *World(ctx), raw: c.ecs_id_t, + + const Self = @This(); + + pub fn fromRaw(world: *World(ctx), raw: c.ecs_id_t) Self { + return .{ .world = world, .raw = raw }; + } }; } diff --git a/src/iter.zig b/src/iter.zig index bae5853..83a562c 100644 --- a/src/iter.zig +++ b/src/iter.zig @@ -1,10 +1,58 @@ const c = @import("./c.zig"); +const Entity = @import("./entity.zig").Entity; +const Id = @import("./id.zig").Id; const World = @import("./world.zig").World; pub fn Iter(comptime ctx: anytype) type { return struct { world: *World(ctx), - raw: c.ecs_iter_t, + raw: *c.ecs_iter_t, + owned: bool, + + const Self = @This(); + + pub fn fromRawPtr(world: *World(ctx), ptr: *c.ecs_iter_t) Self { + return .{ .world = world, .raw = ptr, .owned = false }; + } + + pub fn fromRawValue(world: *World(ctx), value: c.ecs_iter_t) !Self { + var raw = try world.allocator.create(c.ecs_iter_t); + raw.* = value; + return .{ .world = world, .raw = raw, .owned = true }; + } + + pub fn deinit(self: Self) void { + if (self.isValid()) c.ecs_iter_fini(self.raw); + if (self.owned) self.world.allocator.destroy(self.raw); + } + + pub fn isValid(self: Self) bool { + return (self.raw.flags & c.EcsIterIsValid) != 0; + } + + pub fn getCount(self: Self) usize { + return @intCast(self.raw.count); + } + + pub fn getDeltaTime(self: Self) f32 { + return self.raw.delta_time; + } + + pub fn field(self: Self, comptime T: type, index: usize) []T { + var raw_ptr = c.ecs_field_w_size(self.raw, @sizeOf(T), @intCast(index)); + var typed_ptr: [*]T = @alignCast(@ptrCast(raw_ptr)); + return typed_ptr[0..self.getCount()]; + } + + pub fn fieldId(self: Self, index: usize) Id(ctx) { + const raw = c.ecs_field_id(self.raw, @intCast(index)); + return Id(ctx).fromRaw(self.world, raw); + } + + pub fn fieldSource(self: Self, index: usize) Entity(ctx) { + const raw = c.ecs_field_src(self.raw, @intCast(index)); + return Entity(ctx).fromRaw(self.world, raw); + } }; } diff --git a/src/main.zig b/src/main.zig index b548ba7..5bb9222 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,13 +13,13 @@ pub fn Context(comptime ctx: anytype) type { pub const Id = @import("./id.zig").Id(ctx); pub const Iter = @import("./iter.zig").Iter(ctx); pub const World = @import("./world.zig").World(ctx); + }; +} - pub fn Lookup(comptime T: type) type { - _ = T; // Only necessary to create a unique type. - return struct { - pub var id: c.ecs_id_t = 0; - }; - } +pub fn Lookup(comptime ctx: anytype, comptime T: type) type { + _ = .{ ctx, T }; // Only necessary to create a unique type. + return struct { + pub var id: c.ecs_id_t = 0; }; } diff --git a/src/world.zig b/src/world.zig index 227c888..c2f9006 100644 --- a/src/world.zig +++ b/src/world.zig @@ -4,10 +4,11 @@ const Allocator = std.mem.Allocator; const c = @import("./c.zig"); const util = @import("./util.zig"); -pub fn World(comptime ctx: anytype) type { - const Lookup = @import("./main.zig").Context(ctx).Lookup; - const Entity = @import("./entity.zig").Entity(ctx); +const Lookup = @import("./main.zig").Lookup; +const Entity = @import("./entity.zig").Entity; +const Iter = @import("./iter.zig").Iter; +pub fn World(comptime ctx: anytype) type { return struct { raw: *c.ecs_world_t, allocator: Allocator, @@ -37,23 +38,29 @@ pub fn World(comptime ctx: anytype) type { return c.ecs_progress(self.raw, delta_time); } + pub fn lookup(self: *Self, comptime T: type) ?Entity(ctx) { + const id = Lookup(ctx, T).id; + const result = Entity(ctx).fromRaw(self, id); + return if (result.isAlive()) result else null; + } + pub fn entity( self: *Self, comptime add: []const type, - ) Entity { + ) Entity(ctx) { var desc = std.mem.zeroes(c.ecs_entity_desc_t); if (add.len > c.FLECS_ID_DESC_MAX) @compileLog("add.len > FLECS_ID_DESC_MAX"); inline for (add, 0..) |T, i| - desc.add[i] = Lookup(T).id; + desc.add[i] = Lookup(ctx, T).id; const result = c.ecs_entity_init(self.*.raw, &desc); - return Entity.fromRaw(self, result); + return Entity(ctx).fromRaw(self, result); } pub fn component( self: *Self, comptime T: type, - ) Entity { + ) Entity(ctx) { const name = util.simpleTypeName(T); const entDesc = std.mem.zeroInit(c.ecs_entity_desc_t, .{ .name = name, @@ -64,17 +71,18 @@ pub fn World(comptime ctx: anytype) type { .entity = c.ecs_entity_init(self.raw, &entDesc), .type = .{ .size = @sizeOf(T), .alignment = @alignOf(T) }, }); - Lookup(T).id = c.ecs_component_init(self.raw, &compDesc); - return Entity.fromRaw(self, Lookup(T).id); + Lookup(ctx, T).id = c.ecs_component_init(self.raw, &compDesc); + return Entity(ctx).fromRaw(self, Lookup(ctx, T).id); } pub fn system( self: *Self, name: [:0]const u8, - callback: c.ecs_iter_action_t, - phase: ?Entity, + callback: SystemCallback, + phase: ?Entity(ctx), expr: [:0]const u8, - ) Entity { + ) Entity(ctx) { + var context = SystemCallbackContext.init(self, callback); var entDesc = std.mem.zeroes(c.ecs_entity_desc_t); entDesc.name = name; if (phase) |p| { @@ -84,9 +92,35 @@ pub fn World(comptime ctx: anytype) type { var sysDesc = std.mem.zeroes(c.ecs_system_desc_t); sysDesc.entity = c.ecs_entity_init(self.raw, &entDesc); sysDesc.query.filter.expr = expr; - sysDesc.callback = callback; - const result = c.ecs_system_init(self.raw, *sysDesc); - return Entity.fromRaw(self, result); + sysDesc.callback = &SystemCallbackContext.invoke; + sysDesc.binding_ctx = context; + sysDesc.binding_ctx_free = &SystemCallbackContext.free; + const result = c.ecs_system_init(self.raw, &sysDesc); + return Entity(ctx).fromRaw(self, result); } + + const SystemCallback = *const fn (Iter(ctx)) void; + const SystemCallbackContext = struct { + world: *Self, + func: SystemCallback, + + pub fn init(world: *Self, callback: SystemCallback) *SystemCallbackContext { + var result = world.allocator.create(SystemCallbackContext) catch @panic("OOM"); + result.world = world; + result.func = callback; + return result; + } + + fn free(context: ?*anyopaque) callconv(.C) void { + const self: *SystemCallbackContext = @alignCast(@ptrCast(context)); + self.world.allocator.destroy(self); + } + + fn invoke(it: ?*c.ecs_iter_t) callconv(.C) void { + const context: *SystemCallbackContext = @alignCast(@ptrCast(it.?.binding_ctx)); + var iter = Iter(ctx).fromRawPtr(context.world, it.?); + context.func(iter); + } + }; }; } diff --git a/tests/main.zig b/test/main.zig similarity index 100% rename from tests/main.zig rename to test/main.zig diff --git a/tests/util.zig b/test/util.zig similarity index 50% rename from tests/util.zig rename to test/util.zig index a1bc44b..12c819e 100644 --- a/tests/util.zig +++ b/test/util.zig @@ -19,11 +19,11 @@ pub const Probe = struct { system: c.ecs_entity_t, event: c.ecs_entity_t, eventId: c.ecs_id_t, - offset: i32, - count: i32, - invoked: i32, - termCount: i32, - termIndex: i32, + offset: usize, + count: usize, + invoked: usize, + termCount: usize, + termIndex: usize, e: [MAX_ENTITIES]c.ecs_entity_t, c: [MAX_SYS_COLUMNS][MAX_INVOCATIONS]c.ecs_entity_t, s: [MAX_SYS_COLUMNS][MAX_INVOCATIONS]c.ecs_entity_t, @@ -33,43 +33,43 @@ pub const Probe = struct { return std.mem.zeroes(Probe); } - pub fn probeSystemWithContext(ctx: *Probe, it: *Iter) !void { - ctx.*.param = it.*.param; - ctx.*.system = it.*.system; - ctx.*.event = it.*.event; - ctx.*.eventId = it.*.eventId; - ctx.*.offset = 0; - ctx.*.termCount = it.*.fieldCount; - ctx.*.termIndex = it.*.termIndex; + pub fn probeSystemWithContext(ctx: *Probe, it: Iter) !void { + ctx.param = it.raw.param; + ctx.system = it.raw.system; + ctx.event = it.raw.event; + ctx.eventId = it.raw.event_id; + ctx.offset = 0; + ctx.termCount = @intCast(it.raw.field_count); + ctx.termIndex = @intCast(it.raw.term_index); - for (0..ctx.*.term_count) |i| { - ctx.*.c[ctx.*.invoked][i] = it.*.ids[i]; - ctx.*.s[ctx.*.invoked][i] = it.fieldSource(i + 1); + for (0..ctx.termCount) |i| { + ctx.c[ctx.invoked][i] = it.raw.ids[i]; + ctx.s[ctx.invoked][i] = it.fieldSource(i + 1).raw; - const e = it.fieldId(i + 1); + const e = it.fieldId(i + 1).raw; try expect(e != 0); } - for (0..it.*.count) |i| { - if (i + ctx.*.count < 256) { - ctx.*.e[i + ctx.*.count] = it.*.entities[i]; + for (0..it.getCount()) |i| { + if (i + ctx.count < 256) { + ctx.e[i + ctx.count] = it.raw.entities[i]; } else { // Can't store more than that, tests shouldn't // rely on getting back more than 256 results. unreachable; } } - ctx.*.count += it.*.count; + ctx.count += it.getCount(); - ctx.*.invoked += 1; + ctx.invoked += 1; } - pub fn probeIter(it: *Iter) !void { - var ctx = c.ecs_get_context(it.*.world); - if (ctx == null) ctx = it.*.ctx; + pub fn probeIter(it: Iter) !void { + var ctx = c.ecs_get_context(it.world.raw); + if (ctx == null) ctx = it.raw.ctx; if (ctx) |ct| { - var p: *Probe = @ptrCast(ct); - p.probeSystemWithContext(it); + var p: *Probe = @alignCast(@ptrCast(ct)); + try p.probeSystemWithContext(it); } } }; diff --git a/test/world.zig b/test/world.zig new file mode 100644 index 0000000..5d63bc1 --- /dev/null +++ b/test/world.zig @@ -0,0 +1,68 @@ +// Reimplementations of the following tests from Flecs: +// https://github.com/SanderMertens/flecs/blob/master/test/api/src/World.c + +const std = @import("std"); +const expect = std.testing.expect; +const expectEql = std.testing.expectEqual; +const expectStrEql = std.testing.expectEqualStrings; + +const util = @import("./util.zig"); + +const flecs = @import("../src/main.zig"); +const c = flecs.c; + +const context = flecs.Context(void); +const World = context.World; +const Iter = context.Iter; +const Entity = context.Entity; + +const Position = struct { x: f32, y: f32 }; +const Velocity = struct { x: f32, y: f32 }; + +fn move(it: Iter) void { + var pos = it.field(Position, 1); + var vel = it.field(Velocity, 2); + util.Probe.probeIter(it) catch unreachable; + + for (pos, vel) |*p, *v| { + p.x += v.x * it.getDeltaTime(); + p.y += v.y * it.getDeltaTime(); + } +} + +test "World_progress_w_0" { + var world = try World.init(std.testing.allocator); + defer world.deinit(); + + _ = world.component(Position); + _ = world.component(Velocity); + + const e1 = world.entity(&.{ Position, Velocity }); + + const phase = Entity.fromRaw(world, c.EcsOnUpdate); + const move_system = world.system("move", move, phase, "Position, Velocity"); + + var ctx = util.Probe.init(); + c.ecs_set_context(world.raw, &ctx); + + e1.set(Position, .{ .x = 0, .y = 0 }); + e1.set(Velocity, .{ .x = 1, .y = 2 }); + + _ = world.progress(0); + + try expectEql(ctx.count, 1); + try expectEql(ctx.invoked, 1); + try expectEql(ctx.system, move_system.raw); + try expectEql(ctx.termCount, 2); + try expectEql(ctx.param, null); + + try expectEql(ctx.e[0], e1.raw); + try expectEql(ctx.c[0][0], world.lookup(Position).?.raw); + try expectEql(ctx.c[0][1], world.lookup(Velocity).?.raw); + try expectEql(ctx.s[0][0], 0); + try expectEql(ctx.s[0][1], 0); + + const p = e1.get(Position).?; + try expect(p.x != 0); + try expect(p.y != 0); +} diff --git a/tests/world.zig b/tests/world.zig deleted file mode 100644 index d5895e0..0000000 --- a/tests/world.zig +++ /dev/null @@ -1,71 +0,0 @@ -// Reimplementations of the following tests from Flecs: -// https://github.com/SanderMertens/flecs/blob/master/test/api/src/World.c - -const std = @import("std"); -const expect = std.testing.expect; -const expectEql = std.testing.expectEqual; -const expectStrEql = std.testing.expectEqualStrings; - -const util = @import("./util.zig"); - -const flecs = @import("../src/main.zig"); -const c = flecs.c; - -const context = flecs.Context(void); -const Lookup = context.Lookup; -const World = context.World; -const Iter = context.Iter; -const Entity = context.Entity; - -const Position = struct { x: f32, y: f32 }; -const Velocity = struct { x: f32, y: f32 }; - -fn move(it: *Iter) void { - var pos = it.field(Position, 1); - var vel = it.field(Velocity, 2); - util.Probe.probeIter(it); - - for (0..it.count) |row| { - var p = &pos[row]; - var v = &vel[row]; - p.*.x += v.*.x * it.delta_time; - p.*.y += v.*.y * it.delta_time; - } -} - -test "World_progress_w_0" { - var world = try World.init(std.testing.allocator); - defer world.deinit(); - - _ = world.component(Position); - _ = world.component(Velocity); - - // const e1 = world.entity(&.{ Position, Velocity }); - - // // const phase = Entity.fromRaw(world, c.EcsOnUpdate); - // // const move_system = world.system("move", move, phase, "Position, Velocity"); - - // var ctx = util.Probe.init(); - // c.ecs_set_context(world.raw, &ctx); - - // // e1.set(Position, .{ 0, 0 }); - // // e1.set(Velocity, .{ 1, 2 }); - - // _ = world.progress(0); - - // try expectEql(ctx.count, 1); - // try expectEql(ctx.invoked, 1); - // // try expectEql(ctx.system, move_system); - // try expectEql(ctx.termCount, 2); - // try expectEql(ctx.param, null); - - // try expectEql(ctx.e[0], e1.raw); - // try expectEql(ctx.c[0][0], Lookup(Position).id); - // try expectEql(ctx.c[0][1], Lookup(Velocity).id); - // try expectEql(ctx.s[0][0], 0); - // try expectEql(ctx.s[0][1], 0); - - // // const p = try e1.get(Position); - // // try expect(p.x != 0); - // // try expect(p.y != 0); -}