module: build addressable event handler names, runtime invocation

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-03-10 13:03:38 -07:00 committed by Stephen Gutekanst
parent afc5a50c09
commit 18a391f446

View file

@ -1,6 +1,6 @@
const builtin = @import("builtin"); const builtin = @import("builtin");
const std = @import("std"); const std = @import("std");
const testing = std.testing; const testing = @import("testing.zig");
// TODO: eliminate dependency on ECS here. // TODO: eliminate dependency on ECS here.
const EntityID = @import("ecs/entities.zig").EntityID; const EntityID = @import("ecs/entities.zig").EntityID;
@ -39,9 +39,82 @@ pub fn Modules(comptime mods: anytype) type {
_ = m; _ = m;
_ = allocator; _ = 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 { fn NamespacedComponents(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
inline for (modules) |M| { inline for (modules) |M| {
@ -171,9 +244,123 @@ test Modules {
testing.refAllDeclsRecursive(Sprite2D); testing.refAllDeclsRecursive(Sprite2D);
// access namespaced components // access namespaced components
try testing.expectEqual(Physics.components.location, @TypeOf(modules).components.engine_physics.location); try testing.expect(type, Physics.components.location).eql(@TypeOf(modules).components.engine_physics.location);
try testing.expectEqual(Renderer.components, @TypeOf(modules).components.engine_renderer); try testing.expect(type, Renderer.components).eql(@TypeOf(modules).components.engine_renderer);
// implicitly generated // implicitly generated
_ = @TypeOf(modules).components.entity.id; _ = @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);
}