From 7e0b9dde684ed1d36a0ca2a20b60ad20163e6b66 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Tue, 26 Mar 2024 13:19:58 -0700 Subject: [PATCH] module: components are written in the same style as events Signed-off-by: Stephen Gutekanst --- examples/custom-renderer/Game.zig | 4 +- examples/custom-renderer/Renderer.zig | 8 +- src/ecs/comptime.zig | 8 +- src/ecs/entities.zig | 17 ++- src/ecs/main.zig | 8 +- src/ecs/query.zig | 8 +- src/ecs/systems.zig | 51 ++------ src/gfx/Sprite.zig | 36 +++--- src/gfx/Text.zig | 68 ++++++---- src/module.zig | 180 +++++++++++++++++++++++--- 10 files changed, 265 insertions(+), 123 deletions(-) diff --git a/examples/custom-renderer/Game.zig b/examples/custom-renderer/Game.zig index e6d1ae25..1668193c 100644 --- a/examples/custom-renderer/Game.zig +++ b/examples/custom-renderer/Game.zig @@ -16,8 +16,8 @@ direction: Vec2 = vec2(0, 0), spawning: bool = false, spawn_timer: mach.Timer, -pub const components = struct { - pub const follower = void; +pub const components = .{ + .{ .name = .follower, .type = void }, }; pub const events = .{ diff --git a/examples/custom-renderer/Renderer.zig b/examples/custom-renderer/Renderer.zig index d153137e..3e3a1d59 100644 --- a/examples/custom-renderer/Renderer.zig +++ b/examples/custom-renderer/Renderer.zig @@ -20,10 +20,10 @@ uniform_buffer: *gpu.Buffer, pub const name = .renderer; pub const Mod = mach.Mod(@This()); -pub const components = struct { - pub const location = Vec3; - pub const rotation = Vec3; - pub const scale = f32; +pub const components = .{ + .{ .name = .location, .type = Vec3 }, + .{ .name = .rotation, .type = Vec3 }, + .{ .name = .scale, .type = f32 }, }; pub const events = .{ diff --git a/src/ecs/comptime.zig b/src/ecs/comptime.zig index 30320019..504a5c0b 100644 --- a/src/ecs/comptime.zig +++ b/src/ecs/comptime.zig @@ -42,16 +42,18 @@ pub fn ArchetypeSlicer(comptime all_components: anytype) type { pub fn slice( slicer: @This(), + // TODO: cleanup comptime comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)), - comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))), + comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))), ) []@field( @field(all_components, @tagName(namespace_name)), @tagName(component_name), - ) { + ).type { + // TODO: cleanup comptime const Type = @field( @field(all_components, @tagName(namespace_name)), @tagName(component_name), - ); + ).type; if (namespace_name == .entity and component_name == .id) { const name_id = slicer.archetype.component_names.index("id").?; return slicer.archetype.getColumnValues(name_id, Type).?[0..slicer.archetype.len]; diff --git a/src/ecs/entities.zig b/src/ecs/entities.zig index 89defe40..0fe97d76 100644 --- a/src/ecs/entities.zig +++ b/src/ecs/entities.zig @@ -281,12 +281,13 @@ pub fn Entities(comptime all_components: anytype) type { pub fn setComponent( entities: *Self, entity: EntityID, + // TODO: cleanup comptime comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)), - comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))), + comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))), component: @field( @field(all_components, @tagName(namespace_name)), @tagName(component_name), - ), + ).type, ) !void { const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name); const name_id = try entities.component_names.indexOrPut(entities.allocator, name_str); @@ -483,16 +484,19 @@ pub fn Entities(comptime all_components: anytype) type { pub fn getComponent( entities: *Self, entity: EntityID, + // TODO: cleanup comptime comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)), - comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))), + comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))), ) ?@field( @field(all_components, @tagName(namespace_name)), @tagName(component_name), - ) { + ).type { + // TODO: cleanup comptime const Component = comptime @field( @field(all_components, @tagName(namespace_name)), @tagName(component_name), - ); + ).type; + const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name); const name_id = entities.component_names.index(name_str) orelse return null; @@ -523,8 +527,9 @@ pub fn Entities(comptime all_components: anytype) type { pub fn removeComponent( entities: *Self, entity: EntityID, + // TODO: cleanup comptime comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)), - comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))), + comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))), ) !void { const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name); const name_id = try entities.component_names.indexOrPut(entities.allocator, name_str); diff --git a/src/ecs/main.zig b/src/ecs/main.zig index 401ecb24..0b6e4aa0 100644 --- a/src/ecs/main.zig +++ b/src/ecs/main.zig @@ -47,8 +47,8 @@ test "example" { pointer: u8, pub const name = .physics; - pub const components = struct { - pub const id = u32; + pub const components = .{ + .{ .name = .id, .type = u32 }, }; pub const events = .{ .{ .global = .tick, .handler = tick }, @@ -61,8 +61,8 @@ test "example" { const Renderer = struct { pub const name = .renderer; - pub const components = struct { - pub const id = u16; + pub const components = .{ + .{ .name = .ud, .type = u16 }, }; pub const events = .{ .{ .global = .tick, .handler = tick }, diff --git a/src/ecs/query.zig b/src/ecs/query.zig index a25ebcbf..7c08658d 100644 --- a/src/ecs/query.zig +++ b/src/ecs/query.zig @@ -9,23 +9,27 @@ pub const QueryTag = enum { /// A complex query for entities matching a given criteria pub fn Query(comptime all_components: anytype) type { return union(QueryTag) { + // TODO: cleanup comptime /// Enum matching a namespace. e.g. `.game` or `.physics2d` pub const Namespace = std.meta.FieldEnum(@TypeOf(all_components)); + // TODO: cleanup comptime /// Enum matching a component within a namespace /// e.g. `var a: Component(.physics2d) = .location` pub fn Component(comptime namespace: Namespace) type { const components = @field(all_components, @tagName(namespace)); - if (@typeInfo(components).Struct.decls.len == 0) return enum {}; - return std.meta.DeclEnum(components); + if (@typeInfo(@TypeOf(components)).Struct.fields.len == 0) return enum {}; + return std.meta.FieldEnum(@TypeOf(components)); } + // TODO: cleanup comptime /// Slice of enums matching a component within a namespace /// e.g. `&.{.location, .rotation}` pub fn ComponentList(comptime namespace: Namespace) type { return []const Component(namespace); } + // TODO: cleanup comptime /// Tagged union of namespaces matching lists of components /// e.g. `.physics2d = &.{ .location, .rotation }` pub const NamespaceComponent = T: { diff --git a/src/ecs/systems.zig b/src/ecs/systems.zig index 55117237..66a40727 100644 --- a/src/ecs/systems.zig +++ b/src/ecs/systems.zig @@ -7,6 +7,7 @@ const Entities = @import("entities.zig").Entities; const EntityID = @import("entities.zig").EntityID; const comp = @import("comptime.zig"); const Module = @import("../module.zig").Module; +const NamespacedComponents = @import("../module.zig").NamespacedComponents; pub fn World(comptime mods: anytype) type { const StateT = NamespacedState(mods); @@ -37,8 +38,9 @@ pub fn World(comptime mods: anytype) type { pub inline fn set( m: *@This(), entity: EntityID, - comptime component_name: std.meta.DeclEnum(components), - component: @field(components, @tagName(component_name)), + // TODO: cleanup comptime + comptime component_name: std.meta.FieldEnum(@TypeOf(components)), + component: @field(components, @tagName(component_name)).type, ) !void { const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); const world = @fieldParentPtr(WorldT, "mod", mod_ptr); @@ -50,8 +52,9 @@ pub fn World(comptime mods: anytype) type { pub inline fn get( m: *@This(), entity: EntityID, - comptime component_name: std.meta.DeclEnum(components), - ) ?@field(components, @tagName(component_name)) { + // TODO: cleanup comptime + comptime component_name: std.meta.FieldEnum(@TypeOf(components)), + ) ?@field(components, @tagName(component_name)).type { const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); const world = @fieldParentPtr(WorldT, "mod", mod_ptr); return world.entities.getComponent(entity, module_tag, component_name); @@ -61,7 +64,8 @@ pub fn World(comptime mods: anytype) type { pub inline fn remove( m: *@This(), entity: EntityID, - comptime component_name: std.meta.DeclEnum(components), + // TODO: cleanup comptime + comptime component_name: std.meta.FieldEnum(@TypeOf(components)), ) !void { const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); const world = @fieldParentPtr(WorldT, "mod", mod_ptr); @@ -80,6 +84,7 @@ pub fn World(comptime mods: anytype) type { world.modules.sendGlobal(module_tag, event_name, args); } + // TODO: eliminate this pub fn dispatchNoError(m: *@This()) void { const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); const world = @fieldParentPtr(WorldT, "mod", mod_ptr); @@ -173,42 +178,6 @@ pub fn World(comptime mods: anytype) type { }; } -// TODO: reconsider components concept -fn NamespacedComponents(comptime modules: anytype) type { - var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; - inline for (modules) |M| { - const components = if (@hasDecl(M, "components")) M.components else struct {}; - fields = fields ++ [_]std.builtin.Type.StructField{.{ - .name = @tagName(M.name), - .type = type, - .default_value = &components, - .is_comptime = true, - .alignment = @alignOf(@TypeOf(components)), - }}; - } - - // Builtin components - const entity_components = struct { - pub const id = EntityID; - }; - fields = fields ++ [_]std.builtin.Type.StructField{.{ - .name = "entity", - .type = type, - .default_value = &entity_components, - .is_comptime = true, - .alignment = @alignOf(@TypeOf(entity_components)), - }}; - - return @Type(.{ - .Struct = .{ - .layout = .Auto, - .is_tuple = false, - .fields = fields, - .decls = &[_]std.builtin.Type.Declaration{}, - }, - }); -} - // TODO: reconsider state concept fn NamespacedState(comptime modules: anytype) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; diff --git a/src/gfx/Sprite.zig b/src/gfx/Sprite.zig index 8eef9a3d..7f8923b2 100644 --- a/src/gfx/Sprite.zig +++ b/src/gfx/Sprite.zig @@ -18,25 +18,29 @@ pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline), pub const name = .mach_gfx_sprite; pub const Mod = mach.Mod(@This()); -pub const components = struct { - /// The ID of the pipeline this sprite belongs to. By default, zero. - /// - /// This determines which shader, textures, etc. are used for rendering the sprite. - pub const pipeline = u8; +pub const components = .{ + .{ .name = .pipeline, .type = u8, .description = + \\ The ID of the pipeline this sprite belongs to. By default, zero. + \\ + \\ This determines which shader, textures, etc. are used for rendering the sprite. + }, - /// The sprite model transformation matrix. A sprite is measured in pixel units, starting from - /// (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world - /// origin (0, 0) lives at the center of the window. - /// - /// Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will - /// cover the top-right hand corner of the window. - pub const transform = Mat4x4; + .{ .name = .transform, .type = Mat4x4, .description = + \\ The sprite model transformation matrix. A sprite is measured in pixel units, starting from + \\ (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world + \\ origin (0, 0) lives at the center of the window. + \\ + \\ Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will + \\ cover the top-right hand corner of the window. + }, - /// UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels. - pub const uv_transform = Mat3x3; + .{ .name = .uv_transform, .type = Mat3x3, .description = + \\ UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels. + }, - /// The size of the sprite, in pixels. - pub const size = Vec2; + .{ .name = .size, .type = Vec2, .description = + \\ The size of the sprite, in pixels. + }, }; pub const events = .{ diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index 06126310..9825aaaf 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -27,41 +27,55 @@ pub const Mod = mach.Mod(@This()); // // TODO: allow user to specify projection matrix (3d-space flat text etc.) -pub const components = struct { - /// The ID of the pipeline this text belongs to. By default, zero. - /// - /// This determines which shader, textures, etc. are used for rendering the text. - pub const pipeline = u8; +pub const components = .{ + .{ .name = .pipeline, .type = u8, .description = + \\ The ID of the pipeline this text belongs to. By default, zero. + \\ + \\ This determines which shader, textures, etc. are used for rendering the text. + }, - /// The text model transformation matrix. Text is measured in pixel units, starting from - /// (0, 0) at the top-left corner and extending to the size of the text. By default, the world - /// origin (0, 0) lives at the center of the window. - pub const transform = Mat4x4; + .{ .name = .transform, .type = Mat4x4, .description = + \\ The text model transformation matrix. Text is measured in pixel units, starting from + \\ (0, 0) at the top-left corner and extending to the size of the text. By default, the world + \\ origin (0, 0) lives at the center of the window. + }, - /// String segments of UTF-8 encoded text to render. - /// - /// Expected to match the length of the style component. - pub const text = []const []const u8; + .{ .name = .text, .type = []const []const u8, .description = + \\ String segments of UTF-8 encoded text to render. + \\ + \\ Expected to match the length of the style component. + }, - /// The style to apply to each segment of text. - /// - /// Expected to match the length of the text component. - pub const style = []const mach.ecs.EntityID; + .{ .name = .style, .type = []const mach.ecs.EntityID, .description = + \\ The style to apply to each segment of text. + \\ + \\ Expected to match the length of the text component. + }, - /// Style component: desired font to render text with. - pub const font_name = []const u8; // TODO: ship a default font + // TODO: ship a default font + .{ .name = .font_name, .type = []const u8, .description = + \\ Style component: desired font to render text with. + }, - /// Style component: font size in pixels - pub const font_size = f32; // e.g. 12 * mach.gfx.px_per_pt // 12pt + // e.g. 12 * mach.gfx.px_per_pt // 12pt + .{ .name = .font_size, .type = f32, .description = + \\ Style component: font size in pixels + }, - /// Style component: font weight - pub const font_weight = u16; // e.g. mach.gfx.font_weight_normal + // e.g. mach.gfx.font_weight_normal + .{ .name = .font_weight, .type = u16, .description = + \\ Style component: font weight + }, - /// Style component: italic text - pub const italic = bool; // e.g. false + // e.g. false + .{ .name = .italic, .type = bool, .description = + \\ Style component: italic text + }, - /// Style component: fill color - pub const color = Vec4; // e.g. vec4(0, 0, 0, 1.0), + // e.g. vec4(0, 0, 0, 1.0) + .{ .name = .color, .type = Vec4, .description = + \\ Style component: fill color + }, }; pub const events = .{ diff --git a/src/module.zig b/src/module.zig index 103a317c..36eb33bb 100644 --- a/src/module.zig +++ b/src/module.zig @@ -2,6 +2,8 @@ const builtin = @import("builtin"); const std = @import("std"); const testing = @import("testing.zig"); +const EntityID = @import("./ecs/entities.zig").EntityID; + /// Verifies that M matches the basic layout of a Mach module pub fn Module(comptime M: type) type { if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M)); @@ -11,11 +13,7 @@ pub fn Module(comptime M: type) type { const prefix = "mach: module ." ++ @tagName(M.name) ++ " "; if (!@hasDecl(M, "events")) @compileError(prefix ++ "must have `pub const events = .{};`"); validateEvents("mach: module ." ++ @tagName(M.name) ++ " ", M.events); - - // TODO: move this to ecs - if (@hasDecl(M, "components")) { - if (@typeInfo(M.components) != .Struct) @compileError("Module.components must be `pub const components = struct { ... };`, found type:" ++ @typeName(M.components)); - } + _ = MComponents(M); return M; } @@ -461,6 +459,7 @@ fn ModuleName(comptime mods: anytype) type { }); } +// TODO: tests /// Struct like .{.foo = FooMod, .bar = BarMod} fn NamespacedModules(comptime modules: anytype) type { var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; @@ -483,6 +482,7 @@ fn NamespacedModules(comptime modules: anytype) type { }); } +// TODO: tests fn validateEvents(comptime error_prefix: anytype, comptime events: anytype) void { if (@typeInfo(@TypeOf(events)) != .Struct or !@typeInfo(@TypeOf(events)).Struct.is_tuple) { @compileError(error_prefix ++ "expected a tuple of structs, found: " ++ @typeName(@TypeOf(events))); @@ -525,6 +525,152 @@ fn validateEvents(comptime error_prefix: anytype, comptime events: anytype) void } } +// TODO: tests +/// Returns a struct type defining all module's components by module name, e.g.: +/// +/// ``` +/// struct { +/// builtin: struct { +/// id: @TypeOf() = .{ .type = EntityID, .description = "Entity ID" }, +/// }, +/// physics: struct { +/// location: @TypeOf() = .{ .type = Vec3, .description = null }, +/// rotation: @TypeOf() = .{ .type = Vec2, .description = "rotation component" }, +/// }, +/// renderer: struct { +/// location: @TypeOf() = .{ .type = Vec2, .description = null }, +/// }, +/// } +/// ``` +pub fn NamespacedComponents(comptime modules: anytype) type { + var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; + inline for (modules) |M| { + const MC = MComponents(M); + fields = fields ++ [_]std.builtin.Type.StructField{.{ + .name = @tagName(M.name), + .type = MC, + .default_value = &MC{}, + .is_comptime = true, + .alignment = @alignOf(MC), + }}; + } + + // Builtin components + // TODO: better method of injecting builtin module? + const BuiltinMC = MComponents(struct { + pub const name = .builtin; + pub const components = .{ + .{ .name = .id, .type = EntityID, .description = "Entity ID" }, + }; + }); + fields = fields ++ [_]std.builtin.Type.StructField{.{ + .name = "entity", + .type = BuiltinMC, + .default_value = &BuiltinMC{}, + .is_comptime = true, + .alignment = @alignOf(BuiltinMC), + }}; + + return @Type(.{ + .Struct = .{ + .layout = .Auto, + .is_tuple = false, + .fields = fields, + .decls = &[_]std.builtin.Type.Declaration{}, + }, + }); +} + +// TODO: tests +/// Returns a struct type defining the module's components, e.g.: +/// +/// ``` +/// struct { +/// location: @TypeOf() = .{ .type = Vec3, .description = null }, +/// rotation: @TypeOf() = .{ .type = Vec2, .description = "rotation component" }, +/// } +/// ``` +fn MComponents(comptime M: anytype) type { + const error_prefix = "mach: module ." ++ @tagName(M.name) ++ " .components "; + if (!@hasDecl(M, "components")) { + return struct {}; + } + if (@typeInfo(@TypeOf(M.components)) != .Struct or !@typeInfo(@TypeOf(M.components)).Struct.is_tuple) { + @compileError(error_prefix ++ "expected a tuple of structs, found: " ++ @typeName(@TypeOf(M.components))); + } + var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; + inline for (M.components, 0..) |component, i| { + const Component = @TypeOf(component); + if (@typeInfo(Component) != .Struct) @compileError(std.fmt.comptimePrint( + error_prefix ++ "expected a tuple of structs, found tuple element ({}): {s}", + .{ i, @typeName(Component) }, + )); + + // Verify .name = .foo component name field + const name_tag = if (@hasField(Component, "name")) component.name else @compileError(std.fmt.comptimePrint( + error_prefix ++ "tuple element ({}) missing field `.name = .foo` (component name)", + .{i}, + )); + if (@typeInfo(@TypeOf(name_tag)) != .EnumLiteral) @compileError(std.fmt.comptimePrint( + error_prefix ++ "tuple element ({}) expected field `.name = .foo`, found: {s}", + .{ i, @typeName(@TypeOf(name_tag)) }, + )); + + // Verify .type = Foo, field + if (!@hasField(Component, "type")) @compileError(std.fmt.comptimePrint( + error_prefix ++ "tuple element ({}) missing field `.type = Foo`", + .{i}, + )); + if (@typeInfo(@TypeOf(component.type)) != .Type) @compileError(std.fmt.comptimePrint( + error_prefix ++ "tuple element ({}) expected field `.type = Foo`, found: {s}", + .{ i, @typeName(@TypeOf(component.type)) }, + )); + + const description = blk: { + if (@hasField(Component, "description")) { + if (!isString(@TypeOf(component.description))) @compileError(std.fmt.comptimePrint( + error_prefix ++ "tuple element ({}) expected (optional) field `.description = \"foo\"`, found: {s}", + .{ i, @typeName(@TypeOf(component.description)) }, + )); + break :blk component.description; + } else break :blk null; + }; + + const NSComponent = struct { + type: type, + description: ?[]const u8, + }; + const ns_component = NSComponent{ .type = component.type, .description = description }; + fields = fields ++ [_]std.builtin.Type.StructField{.{ + .name = @tagName(name_tag), + .type = NSComponent, + .default_value = &ns_component, + .is_comptime = true, + .alignment = @alignOf(NSComponent), + }}; + } + return @Type(.{ + .Struct = .{ + .layout = .Auto, + .is_tuple = false, + .fields = fields, + .decls = &[_]std.builtin.Type.Declaration{}, + }, + }); +} + +// TODO: tests +// TODO: stricter enforcement +fn isString(comptime S: type) bool { + return switch (@typeInfo(S)) { + .Pointer => |p| switch (@typeInfo(p.child)) { + .Array => |a| a.child == u8, + else => false, + }, + else => false, + }; +} + test { testing.refAllDeclsRecursive(@This()); } @@ -538,9 +684,8 @@ test Module { pub const name = .engine_physics; /// Physics module components - pub const components = struct { - /// A location component - pub const location = @Vector(3, f32); + pub const components = .{ + .{ .name = .location, .type = @Vector(3, f32), .description = "A location component" }, }; pub const events = .{ @@ -560,9 +705,8 @@ test Modules { pub const name = .engine_physics; /// Physics module components - pub const components = struct { - /// A location component - pub const location = @Vector(3, f32); + pub const components = .{ + .{ .name = .location, .type = @Vector(3, f32), .description = "A location component" }, }; pub const events = .{ @@ -579,7 +723,7 @@ test Modules { }; /// Renderer module components - pub const components = struct {}; + pub const components = .{}; fn tick() !void {} }); @@ -604,7 +748,7 @@ test Modules { test "event name" { const Physics = Module(struct { pub const name = .engine_physics; - pub const components = struct {}; + pub const components = .{}; pub const events = .{ .{ .global = .foo, .handler = foo }, .{ .global = .bar, .handler = bar }, @@ -620,7 +764,7 @@ test "event name" { const Renderer = Module(struct { pub const name = .engine_renderer; - pub const components = struct {}; + pub const components = .{}; pub const events = .{ .{ .global = .foo_unused, .handler = fn (f32, i32) void }, .{ .global = .bar_unused, .handler = fn (i32, f32) void }, @@ -817,7 +961,7 @@ test "event name calling" { }; const Physics = Module(struct { pub const name = .engine_physics; - pub const components = struct {}; + pub const components = .{}; pub const events = .{ .{ .global = .tick, .handler = tick }, .{ .local = .update, .handler = update }, @@ -838,7 +982,7 @@ test "event name calling" { }); const Renderer = Module(struct { pub const name = .engine_renderer; - pub const components = struct {}; + pub const components = .{}; pub const events = .{ .{ .global = .tick, .handler = tick }, .{ .local = .update, .handler = update }, @@ -925,7 +1069,7 @@ test "dispatch" { }); const Physics = Module(struct { pub const name = .engine_physics; - pub const components = struct {}; + pub const components = .{}; pub const events = .{ .{ .global = .tick, .handler = tick }, .{ .local = .update, .handler = update }, @@ -946,7 +1090,7 @@ test "dispatch" { }); const Renderer = Module(struct { pub const name = .engine_renderer; - pub const components = struct {}; + pub const components = .{}; pub const events = .{ .{ .global = .tick, .handler = tick }, .{ .global = .frame_done, .handler = fn (i32) void },