diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 69cf0e2..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "flecs"] - path = libs/flecs - url = https://github.com/SanderMertens/flecs.git diff --git a/build.zig b/build.zig index c4353a6..a8cb9df 100644 --- a/build.zig +++ b/build.zig @@ -7,7 +7,6 @@ pub fn build(b: *std.Build) !void { const module = b.createModule(.{ .source_file = .{ .path = "src/main.zig" }, }); - try b.modules.put(b.dupe("flecs-zig-ble"), module); const lib = b.addStaticLibrary(.{ @@ -16,48 +15,33 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }); - - lib.linkLibC(); - lib.addIncludePath(.{ .path = thisDir() ++ "/flecs" }); - lib.addCSourceFile(.{ - .file = .{ .path = thisDir() ++ "/flecs/flecs.c" }, - .flags = &.{"-fno-sanitize=undefined"}, - }); - lib.defineCMacro("FLECS_NO_CPP", null); - lib.defineCMacro("FLECS_USE_OS_ALLOC", null); - if (@import("builtin").mode == .Debug) - lib.defineCMacro("FLECS_SANITIZE", null); - - if (lib.target.isWindows()) - lib.linkSystemLibraryName("ws2_32"); - + setupFlecs(lib); b.installArtifact(lib); const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "tests/main.zig" }, + // Has to be specified so tests can run autodoc tests from src/. + .main_pkg_path = .{ .path = "." }, .target = target, .optimize = optimize, }); - - main_tests.linkLibC(); - main_tests.addIncludePath(.{ .path = thisDir() ++ "/flecs" }); - main_tests.addCSourceFile(.{ - .file = .{ .path = thisDir() ++ "/flecs/flecs.c" }, - .flags = &.{"-fno-sanitize=undefined"}, - }); - main_tests.defineCMacro("FLECS_NO_CPP", null); - main_tests.defineCMacro("FLECS_USE_OS_ALLOC", null); - main_tests.defineCMacro("FLECS_SANITIZE", null); - - if (main_tests.target.isWindows()) - main_tests.linkSystemLibraryName("ws2_32"); - + setupFlecs(main_tests); const run_main_tests = b.addRunArtifact(main_tests); - const test_step = b.step("test", "Run library tests"); test_step.dependOn(&run_main_tests.step); } -inline fn thisDir() []const u8 { - return comptime std.fs.path.dirname(@src().file) orelse "."; +fn setupFlecs(step: *std.Build.CompileStep) void { + step.linkLibC(); + step.addIncludePath(.{ .path = "libs/flecs" }); + step.addCSourceFile(.{ + .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 new file mode 100644 index 0000000..5981635 --- /dev/null +++ b/src/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("flecs.h"); +}); diff --git a/src/component.zig b/src/component.zig new file mode 100644 index 0000000..e848189 --- /dev/null +++ b/src/component.zig @@ -0,0 +1 @@ +const c = @import("./c.zig"); diff --git a/src/entity.zig b/src/entity.zig new file mode 100644 index 0000000..fc72f6f --- /dev/null +++ b/src/entity.zig @@ -0,0 +1,16 @@ +const c = @import("./c.zig"); + +const World = @import("./world.zig").World; + +pub fn Entity(comptime ctx: anytype) type { + return struct { + world: *World(ctx), + raw: c.ecs_entity_t, + + const Self = @This(); + + pub fn fromRaw(world: *World(ctx), raw: c.ecs_entity_t) Self { + return .{ .world = world, .raw = raw }; + } + }; +} diff --git a/src/id.zig b/src/id.zig new file mode 100644 index 0000000..e893ac7 --- /dev/null +++ b/src/id.zig @@ -0,0 +1,10 @@ +const c = @import("./c.zig"); + +const World = @import("./world.zig").World; + +pub fn Id(comptime ctx: anytype) type { + return struct { + world: *World(ctx), + raw: c.ecs_id_t, + }; +} diff --git a/src/iter.zig b/src/iter.zig new file mode 100644 index 0000000..bae5853 --- /dev/null +++ b/src/iter.zig @@ -0,0 +1,10 @@ +const c = @import("./c.zig"); + +const World = @import("./world.zig").World; + +pub fn Iter(comptime ctx: anytype) type { + return struct { + world: *World(ctx), + raw: c.ecs_iter_t, + }; +} diff --git a/src/main.zig b/src/main.zig index e69de29..b548ba7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -0,0 +1,29 @@ +pub usingnamespace @import("./component.zig"); +pub usingnamespace @import("./entity.zig"); +pub usingnamespace @import("./id.zig"); +pub usingnamespace @import("./iter.zig"); +pub usingnamespace @import("./system.zig"); +pub usingnamespace @import("./world.zig"); + +pub const c = @import("./c.zig"); + +pub fn Context(comptime ctx: anytype) type { + return struct { + pub const Entity = @import("./entity.zig").Entity(ctx); + 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; + }; + } + }; +} + +test { + const std = @import("std"); + std.testing.refAllDecls(@This()); +} diff --git a/src/system.zig b/src/system.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..0ca460b --- /dev/null +++ b/src/util.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +/// Gets the simplified type name of the specified type. +/// That is, without any namespace qualifiers. +pub fn simpleTypeName(comptime T: type) [:0]const u8 { + const fullName = @typeName(T); + const index = std.mem.lastIndexOf(u8, fullName, "."); + return if (index) |i| fullName[(i + 1)..] else fullName; +} diff --git a/src/world.zig b/src/world.zig new file mode 100644 index 0000000..227c888 --- /dev/null +++ b/src/world.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +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); + + return struct { + raw: *c.ecs_world_t, + allocator: Allocator, + + const Self = @This(); + + pub fn init(alloc: Allocator) !*Self { + var result = try alloc.create(Self); + result.raw = c.ecs_init().?; + result.allocator = alloc; + return result; + } + + pub fn initWithArgs(alloc: Allocator, args: [][*:0]u8) !*Self { + var result = try alloc.create(Self); + result.raw = c.ecs_init_w_args(args.len, args.ptr).?; + result.allocator = alloc; + return result; + } + + pub fn deinit(self: *Self) void { + _ = c.ecs_fini(self.raw); + self.allocator.destroy(self); + } + + pub fn progress(self: *Self, delta_time: f32) bool { + return c.ecs_progress(self.raw, delta_time); + } + + pub fn entity( + self: *Self, + comptime add: []const type, + ) Entity { + 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; + const result = c.ecs_entity_init(self.*.raw, &desc); + return Entity.fromRaw(self, result); + } + + pub fn component( + self: *Self, + comptime T: type, + ) Entity { + const name = util.simpleTypeName(T); + const entDesc = std.mem.zeroInit(c.ecs_entity_desc_t, .{ + .name = name, + .symbol = name, + .use_low_id = true, + }); + const compDesc = std.mem.zeroInit(c.ecs_component_desc_t, .{ + .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); + } + + pub fn system( + self: *Self, + name: [:0]const u8, + callback: c.ecs_iter_action_t, + phase: ?Entity, + expr: [:0]const u8, + ) Entity { + var entDesc = std.mem.zeroes(c.ecs_entity_desc_t); + entDesc.name = name; + if (phase) |p| { + entDesc.add[0] = c.ecs_pair(c.EcsDependsOn, p.raw); + entDesc.add[1] = p.raw; + } + 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); + } + }; +} diff --git a/tests/main.zig b/tests/main.zig new file mode 100644 index 0000000..437b4fa --- /dev/null +++ b/tests/main.zig @@ -0,0 +1,4 @@ +test { + _ = @import("../src/main.zig"); + _ = @import("./world.zig"); +} diff --git a/tests/util.zig b/tests/util.zig new file mode 100644 index 0000000..a1bc44b --- /dev/null +++ b/tests/util.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const expect = std.testing.expect; +const expectEql = std.testing.expectEqual; +const expectStrEql = std.testing.expectEqualStrings; + +const flecs = @import("../src/main.zig"); +const c = flecs.c; + +const context = flecs.Context(void); +const Entity = context.Entity; +const Iter = context.Iter; +const Id = context.Id; + +pub const MAX_SYS_COLUMNS = 20; +pub const MAX_ENTITIES = 256; +pub const MAX_INVOCATIONS = 1024; + +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, + 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, + param: ?*anyopaque, + + pub fn init() Probe { + 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; + + for (0..ctx.*.term_count) |i| { + ctx.*.c[ctx.*.invoked][i] = it.*.ids[i]; + ctx.*.s[ctx.*.invoked][i] = it.fieldSource(i + 1); + + const e = it.fieldId(i + 1); + try expect(e != 0); + } + + for (0..it.*.count) |i| { + if (i + ctx.*.count < 256) { + ctx.*.e[i + ctx.*.count] = it.*.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.*.invoked += 1; + } + + pub fn probeIter(it: *Iter) !void { + var ctx = c.ecs_get_context(it.*.world); + if (ctx == null) ctx = it.*.ctx; + if (ctx) |ct| { + var p: *Probe = @ptrCast(ct); + p.probeSystemWithContext(it); + } + } +}; diff --git a/tests/world.zig b/tests/world.zig new file mode 100644 index 0000000..d5895e0 --- /dev/null +++ b/tests/world.zig @@ -0,0 +1,71 @@ +// 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); +}