parent
							
								
									5e1ad2b90a
								
							
						
					
					
						commit
						1dda16fa01
					
				
				 7 changed files with 392 additions and 91 deletions
			
			
		@ -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 "<unknown>" else "<unknown>"; | 
				
			||||||
 | 
					            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); | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue