From 03285989458719ad6191cfcaddb723676f590e6b Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Wed, 27 Mar 2024 05:38:37 -0700 Subject: [PATCH] module: merge ecs/systems.zig -> module.zig Signed-off-by: Stephen Gutekanst --- src/ecs/main.zig | 21 ++-- src/ecs/systems.zig | 212 ---------------------------------- src/engine.zig | 44 +++---- src/main.zig | 7 +- src/module.zig | 271 +++++++++++++++++++++++++++++++++++++------- 5 files changed, 261 insertions(+), 294 deletions(-) delete mode 100644 src/ecs/systems.zig diff --git a/src/ecs/main.zig b/src/ecs/main.zig index c5d4451b..612f45f3 100644 --- a/src/ecs/main.zig +++ b/src/ecs/main.zig @@ -19,7 +19,7 @@ pub const EntityID = @import("entities.zig").EntityID; pub const Entities = @import("entities.zig").Entities; pub const Archetype = @import("Archetype.zig"); -pub const World = @import("systems.zig").World; +pub const Modules = @import("../module.zig").Modules; // TODO: // * Iteration @@ -28,13 +28,12 @@ pub const World = @import("systems.zig").World; // * Multiple entities having one value // * Sparse storage? -test "inclusion" { +test { std.testing.refAllDeclsRecursive(@This()); std.testing.refAllDeclsRecursive(@import("Archetype.zig")); std.testing.refAllDeclsRecursive(@import("entities.zig")); std.testing.refAllDeclsRecursive(@import("query.zig")); std.testing.refAllDeclsRecursive(@import("StringTable.zig")); - std.testing.refAllDeclsRecursive(@import("systems.zig")); } test "example" { @@ -54,7 +53,7 @@ test "example" { .{ .global = .tick, .handler = tick }, }; - fn tick(physics: *World(modules).Mod(Physics)) void { + fn tick(physics: *Modules(modules).Mod(Physics)) void { _ = physics; } }; @@ -69,8 +68,8 @@ test "example" { }; fn tick( - physics: *World(modules).Mod(Physics), - renderer: *World(modules).Mod(Renderer), + physics: *Modules(modules).Mod(Physics), + renderer: *Modules(modules).Mod(Renderer), ) void { _ = renderer; _ = physics; @@ -80,9 +79,15 @@ test "example" { //------------------------------------------------------------------------- // Create a world. - var world: World(root.modules) = undefined; + var world: Modules(root.modules) = undefined; try world.init(allocator); - defer world.deinit(); + defer world.deinit(allocator); + + // TODO: better module initialization location + world.mod.physics.entities = &world.entities; + world.mod.physics.allocator = world.entities.allocator; + world.mod.renderer.entities = &world.entities; + world.mod.renderer.allocator = world.entities.allocator; // Initialize module state. var physics = &world.mod.physics; diff --git a/src/ecs/systems.zig b/src/ecs/systems.zig deleted file mode 100644 index 4e978d7d..00000000 --- a/src/ecs/systems.zig +++ /dev/null @@ -1,212 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const StructField = std.builtin.Type.StructField; - -const mach = @import("../main.zig"); -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; -const MComponents = @import("../module.zig").MComponents; - -pub fn World(comptime mods: anytype) type { - const StateT = NamespacedState(mods); - return struct { - allocator: mem.Allocator, - entities: Entities(NamespacedComponents(mods){}), - modules: Modules, - mod: Mods, - - const Modules = mach.Modules(mods); - - const WorldT = @This(); - pub fn Mod(comptime M: anytype) type { - const module_tag = M.name; - const State = @TypeOf(@field(@as(StateT, undefined), @tagName(module_tag))); - const components = MComponents(M){}; - return struct { - state: State, - entities: *Entities(NamespacedComponents(mods){}), - allocator: mem.Allocator, - - pub const IsInjectedArgument = void; - - /// Sets the named component to the specified value for the given entity, - /// moving the entity from it's current archetype table to the new archetype - /// table if required. - pub inline fn set( - m: *@This(), - entity: EntityID, - // 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); - try world.entities.setComponent(entity, module_tag, component_name, component); - } - - /// gets the named component of the given type (which must be correct, otherwise undefined - /// behavior will occur). Returns null if the component does not exist on the entity. - pub inline fn get( - m: *@This(), - entity: EntityID, - // 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); - } - - /// Removes the named component from the entity, or noop if it doesn't have such a component. - pub inline fn remove( - m: *@This(), - entity: EntityID, - // 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); - try world.entities.removeComponent(entity, module_tag, component_name); - } - - pub inline fn send(m: *@This(), comptime event_name: Modules.LocalEvent, args: Modules.LocalArgsM(M, event_name)) void { - const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); - const world = @fieldParentPtr(WorldT, "mod", mod_ptr); - world.modules.sendToModule(module_tag, event_name, args); - } - - pub inline fn sendGlobal(m: *@This(), comptime event_name: Modules.GlobalEvent, args: Modules.GlobalArgsM(M, event_name)) void { - const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); - const world = @fieldParentPtr(WorldT, "mod", mod_ptr); - 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); - world.modules.dispatch(world.injectable()) catch |err| @panic(@errorName(err)); - } - - /// Returns a new entity. - pub fn newEntity(m: *@This()) !EntityID { - const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); - const world = @fieldParentPtr(WorldT, "mod", mod_ptr); - return world.entities.new(); - } - - /// Removes an entity. - pub fn removeEntity(m: *@This(), entity: EntityID) !void { - const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); - const world = @fieldParentPtr(WorldT, "mod", mod_ptr); - try world.entities.removeEntity(entity); - } - }; - } - - pub const Mods = blk: { - var fields: []const StructField = &[0]StructField{}; - for (mods) |M| { - fields = fields ++ [_]std.builtin.Type.StructField{.{ - .name = @tagName(M.name), - .type = Mod(M), - .default_value = null, - .is_comptime = false, - .alignment = @alignOf(Mod(M)), - }}; - } - break :blk @Type(.{ - .Struct = .{ - .layout = .Auto, - .is_tuple = false, - .fields = fields, - .decls = &[_]std.builtin.Type.Declaration{}, - }, - }); - }; - - const Injectable = blk: { - var types: []const type = &[0]type{}; - for (@typeInfo(Mods).Struct.fields) |field| { - const ModPtr = @TypeOf(@as(*field.type, undefined)); - types = types ++ [_]type{ModPtr}; - } - break :blk std.meta.Tuple(types); - }; - fn injectable(world: *@This()) Injectable { - var v: Injectable = undefined; - outer: inline for (@typeInfo(Injectable).Struct.fields) |field| { - inline for (@typeInfo(Mods).Struct.fields) |injectable_field| { - if (*injectable_field.type == field.type) { - @field(v, field.name) = &@field(world.mod, injectable_field.name); - - // TODO: better module initialization location - @field(v, field.name).entities = &world.entities; - @field(v, field.name).allocator = world.allocator; - continue :outer; - } - } - @compileError("failed to initialize Injectable field (this is a bug): " ++ field.name ++ " " ++ @typeName(field.type)); - } - return v; - } - - pub fn dispatch(world: *@This()) !void { - try world.modules.dispatch(world.injectable()); - } - - pub fn init(world: *@This(), allocator: mem.Allocator) !void { - // TODO: switch Entities to stack allocation like Modules and World - var entities = try Entities(NamespacedComponents(mods){}).init(allocator); - errdefer entities.deinit(); - world.* = @This(){ - .allocator = allocator, - .entities = entities, - .modules = undefined, - .mod = undefined, - }; - try world.modules.init(allocator); - } - - pub fn deinit(world: *@This()) void { - world.entities.deinit(); - world.modules.deinit(world.allocator); - } - }; -} - -// TODO: reconsider state concept -fn NamespacedState(comptime modules: anytype) type { - var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; - inline for (modules) |M| { - // TODO: can't verify module here because it would introduce a dependency loop - // _ = Module(M); - const state_fields = std.meta.fields(M); - const State = if (state_fields.len > 0) @Type(.{ - .Struct = .{ - .layout = .Auto, - .is_tuple = false, - .fields = state_fields, - .decls = &[_]std.builtin.Type.Declaration{}, - }, - }) else struct {}; - fields = fields ++ [_]std.builtin.Type.StructField{.{ - .name = @tagName(M.name), - .type = State, - .default_value = null, - .is_comptime = false, - .alignment = @alignOf(State), - }}; - } - return @Type(.{ - .Struct = .{ - .layout = .Auto, - .is_tuple = false, - .fields = fields, - .decls = &[_]std.builtin.Type.Declaration{}, - }, - }); -} diff --git a/src/engine.zig b/src/engine.zig index 8f840cc9..90d547d9 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -3,6 +3,7 @@ const mach = @import("main.zig"); const core = mach.core; const gpu = mach.core.gpu; const ecs = mach.ecs; +const module = @import("module.zig"); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); @@ -16,7 +17,7 @@ pub const Engine = struct { encoder: *gpu.CommandEncoder, pub const name = .engine; - pub const Mod = World.Mod(@This()); + pub const Mod = Modules.Mod(@This()); pub const events = .{ .{ .local = .init, .handler = init }, @@ -98,46 +99,35 @@ pub const Engine = struct { }; pub const App = struct { - world: World, + modules: Modules, pub fn init(app: *@This()) !void { - app.* = .{ .world = undefined }; - try app.world.init(allocator); - app.world.modules.sendToModule(.engine, .init, .{}); - try app.world.dispatch(); + app.* = .{ .modules = undefined }; + try app.modules.init(allocator); + app.modules.sendToModule(.engine, .init, .{}); + try app.modules.dispatch(); } pub fn deinit(app: *@This()) void { - app.world.modules.sendToModule(.engine, .deinit, .{}); + app.modules.sendToModule(.engine, .deinit, .{}); // TODO: improve error handling - app.world.dispatch() catch |err| @panic(@errorName(err)); // dispatch .deinit - app.world.deinit(); + app.modules.dispatch() catch |err| @panic(@errorName(err)); // dispatch .deinit + app.modules.deinit(gpa.allocator()); _ = gpa.deinit(); } pub fn update(app: *@This()) !bool { // TODO: better dispatch implementation - app.world.mod.engine.sendGlobal(.tick, .{}); - try app.world.dispatch(); // dispatch .tick - try app.world.dispatch(); // dispatch any events produced by .tick - return app.world.mod.engine.state.should_exit; + app.modules.mod.engine.sendGlobal(.tick, .{}); + try app.modules.dispatch(); // dispatch .tick + try app.modules.dispatch(); // dispatch any events produced by .tick + return app.modules.mod.engine.state.should_exit; } }; -pub const World = ecs.World(modules()); - -fn Modules() type { +pub const Modules = module.Modules(blk: { if (!@hasDecl(@import("root"), "modules")) { @compileError("expected `pub const modules = .{};` in root file"); } - return @TypeOf(@import("root").modules); -} - -fn modules() Modules() { - if (!@hasDecl(@import("root"), "modules")) { - @compileError("expected `pub const modules = .{};` in root file"); - } - // TODO: verify modules (causes loop currently) - // _ = @import("module.zig").Modules(@import("root").modules); - return @import("root").modules; -} + break :blk @import("root").modules; +}); diff --git a/src/main.zig b/src/main.zig index 545b3a02..0dd2f179 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,14 +18,11 @@ pub const testing = @import("testing.zig"); pub const sysaudio = if (build_options.want_sysaudio) @import("sysaudio/main.zig") else struct {}; pub const sysgpu = if (build_options.want_sysgpu) @import("sysgpu/main.zig") else struct {}; -pub const Module = @import("module.zig").Module; -pub const Modules = @import("module.zig").Modules; - // Engine exports pub const App = @import("engine.zig").App; pub const Engine = @import("engine.zig").Engine; -pub const World = @import("engine.zig").World; -pub const Mod = World.Mod; +pub const Modules = @import("engine.zig").Modules; +pub const Mod = Modules.Mod; test { const std = @import("std"); diff --git a/src/module.zig b/src/module.zig index b337979c..7d9b0090 100644 --- a/src/module.zig +++ b/src/module.zig @@ -2,17 +2,19 @@ const builtin = @import("builtin"); const std = @import("std"); const testing = @import("testing.zig"); -const EntityID = @import("./ecs/entities.zig").EntityID; +const Entities = @import("ecs/entities.zig").Entities; +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 { +fn ModuleInterface(comptime M: type) type { if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M)); if (!@hasDecl(M, "name")) @compileError("mach: module must have `pub const name = .foobar;`"); if (@typeInfo(@TypeOf(M.name)) != .EnumLiteral) @compileError("mach: module must have `pub const name = .foobar;`, found type:" ++ @typeName(M.name)); 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: re-enable this validation once module event handler arguments would not pose a type dependency loop + // validateEvents("mach: module ." ++ @tagName(M.name) ++ " ", M.events); _ = MComponents(M); return M; } @@ -27,7 +29,7 @@ fn Serializable(comptime T: type) type { /// Manages comptime .{A, B, C} modules and runtime modules. pub fn Modules(comptime mods: anytype) type { // Verify that each module is valid. - inline for (mods) |M| _ = Module(M); + inline for (mods) |M| _ = ModuleInterface(M); return struct { /// Comptime modules @@ -48,15 +50,32 @@ pub fn Modules(comptime mods: anytype) type { }; const EventQueue = std.fifo.LinearFifo(Event, .Dynamic); + const ModulesT = @This(); + events_mu: std.Thread.RwLock = .{}, args_queue: std.ArrayListUnmanaged(u8) = .{}, events: EventQueue, + mod: ModsByName(mods, ModulesT), + // TODO: pass mods directly instead of NamespacedComponents? + entities: Entities(NamespacedComponents(mods){}), + + pub fn Mod(comptime M: type) type { + const StateT = NamespacedState(ModulesT.modules); + const NSComponents = NamespacedComponents(ModulesT.modules); + return Module(M, ModulesT, StateT, NSComponents); + } pub fn init(m: *@This(), allocator: std.mem.Allocator) !void { + // TODO: switch Entities to stack allocation like Modules is + var entities = try Entities(NamespacedComponents(mods){}).init(allocator); + errdefer entities.deinit(); + // TODO: custom event queue allocation sizes m.* = .{ + .entities = entities, .args_queue = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 8 * 1024 * 1024), .events = EventQueue.init(allocator), + .mod = undefined, }; errdefer m.args_queue.deinit(allocator); errdefer m.events.deinit(); @@ -66,20 +85,21 @@ pub fn Modules(comptime mods: anytype) type { pub fn deinit(m: *@This(), allocator: std.mem.Allocator) void { m.args_queue.deinit(allocator); m.events.deinit(); + m.entities.deinit(); } /// Returns an args tuple representing the standard, uninjected, arguments which the given /// local event handler requires. fn LocalArgs(module_name: ModuleName(mods), event_name: LocalEvent) type { inline for (modules) |M| { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module if (M.name != module_name) continue; return LocalArgsM(M, event_name); } } pub fn LocalArgsM(comptime M: type, event_name: LocalEvent) type { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module inline for (M.events) |event| { const Ev = @TypeOf(event); const name_tag = if (@hasField(Ev, "local")) event.local else continue; @@ -106,14 +126,14 @@ pub fn Modules(comptime mods: anytype) type { /// global event handler requires. fn GlobalArgs(module_name: ModuleName(mods), event_name: GlobalEvent) type { inline for (modules) |M| { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module if (M.name != module_name) continue; return GlobalArgsM(M, event_name); } } pub fn GlobalArgsM(comptime M: type, event_name: GlobalEvent) type { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module inline for (M.events) |event| { const Ev = @TypeOf(event); const name_tag = if (@hasField(Ev, "global")) event.global else continue; @@ -194,7 +214,33 @@ pub fn Modules(comptime mods: anytype) type { } /// Dispatches pending events, invoking their event handlers. - pub fn dispatch(m: *@This(), injectable: anytype) !void { + pub fn dispatch(m: *@This()) !void { + const Injectable = comptime blk: { + var types: []const type = &[0]type{}; + for (@typeInfo(ModsByName(mods, ModulesT)).Struct.fields) |field| { + const ModPtr = @TypeOf(@as(*field.type, undefined)); + types = types ++ [_]type{ModPtr}; + } + break :blk std.meta.Tuple(types); + }; + var injectable: Injectable = undefined; + outer: inline for (@typeInfo(Injectable).Struct.fields) |field| { + inline for (@typeInfo(ModsByName(mods, ModulesT)).Struct.fields) |injectable_field| { + if (*injectable_field.type == field.type) { + @field(injectable, field.name) = &@field(m.mod, injectable_field.name); + + // TODO: better module initialization location + @field(injectable, field.name).entities = &m.entities; + @field(injectable, field.name).allocator = m.entities.allocator; + continue :outer; + } + } + @compileError("failed to initialize Injectable field (this is a bug): " ++ field.name ++ " " ++ @typeName(field.type)); + } + return m.dispatchInternal(injectable); + } + + pub fn dispatchInternal(m: *@This(), injectable: anytype) !void { // TODO: verify injectable arguments are valid, e.g. not comptime types // TODO: optimize to reduce send contention @@ -217,10 +263,8 @@ pub fn Modules(comptime mods: anytype) type { m.events_mu.unlock(); if (ev.module_name) |module_name| { - // TODO: dispatch arguments try @This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), ev.args_slice, injectable); } else { - // TODO: dispatch arguments try @This().callGlobal(@enumFromInt(ev.event_name), ev.args_slice, injectable); } } @@ -232,7 +276,7 @@ pub fn Modules(comptime mods: anytype) type { switch (event_name) { inline else => |ev_name| { inline for (modules) |M| { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module inline for (M.events) |event| { const Ev = @TypeOf(event); const name_tag = if (@hasField(Ev, "global")) event.global else continue; @@ -260,7 +304,7 @@ pub fn Modules(comptime mods: anytype) type { switch (event_name) { inline else => |ev_name| { const M = @field(NamespacedModules(@This().modules){}, @tagName(mod_name)); - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module inline for (M.events) |event| { const Ev = @TypeOf(event); @@ -297,6 +341,115 @@ pub fn Modules(comptime mods: anytype) type { }; } +pub fn ModsByName(comptime mods: anytype, comptime ModulesT: type) type { + var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; + for (mods) |M| { + const StateT = NamespacedState(mods); + const NSComponents = NamespacedComponents(mods); + const Mod = Module(M, ModulesT, StateT, NSComponents); + fields = fields ++ [_]std.builtin.Type.StructField{.{ + .name = @tagName(M.name), + .type = Mod, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(Mod), + }}; + } + return @Type(.{ + .Struct = .{ + .layout = .Auto, + .is_tuple = false, + .fields = fields, + .decls = &[_]std.builtin.Type.Declaration{}, + }, + }); +} + +pub fn Module( + comptime M: anytype, + comptime ModulesT: type, + comptime StateT: type, + comptime NSComponents: type, +) type { + const module_tag = M.name; + const State = @TypeOf(@field(@as(StateT, undefined), @tagName(module_tag))); + const components = MComponents(M){}; + return struct { + state: State, + entities: *Entities(NSComponents{}), + // TODO: eliminate this global allocator and/or rethink allocation strategies for modules + allocator: std.mem.Allocator, + + pub const IsInjectedArgument = void; + + /// Returns a new entity. + pub fn newEntity(m: *@This()) !EntityID { + return m.entities.new(); + } + + /// Removes an entity. + pub fn removeEntity(m: *@This(), entity: EntityID) !void { + try m.entities.removeEntity(entity); + } + + /// Sets the named component to the specified value for the given entity, + /// moving the entity from it's current archetype table to the new archetype + /// table if required. + pub inline fn set( + m: *@This(), + entity: EntityID, + // TODO: cleanup comptime + comptime component_name: std.meta.FieldEnum(@TypeOf(components)), + component: @field(components, @tagName(component_name)).type, + ) !void { + try m.entities.setComponent(entity, module_tag, component_name, component); + } + + /// gets the named component of the given type (which must be correct, otherwise undefined + /// behavior will occur). Returns null if the component does not exist on the entity. + pub inline fn get( + m: *@This(), + entity: EntityID, + // TODO: cleanup comptime + comptime component_name: std.meta.FieldEnum(@TypeOf(components)), + ) ?@field(components, @tagName(component_name)).type { + return m.entities.getComponent(entity, module_tag, component_name); + } + + /// Removes the named component from the entity, or noop if it doesn't have such a component. + pub inline fn remove( + m: *@This(), + entity: EntityID, + // TODO: cleanup comptime + comptime component_name: std.meta.FieldEnum(@TypeOf(components)), + ) !void { + try m.entities.removeComponent(entity, module_tag, component_name); + } + + pub inline fn send(m: *@This(), comptime event_name: ModulesT.LocalEvent, args: ModulesT.LocalArgsM(M, event_name)) void { + const MByName = ModsByName(ModulesT.modules, ModulesT); + const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); + const modules = @fieldParentPtr(ModulesT, "mod", mod_ptr); + modules.sendToModule(module_tag, event_name, args); + } + + pub inline fn sendGlobal(m: *@This(), comptime event_name: ModulesT.GlobalEvent, args: ModulesT.GlobalArgsM(M, event_name)) void { + const MByName = ModsByName(ModulesT.modules, ModulesT); + const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); + const modules = @fieldParentPtr(ModulesT, "mod", mod_ptr); + modules.sendGlobal(module_tag, event_name, args); + } + + // TODO: eliminate this + pub fn dispatchNoError(m: *@This()) void { + const MByName = ModsByName(ModulesT.modules, ModulesT); + const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); + const modules = @fieldParentPtr(ModulesT, "mod", mod_ptr); + modules.dispatch() catch |err| @panic(@errorName(err)); + } + }; +} + // TODO: see usage location fn TupleHACK(comptime types: []const type) type { return CreateUniqueTupleHACK(types.len, types[0..types.len].*); @@ -388,7 +541,7 @@ fn LocalEventEnum(comptime mods: anytype) type { var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var i: u32 = 0; for (mods) |M| { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module inline for (M.events) |event| { const Event = @TypeOf(event); const name_tag = if (@hasField(Event, "local")) event.local else continue; @@ -418,7 +571,7 @@ fn GlobalEventEnum(comptime mods: anytype) type { var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var i: u32 = 0; for (mods) |M| { - _ = Module(M); // Validate the module + _ = ModuleInterface(M); // Validate the module inline for (M.events) |event| { const Event = @TypeOf(event); const name_tag = if (@hasField(Event, "global")) event.global else continue; @@ -525,6 +678,7 @@ fn validateEvents(comptime error_prefix: anytype, comptime events: anytype) void } } +// TODO: does it need to be pub? // TODO: tests /// Returns a struct type defining all module's components by module name, e.g.: /// @@ -590,7 +744,7 @@ pub fn NamespacedComponents(comptime modules: anytype) type { /// rotation: @TypeOf() = .{ .type = Vec2, .description = "rotation component" }, /// } /// ``` -pub fn MComponents(comptime M: anytype) type { +fn MComponents(comptime M: anytype) type { const error_prefix = "mach: module ." ++ @tagName(M.name) ++ " .components "; if (!@hasDecl(M, "components")) { return struct {}; @@ -659,6 +813,39 @@ pub fn MComponents(comptime M: anytype) type { }); } +// TODO: reconsider state concept +fn NamespacedState(comptime modules: anytype) type { + var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; + inline for (modules) |M| { + // TODO: can't verify module here because it would introduce a dependency loop + // _ = ModuleInterface(M); + const state_fields = std.meta.fields(M); + const State = if (state_fields.len > 0) @Type(.{ + .Struct = .{ + .layout = .Auto, + .is_tuple = false, + .fields = state_fields, + .decls = &[_]std.builtin.Type.Declaration{}, + }, + }) else struct {}; + fields = fields ++ [_]std.builtin.Type.StructField{.{ + .name = @tagName(M.name), + .type = State, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(State), + }}; + } + 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 { @@ -675,8 +862,8 @@ test { testing.refAllDeclsRecursive(@This()); } -test Module { - _ = Module(struct { +test ModuleInterface { + _ = ModuleInterface(struct { // Physics module state pointer: usize, @@ -697,7 +884,7 @@ test Module { } test Modules { - const Physics = Module(struct { + const Physics = ModuleInterface(struct { // Physics module state pointer: usize, @@ -716,7 +903,7 @@ test Modules { fn tick() !void {} }); - const Renderer = Module(struct { + const Renderer = ModuleInterface(struct { pub const name = .engine_renderer; pub const events = .{ .{ .global = .tick, .handler = tick }, @@ -728,7 +915,7 @@ test Modules { fn tick() !void {} }); - const Sprite2D = Module(struct { + const Sprite2D = ModuleInterface(struct { pub const name = .engine_sprite2d; pub const events = .{}; }); @@ -746,7 +933,7 @@ test Modules { } test "event name" { - const Physics = Module(struct { + const Physics = ModuleInterface(struct { pub const name = .engine_physics; pub const components = .{}; pub const events = .{ @@ -762,7 +949,7 @@ test "event name" { fn bam() !void {} }); - const Renderer = Module(struct { + const Renderer = ModuleInterface(struct { pub const name = .engine_renderer; pub const components = .{}; pub const events = .{ @@ -778,7 +965,7 @@ test "event name" { fn bar() !void {} // same .bar name as .engine_physics.bar }); - const Sprite2D = Module(struct { + const Sprite2D = ModuleInterface(struct { pub const name = .engine_sprite2d; pub const events = .{ .{ .global = .tick, .handler = tick }, @@ -789,19 +976,19 @@ test "event name" { fn foobar() void {} }); - const Mods = Modules(.{ + const Ms = Modules(.{ Physics, Renderer, Sprite2D, }); - const locals = @typeInfo(Mods.LocalEvent).Enum; + const locals = @typeInfo(Ms.LocalEvent).Enum; try testing.expect(type, u1).eql(locals.tag_type); try testing.expect(usize, 2).eql(locals.fields.len); try testing.expect([]const u8, "baz").eql(locals.fields[0].name); try testing.expect([]const u8, "bam").eql(locals.fields[1].name); - const globals = @typeInfo(Mods.GlobalEvent).Enum; + const globals = @typeInfo(Ms.GlobalEvent).Enum; try testing.expect(type, u3).eql(globals.tag_type); try testing.expect(usize, 6).eql(globals.fields.len); try testing.expect([]const u8, "foo").eql(globals.fields[0].name); @@ -813,24 +1000,24 @@ test "event name" { } test ModuleName { - const Physics = Module(struct { + const Physics = ModuleInterface(struct { pub const name = .engine_physics; pub const events = .{}; }); - const Renderer = Module(struct { + const Renderer = ModuleInterface(struct { pub const name = .engine_renderer; pub const events = .{}; }); - const Sprite2D = Module(struct { + const Sprite2D = ModuleInterface(struct { pub const name = .engine_sprite2d; pub const events = .{}; }); - const Mods = Modules(.{ + const Ms = Modules(.{ Physics, Renderer, Sprite2D, }); - const info = @typeInfo(ModuleName(Mods.modules)).Enum; + const info = @typeInfo(ModuleName(Ms.modules)).Enum; try testing.expect(type, u2).eql(info.tag_type); try testing.expect(usize, 3).eql(info.fields.len); @@ -959,7 +1146,7 @@ test "event name calling" { var physics_calc: usize = 0; var renderer_updates: usize = 0; }; - const Physics = Module(struct { + const Physics = ModuleInterface(struct { pub const name = .engine_physics; pub const components = .{}; pub const events = .{ @@ -980,7 +1167,7 @@ test "event name calling" { global.physics_calc += 1; } }); - const Renderer = Module(struct { + const Renderer = ModuleInterface(struct { pub const name = .engine_renderer; pub const components = .{}; pub const events = .{ @@ -1063,11 +1250,11 @@ test "dispatch" { pub const IsInjectedArgument = void; }{}; - const Minimal = Module(struct { + const Minimal = ModuleInterface(struct { pub const name = .engine_minimal; pub const events = .{}; }); - const Physics = Module(struct { + const Physics = ModuleInterface(struct { pub const name = .engine_physics; pub const components = .{}; pub const events = .{ @@ -1088,7 +1275,7 @@ test "dispatch" { global.physics_calc += 1; } }); - const Renderer = Module(struct { + const Renderer = ModuleInterface(struct { pub const name = .engine_renderer; pub const components = .{}; pub const events = .{ @@ -1138,11 +1325,11 @@ test "dispatch" { // injected arguments. modules.sendGlobal(.engine_renderer, .tick, .{}); try testing.expect(usize, 0).eql(global.ticks); - try modules.dispatch(.{&foo}); + try modules.dispatchInternal(.{&foo}); try testing.expect(usize, 2).eql(global.ticks); // TODO: make sendDynamic take an args type to avoid footguns with comptime values, etc. modules.sendDynamic(@intFromEnum(@as(GE, .tick)), .{}); - try modules.dispatch(.{&foo}); + try modules.dispatchInternal(.{&foo}); try testing.expect(usize, 4).eql(global.ticks); // Global events which are not handled by anyone yet can be written as `pub const fooBar = fn() void;` @@ -1152,7 +1339,7 @@ test "dispatch" { // Local events modules.sendToModule(.engine_renderer, .update, .{}); - try modules.dispatch(.{&foo}); + try modules.dispatchInternal(.{&foo}); try testing.expect(usize, 1).eql(global.renderer_updates); modules.sendToModule(.engine_physics, .update, .{}); modules.sendToModuleDynamic( @@ -1160,14 +1347,14 @@ test "dispatch" { @intFromEnum(@as(LE, .calc)), .{}, ); - try modules.dispatch(.{&foo}); + try modules.dispatchInternal(.{&foo}); try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_calc); // Local events modules.sendToModule(.engine_renderer, .basic_args, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); // TODO: match arguments against fn ArgsTuple, for correctness and type inference modules.sendToModule(.engine_renderer, .injected_args, .{ .@"0" = @as(u32, 1), .@"1" = @as(u32, 2) }); - try modules.dispatch(.{&foo}); + try modules.dispatchInternal(.{&foo}); try testing.expect(usize, 3).eql(global.basic_args_sum); try testing.expect(usize, 3).eql(foo.injected_args_sum); }