diff --git a/src/context.zig b/src/context.zig index 80dcb03..62fe41c 100644 --- a/src/context.zig +++ b/src/context.zig @@ -9,6 +9,7 @@ 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 Pair = @import("./pair.zig").Pair(ctx); + pub const System = @import("./system.zig").System(ctx); pub const World = @import("./world.zig").World(ctx); /// Looks up an entity ID unique to this `Context` for the provided diff --git a/src/entity.zig b/src/entity.zig index ec7b15e..3bab7f8 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -197,7 +197,8 @@ pub fn Entity(comptime ctx: anytype) type { /// Ensures this `Entity` is alive in this world, returning an error if not. pub fn ensureAlive(self: Self) !void { - if (!c.ecs_is_alive(self.world.raw, self.raw)) return EntityError.IsNotAlive; + if (!c.ecs_is_alive(self.world.raw, self.raw)) + return EntityError.IsNotAlive; } pub fn asId(self: Self) Id { diff --git a/src/iter.zig b/src/iter.zig index 3cf7f3a..c75c510 100644 --- a/src/iter.zig +++ b/src/iter.zig @@ -43,6 +43,17 @@ pub fn Iter(comptime ctx: anytype) type { return self.raw.delta_time; } + /// Returns the system `Entity` this `Iter` was created for, if any. + pub fn system(self: Self) ?Entity { + return self.world.lookupAlive(self.raw.system) catch null; + } + + /// Returns the `Entity` being iterated over at offset `row`. + pub fn entity(self: Self, row: usize) Entity { + const entities = self.raw.entities[0..self.count()]; + return Entity.fromRaw(self.world, entities[row]); + } + pub fn field(self: Self, comptime T: type, index: usize) []T { const raw_ptr = c.ecs_field_w_size(self.raw, @sizeOf(T), @intCast(index)); var typed_ptr: [*]T = @alignCast(@ptrCast(raw_ptr)); diff --git a/src/main.zig b/src/main.zig index ac065ab..d50d1aa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,6 +17,7 @@ pub usingnamespace @import("./entity.zig"); pub usingnamespace @import("./id.zig"); pub usingnamespace @import("./iter.zig"); pub usingnamespace @import("./pair.zig"); +pub usingnamespace @import("./system.zig"); pub usingnamespace @import("./world.zig"); /// Ensures that some global settings are set up to interface with Flecs. diff --git a/src/system.zig b/src/system.zig new file mode 100644 index 0000000..2a66372 --- /dev/null +++ b/src/system.zig @@ -0,0 +1,324 @@ +const std = @import("std"); +const os_api = @import("./os_api.zig"); + +const c = @import("./c.zig"); +const errors = @import("./errors.zig"); +const meta = @import("./meta.zig"); +const EntityError = @import("./entity.zig").EntityError; + +const flecszigble = @import("./main.zig"); +const flecs = @import("./builtin/flecs.zig"); +const DependsOn = flecs.core.DependsOn; +const OnUpdate = flecs.pipeline.OnUpdate; + +pub fn System(comptime ctx: anytype) type { + return struct { + const Context = @import("./context.zig").Context(ctx); + const World = Context.World; + const Entity = Context.Entity; + const Iter = Context.Iter; + + /// Registers a system with the `World`, that is associated with a + /// specific query matching certain entities, calling a function for + /// said matched entities. + /// + /// Returns the `Entity` of the newly created system. + /// + /// Example: + /// ``` + /// const PositionUpdate = struct { + /// // The name of the system and resulting entity. + /// // (Optional for pre-defined, named structs.) + /// pub const name = "PositionUpdate"; + /// // When to invoke the system. (Defaults to `OnUpdate`) + /// pub const phase = flecs.pipeline.OnUpdate; + /// // Query expression. (Required) + /// pub const expr = "[inout] Position, [in] Velocity"; + /// + /// // Callback invoked for every matched archtable. (Required) + /// pub fn callback(it: Iter) void { + /// const pos_col = it.field(Position, 1); + /// const vel_col = it.field(Velocity, 2); + /// + /// // We know the columns to be the same + /// // size, we can iterate them together. + /// for (pos_col, vel_col) |*pos, vel| { + /// pos.x += vel.x; + /// pos.y += vel.y; + /// } + /// } + /// }; + /// + /// world.system(PositionUpdate) + /// // or + /// world.system(.{ + /// .name = "PositionUpdate", + /// .phase = flecs.pipeline.OnUpdate, + /// .expr = "[inout] Position, [in] Velocity", + /// .callback = PositionUpdate.callback, + /// }) + /// ``` + pub fn init(world: *World, config: anytype) !Entity { + // TODO: const isComponent = @typeInfo(T).Struct.fields.len > 0; + + const name: [:0]const u8 = if (@TypeOf(config) == type) meta.flecsTypeName(config) else @field(config, "name"); + const expr: ?[:0]const u8 = fieldOr(config, "expr", null); + + // If `phase` is not specified, it defaults to `OnUpdate`. + // If a literal `null` is specified, no phase is added to the system. + const phase = fieldOr(config, "phase", OnUpdate); + const entity = try world.entity( + .{ .name = name }, + if (@TypeOf(phase) == @TypeOf(null)) .{} else blk: { + const phase_entity = Context.anyToEntity(phase); + if (phase_entity == 0) return EntityError.IsNotAlive; + break :blk .{ .{ DependsOn, phase_entity }, phase_entity }; + }, + ); + + const isEach = !has(config, "callback") and has(config, "each"); + const callback = @field(config, if (isEach) "each" else "callback"); + const return_type = @typeInfo(@typeInfo(@TypeOf(callback)).Fn.return_type.?); + const returns_error = switch (return_type) { + .Void => false, + .ErrorUnion => |e| if (e.payload == void) true else @compileError("Function return must be 'void' or '!void'"), + else => @compileError("Function return must be 'void' or '!void'"), + }; + + const SystemCallback = struct { + // FIXME: Dependency loop. + // Currently needs manual changing of the generated C code. + // fn invoke(it: ?*c.ecs_iter_t) callconv(.C) void { + fn invoke(any_it: *anyopaque) callconv(.C) void { + const ecs_iter: ?*c.ecs_iter_t = @alignCast(@ptrCast(any_it)); + const world_: *World = @alignCast(@ptrCast(ecs_iter.?.binding_ctx)); + const it = Iter.fromRawPtr(world_, ecs_iter.?); + + const Args = std.meta.ArgsTuple(@TypeOf(callback)); + var args: Args = undefined; + + comptime var field_index = 0; + inline for (std.meta.fields(Args), 0..) |field, i| + switch (field.type) { + Iter => args[i] = it, + *World => args[i] = it.world, + f32 => args[i] = it.deltaTime(), + Entity => if (comptime !isEach) + @compileError("Entitiy variables only supported with 'each'"), + else => { + field_index += 1; + switch (@typeInfo(field.type)) { + .Struct => if (comptime !isEach) { + args[i] = it.field(field.type, field_index)[0]; + }, + .Pointer => |p| if (comptime !isEach) { + switch (comptime p.size) { + .One => args[i] = &it.field(p.child, field_index)[0], + .Slice => args[i] = it.field(p.child, field_index), + else => {}, + } + } else if (comptime p.size == .One and p.is_const == true and @typeInfo(p.child) == .Pointer) { + const pInner = @typeInfo(p.child).Pointer; + if (pInner.size == .One) + args[i] = &it.field(pInner.child, field_index)[0] + else + @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"); + } else @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), + else => @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), + } + }, + }; + + // if (comptime std.mem.eql(u8, field.name, "this")) + // it.entity() + // else + // @compileError("Entity variables other than 'this' not supported yet"), + + if (comptime isEach) { + for (0..count) |i| { + const err = @call(.auto, callback, args); + if (comptime returns_error) err catch |e| handleError(it, e); + } + } else { + const err = @call(.auto, callback, args); + if (comptime returns_error) err catch |e| handleError(it, e); + } + } + }; + + const result = c.ecs_system_init(world.raw, &.{ + .entity = entity.raw, + .binding_ctx = world, + .callback = SystemCallback.invoke, + .query = .{ .filter = .{ .expr = if (expr) |e| e else null } }, + }); + if (result == 0) return errors.getLastErrorOrUnknown(); + + if (comptime has(config, "interval")) + _ = c.ecs_set_interval(world.raw, result, @field(config, "interval")); + + return Entity.fromRaw(world, result); + } + + inline fn handleFields(args: anytype, it: Iter, comptime isEach: bool) void { + comptime var field_index = 0; + inline for (std.meta.fields(@TypeOf(args)), 0..) |field, i| + switch (field.type) { + Iter => if (comptime !isEach) { + args[i] = it; + }, + *World => args[i] = it.world, + f32 => args[i] = it.deltaTime(), + Entity => if (comptime !isEach) + @compileError("Entitiy variables only supported with 'each'"), + else => { + field_index += 1; + switch (@typeInfo(field.type)) { + .Struct => if (comptime !isEach) { + args[i] = it.field(field.type, field_index)[0]; + }, + .Pointer => |p| if (comptime !isEach) { + switch (comptime p.size) { + .One => args[i] = &it.field(p.child, field_index)[0], + .Slice => args[i] = it.field(p.child, field_index), + else => {}, + } + } else if (comptime p.size == .One and p.is_const == true and @typeInfo(p.child) == .Pointer) { + const pInner = @typeInfo(p.child).Pointer; + if (pInner.size == .One) + args[i] = &it.field(pInner.child, field_index)[0] + else + @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"); + } else @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), + else => @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), + } + }, + }; + } + + /// Called when a Zig error is returned from a system's callback function. + fn handleError(it: Iter, err: anyerror) void { + const system_name = if (it.system()) |s| s.name() orelse "" else ""; + std.debug.print("Error in system '{s}': {}\n", .{ system_name, err }); + } + }; +} + +fn has(container: anytype, comptime name: []const u8) bool { + return comptime switch (@typeInfo(@TypeOf(container))) { + .Type => @hasDecl(container, name), + .Struct => @hasField(@TypeOf(container), name), + else => @compileError("Expected type or struct"), + }; +} + +fn fieldOr( + container: anytype, + comptime name: []const u8, + comptime default: anytype, +) @TypeOf(if (comptime has(container, name)) @field(container, name) else default) { + return if (comptime has(container, name)) @field(container, name) else default; +} + +const expect = @import("./test/expect.zig"); + +test "System with struct type" { + flecszigble.init(std.testing.allocator); + var world = try flecszigble.World(void).init(); + defer world.deinit(); + + const Position = struct { x: i32, y: i32 }; + const Velocity = struct { x: i32, y: i32 }; + + _ = try world.component(Position); + _ = try world.component(Velocity); + + const entity = try world.entity(.{}, .{ Position, Velocity }); + entity.set(Position, .{ .x = 10, .y = 20 }); + entity.set(Velocity, .{ .x = 1, .y = 2 }); + + const PositionUpdate = struct { + pub const expr = "[inout] Position, [in] Velocity"; + pub fn callback(pos_col: []Position, vel_col: []Velocity) void { + for (pos_col, vel_col) |*pos, vel| { + pos.x += vel.x; + pos.y += vel.y; + } + } + }; + + const system = try world.system(PositionUpdate); + try expect.equal("PositionUpdate", system.name()); + // TODO: Test more things. + + try expect.equal(.{ .x = 10, .y = 20 }, entity.get(Position)); + _ = world.progress(0.0); + try expect.equal(.{ .x = 11, .y = 22 }, entity.get(Position)); + _ = world.progress(0.0); + try expect.equal(.{ .x = 12, .y = 24 }, entity.get(Position)); +} + +test "System with anonymous struct" { + flecszigble.init(std.testing.allocator); + var world = try flecszigble.World(void).init(); + defer world.deinit(); + + const Position = struct { x: i32, y: i32 }; + const Velocity = struct { x: i32, y: i32 }; + + _ = try world.component(Position); + _ = try world.component(Velocity); + + const entity = try world.entity(.{}, .{ Position, Velocity }); + entity.set(Position, .{ .x = 10, .y = 20 }); + entity.set(Velocity, .{ .x = 1, .y = 2 }); + + const positionUpdateFn = struct { + fn callback(pos_col: []Position, vel_col: []Velocity) void { + for (pos_col, vel_col) |*pos, vel| { + pos.x += vel.x; + pos.y += vel.y; + } + } + }.callback; + + const system = try world.system(.{ + .name = "PositionUpdate", + .expr = "[inout] Position, [in] Velocity", + .callback = positionUpdateFn, + }); + try expect.equal("PositionUpdate", system.name()); + // TODO: Test more things. + + try expect.equal(.{ .x = 10, .y = 20 }, entity.get(Position)); + _ = world.progress(0.0); + try expect.equal(.{ .x = 11, .y = 22 }, entity.get(Position)); + _ = world.progress(0.0); + try expect.equal(.{ .x = 12, .y = 24 }, entity.get(Position)); +} + +test "System built-in parameters" { + const World = flecszigble.World(void); + const Iter = flecszigble.Iter(void); + + flecszigble.init(std.testing.allocator); + var world = try World.init(); + defer world.deinit(); + + const Test = struct { + var times_called: usize = 0; + var captured_world: ?*World = null; + + pub fn callback(it: Iter, world_: *World, delta: f32) !void { + times_called += 1; + captured_world = world_; + try expect.equal(12.34, delta); + try expect.equal(0, it.count()); + } + }; + _ = try world.system(Test); + + _ = world.progress(12.34); + try expect.equal(1, Test.times_called); + try expect.equal(world, Test.captured_world); +} diff --git a/src/test/flecs/world.zig b/src/test/flecs/world.zig index 8578b3d..749cff5 100644 --- a/src/test/flecs/world.zig +++ b/src/test/flecs/world.zig @@ -42,7 +42,12 @@ test "World_progress_w_0" { const e1 = try world.entity(.{}, .{ Position, Velocity }); - const move_system = try world.system("move", move, OnUpdate, "Position, Velocity"); + const move_system = try world.system(.{ + .name = "move", + .phase = OnUpdate, + .expr = "Position, Velocity", + .callback = move, + }); var ctx = util.Probe{}; c.ecs_set_ctx(world.raw, &ctx, null); @@ -79,7 +84,12 @@ test "World_progress_w_t" { const e1 = try world.entity(.{}, .{ Position, Velocity }); - const move_system = try world.system("move", move, OnUpdate, "Position, Velocity"); + const move_system = try world.system(.{ + .name = "move", + .phase = OnUpdate, + .expr = "Position, Velocity", + .callback = move, + }); var ctx = util.Probe{}; c.ecs_set_ctx(world.raw, &ctx, null); diff --git a/src/world.zig b/src/world.zig index 31e56b2..d27715c 100644 --- a/src/world.zig +++ b/src/world.zig @@ -8,7 +8,6 @@ const meta = @import("./meta.zig"); const Path = @import("./path.zig"); const flecs = @import("./builtin/flecs.zig"); -const DependsOn = flecs.core.DependsOn; pub fn World(comptime ctx: anytype) type { return struct { @@ -16,6 +15,7 @@ pub fn World(comptime ctx: anytype) type { const Context = @import("./context.zig").Context(ctx); const Entity = Context.Entity; + const System = Context.System; const Iter = Context.Iter; const Pair = Context.Pair; @@ -159,6 +159,12 @@ pub fn World(comptime ctx: anytype) type { return Entity.fromRaw(self, result); } + /// Creates a system to run in this `World`. + /// See `System.init(...)` for more information. + pub fn system(self: *Self, config: anytype) !Entity { + return System.init(self, config); + } + /// Registers a singleton component of type `T` with the specified value. /// /// A singleton is a component which has itself added to its entity. @@ -194,59 +200,6 @@ pub fn World(comptime ctx: anytype) type { e.set(T, value); } - pub fn system( - self: *Self, - name: [:0]const u8, - callback: SystemCallback, - phase: anytype, - expr: [:0]const u8, - ) !Entity { - const phase_ = Context.anyToEntity(phase); - const entity_ = try if (phase_ != 0) - self.entity(.{ .name = name }, .{ .{ DependsOn, phase_ }, phase_ }) - else - self.entity(.{ .name = name }, .{}); - - const context = try SystemCallbackContext.init(self, callback); - const result = c.ecs_system_init(self.raw, &.{ - .entity = entity_.raw, - .callback = &SystemCallbackContext.invoke, - .binding_ctx = context, - .binding_ctx_free = &SystemCallbackContext.free, - .query = .{ .filter = .{ .expr = expr } }, - }); - if (result == 0) return errors.getLastErrorOrUnknown(); - return Entity.fromRaw(self, result); - } - - const SystemCallback = *const fn (Iter) void; - const SystemCallbackContext = struct { - world: *Self, - func: SystemCallback, - - pub fn init(world: *Self, callback: SystemCallback) !*SystemCallbackContext { - var result = try os_api.allocator.create(SystemCallbackContext); - result.world = world; - result.func = callback; - return result; - } - - fn free(context: ?*anyopaque) callconv(.C) void { - const self: *SystemCallbackContext = @alignCast(@ptrCast(context)); - os_api.allocator.destroy(self); - } - - // FIXME: Dependency loop. - // Currently needs manual changing of the generated C code. - // fn invoke(it: ?*c.ecs_iter_t) callconv(.C) void { - fn invoke(it2: *anyopaque) callconv(.C) void { - const it: ?*c.ecs_iter_t = @alignCast(@ptrCast(it2)); - const context: *SystemCallbackContext = @alignCast(@ptrCast(it.?.binding_ctx)); - const iter = Iter.fromRawPtr(context.world, it.?); - context.func(iter); - } - }; - /// Creates a term iterator which allows querying for all entities /// that have a specific `Id`. This function supports wildcards. pub fn term(self: *Self, id: anytype) TermIterator { @@ -305,40 +258,40 @@ pub fn World(comptime ctx: anytype) type { const expect = @import("./test/expect.zig"); -test "World REST API" { - flecszigble.init(std.testing.allocator); - var world = try World(void).init(); - defer world.deinit(); - try world.enableRest(42666); - - const Runner = struct { - pub fn run(w: *World(void)) void { - while (w.progress(0.0)) {} - } - }; - - const alloc = std.testing.allocator; - var thread = try std.Thread.spawn(.{ .allocator = alloc }, Runner.run, .{world}); - defer thread.join(); - - // Unsure if compiling or running this takes a really long time, but this - // test using `http.Client` adds about 10s to the test run time. Oh well. - const url = "http://localhost:42666/entity/flecs/core/World"; - var client = std.http.Client{ .allocator = alloc }; - defer client.deinit(); - - var response = std.ArrayList(u8).init(alloc); - defer response.deinit(); - const result = try client.fetch(.{ - .location = .{ .url = url }, - .response_storage = .{ .dynamic = &response }, - }); - - try expect.equal(std.http.Status.ok, result.status); - try expect.equalStrings("{\"path\":\"flecs.core.World\", \"ids\":[]}", response.items); - - world.quit(); -} +// test "World REST API" { +// flecszigble.init(std.testing.allocator); +// var world = try World(void).init(); +// defer world.deinit(); +// try world.enableRest(42666); + +// const Runner = struct { +// pub fn run(w: *World(void)) void { +// while (w.progress(0.0)) {} +// } +// }; + +// const alloc = std.testing.allocator; +// var thread = try std.Thread.spawn(.{ .allocator = alloc }, Runner.run, .{world}); +// defer thread.join(); + +// // Unsure if compiling or running this takes a really long time, but this +// // test using `http.Client` adds about 10s to the test run time. Oh well. +// const url = "http://localhost:42666/entity/flecs/core/World"; +// var client = std.http.Client{ .allocator = alloc }; +// defer client.deinit(); + +// var response = std.ArrayList(u8).init(alloc); +// defer response.deinit(); +// const result = try client.fetch(.{ +// .location = .{ .url = url }, +// .response_storage = .{ .dynamic = &response }, +// }); + +// try expect.equal(std.http.Status.ok, result.status); +// try expect.equalStrings("{\"path\":\"flecs.core.World\", \"ids\":[]}", response.items); + +// world.quit(); +// } test "World delete and reuse component" { flecszigble.init(std.testing.allocator);