Skip to content

Modules

A graphite module is a struct.

my_module.zig
pub const MyModule = struct {
_: u8 = 0, // registering a module with no fields causes a compilation error.
pub fn init(_: Allocator) !void {
return MyModule{};
}
pub fn deinit(self: *MyModule) void {}
};

Some of its functions (hooks) will be inlined at compile time:

my_module.zig
pub const MyModule = struct {
...
// if MyModule is registered, this functon will
// be called inline when a message is sent.
pub fn onChatMessage(h: hook.ChatMessageHook) !void {
std.log.info("got message from {s}: {s}", .{
h.client.username.items,
h.message,
});
}
};

The dispatcher reflects on hook types at compile-time to inject required arguments. Any hook may take one of the following argument types, the ordering is interchangeable:

  • *@This(): current module
  • *Context: game context
  • anytype: module registry
  • *zcs.CmdBuf: a zcs CmdBuf

Since the final ModuleRegistry type is a tuple that depends on every registered module types, individual modules can’t directly access it. Instead, the dispatcher uses anytype. It’ll inject a pointer to the module registry for each parameter defined with type anytype. See more.

A hook function may also take a specific structure that contains data regarding the triggered event. You may look up the sources to find more info on each struct.

Here are a few hook definitions:

pub const CounterModule = struct {
counter: usize = 0,
...
// onJoin means the dispatcher will call this
// hook when a player joins
pub fn onJoin(self: *@This(), h: hook.JoinHook) !void {
self.counter += 1;
std.log.info("{s} joined, counter = {d}", .{
h.client.username.items,
self.counter,
});
}
// called when a player leaves
pub fn onQuit(self: *@This(), client: *Client) !void {
self.counter -= 1;
sef.log.info("{s} left, counter = {d}", .{
h.client.username.items,
self.counter,
});
}
};

Modules aren’t loaded dynamically at runtime like Bukkit plugins; registering a new module requires a recompilation of the Graphite codebase:

core/src/main.zig
pub const Modules = .{
...
MyModule,
};

Here are two module declarations:

// module_a.zig:
pub const ModuleA = struct {
counter: usize = 0,
...
};
// module_b.zig:
pub const ModuleB = struct {
_: u8,
...
};

At runtime, ModuleB may look up ModuleA and read from/write to its state, and vice versa. Here’s an example:

module_b.zig
pub const ModuleB = struct {
...
pub fn onJoin(
reg: anytype,
h: hook.JoinHook,
) !void {
const module_a = reg.get(ModuleA);
module_a.counter += 1;
std.log.info("{s} joined, counter: {d}", .{
h.client.username.items,
module_a.counter,
});
}
}

If ModuleA isn’t registered when ModuleRegistry.get is evaluated, this won’t compile.

It would be common to express modules as functions that take comptime options and return a struct, such as:

module_a.zig
pub const ModuleAOptions = struct {
name: []const u8,
some_flag: bool = true,
some_number: i32 = 4,
...
};
pub fn ModuleA(comptime opt: ModuleAOptions) type {
return struct {
counter: usize,
pub fn init(_: Allocator) !void {
std.log.info("hello from " ++ opt.name ++ "!");
return @This(){
.counter = 0,
};
}
};
}

ModuleB can still depend on ModuleA:

module_b.zig
pub const ModuleBOptions = struct {
module_a: type,
};
pub fn ModuleB(comptime opt: ModuleBOptions) type {
return struct {
_: u8,
...
pub fn onJoin(reg: anytype) !void {
const module_a = reg.get(opt.module_a);
module_a.counter += 1;
std.log.info("counter: {d}", .{ module_a.counter });
}
}
}

You’d need to register both modules for this to compile, here’s what this would look like:

core/src/main.zig
const ModuleA = @import("module_a.zig").ModuleA(.{
name = "graphite",
some_flag = true,
});
const ModuleB = @import("module_b.zig").ModuleB(.{
module_a = ModuleA,
});
pub const Modules = .{
ModuleA,
ModuleB,
};