module: build addressable event handler names, runtime invocation
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
afc5a50c09
commit
18a391f446
1 changed files with 190 additions and 3 deletions
193
src/module.zig
193
src/module.zig
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue