162 lines
5.5 KiB
Zig
162 lines
5.5 KiB
Zig
const std = @import("std");
|
|
const registry = @import("registry.zig");
|
|
|
|
/// generates versioned "handles" (https://floooh.github.io/2018/06/17/handles-vs-pointers.html)
|
|
/// you choose the type of the handle (aka its size) and how much of that goes to the index and the version.
|
|
/// the bitsize of version + id must equal the handle size.
|
|
pub fn Handles(comptime HandleType: type, comptime IndexType: type, comptime VersionType: type) type {
|
|
std.debug.assert(@typeInfo(HandleType) == .Int and std.meta.Int(.unsigned, @bitSizeOf(HandleType)) == HandleType);
|
|
std.debug.assert(@typeInfo(IndexType) == .Int and std.meta.Int(.unsigned, @bitSizeOf(IndexType)) == IndexType);
|
|
std.debug.assert(@typeInfo(VersionType) == .Int and std.meta.Int(.unsigned, @bitSizeOf(VersionType)) == VersionType);
|
|
|
|
if (@bitSizeOf(IndexType) + @bitSizeOf(VersionType) != @bitSizeOf(HandleType))
|
|
@compileError("IndexType and VersionType must sum to HandleType's bit count");
|
|
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
handles: []HandleType,
|
|
append_cursor: IndexType = 0,
|
|
last_destroyed: ?IndexType = null,
|
|
allocator: std.mem.Allocator,
|
|
|
|
const invalid_id = std.math.maxInt(IndexType);
|
|
|
|
pub const Iterator = struct {
|
|
hm: Self,
|
|
index: usize = 0,
|
|
|
|
pub fn init(hm: Self) @This() {
|
|
return .{ .hm = hm };
|
|
}
|
|
|
|
pub fn next(self: *@This()) ?HandleType {
|
|
if (self.index == self.hm.append_cursor) return null;
|
|
|
|
for (self.hm.handles[self.index..self.hm.append_cursor]) |h| {
|
|
self.index += 1;
|
|
if (self.hm.alive(h)) {
|
|
return h;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
pub fn init(allocator: std.mem.Allocator) Self {
|
|
return initWithCapacity(allocator, 32);
|
|
}
|
|
|
|
pub fn initWithCapacity(allocator: std.mem.Allocator, capacity: usize) Self {
|
|
return Self{
|
|
.handles = allocator.alloc(HandleType, capacity) catch unreachable,
|
|
.allocator = allocator,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: Self) void {
|
|
self.allocator.free(self.handles);
|
|
}
|
|
|
|
pub fn extractId(_: Self, handle: HandleType) IndexType {
|
|
return @truncate(IndexType, handle & registry.entity_traits.entity_mask);
|
|
}
|
|
|
|
pub fn extractVersion(_: Self, handle: HandleType) VersionType {
|
|
return @truncate(VersionType, handle >> registry.entity_traits.entity_shift);
|
|
}
|
|
|
|
fn forge(id: IndexType, version: VersionType) HandleType {
|
|
return id | @as(HandleType, version) << registry.entity_traits.entity_shift;
|
|
}
|
|
|
|
pub fn create(self: *Self) HandleType {
|
|
if (self.last_destroyed == null) {
|
|
// ensure capacity and grow if needed
|
|
if (self.handles.len - 1 == self.append_cursor) {
|
|
self.handles = self.allocator.realloc(self.handles, self.handles.len * 2) catch unreachable;
|
|
}
|
|
|
|
const id = self.append_cursor;
|
|
const handle = forge(self.append_cursor, 0);
|
|
self.handles[id] = handle;
|
|
|
|
self.append_cursor += 1;
|
|
return handle;
|
|
}
|
|
|
|
const version = self.extractVersion(self.handles[self.last_destroyed.?]);
|
|
const destroyed_id = self.extractId(self.handles[self.last_destroyed.?]);
|
|
|
|
const handle = forge(self.last_destroyed.?, version);
|
|
self.handles[self.last_destroyed.?] = handle;
|
|
|
|
self.last_destroyed = if (destroyed_id == invalid_id) null else destroyed_id;
|
|
|
|
return handle;
|
|
}
|
|
|
|
pub fn remove(self: *Self, handle: HandleType) !void {
|
|
const id = self.extractId(handle);
|
|
if (id > self.append_cursor or self.handles[id] != handle)
|
|
return error.RemovedInvalidHandle;
|
|
|
|
const next_id = self.last_destroyed orelse invalid_id;
|
|
if (next_id == id) return error.ExhaustedEntityRemoval;
|
|
|
|
const version = self.extractVersion(handle);
|
|
self.handles[id] = forge(next_id, version +% 1);
|
|
|
|
self.last_destroyed = id;
|
|
}
|
|
|
|
pub fn alive(self: Self, handle: HandleType) bool {
|
|
const id = self.extractId(handle);
|
|
return id < self.append_cursor and self.handles[id] == handle;
|
|
}
|
|
|
|
pub fn iterator(self: Self) Iterator {
|
|
return Iterator.init(self);
|
|
}
|
|
};
|
|
}
|
|
|
|
test "handles" {
|
|
var hm = Handles(u32, u20, u12).init(std.testing.allocator);
|
|
defer hm.deinit();
|
|
|
|
const e0 = hm.create();
|
|
const e1 = hm.create();
|
|
const e2 = hm.create();
|
|
|
|
std.debug.assert(hm.alive(e0));
|
|
std.debug.assert(hm.alive(e1));
|
|
std.debug.assert(hm.alive(e2));
|
|
|
|
hm.remove(e1) catch unreachable;
|
|
std.debug.assert(!hm.alive(e1));
|
|
|
|
try std.testing.expectError(error.RemovedInvalidHandle, hm.remove(e1));
|
|
|
|
var e_tmp = hm.create();
|
|
std.debug.assert(hm.alive(e_tmp));
|
|
|
|
hm.remove(e_tmp) catch unreachable;
|
|
std.debug.assert(!hm.alive(e_tmp));
|
|
|
|
hm.remove(e0) catch unreachable;
|
|
std.debug.assert(!hm.alive(e0));
|
|
|
|
hm.remove(e2) catch unreachable;
|
|
std.debug.assert(!hm.alive(e2));
|
|
|
|
e_tmp = hm.create();
|
|
std.debug.assert(hm.alive(e_tmp));
|
|
|
|
e_tmp = hm.create();
|
|
std.debug.assert(hm.alive(e_tmp));
|
|
|
|
e_tmp = hm.create();
|
|
std.debug.assert(hm.alive(e_tmp));
|
|
}
|