diff --git a/src/module.zig b/src/module.zig index 16810695..109b0c67 100644 --- a/src/module.zig +++ b/src/module.zig @@ -1,6 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); -const testing = std.testing; +const testing = @import("testing.zig"); // TODO: eliminate dependency on ECS here. const EntityID = @import("ecs/entities.zig").EntityID; @@ -39,9 +39,82 @@ pub fn Modules(comptime mods: anytype) type { _ = m; _ = allocator; } + + inline fn call(event_name: EventName(mods), args: anytype) void { + switch (event_name) { + inline else => |name| { + inline for (modules) |M| { + if (@hasDecl(M, @tagName(name))) { + switch (@typeInfo(@TypeOf(@field(M, @tagName(name))))) { + .Fn => { + const handler = @field(M, @tagName(name)); + callHandler(handler, args); + }, + else => {}, + } + } + } + }, + } + } + + inline fn callHandler(handler: anytype, args: anytype) void { + @call(.auto, handler, args); + } }; } +/// enum describing every possible comptime-known event name +fn EventName(comptime mods: anytype) type { + var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; + var i: u32 = 0; + for (mods) |M| { + // Global event handlers + for (@typeInfo(M).Struct.decls) |decl| { + switch (@typeInfo(@TypeOf(@field(M, decl.name)))) { + .Fn => { + const exists_already = blk2: { + for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, decl.name)) break :blk2 true; + break :blk2 false; + }; + if (!exists_already) { + enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = decl.name, .value = i }}; + i += 1; + } + }, + else => {}, + } + } + + // Local event handlers + if (@hasDecl(M, "local")) { + for (@typeInfo(M.local).Struct.decls) |decl| { + switch (@typeInfo(@TypeOf(@field(M.local, decl.name)))) { + .Fn => { + const exists_already = blk2: { + for (enum_fields) |existing| if (std.mem.eql(u8, existing.name, decl.name)) break :blk2 true; + break :blk2 false; + }; + if (!exists_already) { + enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = decl.name, .value = i }}; + i += 1; + } + }, + else => {}, + } + } + } + } + return @Type(.{ + .Enum = .{ + .tag_type = std.math.IntFittingRange(0, enum_fields.len - 1), + .fields = enum_fields, + .decls = &[_]std.builtin.Type.Declaration{}, + .is_exhaustive = true, + }, + }); +} + fn NamespacedComponents(comptime modules: anytype) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; inline for (modules) |M| { @@ -171,9 +244,123 @@ test Modules { testing.refAllDeclsRecursive(Sprite2D); // access namespaced components - try testing.expectEqual(Physics.components.location, @TypeOf(modules).components.engine_physics.location); - try testing.expectEqual(Renderer.components, @TypeOf(modules).components.engine_renderer); + try testing.expect(type, Physics.components.location).eql(@TypeOf(modules).components.engine_physics.location); + try testing.expect(type, Renderer.components).eql(@TypeOf(modules).components.engine_renderer); // implicitly generated _ = @TypeOf(modules).components.entity.id; } + +test EventName { + const Physics = Module(struct { + pub const name = .engine_physics; + pub const components = struct {}; + + pub fn foo() !void {} + pub fn bar() !void {} + + pub const local = struct { + pub fn baz() !void {} + pub fn bam() !void {} + }; + }); + + const Renderer = Module(struct { + pub const name = .engine_renderer; + pub const components = struct {}; + + pub fn tick() !void {} + pub fn foo() !void {} // same .foo name as .engine_physics.foo + pub fn bar() !void {} // same .bar name as .engine_physics.bar + }); + + const Sprite2D = Module(struct { + pub const name = .engine_sprite2d; + + pub fn tick() void {} // same .tick as .engine_renderer.tick + pub const local = struct { + pub fn foobar() void {} + }; + }); + + const Mods = Modules(.{ + Physics, + Renderer, + Sprite2D, + }); + const info = @typeInfo(EventName(Mods.modules)).Enum; + + try testing.expect(type, u3).eql(info.tag_type); + try testing.expect(usize, 6).eql(info.fields.len); + try testing.expect([]const u8, "foo").eql(info.fields[0].name); + try testing.expect([]const u8, "bar").eql(info.fields[1].name); + try testing.expect([]const u8, "baz").eql(info.fields[2].name); + try testing.expect([]const u8, "bam").eql(info.fields[3].name); + try testing.expect([]const u8, "tick").eql(info.fields[4].name); + try testing.expect([]const u8, "foobar").eql(info.fields[5].name); +} + +test "event name calling" { + // TODO: verify that event handlers error return signatures are correct + const global = struct { + var ticks: usize = 0; + var physics_updates: usize = 0; + var renderer_updates: usize = 0; + }; + const Physics = Module(struct { + pub const name = .engine_physics; + pub const components = struct {}; + + pub fn tick() void { + global.ticks += 1; + } + + pub const local = struct { + pub fn update() void { + global.physics_updates += 1; + } + }; + }); + const Renderer = Module(struct { + pub const name = .engine_physics; + pub const components = struct {}; + + pub fn tick() void { + global.ticks += 1; + } + + pub const local = struct { + pub fn update() void { + global.renderer_updates += 1; + } + }; + }); + + var modules: Modules(.{ + Physics, + Renderer, + }) = undefined; + try modules.init(testing.allocator); + defer modules.deinit(testing.allocator); + + @TypeOf(modules).call(.tick, .{}); + try testing.expect(usize, 2).eql(global.ticks); + + // Check we can use .call() with a runtime-known value. + const alloc = try testing.allocator.create(u3); + defer testing.allocator.destroy(alloc); + const E = EventName(@TypeOf(modules).modules); + alloc.* = @intFromEnum(@as(E, .tick)); + + var event_name = @as(E, @enumFromInt(alloc.*)); + @TypeOf(modules).call(event_name, .{}); + try testing.expect(usize, 4).eql(global.ticks); + + // Now check call() with a valid enum, but not a valid global event handler + alloc.* = @intFromEnum(@as(E, .update)); + event_name = @as(E, @enumFromInt(alloc.*)); + @TypeOf(modules).call(event_name, .{}); + try testing.expect(usize, 4).eql(global.ticks); + try testing.expect(usize, 0).eql(global.physics_updates); + try testing.expect(usize, 0).eql(global.renderer_updates); +}