implement snake enemy
This commit is contained in:
parent
c9543c72ad
commit
11be4f21ac
22
build.zig
22
build.zig
@ -13,6 +13,7 @@ pub fn build(b: *std.Build) !void {
|
||||
const has_console = b.option(bool, "console", "Show console (Window only)") orelse (optimize == .Debug);
|
||||
|
||||
const isWasm = target.result.cpu.arch.isWasm();
|
||||
const isWindows = target.result.os.tag == .windows;
|
||||
|
||||
if (isWasm) {
|
||||
has_tracy = false;
|
||||
@ -25,7 +26,12 @@ pub fn build(b: *std.Build) !void {
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, .with_sokol_imgui = has_imgui, .vulkan = true });
|
||||
const dep_sokol = b.dependency("sokol", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.with_sokol_imgui = has_imgui,
|
||||
.vulkan = if (isWasm or isWindows) null else true
|
||||
});
|
||||
mod_main.linkLibrary(dep_sokol.artifact("sokol_clib"));
|
||||
mod_main.addImport("sokol", dep_sokol.module("sokol"));
|
||||
|
||||
@ -62,7 +68,8 @@ pub fn build(b: *std.Build) !void {
|
||||
{
|
||||
var cflags_buffer: [64][]const u8 = undefined;
|
||||
var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer);
|
||||
switch (sokol.resolveSokolBackend(.vulkan, target.result)) {
|
||||
const backend: sokol.SokolBackend = if (isWasm or isWindows) .auto else .vulkan;
|
||||
switch (sokol.resolveSokolBackend(backend, target.result)) {
|
||||
.d3d11 => try cflags.appendBounded("-DSOKOL_D3D11"),
|
||||
.metal => try cflags.appendBounded("-DSOKOL_METAL"),
|
||||
.gl => try cflags.appendBounded("-DSOKOL_GLCORE"),
|
||||
@ -118,8 +125,8 @@ fn buildNative(b: *std.Build, name: []const u8, mod: *std.Build.Module, has_cons
|
||||
.root_source_file = b.path("tools/png-to-icon.zig"),
|
||||
}),
|
||||
});
|
||||
const dep_stb_image = b.dependency("stb_image", .{});
|
||||
png_to_icon_tool.root_module.addImport("stb_image", dep_stb_image.module("stb_image"));
|
||||
const dep_stb = b.dependency("stb", .{});
|
||||
png_to_icon_tool.root_module.addImport("stb_image", dep_stb.module("stb_image"));
|
||||
|
||||
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
|
||||
png_to_icon_step.addFileArg(b.path("src/assets/icon.png"));
|
||||
@ -209,7 +216,11 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) !void {
|
||||
|
||||
const dep_emsdk = opts.dep_sokol.builder.dependency("emsdk", .{});
|
||||
|
||||
patchWasmIncludeDirs(opts.mod_main, dep_emsdk.path("upstream/emscripten/cache/sysroot/include"), &(opts.dep_sokol.artifact("sokol_clib").step));
|
||||
patchWasmIncludeDirs(
|
||||
opts.mod_main,
|
||||
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
|
||||
&(opts.dep_sokol.artifact("sokol_clib").step)
|
||||
);
|
||||
|
||||
// create a build step which invokes the Emscripten linker
|
||||
const link_step = try sokol.emLinkStep(b, .{
|
||||
@ -221,6 +232,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) !void {
|
||||
.use_emmalloc = true,
|
||||
.use_filesystem = false,
|
||||
.shell_file_path = b.path("src/engine/shell.html"),
|
||||
.extra_args = &.{ "-sASSERTIONS=1", "-sALLOW_MEMORY_GROWTH=1" }
|
||||
});
|
||||
// attach to default target
|
||||
b.getInstallStep().dependOn(&link_step.step);
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Assets = @import("./assets.zig");
|
||||
const State = @import("./state.zig");
|
||||
|
||||
const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
|
||||
|
||||
const Engine = @import("./engine/root.zig");
|
||||
const Nanoseconds = Engine.Nanoseconds;
|
||||
const Vec2 = Engine.Vec2;
|
||||
@ -87,20 +90,54 @@ const Enemy = struct {
|
||||
kinetic: Kinetic,
|
||||
speed: f32,
|
||||
size: f32,
|
||||
target: ?Vec2 = null,
|
||||
distance_to_target: ?f32 = null,
|
||||
|
||||
dead: bool = false
|
||||
dead: bool = false,
|
||||
|
||||
const List = GenerationalArrayList(Enemy);
|
||||
const Id = List.Id;
|
||||
};
|
||||
|
||||
pub const Manager = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
kind: union(enum) {
|
||||
snake: struct {
|
||||
enemies: std.ArrayList(Enemy.Id) = .empty,
|
||||
},
|
||||
},
|
||||
|
||||
pub fn deinit(self: Manager) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
const List = GenerationalArrayList(Manager);
|
||||
const Id = List.Id;
|
||||
};
|
||||
|
||||
const Wave = struct {
|
||||
kind: Kind,
|
||||
started_at: Nanoseconds,
|
||||
duration: Nanoseconds,
|
||||
enemies_spawned: u32,
|
||||
total_enemies: u32,
|
||||
|
||||
min_snake_length: u32,
|
||||
max_snake_length: u32,
|
||||
|
||||
const Kind = enum {
|
||||
regular,
|
||||
snake
|
||||
};
|
||||
|
||||
const Info = struct {
|
||||
kind: Kind,
|
||||
enemies: u32,
|
||||
duration_s: u32,
|
||||
starts_at_s: u32,
|
||||
|
||||
min_snake_length: u32 = 3,
|
||||
max_snake_length: u32 = 5,
|
||||
};
|
||||
};
|
||||
|
||||
@ -117,10 +154,19 @@ const world_size = Vec2.init(20 * 16, 15 * 16);
|
||||
const invincibility_duration_s = 0.5;
|
||||
const pickup_spawn_duration_s = Range.init(1, 5);
|
||||
const wave_infos = [_]Wave.Info{
|
||||
.{
|
||||
.enemies = 10,
|
||||
.duration_s = 10,
|
||||
.starts_at_s = 0
|
||||
// .{
|
||||
// .kind = .regular,
|
||||
// .enemies = 10,
|
||||
// .duration_s = 10,
|
||||
// .starts_at_s = 0
|
||||
// },
|
||||
Wave.Info{
|
||||
.kind = .snake,
|
||||
.enemies = 1,
|
||||
.duration_s = 1,
|
||||
.starts_at_s = 0,
|
||||
.min_snake_length = 5,
|
||||
.max_snake_length = 5
|
||||
}
|
||||
};
|
||||
|
||||
@ -128,7 +174,8 @@ gpa: Allocator,
|
||||
assets: *Assets,
|
||||
rng: RNGState,
|
||||
|
||||
enemies: std.ArrayList(Enemy) = .empty,
|
||||
managers: Manager.List = .empty,
|
||||
enemies: Enemy.List = .empty,
|
||||
|
||||
player: Player = .{},
|
||||
bullets: std.ArrayList(Bullet) = .empty,
|
||||
@ -166,6 +213,11 @@ pub fn init(gpa: Allocator, seed: u64, assets: *Assets, state: State) CombatScre
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CombatScreen) void {
|
||||
var manager_iter = self.managers.iterator();
|
||||
while (manager_iter.nextItem()) |manager| {
|
||||
manager.deinit();
|
||||
}
|
||||
|
||||
self.bullets.deinit(self.gpa);
|
||||
self.enemies.deinit(self.gpa);
|
||||
self.waves.deinit(self.gpa);
|
||||
@ -173,9 +225,19 @@ pub fn deinit(self: *CombatScreen) void {
|
||||
self.spawned_waves.deinit(self.gpa);
|
||||
self.lasers.deinit(self.gpa);
|
||||
self.bombs.deinit(self.gpa);
|
||||
self.managers.deinit(self.gpa);
|
||||
}
|
||||
|
||||
pub fn spawnEnemy(self: *CombatScreen) !void {
|
||||
const EnemyOptions = struct {
|
||||
pos: ?Vec2 = null
|
||||
};
|
||||
|
||||
pub fn spawnEnemy(self: *CombatScreen, opts: EnemyOptions) !Enemy.Id {
|
||||
|
||||
var pos: Vec2 = undefined;
|
||||
if (opts.pos) |opts_pos| {
|
||||
pos = opts_pos;
|
||||
} else {
|
||||
const spawn_area_margin = 20;
|
||||
const spawn_area_size = 10;
|
||||
|
||||
@ -208,11 +270,12 @@ pub fn spawnEnemy(self: *CombatScreen) !void {
|
||||
|
||||
const rand = self.rng.random();
|
||||
const spawn_area = spawn_areas[rand.uintLessThan(usize, spawn_areas.len)];
|
||||
const pos = Vec2.initRandomRect(rand, spawn_area);
|
||||
pos = Vec2.initRandomRect(rand, spawn_area);
|
||||
}
|
||||
|
||||
try self.enemies.append(self.gpa, .{
|
||||
return try self.enemies.insert(self.gpa, Enemy{
|
||||
.kinetic = .{ .pos = pos },
|
||||
.speed = 10,
|
||||
.speed = 50,
|
||||
.size = 20
|
||||
});
|
||||
}
|
||||
@ -255,10 +318,13 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
|
||||
try self.spawned_waves.append(self.gpa, i);
|
||||
try self.waves.append(self.gpa, .{
|
||||
.kind = wave_info.kind,
|
||||
.started_at = self.wave_timer,
|
||||
.duration = @as(u64, wave_info.duration_s) * std.time.ns_per_s,
|
||||
.enemies_spawned = 0,
|
||||
.total_enemies = wave_info.enemies
|
||||
.total_enemies = wave_info.enemies,
|
||||
.min_snake_length = wave_info.min_snake_length,
|
||||
.max_snake_length = wave_info.max_snake_length,
|
||||
});
|
||||
}
|
||||
|
||||
@ -275,7 +341,45 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
const expected_enemies: u32 = @intFromFloat(wave_total_enemies * percent_complete);
|
||||
|
||||
while (wave.enemies_spawned < expected_enemies) {
|
||||
try self.spawnEnemy();
|
||||
switch (wave.kind) {
|
||||
.regular => {
|
||||
_ = try self.spawnEnemy(.{});
|
||||
},
|
||||
.snake => {
|
||||
var arena = std.heap.ArenaAllocator.init(self.gpa);
|
||||
var enemies: std.ArrayList(Enemy.Id) =.empty;
|
||||
|
||||
assert(wave.min_snake_length >= 1);
|
||||
const rand = self.rng.random();
|
||||
const snake_length = wave.min_snake_length + rand.uintAtMost(u32, wave.max_snake_length - wave.min_snake_length);
|
||||
|
||||
const head_id = try self.spawnEnemy(.{ });
|
||||
try enemies.append(arena.allocator(), head_id);
|
||||
|
||||
const head = self.enemies.getAssumeExists(head_id);
|
||||
const head_pos = head.kinetic.pos;
|
||||
const world_center = world_size.divideScalar(2);
|
||||
const dir_to_center = world_center.sub(head_pos).normalized();
|
||||
|
||||
const gap = 20;
|
||||
for (0..snake_length) |i| {
|
||||
const tail_pos = head_pos.sub(dir_to_center.multiplyScalar(@floatFromInt(i * gap)));
|
||||
const tail_id = try self.spawnEnemy(.{
|
||||
.pos = tail_pos
|
||||
});
|
||||
try enemies.append(arena.allocator(), tail_id);
|
||||
}
|
||||
|
||||
_ = try self.managers.insert(self.gpa, Manager{
|
||||
.arena = arena,
|
||||
.kind = .{
|
||||
.snake = .{
|
||||
.enemies = enemies
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
wave.enemies_spawned += 1;
|
||||
}
|
||||
|
||||
@ -404,7 +508,8 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
|
||||
const bullet_rect = getCenteredRect(bullet.kinetic.pos, bullet.size);
|
||||
|
||||
for (self.enemies.items) |*enemy| {
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.nextItem()) |enemy| {
|
||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||
if (enemy_rect.hasOverlap(bullet_rect)) {
|
||||
enemy.dead = true;
|
||||
@ -431,7 +536,8 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
};
|
||||
const laser_quad = laser_line.getQuad(laser.size);
|
||||
|
||||
for (self.enemies.items) |*enemy| {
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.nextItem()) |enemy| {
|
||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||
if (laser_quad.isRectOverlap(enemy_rect)) {
|
||||
enemy.dead = true;
|
||||
@ -469,7 +575,8 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
if (frame.time_ns >= bomb.explode_at) {
|
||||
destroy = true;
|
||||
|
||||
for (self.enemies.items) |*enemy| {
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.nextItem()) |enemy| {
|
||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||
if (enemy_rect.checkCircleOverlap(bomb.kinetic.pos, bomb.explosion_radius)) {
|
||||
enemy.dead = true;
|
||||
@ -493,13 +600,76 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
}
|
||||
}
|
||||
|
||||
for (self.enemies.items) |*enemy| {
|
||||
const dir_to_player = self.player.kinetic.pos.sub(enemy.kinetic.pos).normalized();
|
||||
enemy.kinetic.vel = dir_to_player.multiplyScalar(50);
|
||||
enemy.kinetic.update(dt, .{});
|
||||
{
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.nextItem()) |enemy| {
|
||||
enemy.target = null;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var manager_iter = self.managers.iterator();
|
||||
while (manager_iter.next()) |tuple| {
|
||||
var destroy: bool = false;
|
||||
const manager_id = tuple.id;
|
||||
const manager = tuple.item;
|
||||
|
||||
switch (manager.kind) {
|
||||
.snake => |*snake| {
|
||||
var index: usize = 0;
|
||||
while (index < snake.enemies.items.len) {
|
||||
const enemy_id = snake.enemies.items[index];
|
||||
if (self.enemies.exists(enemy_id)) {
|
||||
index += 1;
|
||||
} else {
|
||||
_ = snake.enemies.orderedRemove(index);
|
||||
}
|
||||
}
|
||||
|
||||
for (1..snake.enemies.items.len) |i| {
|
||||
const enemy_id = snake.enemies.items[i];
|
||||
const enemy = self.enemies.getAssumeExists(enemy_id);
|
||||
const follow_enemy_id = snake.enemies.items[i-1];
|
||||
const follow_enemy = self.enemies.getAssumeExists(follow_enemy_id);
|
||||
|
||||
enemy.target = follow_enemy.kinetic.pos;
|
||||
enemy.distance_to_target = 20;
|
||||
}
|
||||
|
||||
if (snake.enemies.items.len <= 1) {
|
||||
destroy = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (destroy) {
|
||||
manager.deinit();
|
||||
self.managers.removeAssumeExists(manager_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.nextItem()) |enemy| {
|
||||
if (enemy.target == null) {
|
||||
enemy.target = self.player.kinetic.pos;
|
||||
}
|
||||
|
||||
var apply_vel = true;
|
||||
const to_target = enemy.target.?.sub(enemy.kinetic.pos);
|
||||
if (enemy.distance_to_target) |distance_to_target| {
|
||||
apply_vel = to_target.length() > distance_to_target;
|
||||
}
|
||||
|
||||
if (apply_vel) {
|
||||
const dir_to_target = to_target.normalized();
|
||||
enemy.kinetic.vel = dir_to_target.multiplyScalar(enemy.speed);
|
||||
} else {
|
||||
enemy.kinetic.vel = .zero;
|
||||
}
|
||||
|
||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||
|
||||
if (enemy_rect.hasOverlap(self.player.getRect())) {
|
||||
var is_invincible = false;
|
||||
if (self.player.invincible_until) |invincible_until| {
|
||||
@ -515,11 +685,13 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
}
|
||||
}
|
||||
|
||||
enemy.kinetic.update(dt, .{});
|
||||
frame.drawRectangle(.{
|
||||
.rect = enemy_rect,
|
||||
.color = rgb(20, 200, 20)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var index: usize = 0;
|
||||
@ -563,12 +735,10 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
}
|
||||
|
||||
{
|
||||
var i: usize = 0;
|
||||
while (i < self.enemies.items.len) {
|
||||
if (self.enemies.items[i].dead) {
|
||||
_ = self.enemies.swapRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.next()) |tuple| {
|
||||
if (tuple.item.dead) {
|
||||
self.enemies.removeAssumeExists(tuple.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -586,7 +756,7 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
frame.drawTextFormat(.init(10, 70), text_opts, "{?}", .{ self.player.gun });
|
||||
|
||||
result.player_died = (self.player.health == 0);
|
||||
if (self.enemies.items.len == 0 and self.waves.items.len == 0 and self.spawned_waves.items.len == wave_infos.len) {
|
||||
if (self.enemies.count == 0 and self.waves.items.len == 0 and self.spawned_waves.items.len == wave_infos.len) {
|
||||
result.player_finished = true;
|
||||
}
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ pub fn debug(self: *Game) !void {
|
||||
defer imgui.endWindow();
|
||||
|
||||
if (imgui.button("Spawn enemy")) {
|
||||
try self.combat_screen.spawnEnemy();
|
||||
_ = try self.combat_screen.spawnEnemy(.{});
|
||||
}
|
||||
|
||||
if (imgui.button("Restart")) {
|
||||
@ -148,6 +148,6 @@ pub fn debug(self: *Game) !void {
|
||||
|
||||
imgui.textFmt("Waves: {}\n", .{screen.waves.items.len});
|
||||
imgui.textFmt("Bullets: {}\n", .{screen.bullets.items.len});
|
||||
imgui.textFmt("Enemies: {}\n", .{screen.enemies.items.len});
|
||||
imgui.textFmt("Enemies: {}\n", .{screen.enemies.count});
|
||||
imgui.textFmt("Time until next pickup: {d:.2}s\n", .{@as(f32, @floatFromInt(time_left_til_pickup)) / std.time.ns_per_s});
|
||||
}
|
||||
|
||||
480
src/generational_array_list.zig
Normal file
480
src/generational_array_list.zig
Normal file
@ -0,0 +1,480 @@
|
||||
const std = @import("std");
|
||||
const tracy = @import("tracy");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Index = u24;
|
||||
const Generation = u8;
|
||||
|
||||
pub fn GenerationalArrayList(Item: type) type {
|
||||
assert(@bitSizeOf(Generation) % 8 == 0);
|
||||
assert(@bitSizeOf(Index) % 8 == 0);
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
items: [*]Item,
|
||||
generations: [*]Generation,
|
||||
unused: [*]u8,
|
||||
|
||||
len: u32,
|
||||
capacity: u32,
|
||||
|
||||
count: u32,
|
||||
|
||||
pub const empty = Self{
|
||||
.items = &[_]Item{},
|
||||
.generations = &[_]Generation{},
|
||||
.unused = &[_]u8{},
|
||||
.capacity = 0,
|
||||
.len = 0,
|
||||
.count = 0
|
||||
};
|
||||
|
||||
pub const Id = packed struct {
|
||||
generation: Generation,
|
||||
index: Index,
|
||||
|
||||
// TODO: Maybe `Id.Optional` type should be created to ensure .wrap() and .toOptional()
|
||||
pub const none = Id{
|
||||
.generation = std.math.maxInt(Generation),
|
||||
.index = std.math.maxInt(Index),
|
||||
};
|
||||
|
||||
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
if (self == Id.none) {
|
||||
try writer.print("Id({s}){{ .none }}", .{ @typeName(Item) });
|
||||
} else {
|
||||
try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Item), self.index, self.generation });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn asInt(self: Id) u32 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ItemWithId = struct {
|
||||
id: Id,
|
||||
item: *Item,
|
||||
};
|
||||
|
||||
pub const Iterator = struct {
|
||||
array_list: *Self,
|
||||
index: Index,
|
||||
|
||||
pub fn nextId(self: *Iterator) ?Id {
|
||||
while (self.index < self.array_list.len) {
|
||||
const index = self.index;
|
||||
self.index += 1;
|
||||
|
||||
// TODO: Inline the `byte_index` calculate for better speed.
|
||||
// Probably not needed. Idk
|
||||
if (self.array_list.isUnused(index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Id{
|
||||
.index = @intCast(index),
|
||||
.generation = self.array_list.generations[index]
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn nextItem(self: *Iterator) ?*Item {
|
||||
if (self.nextId()) |id| {
|
||||
return &self.array_list.items[id.index];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn next(self: *Iterator) ?ItemWithId {
|
||||
if (self.nextId()) |id| {
|
||||
return ItemWithId{
|
||||
.id = id,
|
||||
.item = &self.array_list.items[id.index]
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Metadata = extern struct {
|
||||
len: u32,
|
||||
count: u32
|
||||
};
|
||||
|
||||
fn divCeilGeneration(num: u32) u32 {
|
||||
return std.math.divCeil(u32, num, @bitSizeOf(Generation)) catch unreachable;
|
||||
}
|
||||
|
||||
fn divFloorGeneration(num: u32) u32 {
|
||||
return @divFloor(num, @bitSizeOf(Generation));
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacityPrecise(self: *Self, allocator: Allocator, new_capacity: u32) !void {
|
||||
if (new_capacity > std.math.maxInt(Index)) {
|
||||
return error.OutOfIndexSpace;
|
||||
}
|
||||
|
||||
// TODO: Shrinking is not supported
|
||||
assert(new_capacity >= self.capacity);
|
||||
|
||||
const unused_bit_array_len = divCeilGeneration(self.capacity);
|
||||
const new_unused_bit_array_len = divCeilGeneration(new_capacity);
|
||||
|
||||
// TODO: Handle allocation failure case
|
||||
const new_unused = try allocator.realloc(self.unused[0..unused_bit_array_len], new_unused_bit_array_len);
|
||||
const new_items = try allocator.realloc(self.items[0..self.capacity], new_capacity);
|
||||
const new_generations = try allocator.realloc(self.generations[0..self.capacity], new_capacity);
|
||||
|
||||
self.unused = new_unused.ptr;
|
||||
self.items = new_items.ptr;
|
||||
self.generations = new_generations.ptr;
|
||||
self.capacity = new_capacity;
|
||||
}
|
||||
|
||||
fn growCapacity(current: u32, minimum: u32) u32 {
|
||||
const init_capacity = @as(comptime_int, @max(1, std.atomic.cache_line / @sizeOf(Item)));
|
||||
|
||||
var new = current;
|
||||
while (true) {
|
||||
new +|= new / 2 + init_capacity;
|
||||
if (new >= minimum) {
|
||||
return new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacity(self: *Self, allocator: Allocator, new_capacity: u32) !void {
|
||||
if (self.capacity >= new_capacity) return;
|
||||
|
||||
const better_capacity = Self.growCapacity(self.capacity, new_capacity);
|
||||
try self.ensureTotalCapacityPrecise(allocator, better_capacity);
|
||||
}
|
||||
|
||||
pub fn clearRetainingCapacity(self: *Self) void {
|
||||
self.count = 0;
|
||||
self.len = 0;
|
||||
}
|
||||
|
||||
pub fn ensureUnusedCapacity(self: *Self, allocator: Allocator, unused_capacity: u32) !void {
|
||||
try self.ensureTotalCapacity(allocator, self.len + unused_capacity);
|
||||
}
|
||||
|
||||
fn findFirstUnused(self: *Self) ?Index {
|
||||
for (0..divCeilGeneration(self.len)) |byte_index| {
|
||||
if (self.unused[byte_index] != 0) {
|
||||
const found = @ctz(self.unused[byte_index]) + byte_index * @bitSizeOf(Generation);
|
||||
if (found < self.len) {
|
||||
return @intCast(found);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn markUnused(self: *Self, index: Index, unused: bool) void {
|
||||
assert(index < self.len);
|
||||
|
||||
const byte_index = divFloorGeneration(index);
|
||||
const bit_index = @mod(index, @bitSizeOf(Generation));
|
||||
const bit_flag = @as(u8, 1) << @intCast(bit_index);
|
||||
if (unused) {
|
||||
self.unused[byte_index] |= bit_flag;
|
||||
} else {
|
||||
self.unused[byte_index] &= ~bit_flag;
|
||||
}
|
||||
}
|
||||
|
||||
fn isUnused(self: *Self, index: Index) bool {
|
||||
assert(index < self.len);
|
||||
|
||||
const byte_index = divFloorGeneration(index);
|
||||
const bit_index = @mod(index, @bitSizeOf(Generation));
|
||||
const bit_flag = @as(u8, 1) << @intCast(bit_index);
|
||||
return (self.unused[byte_index] & bit_flag) != 0;
|
||||
}
|
||||
|
||||
pub fn insertUndefined(self: *Self, allocator: Allocator) !Id {
|
||||
var unused_index: Index = undefined;
|
||||
|
||||
if (self.findFirstUnused()) |index| {
|
||||
unused_index = index;
|
||||
} else {
|
||||
try self.ensureUnusedCapacity(allocator, 1);
|
||||
|
||||
unused_index = @intCast(self.len);
|
||||
self.len += 1;
|
||||
self.generations[unused_index] = 0;
|
||||
}
|
||||
|
||||
self.markUnused(unused_index, false);
|
||||
self.count += 1;
|
||||
|
||||
const id = Id{
|
||||
.index = @intCast(unused_index),
|
||||
.generation = self.generations[unused_index]
|
||||
};
|
||||
|
||||
assert(id != Id.none);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn insert(self: *Self, allocator: Allocator, item: Item) !Id {
|
||||
const id = try self.insertUndefined(allocator);
|
||||
|
||||
const new_item_ptr = self.getAssumeExists(id);
|
||||
new_item_ptr.* = item;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn exists(self: *Self, id: Id) bool {
|
||||
if (id.index >= self.len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.isUnused(id.index)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.generations[id.index] != id.generation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn removeAssumeExists(self: *Self, id: Id) void {
|
||||
assert(self.exists(id));
|
||||
|
||||
self.markUnused(id.index, true);
|
||||
// TODO: Maybe a log should be shown when a wrap-around occurs?
|
||||
self.generations[id.index] +%= 1;
|
||||
self.count -= 1;
|
||||
}
|
||||
|
||||
pub fn remove(self: *Self, id: Id) bool {
|
||||
if (!self.exists(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.removeAssumeExists(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getAssumeExists(self: *Self, id: Id) *Item {
|
||||
assert(self.exists(id));
|
||||
|
||||
return &self.items[id.index];
|
||||
}
|
||||
|
||||
pub fn get(self: *Self, id: Id) ?*Item {
|
||||
if (self.exists(id)) {
|
||||
return self.getAssumeExists(id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterator(self: *Self) Iterator {
|
||||
return Iterator{
|
||||
.array_list = self,
|
||||
.index = 0
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||
allocator.free(self.unused[0..divCeilGeneration(self.capacity)]);
|
||||
allocator.free(self.generations[0..self.capacity]);
|
||||
allocator.free(self.items[0..self.capacity]);
|
||||
}
|
||||
|
||||
pub fn clone(self: *Self, allocator: Allocator) !Self {
|
||||
const items = try allocator.dupe(Item, self.items[0..self.capacity]);
|
||||
errdefer allocator.free(items);
|
||||
|
||||
const generations = try allocator.dupe(Generation, self.generations[0..self.capacity]);
|
||||
errdefer allocator.free(generations);
|
||||
|
||||
const unused = try allocator.dupe(u8, self.unused[0..divCeilGeneration(self.capacity)]);
|
||||
errdefer allocator.free(unused);
|
||||
|
||||
return Self{
|
||||
.items = items.ptr,
|
||||
.generations = generations.ptr,
|
||||
.unused = unused.ptr,
|
||||
.len = self.len,
|
||||
.count = self.count,
|
||||
.capacity = self.capacity
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getMetadata(self: *Self) Metadata {
|
||||
return Metadata{
|
||||
.len = self.len,
|
||||
.count = self.count
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *Self, writer: *std.Io.Writer, endian: std.builtin.Endian) !void {
|
||||
const zone = tracy.beginZone(@src(), .{ .name = "gen array list write" });
|
||||
defer zone.end();
|
||||
|
||||
try writer.writeSliceEndian(Item, self.items[0..self.len], endian);
|
||||
try writer.writeSliceEndian(Generation, self.generations[0..self.len], endian);
|
||||
try writer.writeAll(self.unused[0..divCeilGeneration(self.len)]);
|
||||
}
|
||||
|
||||
pub fn read(
|
||||
self: *Self,
|
||||
allocator: Allocator,
|
||||
reader: *std.Io.Reader,
|
||||
endian: std.builtin.Endian,
|
||||
metadata: Metadata
|
||||
) !void {
|
||||
const zone = tracy.beginZone(@src(), .{ .name = "gen array list read" });
|
||||
defer zone.end();
|
||||
|
||||
try self.ensureTotalCapacity(allocator, metadata.len);
|
||||
|
||||
try reader.readSliceEndian(Item, self.items[0..metadata.len], endian);
|
||||
try reader.readSliceEndian(Generation, self.generations[0..metadata.len], endian);
|
||||
try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]);
|
||||
|
||||
self.len = metadata.len;
|
||||
self.count = metadata.count;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const TestArray = GenerationalArrayList(u32);
|
||||
|
||||
test "insert & remove" {
|
||||
const expect = std.testing.expect;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
const id1 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id1));
|
||||
try expect(array_list.remove(id1));
|
||||
try expect(!array_list.exists(id1));
|
||||
try expect(!array_list.remove(id1));
|
||||
|
||||
const id2 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id2));
|
||||
try expect(!array_list.exists(id1));
|
||||
try expect(id1.index == id2.index);
|
||||
}
|
||||
|
||||
test "generation wrap around" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
// Grow array list so that at least 1 slot exists
|
||||
const id1 = try array_list.insert(gpa, 10);
|
||||
array_list.removeAssumeExists(id1);
|
||||
|
||||
// Artificially increase generation count
|
||||
array_list.generations[id1.index] = std.math.maxInt(Generation);
|
||||
|
||||
// Check if generation wraps around
|
||||
const id2 = try array_list.insert(gpa, 10);
|
||||
array_list.removeAssumeExists(id2);
|
||||
try expectEqual(id1.index, id2.index);
|
||||
try expectEqual(0, array_list.generations[id1.index]);
|
||||
}
|
||||
|
||||
test "iterator" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
// Create array which has a hole
|
||||
const id1 = try array_list.insert(gpa, 1);
|
||||
const id2 = try array_list.insert(gpa, 2);
|
||||
const id3 = try array_list.insert(gpa, 3);
|
||||
|
||||
array_list.removeAssumeExists(id2);
|
||||
|
||||
var iter = array_list.iterator();
|
||||
try expectEqual(
|
||||
TestArray.ItemWithId{
|
||||
.id = id1,
|
||||
.item = array_list.getAssumeExists(id1)
|
||||
},
|
||||
iter.next().?
|
||||
);
|
||||
try expectEqual(
|
||||
TestArray.ItemWithId{
|
||||
.id = id3,
|
||||
.item = array_list.getAssumeExists(id3)
|
||||
},
|
||||
iter.next().?
|
||||
);
|
||||
try expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test "read & write" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list1: TestArray = .empty;
|
||||
defer array_list1.deinit(gpa);
|
||||
|
||||
var array_list2: TestArray = .empty;
|
||||
defer array_list2.deinit(gpa);
|
||||
|
||||
const id1 = try array_list1.insert(gpa, 1);
|
||||
const id2 = try array_list1.insert(gpa, 2);
|
||||
const id3 = try array_list1.insert(gpa, 3);
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&buffer);
|
||||
const native_endian = builtin.cpu.arch.endian();
|
||||
|
||||
try array_list1.write(&writer, native_endian);
|
||||
|
||||
var reader = std.Io.Reader.fixed(writer.buffered());
|
||||
try array_list2.read(gpa, &reader, native_endian, array_list1.getMetadata());
|
||||
|
||||
try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*);
|
||||
try expectEqual(array_list1.getAssumeExists(id2).*, array_list2.getAssumeExists(id2).*);
|
||||
try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*);
|
||||
try expectEqual(array_list1.count, array_list2.count);
|
||||
}
|
||||
|
||||
test "clear retaining capacity" {
|
||||
const expect = std.testing.expect;
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
const id1 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id1));
|
||||
array_list.clearRetainingCapacity();
|
||||
|
||||
const id2 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id2));
|
||||
|
||||
try expectEqual(id1, id2);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user