Skip to content

Commit

Permalink
make global vs. local (module-to-module) messages less magical
Browse files Browse the repository at this point in the history
Previously an ECS module could send a global message like `.tick`, `.init`,
etc. to any other ECS module that was interested in listening to it. If nobody
was listening for it, no problem. Whether someone is listening is determined by
whether a function with that name is defined, e.g. `pub fn tick()`

For module-to-module messages, the module name and message name were combined
so that one module could send a message to another module. For example, a `.gfx_sprite`
module may be listening for an `.init` message, by defining a `pub fn gfxSpriteInit()`
function.

Because module-to-module messages were defined as that _combination_ of the module
name and message name (`.gfx_sprite` + `.init` -> `pub fn gfxSpriteInit`) it was easy
to have a typo mean your message wouldn't get delivered. The conversion felt magical,
which is not good. Additionally, because message handling was optional, you'd get no
compile error for this - the message handler just would not run.

After this change, a message may be sent to everyone (`send(null, .foo)`) or to a specific
module (`send(.gfx_sprite, .init)`). With the latter, it is enforced that there is someone
to recieve it on the other side - you'll get a comptime error otherwise.

Prior to this change to handle a message `send(.gfxSpriteInit)` you would define e.g.:

```
pub fn gfxSpriteInit(...) { ... }
```

and the `gfxSprite` prefix must match your ECS module name. After this change,
`send(.gfx_sprite, .init)` can be handled by defining:

```
pub const local = struct {
    pub fn init(...) { ... }
};
```

Since the `send(.gfx_sprite, .init)` API call is targeted at a specific ECS module (the
`.gfx_sprite` module) it _must_ define the event handler inside of the `pub const local`
namespace. This is to prevent collisions with global events:

```
pub const name = .gfx_sprite;

pub fn init(...) { ... } // can be called only via: send(null, .init)

pub const local = struct {
    pub fn init(...) { ... } // can be called only via: send(.gfx_sprite, .init)
};
```

This isn't perfect, having to write a namespace `local` feels a bit strange - but this is
definitely an improvement over the magical "`gfxSprite` prefix must match your ECS module name"
from before.

Signed-off-by: Stephen Gutekanst <[email protected]>
  • Loading branch information
emidoots committed Dec 17, 2023
1 parent 28aadbb commit ef06fb6
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,5 @@ test "example" {

//-------------------------------------------------------------------------
// Send events to modules
try world.send(.tick, .{});
try world.send(null, .tick, .{});
}
59 changes: 26 additions & 33 deletions src/systems.zig
Original file line number Diff line number Diff line change
Expand Up @@ -97,34 +97,7 @@ pub fn World(comptime mods: anytype) type {
pub fn send(m: *@This(), comptime msg_tag: anytype, args: anytype) !void {
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
const world = @fieldParentPtr(Self, "mod", mod_ptr);

// Convert module_tag=.engine_renderer msg_tag=.render_now to "engineRendererRenderNow"
comptime var str: []const u8 = "";
comptime {
var next_upper = false;
inline for (@tagName(module_tag)) |c| {
if (c == '_') {
next_upper = true;
} else if (next_upper) {
str = str ++ [1]u8{upper(c)};
next_upper = false;
} else {
str = str ++ [1]u8{c};
}
}
next_upper = true;
inline for (@tagName(msg_tag)) |c| {
if (c == '_') {
next_upper = true;
} else if (next_upper) {
str = str ++ [1]u8{upper(c)};
next_upper = false;
} else {
str = str ++ [1]u8{c};
}
}
}
return world.sendStr(str, args);
return world.sendStr(module_tag, @tagName(msg_tag), args);
}

/// Returns a new entity.
Expand Down Expand Up @@ -185,21 +158,41 @@ pub fn World(comptime mods: anytype) type {
/// name conflicts, events sent by modules provided by a library should prefix their events
/// with their module name. For example, a module named `.ziglibs_imgui` should use event
/// names like `.ziglibsImguiClick`, `.ziglibsImguiFoobar`.
pub fn send(world: *Self, comptime msg_tag: anytype, args: anytype) !void {
return world.sendStr(@tagName(msg_tag), args);
pub fn send(world: *Self, comptime optional_module_tag: anytype, comptime msg_tag: anytype, args: anytype) !void {
return world.sendStr(optional_module_tag, @tagName(msg_tag), args);
}

pub fn sendStr(world: *Self, comptime msg: anytype, args: anytype) !void {
pub fn sendStr(world: *Self, comptime optional_module_tag: anytype, comptime msg: anytype, args: anytype) !void {
// Check for any module that has a handler function named msg (e.g. `fn init` would match "init")
inline for (modules.modules) |M| {
if (!@hasDecl(M, msg)) continue;
const EventHandlers = blk: {
switch (@typeInfo(@TypeOf(optional_module_tag))) {
.Null => break :blk M,
.EnumLiteral => {
// Send this message only to the specified module
if (M.name != optional_module_tag) continue;
if (!@hasDecl(M, "local")) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
if (!@hasDecl(M.local, msg)) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
break :blk M.local;
},
.Optional => if (optional_module_tag) |v| {
// Send this message only to the specified module
if (M.name != v) continue;
if (!@hasDecl(M, "local")) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
if (!@hasDecl(M.local, msg)) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
break :blk M.local;
},
else => @panic("unexpected optional_module_tag type: " ++ @typeName(@TypeOf(optional_module_tag))),
}
};
if (!@hasDecl(EventHandlers, msg)) continue;

// Determine which parameters the handler function wants. e.g.:
//
// pub fn init(eng: *mach.Engine) !void
// pub fn init(eng: *mach.Engine, mach: *mach.Mod(.engine)) !void
//
const handler = @field(M, msg);
const handler = @field(EventHandlers, msg);

// Build a tuple of parameters that we can pass to the function, based on what
// *mach.Mod(.foo) types it expects as arguments.
Expand Down

0 comments on commit ef06fb6

Please sign in to comment.