From 5fe126b6960906f5c1b2f82310816802b736ac08 Mon Sep 17 00:00:00 2001 From: abux Date: Thu, 9 Jul 2026 22:30:04 +0100 Subject: [PATCH] HelloWorld --- .gitignore | 1 + build.zig | 27 ++++ build.zig.zon | 17 +++ src/app.zig | 99 ++++++++++++++ src/event/event.zig | 5 + src/event/queue.zig | 87 +++++++++++++ src/event/root.zig | 3 + src/event/storage.zig | 165 +++++++++++++++++++++++ src/example/root.zig | 1 + src/example/simple/component.zig | 43 ++++++ src/example/simple/event.zig | 18 +++ src/example/simple/plugin.zig | 40 ++++++ src/example/simple/resource.zig | 2 + src/example/simple/root.zig | 3 + src/example/simple/stage.zig | 5 + src/example/simple/system.zig | 207 +++++++++++++++++++++++++++++ src/plugin/context.zig | 3 + src/plugin/plugin.zig | 14 ++ src/plugin/root.zig | 3 + src/plugin/storage.zig | 61 +++++++++ src/resource/resource.zig | 5 + src/resource/root.zig | 2 + src/resource/storage.zig | 170 ++++++++++++++++++++++++ src/scheduler/context.zig | 3 + src/scheduler/root.zig | 3 + src/scheduler/storage.zig | 107 +++++++++++++++ src/scheduler/system.zig | 4 + src/template/handle.zig | 8 ++ src/template/root.zig | 2 + src/template/type_id.zig | 9 ++ src/world/archetype.zig | 143 ++++++++++++++++++++ src/world/column.zig | 151 +++++++++++++++++++++ src/world/column_typed.zig | 61 +++++++++ src/world/entity_record.zig | 2 + src/world/query.zig | 140 ++++++++++++++++++++ src/world/query_filters.zig | 2 + src/world/root.zig | 2 + src/world/signature.zig | 26 ++++ src/world/world.zig | 217 +++++++++++++++++++++++++++++++ 39 files changed, 1861 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/app.zig create mode 100644 src/event/event.zig create mode 100644 src/event/queue.zig create mode 100644 src/event/root.zig create mode 100644 src/event/storage.zig create mode 100644 src/example/root.zig create mode 100644 src/example/simple/component.zig create mode 100644 src/example/simple/event.zig create mode 100644 src/example/simple/plugin.zig create mode 100644 src/example/simple/resource.zig create mode 100644 src/example/simple/root.zig create mode 100644 src/example/simple/stage.zig create mode 100644 src/example/simple/system.zig create mode 100644 src/plugin/context.zig create mode 100644 src/plugin/plugin.zig create mode 100644 src/plugin/root.zig create mode 100644 src/plugin/storage.zig create mode 100644 src/resource/resource.zig create mode 100644 src/resource/root.zig create mode 100644 src/resource/storage.zig create mode 100644 src/scheduler/context.zig create mode 100644 src/scheduler/root.zig create mode 100644 src/scheduler/storage.zig create mode 100644 src/scheduler/system.zig create mode 100644 src/template/handle.zig create mode 100644 src/template/root.zig create mode 100644 src/template/type_id.zig create mode 100644 src/world/archetype.zig create mode 100644 src/world/column.zig create mode 100644 src/world/column_typed.zig create mode 100644 src/world/entity_record.zig create mode 100644 src/world/query.zig create mode 100644 src/world/query_filters.zig create mode 100644 src/world/root.zig create mode 100644 src/world/signature.zig create mode 100644 src/world/world.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dfcbd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..f6730a9 --- /dev/null +++ b/build.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + // --- TARGET --- + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // --- MOD --- + const mod = b.addModule("app", .{ + .root_source_file = b.path("src/app.zig"), + + .target = target, + .optimize = optimize, + }); + + // --- TESTS --- + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + // --- RUN TESTS --- + const run_mod_tests = b.addRunArtifact(mod_tests); + + // --- STEP BRO --- + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..9b89e5d --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .app, + + .version = "0.0.0", + + .fingerprint = 0xc96e70cffd7de500, + + .minimum_zig_version = "0.16.0", + + .dependencies = .{}, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 0000000..a09a1c7 --- /dev/null +++ b/src/app.zig @@ -0,0 +1,99 @@ +// --- LIBS --- +const std = @import("std"); +const root = @import("root"); +pub const event = @import("event/root.zig"); +pub const plugin = @import("plugin/root.zig"); +pub const resource = @import("resource/root.zig"); +pub const scheduler = @import("scheduler/root.zig"); +pub const template = @import("template/root.zig"); +pub const ecs = @import("world/root.zig"); +const Self = @This(); + +pub const stage = struct { + pub const Init = struct {}; + pub const Deinit = struct {}; + pub const Inputs = struct {}; + pub const Update = struct {}; + pub const Draw = struct {}; +}; + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub const Options = struct { + seed: u64 = 8490694, +}; +pub const options: Options = if (@hasDecl( + root, + "app_options", +)) root.app_options else .{}; + +// +// FIELDS +// + +events: event.Storage, +plugins: plugin.Storage, +resources: resource.Storage, +schedules: scheduler.Storage, +world: ecs.World, +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .events = .init(alloc), + .plugins = .init(alloc), + .resources = .init(alloc), + .schedules = .init(alloc), + .world = .init(alloc), + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + self.world.deinit(); + self.events.deinit(); + self.plugins.deinit(); + self.resources.deinit(); + self.schedules.deinit(); +} + +// +// EXAMPLES +// + +const example = @import("example/root.zig"); + +// ---------------------------------------------------- +// ---------------------------------------------------- +test "Main" { + // --- APP --- + var app: Self = .init(std.testing.allocator); + defer app.deinit(); + + { // PLUGINS + // --- ADD PLUGINS --- + try app.plugins.addMany(&.{ + example.simple.plugin, + }); + + // --- BUILD PLUGINS --- + try app.plugins.build(.{ + .app = &app, + }); + } + { // SYSTEMS + try app.schedules.runMany(&.{ + example.simple.stage.Init, + example.simple.stage.Inputs, + example.simple.stage.Update, + example.simple.stage.Draw, + example.simple.stage.Deinit, + }, .{ + .app = &app, + }); + } +} diff --git a/src/event/event.zig b/src/event/event.zig new file mode 100644 index 0000000..7f288d8 --- /dev/null +++ b/src/event/event.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +ptr: *anyopaque, +clear_fn: *const fn (*anyopaque) void, +destroy_fn: *const fn (*anyopaque, std.mem.Allocator) void, diff --git a/src/event/queue.zig b/src/event/queue.zig new file mode 100644 index 0000000..4405dfc --- /dev/null +++ b/src/event/queue.zig @@ -0,0 +1,87 @@ +const std = @import("std"); + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn Queue(comptime T: type) type { + return struct { + const Self = @This(); + + // + // TYPES + // + + head: usize, + data: std.ArrayList(T), + alloc: std.mem.Allocator, + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .head = 0, + .data = .empty, + .alloc = alloc, + }; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn deinit(self: *Self) void { + self.data.deinit(self.alloc); + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn clear(self: *Self) void { + self.head = 0; + self.data.clearRetainingCapacity(); + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn append(self: *Self, data: T) !void { + try self.data.append(self.alloc, data); + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn items(self: *Self) []const T { + return self.data.items; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn itemsMut(self: *Self) []T { + return self.data.items; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn current(self: *Self) ?*T { + if (self.itemsMut().len == 0) return null; + return &self.itemsMut()[0]; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn consume(self: *Self) ?T { + if (self.head >= self.data.items.len) + return null; + + defer self.head += 1; + return self.data.items[self.head]; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn len(self: *const Self) usize { + return self.data.items.len; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn isEmpty(self: *const Self) bool { + return self.len() == 0; + } + }; +} diff --git a/src/event/root.zig b/src/event/root.zig new file mode 100644 index 0000000..cde2ca7 --- /dev/null +++ b/src/event/root.zig @@ -0,0 +1,3 @@ +pub const Event = @import("event.zig"); +pub const Storage = @import("storage.zig"); +pub const Queue = @import("queue.zig").Queue; diff --git a/src/event/storage.zig b/src/event/storage.zig new file mode 100644 index 0000000..3d03be8 --- /dev/null +++ b/src/event/storage.zig @@ -0,0 +1,165 @@ +// --- LIBS --- +const std = @import("std"); +const Event = @import("event.zig"); +const TypeID = @import("../template/type_id.zig").of; +const Queue = @import("queue.zig").Queue; +const Self = @This(); + +// +// FIELDS +// + +data: std.AutoHashMap(u64, Event), +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .data = .init(alloc), + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + var it = self.data.valueIterator(); + while (it.next()) |v| { + v.destroy_fn(v.ptr, self.alloc); + } + self.data.deinit(); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addMany( + self: *Self, + comptime Ts: []const type, +) !void { + inline for (Ts) |T| { + try self.add(T); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn add( + self: *Self, + comptime T: type, +) !void { + // --- NEW QUEUE --- + const queue = try self.alloc.create(Queue(T)); + queue.* = .init(self.alloc); + + // --- NEW EVENT --- + try self.data.put(TypeID(T), .{ + .ptr = queue, + + .clear_fn = struct { + fn clear(ptr: *anyopaque) void { + const q: *Queue(T) = @ptrCast(@alignCast(ptr)); + q.clear(); + } + }.clear, + + .destroy_fn = struct { + fn destroy( + ptr: *anyopaque, + alloc: std.mem.Allocator, + ) void { + const q: *Queue(T) = @ptrCast(@alignCast(ptr)); + q.deinit(); + alloc.destroy(q); + } + }.destroy, + }); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn has( + self: *Self, + comptime T: type, +) bool { + return self.data.contains(TypeID(T)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn sendMany( + self: *Self, + values: anytype, +) !void { + inline for (values) |value| { + try self.send(value); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn send( + self: *Self, + value: anytype, +) !void { + // --- METADATA --- + const T = @TypeOf(value); + const id = TypeID(T); + + // --- AUTO REGISTER --- + if (!self.has(T)) { + try self.add(T); + } + + // --- GET STORAGE --- + const storage = + self.data.getPtr(id) orelse + return error.NoEvent; + + // --- CAST QUEUE --- + const queue: *Queue(T) = @ptrCast( + @alignCast(storage.ptr), + ); + + // --- APPEND EVENT --- + try queue.append(value); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn get( + self: *Self, + comptime T: type, +) ?*Queue(T) { + const event = self.data.getPtr(TypeID(T)) orelse return null; + return @ptrCast(@alignCast(event.ptr)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn clear(self: *Self) void { + var it = self.data.valueIterator(); + while (it.next()) |storage| { + storage.clear_fn(storage.ptr); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn current( + self: *Self, + comptime T: type, +) ?*T { + const queue = self.get(T) orelse return null; + return queue.current(); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn consume( + self: *Self, + comptime T: type, +) ?T { + const queue = self.get(T) orelse return null; + return queue.consume(); +} diff --git a/src/example/root.zig b/src/example/root.zig new file mode 100644 index 0000000..1f1879c --- /dev/null +++ b/src/example/root.zig @@ -0,0 +1 @@ +pub const simple = @import("simple/root.zig"); diff --git a/src/example/simple/component.zig b/src/example/simple/component.zig new file mode 100644 index 0000000..98c1c84 --- /dev/null +++ b/src/example/simple/component.zig @@ -0,0 +1,43 @@ +const std = @import("std"); + +pub const Transform = struct { + pos: @Vector(2, f32) = .{ 0, 0 }, + size: @Vector(2, f32) = .{ 1, 1 }, + + pub fn format( + self: @This(), + writer: *std.Io.Writer, + ) !void { + try writer.print("pos=({}, {}) size=({}, {})", .{ + self.pos[0], self.pos[1], + self.size[0], self.size[1], + }); + } +}; + +pub const Material = struct { + albedo: @Vector(4, u8) = .{ 255, 255, 255, 255 }, + + pub fn format( + self: @This(), + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + try writer.print("rgba({}, {}, {}, {})", .{ + self.albedo[0], + self.albedo[1], + self.albedo[2], + self.albedo[3], + }); + } +}; + +pub const Player = struct { + name: []const u8, + + pub fn format( + self: @This(), + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + try writer.print("\"{s}\"", .{self.name}); + } +}; diff --git a/src/example/simple/event.zig b/src/example/simple/event.zig new file mode 100644 index 0000000..59b69a6 --- /dev/null +++ b/src/example/simple/event.zig @@ -0,0 +1,18 @@ +pub const Login = struct { + username: []const u8, + password: []const u8, +}; + +pub const Register = struct { + username: []const u8, + password: []const u8, +}; + +pub const Connect = struct { + ip: []const u8 = "localhost", + port: u16 = 4444, +}; + +pub const ConnectionAccepted = struct { + id: u32, +}; diff --git a/src/example/simple/plugin.zig b/src/example/simple/plugin.zig new file mode 100644 index 0000000..2321900 --- /dev/null +++ b/src/example/simple/plugin.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const App = @import("../../app.zig"); +const system = @import("system.zig"); +const stage = @import("stage.zig"); + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub const plugin: App.plugin.Plugin = .{ + .name = "Simple", + .description = "Simple plugin example", + .build = build, +}; + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +fn build( + self: *App.plugin.Plugin, + ctx: App.plugin.Context, +) !void { + + // --- DEBUG --- + std.debug.print( + "\x1b[33mPLUGIN\x1b[0m\n" ++ + \\| Name: {s} + \\| Description: {s} + \\ + , + .{ + self.name, + self.description, + }, + ); + + // --- ADD SYSTEMS --- + try ctx.app.schedules.addMany(stage.Init, &.{ + system.test_resources, + system.test_events, + system.test_ecs, + }); +} diff --git a/src/example/simple/resource.zig b/src/example/simple/resource.zig new file mode 100644 index 0000000..5948d6a --- /dev/null +++ b/src/example/simple/resource.zig @@ -0,0 +1,2 @@ +pub const Server = struct { id: u32 = 0 }; +pub const Client = struct { id: u32 = 0 }; diff --git a/src/example/simple/root.zig b/src/example/simple/root.zig new file mode 100644 index 0000000..30b184a --- /dev/null +++ b/src/example/simple/root.zig @@ -0,0 +1,3 @@ +pub const plugin = @import("plugin.zig").plugin; +pub const system = @import("system.zig"); +pub const stage = @import("stage.zig"); diff --git a/src/example/simple/stage.zig b/src/example/simple/stage.zig new file mode 100644 index 0000000..7136761 --- /dev/null +++ b/src/example/simple/stage.zig @@ -0,0 +1,5 @@ +pub const Init = struct {}; +pub const Deinit = struct {}; +pub const Inputs = struct {}; +pub const Update = struct {}; +pub const Draw = struct {}; diff --git a/src/example/simple/system.zig b/src/example/simple/system.zig new file mode 100644 index 0000000..93aeb6a --- /dev/null +++ b/src/example/simple/system.zig @@ -0,0 +1,207 @@ +const std = @import("std"); +const App = @import("../../app.zig"); + +const event = @import("event.zig"); +const resource = @import("resource.zig"); +const component = @import("component.zig"); + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn test_resources( + ctx: App.scheduler.Context, +) !void { + // --- SET NEW --- + try ctx.app.resources.set(resource.Server{}); + try ctx.app.resources.set(resource.Client{}); + + // --- CONTAINS DEBUG --- + std.debug.print( + "\x1b[34mRESOURCES EXIST\x1b[0m\n" ++ + \\| Server: {} + \\| Client: {} + \\ + , + .{ + ctx.app.resources.contains(resource.Server), + ctx.app.resources.contains(resource.Client), + }, + ); + + // --- GET READONLY RESOURCES --- + _ = try ctx.app.resources.getMany(struct { + server: ?*const resource.Server, + client: ?*const resource.Client, + }); +} + +const User = struct { + id: u32 = 0, + username: []const u8, + password: []const u8, +}; + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn test_events( + ctx: App.scheduler.Context, +) !void { + // --- DEBUG --- + std.debug.print( + "\x1b[35mEVENTS\x1b[0m\n", + .{}, + ); + + // --- CLEAR EVENTS --- + defer ctx.app.events.clear(); + + // --- USER LIST --- + var users: std.ArrayList(User) = .empty; + defer users.deinit(ctx.app.alloc); + + // --- ID`S --- + var next_register_id: u32 = 0; + var next_connected_id: u32 = 0; + + // --- ADD ADMIN USER --- + try users.append(ctx.app.alloc, .{ + .id = next_register_id, + .username = "admin", + .password = "1234", + }); + + { // NEW USER + try ctx.app.events.sendMany(.{ + event.Register{ + .username = "abux", + .password = "qwerty", + }, + event.Login{ + .username = "abux", + .password = "qwerty", + }, + }); + } + + // --- REGISTER LOOP --- + if (ctx.app.events.get(event.Register)) |queue| { + for (queue.items()) |reg| { + next_register_id += 1; + try users.append(ctx.app.alloc, .{ + .id = next_register_id, + .username = reg.username, + .password = reg.password, + }); + } + queue.clear(); + } + + // --- LOGIN LOOP --- + if (ctx.app.events.get(event.Login)) |queue| { + for (queue.items()) |login| { + for (users.items) |user| { + // --- CHECK CREDS --- + if (!std.mem.eql(u8, user.username, login.username)) continue; + if (!std.mem.eql(u8, user.password, login.password)) continue; + + // --- SEND ACCEPTED CONNECTION --- + next_connected_id += 1; + try ctx.app.events.send(event.ConnectionAccepted{ + .id = next_connected_id, + }); + } + } + queue.clear(); + } + + // --- CONNECTED --- + if (ctx.app.events.consume(event.ConnectionAccepted)) |ca| { + std.debug.print( + \\ | Connected: + \\ | ID: {} + \\ + , + .{ + ca.id, + }, + ); + } + + // --- DEBUG --- + std.debug.print( + "\x1b[31mUSERS\x1b[0m\n", + .{}, + ); + for (users.items) |user| { + std.debug.print( + \\ | ID: {} + \\ | Username: {s} + \\ | Password: {s} + \\ + , + .{ + user.id, + user.username, + user.password, + }, + ); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn test_ecs( + ctx: App.scheduler.Context, +) !void { + // --- SPAWN ENTITY --- + try ctx.app.world.spawn(.{ + component.Player{ .name = "abux" }, + component.Material{}, + component.Transform{}, + }); + try ctx.app.world.spawn(.{ + component.Material{}, + component.Transform{}, + }); + + // --- DEBUG --- + std.debug.print( + "\x1b[35mENTITIES\x1b[0m\n", + .{}, + ); + + // --- QUERY --- + var query = ctx.app.world.query(struct { + player: ?*component.Player, + material: *component.Material, + transform: *component.Transform, + }, .{}); + while (query.next()) |e| { + // --- UPDATE POS --- + e.transform.pos[0] += 1; + e.transform.pos[1] += 1; + + // --- CHANGE PLAYER'S COLOR --- + if (e.player) |_| { + e.material.albedo = .{ 20, 200, 20, 255 }; + } + + // --- ENTITIES DEBUG --- + std.debug.print("\x1b[36m", .{}); + std.debug.print( + \\| ID: {} + \\| Components: + \\ | Player: {?f} + \\ | Material: {f} + \\ | Transform: {f} + \\ + , + .{ + query.entity(), + e.player, + e.material, + e.transform, + }, + ); + std.debug.print("\x1b[0m", .{}); + } +} diff --git a/src/plugin/context.zig b/src/plugin/context.zig new file mode 100644 index 0000000..d916681 --- /dev/null +++ b/src/plugin/context.zig @@ -0,0 +1,3 @@ +const App = @import("../app.zig"); + +app: *App, diff --git a/src/plugin/plugin.zig b/src/plugin/plugin.zig new file mode 100644 index 0000000..3fc52fd --- /dev/null +++ b/src/plugin/plugin.zig @@ -0,0 +1,14 @@ +const Context = @import("context.zig"); +const Self = @This(); + +// +// FIELDS +// + +// --- ID --- +name: []const u8, +description: []const u8 = "", +version: [3]u8 = .{ 0, 0, 0 }, + +// --- CALLBACKS --- +build: *const fn (*Self, Context) anyerror!void, diff --git a/src/plugin/root.zig b/src/plugin/root.zig new file mode 100644 index 0000000..cfc824a --- /dev/null +++ b/src/plugin/root.zig @@ -0,0 +1,3 @@ +pub const Plugin = @import("plugin.zig"); +pub const Storage = @import("storage.zig"); +pub const Context = @import("context.zig"); diff --git a/src/plugin/storage.zig b/src/plugin/storage.zig new file mode 100644 index 0000000..0996539 --- /dev/null +++ b/src/plugin/storage.zig @@ -0,0 +1,61 @@ +// --- LIBS --- +const std = @import("std"); +const Plugin = @import("plugin.zig"); +const Context = @import("context.zig"); +const Self = @This(); + +// +// FIELDS +// + +data: std.ArrayList(Plugin), +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .data = .empty, + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + self.data.deinit(self.alloc); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn add( + self: *Self, + plugin: Plugin, +) !void { + try self.data.append( + self.alloc, + plugin, + ); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addMany( + self: *Self, + plugins: []const Plugin, +) !void { + for (plugins) |plugin| { + try self.add(plugin); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn build( + self: *Self, + ctx: Context, +) !void { + for (self.data.items) |*plugin| { + try plugin.build(plugin, ctx); + } +} diff --git a/src/resource/resource.zig b/src/resource/resource.zig new file mode 100644 index 0000000..21a1a91 --- /dev/null +++ b/src/resource/resource.zig @@ -0,0 +1,5 @@ +const std = @import("std"); +const Self = @This(); + +ptr: *anyopaque, +destroy: *const fn (*anyopaque, std.mem.Allocator) void, diff --git a/src/resource/root.zig b/src/resource/root.zig new file mode 100644 index 0000000..6280169 --- /dev/null +++ b/src/resource/root.zig @@ -0,0 +1,2 @@ +pub const Resource = @import("resource.zig"); +pub const Storage = @import("storage.zig"); diff --git a/src/resource/storage.zig b/src/resource/storage.zig new file mode 100644 index 0000000..4b8636f --- /dev/null +++ b/src/resource/storage.zig @@ -0,0 +1,170 @@ +// --- LIBS --- +const std = @import("std"); +const Resource = @import("resource.zig"); +const TypeID = @import("../template/type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +data: std.AutoHashMap(u64, Resource), +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .data = .init(alloc), + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + var it = self.data.valueIterator(); + while (it.next()) |v| { + v.destroy(v.ptr, self.alloc); + } + self.data.deinit(); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn set( + self: *Self, + value: anytype, +) !void { + _ = try self.getOrSet(value); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn remove( + self: *Self, + comptime T: type, +) void { + _ = self.data.remove(TypeID(T)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn getOrSet( + self: *Self, + value: anytype, +) !*@TypeOf(value) { + // --- DATA --- + const T = @TypeOf(value); + const id = TypeID(T); + + // --- RETURN IF ALREADY EXISTS --- + if (self.data.getPtr(id)) |res| { + const v: *T = @ptrCast(@alignCast(res.ptr)); + v.* = value; + return v; + } + + // --- NEW PTR --- + const ptr = try self.alloc.create(T); + errdefer self.alloc.destroy(ptr); + + // --- ASSIGN VALUE --- + ptr.* = value; + + // --- ADD NEW --- + try self.data.put(id, .{ + .ptr = ptr, + .destroy = struct { + pub fn destroy(_ptr: *anyopaque, _alloc: std.mem.Allocator) void { + const v: *T = @ptrCast(@alignCast(_ptr)); + _alloc.destroy(v); + } + }.destroy, + }); + + // --- RESULT --- + return @ptrCast(@alignCast(ptr)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn getPtr( + self: *Self, + comptime T: type, +) ?*T { + const res = self.data.getPtr(TypeID(T)) orelse return null; + return @ptrCast(@alignCast(res.ptr)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn getConstPtr( + self: *Self, + comptime T: type, +) ?*const T { + const ptr = self.getPtr(T) orelse return null; + return ptr; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn require( + self: *Self, + comptime T: type, +) error{ResourceNotSet}!*T { + return self.getPtr(T) orelse return error.ResourceNotSet; +} + +/// ---------------------------------------------------- +/// Example +/// ``` +/// const res = try app.resources.getMany(struct { +/// server: ?*const Server, +/// client: ?*const Client, +/// player: *Entity, +/// }); +/// ``` +/// ---------------------------------------------------- +pub fn getMany( + self: *Self, + comptime Result: type, +) error{ResourceNotSet}!Result { + // --- RESULT --- + var result: Result = undefined; + + // --- GET FIELDS --- + inline for (std.meta.fields(Result)) |field| { + // --- FIELD INFO --- + const FieldType = field.type; + + switch (@typeInfo(FieldType)) { + .optional => |opt| { + const ptr_info = @typeInfo(opt.child).pointer; + @field(result, field.name) = self.getPtr(ptr_info.child); + }, + + .pointer => |ptr| { + @field(result, field.name) = + self.getPtr(ptr.child) orelse + return error.ResourceNotSet; + }, + + else => @compileError( + "getMany fields must be *T or ?*T; field '" ++ field.name ++ "' has type '" ++ + @typeName(FieldType) ++ "'", + ), + } + } + + return result; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn contains( + self: *const Self, + comptime T: type, +) bool { + return self.data.contains(TypeID(T)); +} diff --git a/src/scheduler/context.zig b/src/scheduler/context.zig new file mode 100644 index 0000000..d916681 --- /dev/null +++ b/src/scheduler/context.zig @@ -0,0 +1,3 @@ +const App = @import("../app.zig"); + +app: *App, diff --git a/src/scheduler/root.zig b/src/scheduler/root.zig new file mode 100644 index 0000000..cdf0dd1 --- /dev/null +++ b/src/scheduler/root.zig @@ -0,0 +1,3 @@ +pub const System = @import("system.zig"); +pub const Storage = @import("storage.zig"); +pub const Context = @import("context.zig"); diff --git a/src/scheduler/storage.zig b/src/scheduler/storage.zig new file mode 100644 index 0000000..a054544 --- /dev/null +++ b/src/scheduler/storage.zig @@ -0,0 +1,107 @@ +// --- LIBS --- +const std = @import("std"); +const System = @import("system.zig"); +const Context = @import("context.zig"); +const TypeID = @import("../template/type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +data: std.ArrayList(System), +stages: std.AutoHashMap(u64, usize), +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .data = .empty, + .stages = .init(alloc), + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + self.data.deinit(self.alloc); + self.stages.deinit(); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addStage( + self: *Self, + comptime T: type, +) !void { + // --- GET ID --- + const stage_id = TypeID(T); + + // --- INSERT --- + if (self.stages.contains(stage_id)) return; + try self.stages.put(stage_id, 0); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn add( + self: *Self, + comptime T: type, + execute: *const fn (Context) anyerror!void, +) !void { + // --- GET ID --- + const stage_id = TypeID(T); + + // --- NEW SYSTEM --- + try self.data.append(self.alloc, .{ + .stage_id = stage_id, + .execute = execute, + }); + errdefer _ = self.data.swapRemove(self.data.items.len - 1); + + // --- NEW STAGE --- + try self.addStage(T); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addMany( + self: *Self, + comptime T: type, + values: []const *const fn (Context) anyerror!void, +) !void { + for (values) |exe| { + try self.add(T, exe); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn run( + self: *Self, + comptime T: type, + ctx: Context, +) !void { + // --- GET ID --- + const stage_id = TypeID(T); + + // --- RUN --- + for (self.data.items) |sys| { + if (sys.stage_id != stage_id) continue; + try sys.execute(ctx); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn runMany( + self: *Self, + comptime Ts: []const type, + ctx: Context, +) !void { + inline for (Ts) |T| { + try self.run(T, ctx); + } +} diff --git a/src/scheduler/system.zig b/src/scheduler/system.zig new file mode 100644 index 0000000..ad3a8a2 --- /dev/null +++ b/src/scheduler/system.zig @@ -0,0 +1,4 @@ +const Context = @import("context.zig"); + +stage_id: u64, +execute: *const fn (ctx: Context) anyerror!void, diff --git a/src/template/handle.zig b/src/template/handle.zig new file mode 100644 index 0000000..4e72149 --- /dev/null +++ b/src/template/handle.zig @@ -0,0 +1,8 @@ +pub fn Handle(comptime T: type) type { + return struct { + pub const Type: type = T; + + idx: u32 = 0, + gen: u32 = 0, + }; +} diff --git a/src/template/root.zig b/src/template/root.zig new file mode 100644 index 0000000..f43e5da --- /dev/null +++ b/src/template/root.zig @@ -0,0 +1,2 @@ +pub const TypeID = @import("type_id.zig").of; +pub const Handle = @import("handle.zig").Handle; diff --git a/src/template/type_id.zig b/src/template/type_id.zig new file mode 100644 index 0000000..1b0df09 --- /dev/null +++ b/src/template/type_id.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const root = @import("../app.zig"); + +pub fn of(comptime T: type) u64 { + return std.hash.Wyhash.hash( + root.options.seed, + @typeName(T), + ); +} diff --git a/src/world/archetype.zig b/src/world/archetype.zig new file mode 100644 index 0000000..c8fceb5 --- /dev/null +++ b/src/world/archetype.zig @@ -0,0 +1,143 @@ +// --- LIBS --- +const std = @import("std"); +const Signature = @import("signature.zig"); +const Column = @import("column.zig"); +const TypeID = @import("../template/type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +signature: Signature, +type_map: std.AutoHashMap(u64, usize), +entities: std.ArrayList(u64), +columns: std.ArrayList(Column), +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init( + signature: Signature, + alloc: std.mem.Allocator, +) Self { + return .{ + .signature = signature, + .type_map = .init(alloc), + .entities = .empty, + .columns = .empty, + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + for (self.columns.items) |*col| { + col.deinit(); + } + + self.columns.deinit(self.alloc); + self.entities.deinit(self.alloc); + self.type_map.deinit(); + + self.alloc.free(self.signature.ids); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addColumn( + self: *Self, + comptime T: type, +) !void { + const idx = self.columns.items.len; + + try self.columns.append( + self.alloc, + try .init(T, self.alloc), + ); + + try self.type_map.put( + TypeID(T), + idx, + ); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addColumns( + self: *Self, + components: anytype, +) !void { + inline for (components) |T| { + try self.addColumn(T); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn add( + self: *Self, + entity: u64, + components: anytype, +) !void { + const fields = std.meta.fields(@TypeOf(components)); + + if (fields.len != self.columns.items.len) { + return error.MissingComponent; + } + + // const row = self.entities.items.len; + try self.entities.append(self.alloc, entity); + + inline for (fields) |field| { + const component = @field(components, field.name); + + const T = @TypeOf(component); + + const idx = self.type_map.get(TypeID(T)) orelse { + return error.ComponentNotFound; + }; + + try self.columns.items[idx].append(&component); + } + + // for (self.columns.items) |*col| { + // if (col. != row + 1) { + // return error.RowDesync; + // } + // } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn get( + self: *Self, + comptime T: type, + row: usize, +) ?*T { + const idx = self.type_map.get(TypeID(T)) orelse return null; + const ptr = self.columns.items[idx].getPtr(row) orelse return null; + return @ptrCast(@alignCast(ptr)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn remove( + self: *Self, + row: usize, +) ?u64 { + const last_entity = self.entities.items[self.entities.items.len - 1]; + + _ = self.entities.swapRemove(row); + + for (self.columns.items) |*col| { + col.swapRemove(row); + } + + if (row < self.entities.items.len) { + return last_entity; + } + + return null; +} diff --git a/src/world/column.zig b/src/world/column.zig new file mode 100644 index 0000000..3e17281 --- /dev/null +++ b/src/world/column.zig @@ -0,0 +1,151 @@ +const std = @import("std"); +const TypeID = @import("../template/type_id.zig").of; +const TypedColumn = @import("column_typed.zig").TypedColumn; +const Self = @This(); + +// +// FIELDS +// + +storage: *anyopaque, + +type_id: u64, + +elem_size: usize, +alignment: usize, + +alloc: std.mem.Allocator, + +append_fn: *const fn ( + storage: *anyopaque, + value: *const anyopaque, + alloc: std.mem.Allocator, +) anyerror!void, + +swap_remove_fn: *const fn ( + storage: *anyopaque, + idx: usize, +) void, + +get_ptr_fn: *const fn ( + storage: *anyopaque, + idx: usize, +) ?*anyopaque, + +deinit_fn: *const fn ( + storage: *anyopaque, + alloc: std.mem.Allocator, +) void, + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn init( + comptime T: type, + alloc: std.mem.Allocator, +) !Self { + const typed = try alloc.create(TypedColumn(T)); + + typed.* = TypedColumn(T).init(); + + return .{ + .storage = typed, + + .type_id = TypeID(T), + + .elem_size = @sizeOf(T), + .alignment = @alignOf(T), + + .alloc = alloc, + + .append_fn = struct { + fn f( + storage: *anyopaque, + value: *const anyopaque, + allocator: std.mem.Allocator, + ) !void { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + try col.append(value, allocator); + } + }.f, + + .swap_remove_fn = struct { + fn f( + storage: *anyopaque, + idx: usize, + ) void { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + col.swapRemove(idx); + } + }.f, + + .get_ptr_fn = struct { + fn f( + storage: *anyopaque, + idx: usize, + ) ?*anyopaque { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + if (idx >= col.items.items.len) { + return null; + } + return &col.items.items[idx]; + } + }.f, + + .deinit_fn = struct { + fn f( + storage: *anyopaque, + allocator: std.mem.Allocator, + ) void { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + col.deinit(allocator); + allocator.destroy(col); + } + }.f, + }; +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn deinit(self: *Self) void { + self.deinit_fn( + self.storage, + self.alloc, + ); +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn append( + self: *Self, + value: *const anyopaque, +) !void { + try self.append_fn( + self.storage, + value, + self.alloc, + ); +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn swapRemove( + self: *Self, + idx: usize, +) void { + self.swap_remove_fn( + self.storage, + idx, + ); +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn getPtr( + self: *Self, + idx: usize, +) ?*anyopaque { + return self.get_ptr_fn( + self.storage, + idx, + ); +} diff --git a/src/world/column_typed.zig b/src/world/column_typed.zig new file mode 100644 index 0000000..844453a --- /dev/null +++ b/src/world/column_typed.zig @@ -0,0 +1,61 @@ +const std = @import("std"); + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn TypedColumn(comptime T: type) type { + return struct { + const Self = @This(); + + // + // FIELDS + // + + items: std.ArrayList(T), + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn init() @This() { + return .{ + .items = .empty, + }; + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn deinit( + self: *Self, + alloc: std.mem.Allocator, + ) void { + self.items.deinit(alloc); + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn append( + self: *Self, + value: *const anyopaque, + alloc: std.mem.Allocator, + ) !void { + const typed: *const T = @ptrCast(@alignCast(value)); + try self.items.append(alloc, typed.*); + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn swapRemove( + self: *Self, + idx: usize, + ) void { + _ = self.items.swapRemove(idx); + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn getPtr( + self: *Self, + idx: usize, + ) *anyopaque { + return &self.items.items[idx]; + } + }; +} diff --git a/src/world/entity_record.zig b/src/world/entity_record.zig new file mode 100644 index 0000000..b2ef352 --- /dev/null +++ b/src/world/entity_record.zig @@ -0,0 +1,2 @@ +archetype_idx: usize, +row: usize, diff --git a/src/world/query.zig b/src/world/query.zig new file mode 100644 index 0000000..2b9d407 --- /dev/null +++ b/src/world/query.zig @@ -0,0 +1,140 @@ +const std = @import("std"); +const World = @import("world.zig"); +const Archetype = @import("archetype.zig"); +const QueryFilters = @import("query_filters.zig"); +const TypeID = @import("../template/type_id.zig").of; + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn Query( + comptime Result: type, + comptime Filters: QueryFilters, +) type { + return struct { + const Self = @This(); + + const result_fields = std.meta.fields(Result); + + // + // FIELDS + // + + world: *World, + arch_cursor: usize, + row_cursor: usize, + + current_arch: ?*Archetype, + current_row: usize, + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn init(world: *World) Self { + return .{ + .world = world, + .arch_cursor = 0, + .row_cursor = 0, + + .current_arch = null, + .current_row = 0, + }; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + fn matches(arch: *Archetype) bool { + // --- REQUIRED COMPONENTS --- + inline for (result_fields) |field| { + // TODO: Clean this shit up + const FieldT = field.type; + const is_optional = @typeInfo(FieldT) == .optional; + + const PtrT = if (is_optional) @typeInfo(FieldT).optional.child else FieldT; + const T = std.meta.Child(PtrT); + + if (!arch.type_map.contains(TypeID(T))) { + // --- SKIP OPTIONALS --- + if (!is_optional) { + return false; + } + } + } + + // --- WITH --- + inline for (Filters.with) |T| { + if (!arch.type_map.contains(TypeID(T))) { + return false; + } + } + + // --- WITHOUT --- + inline for (Filters.without) |T| { + if (arch.type_map.contains(TypeID(T))) { + return false; + } + } + + return true; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn next(self: *Self) ?Result { + while (self.arch_cursor < self.world.archetypes.items.len) { + var arch = &self.world.archetypes.items[self.arch_cursor]; + + // --- SKIP ARCHETYPES --- + if (!matches(arch)) { + self.arch_cursor += 1; + self.row_cursor = 0; + continue; + } + + // --- END ROWS --- + if (self.row_cursor >= arch.entities.items.len) { + self.arch_cursor += 1; + self.row_cursor = 0; + continue; + } + + const row = self.row_cursor; + + self.row_cursor += 1; + self.current_arch = arch; + self.current_row = row; + + var result: Result = undefined; + + inline for (result_fields) |field| { + // TODO: Clean this shit up + const FieldT = field.type; + + const is_optional = @typeInfo(FieldT) == .optional; + + const PtrT = if (is_optional) @typeInfo(FieldT).optional.child else FieldT; + + const T = std.meta.Child(PtrT); + + const ptr = arch.get(T, row); + + if (is_optional) { + @field(result, field.name) = ptr; + } else { + @field(result, field.name) = ptr orelse return null; + } + } + + return result; + } + + return null; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn entity(self: *const Self) u64 { + return self.current_arch.?.entities.items[ + self.current_row + ]; + } + }; +} diff --git a/src/world/query_filters.zig b/src/world/query_filters.zig new file mode 100644 index 0000000..4f28ecd --- /dev/null +++ b/src/world/query_filters.zig @@ -0,0 +1,2 @@ +with: []const type = &.{}, +without: []const type = &.{}, diff --git a/src/world/root.zig b/src/world/root.zig new file mode 100644 index 0000000..e6a9653 --- /dev/null +++ b/src/world/root.zig @@ -0,0 +1,2 @@ +pub const World = @import("world.zig"); +pub const Archetype = @import("archetype.zig"); diff --git a/src/world/signature.zig b/src/world/signature.zig new file mode 100644 index 0000000..17e98ad --- /dev/null +++ b/src/world/signature.zig @@ -0,0 +1,26 @@ +// --- LIBS --- +const std = @import("std"); +const root = @import("../app.zig"); +const TypeID = @import("../template/type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +ids: []u64, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn eql(a: Self, b: Self) bool { + return std.mem.eql(u64, a.ids, b.ids); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn hash(self: Self) u64 { + return std.hash.Wyhash.hash( + root.options.seed, + std.mem.sliceAsBytes(self.ids), + ); +} diff --git a/src/world/world.zig b/src/world/world.zig new file mode 100644 index 0000000..cdeb9a2 --- /dev/null +++ b/src/world/world.zig @@ -0,0 +1,217 @@ +// --- LIBS --- +const std = @import("std"); +const Archetype = @import("archetype.zig"); +const Signature = @import("signature.zig"); +const EntityRecord = @import("entity_record.zig"); +const QueryFilters = @import("query_filters.zig"); +const Query = @import("query.zig").Query; +const TypeID = @import("../template/type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +archetypes: std.ArrayList(Archetype), +entity_map: std.AutoHashMap(u64, EntityRecord), +signature_map: std.AutoHashMap(u64, usize), +next_entity: u64, +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .archetypes = .empty, + .entity_map = .init(alloc), + .signature_map = .init(alloc), + .next_entity = 1, + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + // --- ARCHETYPES --- + for (self.archetypes.items) |*arch| { + arch.deinit(); + } + self.entity_map.deinit(); + self.archetypes.deinit(self.alloc); + self.signature_map.deinit(); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn spawn( + self: *Self, + components: anytype, +) !void { + _ = try self.spawnEntity(components); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn despawn( + self: *Self, + entity: u64, +) void { + const record = self.entity_map.get(entity) orelse return; + if (self.archetypes.items[record.archetype_idx].remove(record.row)) |moved| { + self.entity_map.getPtr(moved).?.row = record.row; + } + _ = self.entity_map.remove(entity); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn spawnEntity( + self: *Self, + components: anytype, +) !u64 { + const Components = @TypeOf(components); + + const fields = std.meta.fields(Components); + + // --- SIGNATURE --- + var ids: [fields.len]u64 = undefined; + inline for (fields, 0..) |field, i| { + const component = @field( + components, + field.name, + ); + ids[i] = TypeID(@TypeOf(component)); + } + + // --- SORT TYPE ID'S --- + std.mem.sort( + u64, + &ids, + {}, + std.sort.asc(u64), + ); + + const signature_hash = std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(&ids)); + + // --- FIND OR CREATE ARCHETYPE --- + const archetype_idx = + self.signature_map.get(signature_hash) orelse blk: { + const sig_ids = try self.alloc.dupe(u64, &ids); + + var arch: Archetype = .init(.{ .ids = sig_ids }, self.alloc); + + // --- NEW COLUMNS --- + inline for (fields) |field| { + const component = @field(components, field.name); + try arch.addColumn(@TypeOf(component)); + } + + // --- ADD ARCHETYPE --- + try self.archetypes.append( + self.alloc, + arch, + ); + + // --- ADD SIGNATURE --- + const idx = self.archetypes.items.len - 1; + try self.signature_map.put( + signature_hash, + idx, + ); + + break :blk idx; + }; + + // --- NEW ENTITY --- + const entity = self.next_entity; + self.next_entity += 1; + + // --- ADD ROW --- + var arch = &self.archetypes.items[archetype_idx]; + + const row = arch.entities.items.len; + + try arch.add( + entity, + components, + ); + + // --- STORE ENTITY LOCATION --- + try self.entity_map.put(entity, .{ + .archetype_idx = archetype_idx, + .row = row, + }); + + return entity; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +fn createEntity(self: *Self) u64 { + const entity = self.next_entity; + self.next_entity += 1; + return entity; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +fn createArchetype( + self: *Self, + comptime Components: type, +) !usize { + const fields = + std.meta.fields(Components); + + var ids = try self.alloc.alloc( + u64, + fields.len, + ); + + inline for (fields, 0..) |field, i| { + ids[i] = TypeID(field.type); + } + + const sig: Signature = .{ + .ids = ids, + }; + + var arch: Archetype = .init( + sig, + self.alloc, + ); + + inline for (fields) |field| { + try arch.addColumn( + field.type, + ); + } + + try self.archetypes.append( + self.alloc, + arch, + ); + + return self.archetypes.items.len - 1; +} + +/// ---------------------------------------------------- +/// Example: +/// ``` +/// var query = world.query(struct { +/// player: ?*Player, +/// pos: *Position, +/// vel: *Velocity, +/// }, .{}); +/// while (query.next()) |entity| { +/// if (entity.player) |player| {} +/// } +/// ``` +/// ---------------------------------------------------- +pub fn query( + self: *Self, + comptime Result: type, + comptime Filters: QueryFilters, +) Query(Result, Filters) { + return .init(self); +}