implement snake enemy

This commit is contained in:
Rokas Puzonas 2026-01-31 21:56:02 +02:00
parent c9543c72ad
commit 11be4f21ac
4 changed files with 736 additions and 74 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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});
}

View 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);
}