diff --git a/src/nav/controller.zig b/src/nav/controller.zig index 4854f84..3a5554c 100644 --- a/src/nav/controller.zig +++ b/src/nav/controller.zig @@ -97,6 +97,10 @@ const MAX_HORIZONTAL_DISTANCE: f32 = 78.0; /// How far (px) a `MovementLink` target may sit from an actual node and /// still resolve to it — authored offsets are hand-measured from art. pub const LINK_TOLERANCE: f32 = 4.0; +/// How close a node must be to a remembered waypoint to resume from it +/// after a rebuild (#60), px. Waypoints are node positions, so only a +/// node that moved or went away misses. +const RESUME_TOLERANCE: f32 = 1.0; /// Maximum distance (px) between an entity's world position and a /// movement node before `findNearestNodeInState` treats the entity @@ -240,6 +244,13 @@ pub const State = struct { /// `advanceDirectMoves`. direct_moves: std.AutoArrayHashMapUnmanaged(u64, DirectMove) = .empty, + /// Where each graph walker was heading (its current waypoint) when + /// a rebuild dropped its route, so the re-issue right after resumes + /// that segment (#60). Filled by `snapshotSegments`, consumed and + /// cleared by the next `rehydrateNavigating`; NOT cleared by `reset`, + /// which runs in between, nor by a second rebuild before it. + resume_at: std.AutoHashMapUnmanaged(u64, Position) = .empty, + /// Last graph epoch at which the CMN sweep ran — drives the /// epoch-change invalidation of `ClosestMovementNode` components /// (moved in from the game-side dispatch in v4). @@ -277,6 +288,7 @@ pub const State = struct { self.pending_entities.deinit(self.allocator); self.cache_prune_scratch.deinit(self.allocator); self.direct_moves.deinit(self.allocator); + self.resume_at.deinit(self.allocator); self.pf.deinit(); } @@ -483,6 +495,57 @@ pub const Controller = struct { src: std.builtin.SourceLocation, ) Result { _ = src; + return navigateWith(game, entity, target, .{}); + } + + /// How a re-issued order differs from a fresh `navigate` (#60). + const Reissue = struct { + /// Start from the node at this position (the waypoint the walker + /// was heading to when a rebuild dropped its route) instead of + /// the nearest node, so it carries on along the same segment. + resume_at: ?Position = null, + /// Walker and target on the same node: walk there instead of + /// the #207 snap. A re-issue can find the walker partway down + /// its last segment, which a snap would jump in one frame. + walk_same_node: bool = false, + }; + + /// The node a re-issued order starts from (#60): the waypoint the + /// walker was heading to, while a node still sits there, else the + /// nearest node at or below the walker (a fresh `navigate`'s pick). + fn reissueFromNode(st: *State, pos: Position, resume_at: ?Position) ?NodeId { + if (resume_at) |at| if (st.pf.graph.nearestNodeWithin(at, RESUME_TOLERANCE)) |node| return node; + return findNearestNodeInState(st, pos.x, pos.y); + } + + /// Walker and target on the same node: walk there, or snap (#207)? + /// Only a re-issue walks, and only from outside the arrival radius. + fn walksToSameNode(reissue: Reissue, pos: Position, node_pos: Position) bool { + return reissue.walk_same_node and !withinDirectArrival(pos, node_pos); + } + + /// Start a graph route from `from_node` to `to_node` (the same node + /// walks a one-waypoint route) and track it for arrival. + fn startRoute(st: *State, entity_id: u64, from_node: NodeId, to_node: NodeId, speed: f32) Result { + const maybe_path = st.pf.navigate(entity_id, from_node, to_node, speed) catch |err| { + log.err("navigate failed: entity={d} from={d} to={d}: {}", .{ entity_id, from_node, to_node, err }); + return .{ .rejected = .navigate_error }; + }; + if (maybe_path == null) return .{ .rejected = .no_path }; + + if (!st.addPending(entity_id)) { + // Allocation failure growing the ring. Cancel the route we + // just started so we don't leak the entity in the underlying + // pathfinder (it'd keep moving without the Controller + // tracking it for arrivals). Caller sees `.deferred` and + // retries next frame once memory frees up. + st.pf.cancel(entity_id); + return .{ .deferred = .pending_alloc_failed }; + } + return .accepted; + } + + fn navigateWith(game: anytype, entity: anytype, target: Position, reissue: Reissue) Result { const st = findState(game) orelse return .{ .rejected = .controller_not_setup }; if (!st.nodes_registered) return .{ .deferred = .graph_not_built }; @@ -539,7 +602,7 @@ pub const Controller = struct { // `.rejected` so the dispatch drops the intent rather than // retrying forever. (to_node's null result is already // `.rejected` for the symmetric reason.) - const from_node = findNearestNodeInState(st, pos.x, pos.y) orelse { + const from_node = reissueFromNode(st, .{ .x = pos.x, .y = pos.y }, reissue.resume_at) orelse { return .{ .rejected = .no_nearby_node }; }; @@ -547,7 +610,12 @@ pub const Controller = struct { return .{ .rejected = .no_nearby_node }; }; - if (from_node == to_node) { + // A re-issued order can find the walker partway down its last + // segment, heading to this very node: walk the rest (#60) rather + // than snap. It walks a one-waypoint route like any graph walk, + // so a later rebuild re-validates it and the arrival settles it. + const node_pos = st.pf.graph.getPosition(from_node); + if (from_node == to_node and !walksToSameNode(reissue, .{ .x = pos.x, .y = pos.y }, node_pos)) { // Worker and target resolve to the same node. The worker // isn't necessarily AT that node's world position yet — // the CMN-per-Euclidean-closest snap means the starting @@ -559,7 +627,6 @@ pub const Controller = struct { // in a "too far from target, re-navigate, same node, // redundant" loop forever — #207. Matches the pathfinder's // own waypoint snap at ARRIVAL_THRESHOLD. - const node_pos = st.pf.graph.getPosition(from_node); game.setPosition(entity, .{ .x = node_pos.x, .y = node_pos.y }); // A redundant navigate IS an arrival — settle it so the // `pathfinder__arrived` event fires uniformly (subscribers @@ -569,26 +636,8 @@ pub const Controller = struct { return .redundant; } - const speed = if (game.active_world.ecs_backend.getComponent(entity, MovementSpeed)) |ms| - ms.speed - else - DEFAULT_SPEED; - - const path = st.pf.navigate(entity_id, from_node, to_node, speed) catch |err| { - log.err("navigate failed: entity={d} from={d} to={d}: {}", .{ entity_id, from_node, to_node, err }); - return .{ .rejected = .navigate_error }; - }; - if (path == null) return .{ .rejected = .no_path }; - - if (!st.addPending(entity_id)) { - // Allocation failure growing the ring. Cancel the route we - // just started so we don't leak the entity in the underlying - // pathfinder (it'd keep moving without the Controller - // tracking it for arrivals). Caller sees `.deferred` and - // retries next frame once memory frees up. - st.pf.cancel(entity_id); - return .{ .deferred = .pending_alloc_failed }; - } + const started = startRoute(st, entity_id, from_node, to_node, speedOf(game, entity)); + if (started != .accepted) return started; // Persist the order so a mid-walk save re-issues on load (the // live route above is transient state). @@ -634,22 +683,14 @@ pub const Controller = struct { i += 1; } - const pos = game.getPosition(entity); - const dx = target.x - pos.x; - const dy = target.y - pos.y; - if (@sqrt(dx * dx + dy * dy) < DIRECT_ARRIVAL_RADIUS) { + if (withinDirectArrival(game.getPosition(entity), target)) { settleArrival(game, st, entity_id, null); return .redundant; } - const speed = if (game.active_world.ecs_backend.getComponent(entity, MovementSpeed)) |ms| - ms.speed - else - DEFAULT_SPEED; - st.direct_moves.put(st.allocator, entity_id, .{ .target = target, - .speed = speed, + .speed = speedOf(game, entity), }) catch return .{ .deferred = .pending_alloc_failed }; game.active_world.ecs_backend.addComponent(entity, Navigating{ @@ -1149,10 +1190,32 @@ pub const Controller = struct { /// up to date with the current entity set. pub fn rebuildGraph(game: anytype) void { const st = findState(game) orelse return; + snapshotSegments(st); st.reset(st.allocator); buildGraphIntoState(game, st); } + /// Before a rebuild drops every live route, remember the waypoint + /// each graph walker was heading to, so `rehydrateNavigating` + /// resumes that segment (#60). Without it the re-issue resolves the + /// nearest node at or below the walker: on a downward segment + /// that's the destination (a snap, a jump), elsewhere it can be a + /// node behind it or beside the segment (a walk back, or off it). + /// An allocation failure only loses the hint: those walkers fall + /// back to the nearest-node re-issue. + /// + /// Hints from an earlier rebuild are kept, not cleared: two rebuilds + /// can land before the next `advance` (an explicit one, then a node + /// count change), and the second sees no live routes, since the + /// first dropped them. A live route overwrites its walker's hint. + fn snapshotSegments(st: *State) void { + for (st.pf.active.keys(), st.pf.active.values()) |id, entry| { + const path = entry.path; + if (path.current_index >= path.len) continue; + st.resume_at.put(st.allocator, id, path.positions[path.current_index]) catch return; + } + } + // ------------------------------------------------------------------ // Graph construction // ------------------------------------------------------------------ @@ -1268,6 +1331,7 @@ pub const Controller = struct { const stale = st.nodes_registered and isGraphStale(game, st); if (!count_changed and !stale) return; + snapshotSegments(st); st.reset(st.allocator); buildGraphIntoState(game, st); } @@ -1325,6 +1389,9 @@ pub const Controller = struct { /// A re-issue that no longer resolves (graph changed under the /// save) settles as a failure so the order can't dangle forever. fn rehydrateNavigating(st: *State, game: anytype) void { + // The segment hints are for the re-issue right after a rebuild, + // never a later one. + defer st.resume_at.clearRetainingCapacity(); if (!st.nodes_registered) return; const Entity = @TypeOf(game.*).EntityType; const ecs = &game.active_world.ecs_backend; @@ -1350,7 +1417,10 @@ pub const Controller = struct { .direct => { _ = moveTo(game, entity, target); }, - .graph => switch (navigate(game, entity, target, @src())) { + .graph => switch (navigateWith(game, entity, target, .{ + .resume_at = st.resume_at.get(id), + .walk_same_node = true, + })) { .accepted, .redundant => {}, // Deferred is transient (graph mid-rebuild) — keep the // order, retry next tick. @@ -1597,6 +1667,18 @@ pub fn findNearestNodeInState(st: *State, x: f32, y: f32) ?NodeId { return best; } +/// Walk speed for `entity`: its `MovementSpeed`, else `DEFAULT_SPEED`. +fn speedOf(game: anytype, entity: anytype) f32 { + return if (game.active_world.ecs_backend.getComponent(entity, MovementSpeed)) |ms| ms.speed else DEFAULT_SPEED; +} + +/// Whether `pos` is already within `DIRECT_ARRIVAL_RADIUS` of `target`. +fn withinDirectArrival(pos: Position, target: Position) bool { + const dx = target.x - pos.x; + const dy = target.y - pos.y; + return @sqrt(dx * dx + dy * dy) < DIRECT_ARRIVAL_RADIUS; +} + /// One straight-line walk step: arrived when within /// `DIRECT_ARRIVAL_RADIUS`, else the next position clamped to /// `min(speed·dt, remaining)` so the walker never overshoots. Pure so @@ -1988,3 +2070,100 @@ test "walk origin refreshes between scans after movement and graph rebuild" { const rebuilt = WalkOrigin.resolve(&st, 10, .{ .x = 60, .y = 0 }); try std.testing.expectEqual(@as(f32, 0), rebuilt.distanceToPosition(20, .{ .x = 60, .y = 0 })); } + +test "a rebuild resumes a walker's segment, not the node behind it (#60)" { + var st = State.init(std.testing.allocator); + defer st.deinit(); + // A stair, bottom to top; the walker climbs from `bottom` to `top`. + const bottom = try st.pf.addNode(.{ .x = 0, .y = 0 }, true); + _ = try st.pf.addNode(.{ .x = 0, .y = 93 }, true); + const top = try st.pf.addNode(.{ .x = 0, .y = 186 }, true); + _ = try st.pf.navigate(7, bottom, top, 60); + // Partway up the first segment, heading for the middle node. + st.pf.active.getPtr(7).?.path.current_index = 1; + + Controller.snapshotSegments(&st); + st.reset(std.testing.allocator); + // The rebuilt graph: the same stair, plus the node whose arrival + // triggered the rebuild. + const bottom2 = try st.pf.addNode(.{ .x = 0, .y = 0 }, true); + const middle2 = try st.pf.addNode(.{ .x = 0, .y = 93 }, true); + _ = try st.pf.addNode(.{ .x = 0, .y = 186 }, true); + _ = try st.pf.addNode(.{ .x = 60, .y = 0 }, false); + + const walker: Position = .{ .x = 0, .y = 50 }; + // The nearest node at or below the climber is the one it left: a + // re-issue from there walks it back down. + try std.testing.expectEqual(bottom2, Controller.reissueFromNode(&st, walker, null).?); + // The remembered waypoint carries it on up. + const resume_at = st.resume_at.get(7) orelse return error.TestExpectedSnapshot; + try std.testing.expectEqual(middle2, Controller.reissueFromNode(&st, walker, resume_at).?); +} + +test "a re-issue walks the rest of its last segment; a fresh navigate still snaps (#60, #207)" { + const node: Position = .{ .x = 0, .y = 0 }; + // 40 px short of the destination node, as a walker heading down its + // last segment is when the rebuild lands. + const partway: Position = .{ .x = 0, .y = 40 }; + try std.testing.expect(Controller.walksToSameNode(.{ .walk_same_node = true }, partway, node)); + try std.testing.expect(!Controller.walksToSameNode(.{}, partway, node)); + // Already within the arrival radius: settles in place. + try std.testing.expect(!Controller.walksToSameNode(.{ .walk_same_node = true }, .{ .x = 0, .y = 2 }, node)); +} + +test "a re-issued last leg stays graph-tracked, so the next rebuild re-validates it (#60)" { + var st = State.init(std.testing.allocator); + defer st.deinit(); + const bottom = try st.pf.addNode(.{ .x = 0, .y = 0 }, true); + _ = try st.pf.addNode(.{ .x = 0, .y = 93 }, true); + + // A rebuild found the walker partway down to `bottom`, its + // destination's node: the re-issue walks a one-waypoint route. + try std.testing.expectEqual(Result.accepted, Controller.startRoute(&st, 7, bottom, bottom, 60)); + try std.testing.expect(st.pf.isNavigating(7)); + try std.testing.expect(!st.direct_moves.contains(7)); + try std.testing.expectEqualSlices(u64, &.{7}, st.pending_entities.items); + + // A second rebuild snapshots that leg and drops it with the other + // routes, leaving the order for `rehydrateNavigating` to re-issue + // against the new graph (re-route, or fail it). + Controller.snapshotSegments(&st); + st.reset(std.testing.allocator); + try std.testing.expect(!st.pf.isNavigating(7)); + try std.testing.expect(!st.direct_moves.contains(7)); + try std.testing.expectEqual(@as(usize, 0), st.pending_entities.items.len); + const resume_at = st.resume_at.get(7) orelse return error.TestExpectedSnapshot; + try std.testing.expectEqual(Position{ .x = 0, .y = 0 }, resume_at); + + // The rebuilt graph lost the destination: the hint no longer + // resolves, so the re-issue falls back to the nearest-node pick. + const top2 = try st.pf.addNode(.{ .x = 0, .y = 93 }, true); + try std.testing.expectEqual(@as(?NodeId, null), st.pf.graph.nearestNodeWithin(resume_at, RESUME_TOLERANCE)); + try std.testing.expectEqual(top2, Controller.reissueFromNode(&st, .{ .x = 0, .y = 120 }, resume_at).?); +} + +test "a second rebuild before the re-issue keeps the waypoint hints (#60)" { + var st = State.init(std.testing.allocator); + defer st.deinit(); + const bottom = try st.pf.addNode(.{ .x = 0, .y = 0 }, true); + _ = try st.pf.addNode(.{ .x = 0, .y = 93 }, true); + const top = try st.pf.addNode(.{ .x = 0, .y = 186 }, true); + _ = try st.pf.navigate(7, bottom, top, 60); + st.pf.active.getPtr(7).?.path.current_index = 1; + + Controller.snapshotSegments(&st); + st.reset(std.testing.allocator); + // The second rebuild: no live routes left to snapshot. + Controller.snapshotSegments(&st); + st.reset(std.testing.allocator); + try std.testing.expectEqual(Position{ .x = 0, .y = 93 }, st.resume_at.get(7).?); + + // A walker with a live route gets its hint refreshed. + const bottom2 = try st.pf.addNode(.{ .x = 0, .y = 0 }, true); + _ = try st.pf.addNode(.{ .x = 0, .y = 93 }, true); + const top2 = try st.pf.addNode(.{ .x = 0, .y = 186 }, true); + _ = try st.pf.navigate(7, bottom2, top2, 60); + st.pf.active.getPtr(7).?.path.current_index = 2; + Controller.snapshotSegments(&st); + try std.testing.expectEqual(Position{ .x = 0, .y = 186 }, st.resume_at.get(7).?); +} diff --git a/tests/nav/controller_v4_test.zig b/tests/nav/controller_v4_test.zig index 0537701..c8cba1b 100644 --- a/tests/nav/controller_v4_test.zig +++ b/tests/nav/controller_v4_test.zig @@ -115,6 +115,56 @@ test "tick fires onPathInvalidated when the goal dies and no reroute exists" { try std.testing.expect(!pf.isNavigating(9)); } +test "a one-waypoint route walks the rest of a segment and arrives (#60)" { + const Pf = PathfinderWith(u64, struct {}); + var pf = Pf.init(std.testing.allocator, test_config); + defer pf.deinit(); + + const a = try pf.addNode(.{ .x = 100, .y = 100 }, true); + _ = try pf.addNode(.{ .x = 100, .y = 200 }, true); + + var ctx = CallbackCtx.init(std.testing.allocator); + defer ctx.deinit(); + // 40 px short of `a`, as a re-issue finds a walker on its last leg. + try ctx.positions.put(7, .{ .x = 100, .y = 140 }); + + _ = try pf.navigate(7, a, a, 60.0); + var i: usize = 0; + while (i < 120 and pf.isNavigating(7)) : (i += 1) { + const before = ctx.positions.get(7).?; + pf.tick(&ctx, 0.016); + // Walks, never jumps: no step beyond the engine's 2 px waypoint + // snap (speed·dt is ~1 px here; the jump was the whole 40 px). + try std.testing.expect(before.y - ctx.positions.get(7).?.y <= 2.0 + 0.001); + } + + try std.testing.expectEqual(@as(usize, 1), ctx.arrived.items.len); + try std.testing.expectEqual(a, ctx.arrived.items[0].goal_node); + try std.testing.expectEqual(@as(f32, 100), ctx.positions.get(7).?.y); +} + +test "a one-waypoint route is invalidated when its node goes away (#60)" { + const Pf = PathfinderWith(u64, struct {}); + var pf = Pf.init(std.testing.allocator, test_config); + defer pf.deinit(); + + const a = try pf.addNode(.{ .x = 100, .y = 100 }, true); + _ = try pf.addNode(.{ .x = 900, .y = 900 }, false); + + var ctx = CallbackCtx.init(std.testing.allocator); + defer ctx.deinit(); + try ctx.positions.put(9, .{ .x = 100, .y = 140 }); + + _ = try pf.navigate(9, a, a, 10.0); + pf.tick(&ctx, 0.016); + pf.removeNode(a); + pf.tick(&ctx, 0.016); + + try std.testing.expectEqual(@as(usize, 0), ctx.arrived.items.len); + try std.testing.expectEqual(@as(usize, 1), ctx.invalidated.items.len); + try std.testing.expect(!pf.isNavigating(9)); +} + test "plain ctx without settle callbacks still compiles and ticks" { const Pf = PathfinderWith(u64, struct {}); var pf = Pf.init(std.testing.allocator, test_config); diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/build.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/build.zig new file mode 100644 index 0000000..2971858 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/build.zig @@ -0,0 +1,48 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Static library for linking + const lib = b.addLibrary(.{ + .linkage = .static, + .name = "zig_utils", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(lib); + + // zspec dependency + const zspec = b.dependency("zspec", .{ + .target = target, + .optimize = optimize, + }); + + // zig_utils module for tests + const zig_utils_module = b.addModule("zig_utils", .{ + .root_source_file = b.path("src/root.zig"), + }); + + // Tests with zspec runner + const lib_unit_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("tests/root.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "zspec", .module = zspec.module("zspec") }, + .{ .name = "zig_utils", .module = zig_utils_module }, + }, + }), + .test_runner = .{ .path = zspec.path("src/runner.zig"), .mode = .simple }, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/build.zig.zon b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/build.zig.zon new file mode 100644 index 0000000..c696d43 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/build.zig.zon @@ -0,0 +1,19 @@ +.{ + .name = .zig_utils, + .version = "0.7.3", + .fingerprint = 0x6dc482caf73c4a75, + .minimum_zig_version = "0.16.0", + + .dependencies = .{ + .zspec = .{ + .url = "https://github.com/apotema/zspec/archive/v0.9.2.tar.gz", + .hash = "zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw", + }, + }, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/a_star.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/a_star.zig new file mode 100644 index 0000000..6ceba59 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/a_star.zig @@ -0,0 +1,406 @@ +//! A* (A-Star) Pathfinding Algorithm +//! +//! A best-first search algorithm that finds the shortest path between a source +//! and destination node. Uses heuristics to guide the search, making it more +//! efficient than Dijkstra's algorithm for single-source pathfinding. +//! +//! ## Features +//! - Single-source shortest path (efficient for point-to-point queries) +//! - Multiple built-in heuristics (Euclidean, Manhattan, Chebyshev, Octile) +//! - Custom heuristic support +//! - Entity ID mapping for integration with external systems +//! - Adjacency list representation (memory efficient for sparse graphs) +//! +//! ## When to use A* vs Floyd-Warshall +//! - **A***: Best for single-source queries, large sparse graphs, real-time games +//! - **Floyd-Warshall**: Best when you need all-pairs paths, dense graphs, or +//! when paths are queried repeatedly between many node pairs + +const std = @import("std"); +const heuristics_mod = @import("heuristics.zig"); +const SparseSet = @import("sparse_set.zig").SparseSet; + +pub const Heuristic = heuristics_mod.Heuristic; +pub const HeuristicFn = heuristics_mod.HeuristicFn; +pub const Position = heuristics_mod.Position; + +/// A* pathfinding algorithm with configurable heuristics. +/// Generic over WeightType for memory efficiency. +/// Supports both direct vertex indices and entity ID mapping. +pub fn AStar(comptime WeightType: type) type { + comptime { + const info = @typeInfo(WeightType); + if (info != .int or info.int.signedness != .unsigned) { + @compileError("WeightType must be an unsigned integer type"); + } + } + + const INF = std.math.maxInt(WeightType); + + return struct { + const Self = @This(); + + const Edge = struct { + to: u32, + weight: WeightType, + }; + const EdgeList = std.ArrayListUnmanaged(Edge); + const AdjacencyList = std.ArrayListUnmanaged(EdgeList); + + /// Priority queue node for A* open set + const PQNode = struct { + vertex: u32, + f_score: f32, + + fn compare(_: void, a: PQNode, b: PQNode) std.math.Order { + return std.math.order(a.f_score, b.f_score); + } + }; + + allocator: std.mem.Allocator, + adjacency: AdjacencyList, + positions: SparseSet(u32, Position), + ids: SparseSet(u32, u32), + reverse_ids: SparseSet(u32, u32), + last_key: u32 = 0, + size: u32 = 100, + heuristic_type: Heuristic, + custom_heuristic: ?HeuristicFn, + + pub fn init(allocator: std.mem.Allocator) !Self { + var positions = try SparseSet(u32, Position).init(allocator, 1024, 64); + errdefer positions.deinit(); + + var ids = try SparseSet(u32, u32).init(allocator, 1024, 64); + errdefer ids.deinit(); + + const reverse_ids = try SparseSet(u32, u32).init(allocator, 1024, 64); + + return .{ + .allocator = allocator, + .adjacency = .empty, + .positions = positions, + .ids = ids, + .reverse_ids = reverse_ids, + .heuristic_type = .euclidean, + .custom_heuristic = null, + }; + } + + pub fn deinit(self: *Self) void { + for (self.adjacency.items) |*edges| { + edges.deinit(self.allocator); + } + self.adjacency.deinit(self.allocator); + self.positions.deinit(); + self.ids.deinit(); + self.reverse_ids.deinit(); + } + + /// Set the heuristic type to use for pathfinding + pub fn setHeuristic(self: *Self, heuristic_type: Heuristic) void { + self.heuristic_type = heuristic_type; + self.custom_heuristic = null; + } + + /// Set a custom heuristic function + pub fn setCustomHeuristic(self: *Self, heuristic_fn: HeuristicFn) void { + self.custom_heuristic = heuristic_fn; + } + + /// Set the position of a node (used for heuristic calculation) + pub fn setNodePosition(self: *Self, node: u32, pos: Position) !void { + try self.positions.put(node, pos); + } + + /// Set node position using entity ID mapping + pub fn setNodePositionWithMapping(self: *Self, entity: u32, pos: Position) !void { + const internal_id = try self.getOrCreateMapping(entity); + try self.positions.put(internal_id, pos); + } + + /// Generate a new internal key for entity mapping + fn newKey(self: *Self) u32 { + self.last_key += 1; + return self.last_key - 1; + } + + /// Get or create an internal ID mapping for an entity + fn getOrCreateMapping(self: *Self, entity: u32) !u32 { + if (self.ids.get(entity)) |id| { + return id; + } + const new_id = self.newKey(); + try self.ids.put(entity, new_id); + errdefer _ = self.ids.remove(entity); + try self.reverse_ids.put(new_id, entity); + return new_id; + } + + /// Resize the graph to support a given number of vertices + pub fn resize(self: *Self, size: u32) void { + self.size = size; + } + + /// Reset the graph and prepare for new data + pub fn clean(self: *Self) !void { + self.last_key = 0; + + for (self.adjacency.items) |*edges| { + edges.deinit(self.allocator); + } + self.adjacency.clearRetainingCapacity(); + self.positions.clear(); + self.ids.clear(); + self.reverse_ids.clear(); + + // Initialize adjacency lists for each vertex + try self.adjacency.ensureTotalCapacity(self.allocator, self.size); + for (0..self.size) |_| { + try self.adjacency.append(self.allocator, .empty); + } + } + + pub const AddEdgeError = error{ + InvalidVertex, + OutOfMemory, + }; + + /// Add an edge between two vertices with given weight (direct index) + /// Returns error.InvalidVertex if u or v is out of bounds. + pub fn addEdge(self: *Self, u: u32, v: u32, w: WeightType) AddEdgeError!void { + if (u >= self.adjacency.items.len or v >= self.adjacency.items.len) { + return error.InvalidVertex; + } + try self.adjacency.items[u].append(self.allocator, .{ .to = v, .weight = w }); + } + + /// Add an edge using entity ID mapping (auto-assigns internal indices) + pub fn addEdgeWithMapping(self: *Self, u: u32, v: u32, w: WeightType) !void { + const u_internal = try self.getOrCreateMapping(u); + const v_internal = try self.getOrCreateMapping(v); + try self.addEdge(u_internal, v_internal, w); + } + + /// Calculate heuristic between two internal vertex indices + fn calculateHeuristic(self: *Self, from: u32, to: u32) f32 { + const from_pos = self.positions.get(from) orelse Position{ .x = 0, .y = 0 }; + const to_pos = self.positions.get(to) orelse Position{ .x = 0, .y = 0 }; + + if (self.custom_heuristic) |custom| { + return custom(from_pos, to_pos); + } + return heuristics_mod.calculate(self.heuristic_type, from_pos, to_pos); + } + + /// Run A* algorithm to find shortest path from source to destination. + /// Returns the path cost, or null if no path exists. + /// The path is stored in the provided ArrayList. + pub fn findPath( + self: *Self, + source: u32, + dest: u32, + path: *std.array_list.Managed(u32), + ) !?WeightType { + const n = self.adjacency.items.len; + if (source >= n or dest >= n) { + return null; + } + + path.clearRetainingCapacity(); + + if (source == dest) { + try path.append(source); + return 0; + } + + // Flat arrays instead of HashMaps - O(1) access, cache-friendly + const g_score = try self.allocator.alloc(WeightType, n); + defer self.allocator.free(g_score); + @memset(g_score, INF); + + const came_from = try self.allocator.alloc(u32, n); + defer self.allocator.free(came_from); + @memset(came_from, std.math.maxInt(u32)); // maxInt = no parent + + // BitSet instead of HashMap for closed_set - ~32x smaller, faster + var closed_set = try std.DynamicBitSet.initEmpty(self.allocator, n); + defer closed_set.deinit(); + + var open_set = std.PriorityQueue(PQNode, void, PQNode.compare).initContext({}); + defer open_set.deinit(self.allocator); + + // Initialize source + g_score[source] = 0; + const h = self.calculateHeuristic(source, dest); + try open_set.push(self.allocator, .{ .vertex = source, .f_score = h }); + + while (open_set.pop()) |current| { + if (current.vertex == dest) { + // Reconstruct path + var node = dest; + while (true) { + try path.append(node); + const parent = came_from[node]; + if (parent == std.math.maxInt(u32)) { + break; + } + node = parent; + } + // Reverse to get source -> dest order + std.mem.reverse(u32, path.items); + return g_score[dest]; + } + + if (closed_set.isSet(current.vertex)) { + continue; + } + closed_set.set(current.vertex); + + const current_g = g_score[current.vertex]; + + // Explore neighbors + for (self.adjacency.items[current.vertex].items) |edge| { + if (closed_set.isSet(edge.to)) { + continue; + } + + const tentative_g = current_g +| edge.weight; // Saturating add + const neighbor_g = g_score[edge.to]; + + if (tentative_g < neighbor_g) { + came_from[edge.to] = current.vertex; + g_score[edge.to] = tentative_g; + + const f = @as(f32, @floatFromInt(tentative_g)) + self.calculateHeuristic(edge.to, dest); + try open_set.push(self.allocator, .{ .vertex = edge.to, .f_score = f }); + } + } + } + + return null; // No path found + } + + pub const MappingError = error{ + CorruptedMapping, + OutOfMemory, + }; + + /// Find path using entity ID mapping + /// Returns error.CorruptedMapping if internal nodes cannot be reverse-mapped. + pub fn findPathWithMapping( + self: *Self, + source_entity: u32, + dest_entity: u32, + path: *std.array_list.Managed(u32), + ) MappingError!?WeightType { + const source = self.ids.get(source_entity) orelse return null; + const dest = self.ids.get(dest_entity) orelse return null; + + var internal_path = std.array_list.Managed(u32).init(self.allocator); + defer internal_path.deinit(); + + const cost = self.findPath(source, dest, &internal_path) catch return error.OutOfMemory; + + if (cost != null) { + path.clearRetainingCapacity(); + for (internal_path.items) |internal_id| { + const entity = self.reverse_ids.get(internal_id) orelse return error.CorruptedMapping; + try path.append(entity); + } + } + + return cost; + } + + /// Check if a path exists between two vertices (direct index) + pub fn hasPath(self: *Self, u: usize, v: usize) bool { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = self.findPath(@intCast(u), @intCast(v), &path) catch return false; + return result != null; + } + + /// Check if a path exists between two entities (using ID mapping) + pub fn hasPathWithMapping(self: *Self, u: u32, v: u32) bool { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = self.findPathWithMapping(u, v, &path) catch return false; + return result != null; + } + + /// Get the distance between two vertices (direct index) + /// Note: This runs A* each time - cache results if needed frequently + pub fn value(self: *Self, u: usize, v: usize) WeightType { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = self.findPath(@intCast(u), @intCast(v), &path) catch return INF; + return result orelse INF; + } + + /// Get the distance between two entities (using ID mapping) + pub fn valueWithMapping(self: *Self, u: u32, v: u32) WeightType { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = self.findPathWithMapping(u, v, &path) catch return INF; + return result orelse INF; + } + + pub const PathError = error{ + PathNotFound, + CorruptedMapping, + OutOfMemory, + }; + + /// Build the path from u to v and store in the provided ArrayList + /// Returns error.PathNotFound if no path exists between the nodes. + pub fn setPathWithMapping(self: *Self, path_list: *std.array_list.Managed(u32), u: u32, v: u32) PathError!void { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = try self.findPathWithMapping(u, v, &path); + if (result == null) { + return error.PathNotFound; + } + + path_list.clearRetainingCapacity(); + for (path.items) |node| { + try path_list.append(node); + } + } + + /// Get the next entity in the shortest path from u to v (using ID mapping) + pub fn nextWithMapping(self: *Self, u: u32, v: u32) u32 { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = self.findPathWithMapping(u, v, &path) catch return std.math.maxInt(u32); + if (result == null or path.items.len < 2) { + return std.math.maxInt(u32); + } + return path.items[1]; // Second element is next step + } + + /// Get the next vertex in the shortest path from u to v (direct index) + pub fn next(self: *Self, u: usize, v: usize) u32 { + var path = std.array_list.Managed(u32).init(self.allocator); + defer path.deinit(); + + const result = self.findPath(@intCast(u), @intCast(v), &path) catch return std.math.maxInt(u32); + if (result == null or path.items.len < 2) { + return std.math.maxInt(u32); + } + return path.items[1]; + } + + /// No-op for A* (paths computed on-demand) + pub fn generate(self: *Self) void { + _ = self; + // A* computes paths on-demand, no pre-computation needed + } + }; +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/floyd_warshall.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/floyd_warshall.zig new file mode 100644 index 0000000..765f844 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/floyd_warshall.zig @@ -0,0 +1,239 @@ +//! Floyd-Warshall Algorithm Implementation +//! +//! Computes shortest paths between all pairs of vertices in a weighted graph. +//! Uses dynamic programming to find optimal paths and supports entity ID mapping. +//! +//! Complexity: O(V³) time, O(V²) space +//! +//! Best for: +//! - Dense graphs with many all-pairs queries +//! - Graphs that change infrequently +//! - Pre-computing all shortest paths + +const std = @import("std"); + +/// Floyd-Warshall all-pairs shortest path algorithm. +/// Generic over DistanceType for memory efficiency. +/// Supports both direct vertex indices and entity ID mapping. +pub fn FloydWarshall(comptime DistanceType: type) type { + comptime { + const info = @typeInfo(DistanceType); + if (info != .int or info.int.signedness != .unsigned) { + @compileError("DistanceType must be an unsigned integer type"); + } + } + + const INF = std.math.maxInt(DistanceType); + + return struct { + const Self = @This(); + const RowList = std.array_list.Managed(DistanceType); + const GraphList = std.array_list.Managed(RowList); + + size: u32 = 100, + graph: GraphList, + path: GraphList, + ids: std.AutoHashMap(u32, u32), + /// Reverse mapping: internal index to entity ID for O(1) reverse lookups + reverse_ids: std.AutoHashMap(u32, u32), + last_key: u32 = 0, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .graph = GraphList.init(allocator), + .path = GraphList.init(allocator), + .ids = std.AutoHashMap(u32, u32).init(allocator), + .reverse_ids = std.AutoHashMap(u32, u32).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *Self) void { + for (self.graph.items) |*row| { + row.deinit(); + } + for (self.path.items) |*row| { + row.deinit(); + } + self.graph.deinit(); + self.path.deinit(); + self.ids.deinit(); + self.reverse_ids.deinit(); + } + + /// Generate a new internal key for entity mapping + pub fn newKey(self: *Self) u32 { + self.last_key += 1; + return self.last_key - 1; + } + + /// Add an edge between two vertices with given weight (direct index) + pub fn addEdge(self: *Self, u: u32, v: u32, w: DistanceType) void { + self.graph.items[u].items[v] = w; + } + + /// Get the distance between two vertices (direct index) + pub fn value(self: *Self, u: usize, v: usize) DistanceType { + return self.graph.items[u].items[v]; + } + + /// Check if a path exists between two vertices (direct index) + pub fn hasPath(self: *Self, u: usize, v: usize) bool { + return self.graph.items[u].items[v] != INF; + } + + /// Get the next vertex in the shortest path from u to v (direct index) + pub fn next(self: *Self, u: usize, v: usize) u32 { + return @intCast(self.path.items[u].items[v]); + } + + /// Resize the graph to support a given number of vertices + pub fn resize(self: *Self, size: u32) void { + self.size = size; + } + + /// Add an edge using entity ID mapping (auto-assigns internal indices) + /// Returns error.OutOfMemory if the internal hash map fails to allocate + pub fn addEdgeWithMapping(self: *Self, u: u32, v: u32, w: DistanceType) !void { + if (!self.ids.contains(u)) { + const key = self.newKey(); + try self.ids.put(u, key); + errdefer _ = self.ids.remove(u); + try self.reverse_ids.put(key, u); + } + if (!self.ids.contains(v)) { + const key = self.newKey(); + try self.ids.put(v, key); + errdefer _ = self.ids.remove(v); + try self.reverse_ids.put(key, v); + } + self.addEdge(self.ids.get(u).?, self.ids.get(v).?, w); + } + + /// Get the distance between two entities (using ID mapping) + pub fn valueWithMapping(self: *Self, u: u32, v: u32) DistanceType { + return self.value(self.ids.get(u).?, self.ids.get(v).?); + } + + /// Build the path from u to v and store in the provided ArrayList + /// Returns error.PathNotFound if no path exists between the nodes. + pub fn setPathWithMapping(self: *Self, path_list: *std.array_list.Managed(u32), u_node: u32, v_node: u32) !void { + const initial_len = path_list.items.len; + var current = u_node; + while (current != v_node) { + try path_list.append(current); + current = self.nextWithMapping(current, v_node); + if (current == std.math.maxInt(u32)) { + // Clear partial path data before returning error + path_list.shrinkRetainingCapacity(initial_len); + return error.PathNotFound; + } + } + try path_list.append(v_node); + } + + /// Build the path from u to v and store in the provided unmanaged ArrayList + /// Returns error.PathNotFound if no path exists between the nodes. + pub fn setPathWithMappingUnmanaged(self: *Self, allocator: std.mem.Allocator, path_list: *std.ArrayListUnmanaged(u32), u_node: u32, v_node: u32) !void { + const initial_len = path_list.items.len; + var current = u_node; + while (current != v_node) { + try path_list.append(allocator, current); + current = self.nextWithMapping(current, v_node); + if (current == std.math.maxInt(u32)) { + // Clear partial path data before returning error + path_list.shrinkRetainingCapacity(initial_len); + return error.PathNotFound; + } + } + try path_list.append(allocator, v_node); + } + + /// Get the next entity in the shortest path from u to v (using ID mapping) + /// Uses O(1) reverse lookup via reverse_ids map. + pub fn nextWithMapping(self: *Self, u: u32, v: u32) u32 { + const u_idx = self.ids.get(u) orelse return std.math.maxInt(u32); + const v_idx = self.ids.get(v) orelse return std.math.maxInt(u32); + const next_idx = self.next(u_idx, v_idx); + return self.reverse_ids.get(next_idx) orelse std.math.maxInt(u32); + } + + /// Check if a path exists between two entities (using ID mapping) + pub fn hasPathWithMapping(self: *Self, u: u32, v: u32) bool { + if (self.ids.get(u) == null or self.ids.get(v) == null) { + return false; + } + return self.hasPath(self.ids.get(u).?, self.ids.get(v).?); + } + + /// Reset the graph and prepare for new data + pub fn clean(self: *Self) !void { + self.last_key = 0; + for (self.graph.items) |*row| { + row.deinit(); + } + for (self.path.items) |*row| { + row.deinit(); + } + self.graph.clearRetainingCapacity(); + self.path.clearRetainingCapacity(); + self.ids.clearRetainingCapacity(); + self.reverse_ids.clearRetainingCapacity(); + + // Initialize adjacency matrix and path matrix + for (0..self.size) |_| { + var list = RowList.init(self.allocator); + var list_appended = false; + errdefer if (!list_appended) list.deinit(); + + var row_path = RowList.init(self.allocator); + var row_path_appended = false; + errdefer if (!row_path_appended) row_path.deinit(); + + for (0..self.size) |_| { + try list.append(0); + try row_path.append(0); + } + try self.graph.append(list); + list_appended = true; + + try self.path.append(row_path); + row_path_appended = true; + } + + // Set initial values: 0 for self-loops, INF for no edge + for (0..self.size) |i| { + for (0..self.size) |j| { + self.path.items[i].items[j] = @intCast(j); + if (i == j) { + self.graph.items[i].items[j] = 0; + } else { + self.graph.items[i].items[j] = INF; + } + } + } + } + + /// Run the Floyd-Warshall algorithm to compute all shortest paths + pub fn generate(self: *Self) void { + for (0..self.size) |k| { + for (0..self.size) |i| { + const dist_ik = self.graph.items[i].items[k]; + if (dist_ik == INF) continue; // Skip if no path to k + + for (0..self.size) |j| { + const dist_kj = self.graph.items[k].items[j]; + if (dist_kj == INF) continue; // Skip if no path from k + + const new_dist = dist_ik +| dist_kj; // Saturating add + if (new_dist < self.graph.items[i].items[j]) { + self.graph.items[i].items[j] = new_dist; + self.path.items[i].items[j] = self.path.items[i].items[k]; + } + } + } + } + } + }; +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/floyd_warshall_optimized.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/floyd_warshall_optimized.zig new file mode 100644 index 0000000..eb331f5 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/floyd_warshall_optimized.zig @@ -0,0 +1,567 @@ +//! Optimized Floyd-Warshall Algorithm Implementation +//! +//! High-performance implementation with: +//! - Flat memory layout for cache efficiency +//! - SIMD vectorization for inner loop operations +//! - Multi-threaded parallelization across rows +//! +//! For graphs that change infrequently but require many arbitrary +//! source-destination queries. + +const std = @import("std"); +const builtin = @import("builtin"); + +const INF: u32 = std.math.maxInt(u32); + +/// Configuration for the optimized Floyd-Warshall algorithm +pub const Config = struct { + /// Enable multi-threaded parallelization (recommended for graphs with 256+ nodes) + parallel: bool = true, + /// Enable SIMD vectorization + simd: bool = true, + /// SIMD vector width (elements per vector operation) + /// - 4: 128-bit vectors (SSE, NEON) - widely supported + /// - 8: 256-bit vectors (AVX2) + /// - 16: 512-bit vectors (AVX-512) + vector_width: comptime_int = 4, +}; + +/// Optimized Floyd-Warshall all-pairs shortest path algorithm. +/// Uses flat memory layout, SIMD, and multi-threading for performance. +pub fn FloydWarshallOptimized(comptime config: Config) type { + return struct { + const Self = @This(); + + // SIMD vector configuration + const VectorWidth = config.vector_width; + const DistVector = @Vector(VectorWidth, u32); + const IndexVector = @Vector(VectorWidth, u32); + + size: u32 = 0, + capacity: u32 = 0, + /// Flat distance matrix (size x size), row-major order + dist: []u32, + /// Flat next-hop matrix (size x size), row-major order + next: []u32, + /// Entity ID to internal index mapping + ids: std.AutoHashMap(u32, u32), + /// Reverse mapping: internal index to entity ID. + /// Provides O(1) reverse lookups, significantly faster than the legacy + /// implementation which required O(n) iteration for each reverse lookup. + reverse_ids: std.AutoHashMap(u32, u32), + last_key: u32 = 0, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .dist = &.{}, + .next = &.{}, + .ids = std.AutoHashMap(u32, u32).init(allocator), + .reverse_ids = std.AutoHashMap(u32, u32).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *Self) void { + if (self.dist.len > 0) { + self.allocator.free(self.dist); + } + if (self.next.len > 0) { + self.allocator.free(self.next); + } + self.ids.deinit(); + self.reverse_ids.deinit(); + } + + /// Generate a new internal key for entity mapping + pub fn newKey(self: *Self) u32 { + self.last_key += 1; + return self.last_key - 1; + } + + /// Get flat array index for (row, col) + inline fn index(self: *const Self, row: u32, col: u32) usize { + return @as(usize, row) * @as(usize, self.size) + @as(usize, col); + } + + /// Add an edge between two vertices with given weight (direct index) + pub fn addEdge(self: *Self, u: u32, v: u32, w: u32) void { + self.dist[self.index(u, v)] = w; + } + + /// Get the distance between two vertices (direct index) + pub fn value(self: *const Self, u: u32, v: u32) u32 { + return self.dist[self.index(u, v)]; + } + + /// Check if a path exists between two vertices (direct index) + pub fn hasPath(self: *const Self, u: u32, v: u32) bool { + return self.dist[self.index(u, v)] != INF; + } + + /// Get the next vertex in the shortest path from u to v (direct index) + pub fn getNext(self: *const Self, u: u32, v: u32) u32 { + return self.next[self.index(u, v)]; + } + + /// Resize the graph to support a given number of vertices + pub fn resize(self: *Self, new_size: u32) void { + self.size = new_size; + } + + /// Add an edge using entity ID mapping (auto-assigns internal indices) + /// Returns error.OutOfMemory if the internal hash maps fail to allocate + pub fn addEdgeWithMapping(self: *Self, u: u32, v: u32, w: u32) !void { + if (!self.ids.contains(u)) { + const key = self.newKey(); + try self.ids.put(u, key); + try self.reverse_ids.put(key, u); + } + if (!self.ids.contains(v)) { + const key = self.newKey(); + try self.ids.put(v, key); + try self.reverse_ids.put(key, v); + } + self.addEdge(self.ids.get(u).?, self.ids.get(v).?, w); + } + + /// Get the distance between two entities (using ID mapping) + pub fn valueWithMapping(self: *const Self, u: u32, v: u32) u32 { + const u_idx = self.ids.get(u) orelse return INF; + const v_idx = self.ids.get(v) orelse return INF; + return self.value(u_idx, v_idx); + } + + /// Get the next entity in the shortest path from u to v (using ID mapping) + /// Returns INF if no path exists + pub fn nextWithMapping(self: *const Self, u: u32, v: u32) u32 { + const u_idx = self.ids.get(u) orelse return INF; + const v_idx = self.ids.get(v) orelse return INF; + const next_idx = self.getNext(u_idx, v_idx); + return self.reverse_ids.get(next_idx) orelse INF; + } + + /// Check if a path exists between two entities (using ID mapping) + pub fn hasPathWithMapping(self: *const Self, u: u32, v: u32) bool { + const u_idx = self.ids.get(u) orelse return false; + const v_idx = self.ids.get(v) orelse return false; + return self.hasPath(u_idx, v_idx); + } + + pub const PathError = error{ + NoPathFound, + OutOfMemory, + }; + + /// Build the path from u to v and store in the provided ArrayList + /// Returns error.NoPathFound if no path exists between the nodes + pub fn setPathWithMapping(self: *const Self, path_list: *std.array_list.Managed(u32), u_node: u32, v_node: u32) PathError!void { + var current = u_node; + var steps: usize = 0; + const max_steps: usize = self.size; // a shortest path visits each node at most once + while (current != v_node) { + // Cycle / corrupt next-hop guard: without this, an inconsistent + // `next` matrix (a cycle that never reaches `v_node`) loops + // forever, growing `path_list` unbounded — a hard hang + OOM. + // A real shortest path is at most `size` hops, so anything + // longer is a cycle. Surfaced as a permanent freeze + memory + // blow-up loading a 1000-worker flying-platform colony. + if (steps > max_steps) return error.NoPathFound; + try path_list.append(current); + current = self.nextWithMapping(current, v_node); + if (current == INF) { + return error.NoPathFound; + } + steps += 1; + } + try path_list.append(v_node); + } + + /// Build the path from u to v and store in the provided unmanaged ArrayList + /// Returns error.NoPathFound if no path exists between the nodes + pub fn setPathWithMappingUnmanaged(self: *const Self, allocator: std.mem.Allocator, path_list: *std.ArrayListUnmanaged(u32), u_node: u32, v_node: u32) PathError!void { + var current = u_node; + var steps: usize = 0; + const max_steps: usize = self.size; // a shortest path visits each node at most once + while (current != v_node) { + // See `setPathWithMapping` — guards an inconsistent `next` + // matrix cycle from looping forever (hang + unbounded alloc). + if (steps > max_steps) return error.NoPathFound; + try path_list.append(allocator, current); + current = self.nextWithMapping(current, v_node); + if (current == INF) { + return error.NoPathFound; + } + steps += 1; + } + try path_list.append(allocator, v_node); + } + + pub const CleanError = error{ + SizeOverflow, + OutOfMemory, + }; + + /// Reset the graph and prepare for new data + pub fn clean(self: *Self) CleanError!void { + self.last_key = 0; + self.ids.clearRetainingCapacity(); + self.reverse_ids.clearRetainingCapacity(); + + // Check for overflow before computing matrix_size + const n: usize = self.size; + const matrix_size = std.math.mul(usize, n, n) catch return error.SizeOverflow; + + // Reallocate if needed + if (self.capacity < self.size) { + // Allocate both new arrays first (before freeing old ones) + // This ensures no state change if allocation fails + const new_dist = try self.allocator.alloc(u32, matrix_size); + errdefer self.allocator.free(new_dist); + + const new_next = try self.allocator.alloc(u32, matrix_size); + + // Now safe to free old arrays and update state + if (self.dist.len > 0) { + self.allocator.free(self.dist); + } + if (self.next.len > 0) { + self.allocator.free(self.next); + } + self.dist = new_dist; + self.next = new_next; + self.capacity = self.size; + } + + // Initialize matrices + const dist_slice = self.dist[0..matrix_size]; + const next_slice = self.next[0..matrix_size]; + + // Set all distances to INF + @memset(dist_slice, INF); + + // Initialize next-hop and diagonal + for (0..self.size) |i| { + for (0..self.size) |j| { + const idx = i * self.size + j; + next_slice[idx] = @intCast(j); + } + // Self-loops have distance 0 + dist_slice[i * self.size + i] = 0; + } + } + + /// Run the Floyd-Warshall algorithm to compute all shortest paths + pub fn generate(self: *Self) void { + if (config.parallel and !builtin.single_threaded and self.size > 64) { + self.generateParallel(); + } else if (config.simd) { + self.generateSimd(); + } else { + self.generateScalar(); + } + } + + /// Scalar implementation (baseline) + fn generateScalar(self: *Self) void { + const n = self.size; + for (0..n) |k| { + for (0..n) |i| { + const dist_ik = self.dist[i * n + k]; + if (dist_ik == INF) continue; // Optimization: skip if no path to k + + for (0..n) |j| { + const dist_kj = self.dist[k * n + j]; + if (dist_kj == INF) continue; + + const new_dist = dist_ik +| dist_kj; // Saturating add to prevent overflow + const idx = i * n + j; + if (new_dist < self.dist[idx]) { + self.dist[idx] = new_dist; + self.next[idx] = self.next[i * n + k]; + } + } + } + } + } + + /// SIMD-optimized implementation + fn generateSimd(self: *Self) void { + const n = self.size; + const n_usize: usize = n; + + for (0..n) |k| { + for (0..n) |i| { + const dist_ik = self.dist[i * n_usize + k]; + if (dist_ik == INF) continue; + + const next_ik = self.next[i * n_usize + k]; + const dist_ik_vec: DistVector = @splat(dist_ik); + const next_ik_vec: IndexVector = @splat(next_ik); + + const row_i_start = i * n_usize; + const row_k_start = k * n_usize; + + // Process in SIMD chunks + var j: usize = 0; + while (j + VectorWidth <= n_usize) : (j += VectorWidth) { + // Load dist[k][j..j+VectorWidth] + const dist_kj_vec: DistVector = self.dist[row_k_start + j ..][0..VectorWidth].*; + + // Load current dist[i][j..j+VectorWidth] + const dist_ij_ptr = self.dist[row_i_start + j ..][0..VectorWidth]; + const dist_ij_vec: DistVector = dist_ij_ptr.*; + + // Load current next[i][j..j+VectorWidth] + const next_ij_ptr = self.next[row_i_start + j ..][0..VectorWidth]; + const next_ij_vec: IndexVector = next_ij_ptr.*; + + // Calculate new distances (saturating add) + const new_dist_vec = dist_ik_vec +| dist_kj_vec; + + // Compare: new_dist < dist_ij + const mask = new_dist_vec < dist_ij_vec; + + // Select: if mask then new_dist else dist_ij + dist_ij_ptr.* = @select(u32, mask, new_dist_vec, dist_ij_vec); + next_ij_ptr.* = @select(u32, mask, next_ik_vec, next_ij_vec); + } + + // Handle remaining elements + while (j < n_usize) : (j += 1) { + const dist_kj = self.dist[row_k_start + j]; + if (dist_kj == INF) continue; + + const new_dist = dist_ik +| dist_kj; + const idx = row_i_start + j; + if (new_dist < self.dist[idx]) { + self.dist[idx] = new_dist; + self.next[idx] = next_ik; + } + } + } + } + } + + /// Multi-threaded parallel implementation using row decomposition + /// Based on the semaphore-per-row synchronization pattern + fn generateParallel(self: *Self) void { + const n = self.size; + if (n == 0) return; + + // Determine thread count + const cpu_count = std.Thread.getCpuCount() catch 4; + const thread_count: usize = @min(cpu_count, n); + + // Fall back to SIMD for small graphs or single core + if (thread_count <= 1 or n < 32) { + self.generateSimd(); + return; + } + + // Allocate synchronization counters (one per k value + 1) + // Each counter tracks how many threads have signaled that row k is ready + const sync_counters = self.allocator.alloc(std.atomic.Value(u32), n + 1) catch { + self.generateSimd(); + return; + }; + defer self.allocator.free(sync_counters); + + // Initialize: first counter allows all threads to start, rest are 0 + sync_counters[0] = std.atomic.Value(u32).init(@intCast(thread_count)); + for (1..n + 1) |i| { + sync_counters[i] = std.atomic.Value(u32).init(0); + } + + // Calculate row distribution + const rows_per_thread = n / thread_count; + const extra_rows = n % thread_count; + + // Spawn worker threads - collect row assignments first + const threads = self.allocator.alloc(std.Thread, thread_count - 1) catch { + self.generateSimd(); + return; + }; + defer self.allocator.free(threads); + + const row_ranges = self.allocator.alloc([2]usize, thread_count) catch { + self.generateSimd(); + return; + }; + defer self.allocator.free(row_ranges); + + // Calculate all row assignments upfront + var next_row: usize = 0; + for (0..thread_count) |t| { + const start = next_row; + var end = start + rows_per_thread; + if (t < extra_rows) end += 1; + row_ranges[t] = .{ start, end }; + next_row = end; + } + + // Try to spawn all worker threads + var spawned_count: usize = 0; + var spawn_failed = false; + + for (0..thread_count - 1) |t| { + threads[t] = std.Thread.spawn(.{}, parallelWorker, .{ + self, + row_ranges[t][0], + row_ranges[t][1], + thread_count, + sync_counters, + }) catch { + spawn_failed = true; + break; + }; + spawned_count += 1; + } + + if (spawn_failed) { + // Some threads spawned but not all - we can't use the parallel algorithm + // because it requires exactly thread_count participants for synchronization. + // Signal all sync counters to unblock spawned threads so they complete quickly. + for (0..n + 1) |i| { + _ = sync_counters[i].fetchAdd(@intCast(thread_count), .release); + } + // Join all spawned threads + for (threads[0..spawned_count]) |t| { + t.join(); + } + // Redo with SIMD (threads did partial work but it's incomplete) + self.generateSimd(); + return; + } + + // Main thread processes its portion (last entry in row_ranges) + self.parallelWorkerImpl(row_ranges[thread_count - 1][0], row_ranges[thread_count - 1][1], thread_count, sync_counters); + + // Join all spawned threads + for (threads[0..spawned_count]) |t| { + t.join(); + } + } + + /// Worker function for parallel threads + fn parallelWorker( + self: *Self, + start_row: usize, + end_row: usize, + thread_count: usize, + sync_counters: []std.atomic.Value(u32), + ) void { + self.parallelWorkerImpl(start_row, end_row, thread_count, sync_counters); + } + + /// Implementation of parallel worker logic + fn parallelWorkerImpl( + self: *Self, + start_row: usize, + end_row: usize, + thread_count: usize, + sync_counters: []std.atomic.Value(u32), + ) void { + const n = self.size; + const thread_count_u32: u32 = @intCast(thread_count); + + for (0..n) |k| { + // Wait until row k is ready (counter reaches thread_count) + // Use a spin loop with exponential backoff + var spins: u32 = 0; + while (sync_counters[k].load(.acquire) < thread_count_u32) { + spins += 1; + if (spins < 100) { + std.atomic.spinLoopHint(); + } else { + // Yield to OS scheduler after spinning + std.Thread.yield() catch {}; + spins = 0; + } + } + + // Process our assigned rows for this k iteration (with SIMD) + // Note: Reading row k during iteration k is safe because dist[k][j] and + // dist[i][k] don't change during iteration k (Floyd-Warshall property: + // dist[k][k] = 0, so min(dist[k][j], dist[k][k] + dist[k][j]) = dist[k][j]) + for (start_row..end_row) |i| { + self.processRowSimd(k, i); + } + + // Full barrier: EVERY thread signals completion of iteration k, + // and iteration k+1 only starts once all `thread_count` threads + // have arrived (counter reaches thread_count). The previous code + // had only the owner of row k signal (adding thread_count once), + // which is NOT a barrier — a fast thread could enter iteration + // k+1 and read the next pivot row (k+1) while ITS owner was still + // updating it in iteration k, producing a `dist`/`next` matrix + // that's mutually inconsistent (cycles in `next`). See #13. + _ = sync_counters[k + 1].fetchAdd(1, .release); + } + } + + /// Process a single row with SIMD (for parallel use) + fn processRowSimd(self: *Self, k: usize, i: usize) void { + const n = self.size; + const n_usize: usize = n; + + const dist_ik = self.dist[i * n_usize + k]; + if (dist_ik == INF) return; + + const next_ik = self.next[i * n_usize + k]; + const dist_ik_vec: DistVector = @splat(dist_ik); + const next_ik_vec: IndexVector = @splat(next_ik); + + const row_i_start = i * n_usize; + const row_k_start = k * n_usize; + + // SIMD processing + var j: usize = 0; + while (j + VectorWidth <= n_usize) : (j += VectorWidth) { + const dist_kj_vec: DistVector = self.dist[row_k_start + j ..][0..VectorWidth].*; + const dist_ij_ptr = self.dist[row_i_start + j ..][0..VectorWidth]; + const dist_ij_vec: DistVector = dist_ij_ptr.*; + const next_ij_ptr = self.next[row_i_start + j ..][0..VectorWidth]; + const next_ij_vec: IndexVector = next_ij_ptr.*; + + const new_dist_vec = dist_ik_vec +| dist_kj_vec; + const mask = new_dist_vec < dist_ij_vec; + + dist_ij_ptr.* = @select(u32, mask, new_dist_vec, dist_ij_vec); + next_ij_ptr.* = @select(u32, mask, next_ik_vec, next_ij_vec); + } + + // Handle remaining elements + while (j < n_usize) : (j += 1) { + const dist_kj = self.dist[row_k_start + j]; + if (dist_kj == INF) continue; + + const new_dist = dist_ik +| dist_kj; + const idx = row_i_start + j; + if (new_dist < self.dist[idx]) { + self.dist[idx] = new_dist; + self.next[idx] = next_ik; + } + } + } + }; +} + +/// Parallel + SIMD optimized Floyd-Warshall (best for large graphs 256+ nodes) +/// Uses multi-threading with row decomposition and SIMD vectorization within each thread. +pub const FloydWarshallParallel = FloydWarshallOptimized(.{ + .parallel = true, + .simd = true, +}); + +/// SIMD-only version (no threading overhead for smaller graphs) +pub const FloydWarshallSimd = FloydWarshallOptimized(.{ + .parallel = false, + .simd = true, +}); + +/// Scalar version (for comparison/debugging) +pub const FloydWarshallScalar = FloydWarshallOptimized(.{ + .parallel = false, + .simd = false, +}); diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/heuristics.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/heuristics.zig new file mode 100644 index 0000000..808d5c2 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/heuristics.zig @@ -0,0 +1,103 @@ +//! Heuristic Functions for Pathfinding +//! +//! Provides multiple distance heuristics for A* algorithm optimization. +//! All heuristics use Position (Vector2) for coordinates. +//! +//! ## Available Heuristics +//! - **Euclidean**: Straight-line distance, best for any-angle movement +//! - **Manhattan**: Grid distance, best for 4-directional movement +//! - **Chebyshev**: Chessboard distance, best for 8-dir with equal diagonal cost +//! - **Octile**: Optimal 8-directional with sqrt(2) diagonal cost +//! - **Zero**: No heuristic (Dijkstra's algorithm) +//! +//! ## Heuristic Selection Guide +//! | Movement Type | Recommended Heuristic | +//! |---------------|----------------------| +//! | Free/any-angle | Euclidean | +//! | 4-directional grid | Manhattan | +//! | 8-dir, equal diagonal cost | Chebyshev | +//! | 8-dir, realistic diagonal | Octile | +//! | Unknown/mixed | Zero (safest) | + +const std = @import("std"); +const vector = @import("vector.zig"); + +pub const Position = vector.Position; + +/// Built-in heuristic types for A* pathfinding +pub const Heuristic = enum { + /// Straight-line distance: sqrt((x2-x1)^2 + (y2-y1)^2) + /// Best for: Any-angle movement, open spaces + /// Admissible: Always + euclidean, + + /// Grid distance: |x2-x1| + |y2-y1| + /// Best for: 4-directional grid movement + /// Admissible: For 4-directional movement only + manhattan, + + /// Chessboard distance: max(|x2-x1|, |y2-y1|) + /// Best for: 8-directional movement with equal diagonal cost + /// Admissible: For 8-directional with uniform cost + chebyshev, + + /// Optimal 8-directional: max(dx,dy) + (sqrt(2)-1) * min(dx,dy) + /// Best for: 8-directional movement where diagonal costs sqrt(2) + /// Admissible: For 8-directional with sqrt(2) diagonal cost + octile, + + /// No heuristic (always returns 0) + /// Effect: Degrades A* to Dijkstra's algorithm + /// Use when: You need guaranteed shortest path without heuristic assumptions + zero, +}; + +/// Custom heuristic function type for user-defined heuristics. +/// Must return an estimated cost from position `a` to position `b`. +/// For admissibility, the estimate must never exceed the actual cost. +pub const HeuristicFn = *const fn (a: Position, b: Position) f32; + +/// sqrt(2) - 1, precomputed for octile heuristic +pub const SQRT2_MINUS_1: f32 = std.math.sqrt2 - 1.0; + +/// Calculate heuristic distance between two positions using the specified heuristic type. +pub fn calculate(heuristic: Heuristic, a: Position, b: Position) f32 { + return switch (heuristic) { + .euclidean => euclidean(a, b), + .manhattan => manhattan(a, b), + .chebyshev => chebyshev(a, b), + .octile => octile(a, b), + .zero => 0, + }; +} + +/// Euclidean (straight-line) distance +pub fn euclidean(a: Position, b: Position) f32 { + const dx = b.x - a.x; + const dy = b.y - a.y; + return @sqrt(dx * dx + dy * dy); +} + +/// Squared Euclidean distance (faster, avoids sqrt) +pub fn euclideanSquared(a: Position, b: Position) f32 { + const dx = b.x - a.x; + const dy = b.y - a.y; + return dx * dx + dy * dy; +} + +/// Manhattan (taxicab) distance +pub fn manhattan(a: Position, b: Position) f32 { + return @abs(b.x - a.x) + @abs(b.y - a.y); +} + +/// Chebyshev (chessboard) distance +pub fn chebyshev(a: Position, b: Position) f32 { + return @max(@abs(b.x - a.x), @abs(b.y - a.y)); +} + +/// Octile distance for 8-directional movement +pub fn octile(a: Position, b: Position) f32 { + const dx = @abs(b.x - a.x); + const dy = @abs(b.y - a.y); + return @max(dx, dy) + SQRT2_MINUS_1 * @min(dx, dy); +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/hook_dispatcher.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/hook_dispatcher.zig new file mode 100644 index 0000000..133f5ed --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/hook_dispatcher.zig @@ -0,0 +1,200 @@ +//! Hook Dispatcher +//! +//! Provides a comptime-based hook dispatcher for zero-overhead event handling. +//! Hooks are resolved entirely at compile time, with no runtime overhead. + +const std = @import("std"); + +/// Creates a hook dispatcher from a comptime hook map. +/// +/// The HookMap should be a struct type where each public declaration is either: +/// - A function matching the signature for that hook +/// - A function name matching a hook name (e.g., `game_init`, `scene_load`) +/// +/// Example: +/// ```zig +/// const MyHooks = struct { +/// pub fn game_init(payload: HookPayload) void { +/// // Handle game init +/// } +/// +/// pub fn scene_load(payload: HookPayload) void { +/// const info = payload.scene_load; +/// std.log.info("Scene loaded: {s}", .{info.name}); +/// } +/// }; +/// +/// const Dispatcher = HookDispatcher(EngineHook, HookPayload, MyHooks); +/// Dispatcher.emit(.{ .scene_load = .{ .name = "main" } }); +/// ``` +pub fn HookDispatcher( + comptime HookEnum: type, + comptime PayloadUnion: type, + comptime HookMap: type, +) type { + // Validate that PayloadUnion is a union tagged by HookEnum + const payload_info = @typeInfo(PayloadUnion); + if (payload_info != .@"union") { + @compileError("PayloadUnion must be a union type"); + } + if (payload_info.@"union".tag_type != HookEnum) { + @compileError("PayloadUnion must be tagged by HookEnum"); + } + + return struct { + const Self = @This(); + + /// The hook enum type this dispatcher handles. + pub const Hook = HookEnum; + + /// The payload union type this dispatcher handles. + pub const Payload = PayloadUnion; + + /// The hook handler map type. + pub const Handlers = HookMap; + + /// Emit a hook event. Resolved entirely at comptime - no runtime overhead. + /// + /// If no handler is registered for the hook, this is a no-op. + pub inline fn emit(payload: PayloadUnion) void { + // Use inline switch to resolve hook name at comptime + switch (payload) { + inline else => |_, tag| { + const hook_name = @tagName(tag); + if (@hasDecl(HookMap, hook_name)) { + const handler = @field(HookMap, hook_name); + handler(payload); + } + // No handler registered - that's fine, just a no-op + }, + } + } + + /// Check at comptime if a hook has a handler registered. + pub fn hasHandler(comptime hook: HookEnum) bool { + return @hasDecl(HookMap, @tagName(hook)); + } + + /// Get the number of hooks that have handlers registered. + pub fn handlerCount() comptime_int { + var count: comptime_int = 0; + for (std.enums.values(HookEnum)) |hook| { + if (@hasDecl(HookMap, @tagName(hook))) { + count += 1; + } + } + return count; + } + }; +} + +/// Creates an empty hook dispatcher with no handlers. +/// Useful as a default when no hooks are needed. +pub fn EmptyDispatcher(comptime HookEnum: type, comptime PayloadUnion: type) type { + return HookDispatcher(HookEnum, PayloadUnion, struct {}); +} + +/// Merges multiple hook handler structs into one composite dispatcher. +/// When a hook is emitted, all matching handlers from all structs are called in order. +/// +/// This enables two-way plugin binding: +/// - Plugins can provide engine hook handlers that get merged with game hooks +/// - Each handler struct can implement any subset of hooks +/// +/// Example: +/// ```zig +/// const GameHooks = struct { +/// pub fn game_init(_: HookPayload) void { +/// std.log.info("Game started!", .{}); +/// } +/// }; +/// +/// const PluginHooks = struct { +/// pub fn game_init(_: HookPayload) void { +/// std.log.info("Plugin initialized!", .{}); +/// } +/// pub fn frame_start(payload: HookPayload) void { +/// // Plugin frame logic +/// } +/// }; +/// +/// // Merge game + plugin hooks - both game_init handlers will be called +/// const AllHooks = MergeHooks(EngineHook, HookPayload, .{ GameHooks, PluginHooks }); +/// const Game = engine.GameWith(AllHooks); +/// ``` +pub fn MergeHooks( + comptime HookEnum: type, + comptime PayloadUnion: type, + comptime handler_structs: anytype, +) type { + // Validate that PayloadUnion is a union tagged by HookEnum + const payload_info = @typeInfo(PayloadUnion); + if (payload_info != .@"union") { + @compileError("PayloadUnion must be a union type"); + } + if (payload_info.@"union".tag_type != HookEnum) { + @compileError("PayloadUnion must be tagged by HookEnum"); + } + + return struct { + const Self = @This(); + + /// The hook enum type this dispatcher handles. + pub const Hook = HookEnum; + + /// The payload union type this dispatcher handles. + pub const Payload = PayloadUnion; + + /// Emit a hook event to all registered handlers. + /// Handlers are called in the order the structs appear in handler_structs. + /// If no handler is registered for the hook in any struct, this is a no-op. + pub inline fn emit(payload: PayloadUnion) void { + switch (payload) { + inline else => |_, tag| { + const hook_name = @tagName(tag); + inline for (handler_structs) |H| { + if (@hasDecl(H, hook_name)) { + const handler = @field(H, hook_name); + handler(payload); + } + } + }, + } + } + + /// Check at comptime if any handler struct has a handler for this hook. + pub fn hasHandler(comptime hook: HookEnum) bool { + inline for (handler_structs) |H| { + if (@hasDecl(H, @tagName(hook))) { + return true; + } + } + return false; + } + + /// Get the number of unique hooks that have at least one handler registered. + pub fn handlerCount() comptime_int { + var count: comptime_int = 0; + for (std.enums.values(HookEnum)) |hook| { + if (hasHandler(hook)) { + count += 1; + } + } + return count; + } + + /// Get the total number of handlers across all structs (including duplicates). + pub fn totalHandlerCount() comptime_int { + var count: comptime_int = 0; + for (std.enums.values(HookEnum)) |hook| { + const hook_name = @tagName(hook); + inline for (handler_structs) |H| { + if (@hasDecl(H, hook_name)) { + count += 1; + } + } + } + return count; + } + }; +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/quad_tree.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/quad_tree.zig new file mode 100644 index 0000000..ddd99c3 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/quad_tree.zig @@ -0,0 +1,437 @@ +//! QuadTree spatial partitioning data structure +//! +//! Provides O(log n) spatial queries for points in 2D space. +//! Used for efficient entity lookups and collision detection. + +const std = @import("std"); +const vector = @import("vector.zig"); +const Position = vector.Position; + +/// Axis-aligned bounding box +pub const Rectangle = struct { + x: f32, + y: f32, + width: f32, + height: f32, + + pub const zero = Rectangle{ .x = 0, .y = 0, .width = 0, .height = 0 }; + + /// Create a rectangle from position and size + pub fn fromPosition(pos: Position, width: f32, height: f32) Rectangle { + return .{ .x = pos.x, .y = pos.y, .width = width, .height = height }; + } + + /// Create a rectangle centered on a position + pub fn centered(pos: Position, width: f32, height: f32) Rectangle { + return .{ + .x = pos.x - width / 2, + .y = pos.y - height / 2, + .width = width, + .height = height, + }; + } + + /// Get the center position + pub fn center(self: Rectangle) Position { + return .{ .x = self.x + self.width / 2, .y = self.y + self.height / 2 }; + } + + /// Get the top-left position + pub fn position(self: Rectangle) Position { + return .{ .x = self.x, .y = self.y }; + } + + pub fn contains(self: Rectangle, px: f32, py: f32) bool { + return px >= self.x and px < self.x + self.width and + py >= self.y and py < self.y + self.height; + } + + pub fn containsPosition(self: Rectangle, pos: Position) bool { + return self.contains(pos.x, pos.y); + } + + pub fn intersects(self: Rectangle, other: Rectangle) bool { + return !(other.x >= self.x + self.width or + other.x + other.width <= self.x or + other.y >= self.y + self.height or + other.y + other.height <= self.y); + } +}; + +/// A point with an associated identifier +pub fn EntityPoint(comptime T: type) type { + return struct { + id: T, + pos: Position, + + const Self = @This(); + + pub fn init(id: T, x: f32, y: f32) Self { + return .{ .id = id, .pos = .{ .x = x, .y = y } }; + } + + pub fn fromPosition(id: T, pos: Position) Self { + return .{ .id = id, .pos = pos }; + } + }; +} + +/// QuadTree node for internal storage +fn QuadTreeNode(comptime T: type, comptime capacity: u32) type { + const Point = EntityPoint(T); + + return struct { + total_elements: u32 = 0, + points: [capacity]Point = undefined, + boundary: Rectangle, + divided: bool = false, + nw: u32 = 0, + ne: u32 = 0, + sw: u32 = 0, + se: u32 = 0, + }; +} + +/// QuadTree configuration +pub const QuadTreeConfig = struct { + /// Maximum points per node before subdivision (default: 4) + capacity: u32 = 4, + /// Boundary margin for auto-computed bounds (default: 120.0) + gutter: f32 = 120.0, +}; + +/// QuadTree for efficient spatial partitioning and queries +/// +/// Generic over: +/// - T: ID type for flexibility (u32, u64, custom types) +/// - config: Comptime configuration for capacity and gutter +pub fn QuadTree(comptime T: type, comptime config: QuadTreeConfig) type { + const Point = EntityPoint(T); + const Node = QuadTreeNode(T, config.capacity); + + return struct { + const Self = @This(); + + nodes: std.ArrayListUnmanaged(Node), + + lowest_x: f32 = 0.0, + lowest_y: f32 = 0.0, + highest_x: f32 = 0.0, + highest_y: f32 = 0.0, + + original_boundary: Rectangle, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator, boundary: Rectangle) !Self { + var qt = Self{ + .nodes = .empty, + .original_boundary = boundary, + .allocator = allocator, + .lowest_x = std.math.inf(f32), + .lowest_y = std.math.inf(f32), + .highest_x = -std.math.inf(f32), + .highest_y = -std.math.inf(f32), + }; + try qt.nodes.append(allocator, .{ .boundary = boundary }); + return qt; + } + + pub fn deinit(self: *Self) void { + self.nodes.deinit(self.allocator); + } + + /// Clear all points, keeping the original boundary from init() + pub fn clear(self: *Self) !void { + try self.resize(self.original_boundary); + } + + /// Resize the tree to a new boundary, clearing all points + pub fn resize(self: *Self, boundary: Rectangle) !void { + self.nodes.clearRetainingCapacity(); + self.lowest_x = std.math.inf(f32); + self.lowest_y = std.math.inf(f32); + self.highest_x = -std.math.inf(f32); + self.highest_y = -std.math.inf(f32); + self.original_boundary = boundary; + try self.nodes.append(self.allocator, .{ .boundary = boundary }); + } + + /// Clear the tree and reset with new boundary computed from positions + pub fn resetWithBoundaries(self: *Self, positions: []const Position) !void { + self.nodes.clearRetainingCapacity(); + self.lowest_x = std.math.inf(f32); + self.lowest_y = std.math.inf(f32); + self.highest_x = -std.math.inf(f32); + self.highest_y = -std.math.inf(f32); + + for (positions) |pos| { + if (pos.x < self.lowest_x) self.lowest_x = pos.x; + if (pos.y < self.lowest_y) self.lowest_y = pos.y; + if (pos.x > self.highest_x) self.highest_x = pos.x; + if (pos.y > self.highest_y) self.highest_y = pos.y; + } + + try self.nodes.append(self.allocator, .{ .boundary = .{ + .x = self.lowest_x - config.gutter, + .y = self.lowest_y - config.gutter, + .width = (self.highest_x - self.lowest_x) + config.gutter * 2, + .height = (self.highest_y - self.lowest_y) + config.gutter * 2, + } }); + } + + /// Clear the tree keeping current boundaries + pub fn reset(self: *Self) !void { + const boundary = Rectangle{ + .x = self.lowest_x - config.gutter, + .y = self.lowest_y - config.gutter, + .width = (self.highest_x - self.lowest_x) + config.gutter * 2, + .height = (self.highest_y - self.lowest_y) + config.gutter * 2, + }; + self.nodes.clearRetainingCapacity(); + self.lowest_x = std.math.inf(f32); + self.lowest_y = std.math.inf(f32); + self.highest_x = -std.math.inf(f32); + self.highest_y = -std.math.inf(f32); + try self.nodes.append(self.allocator, .{ .boundary = boundary }); + } + + /// Insert a point into the tree + pub fn insert(self: *Self, point: Point) bool { + if (point.pos.x < self.lowest_x) self.lowest_x = point.pos.x; + if (point.pos.y < self.lowest_y) self.lowest_y = point.pos.y; + if (point.pos.x > self.highest_x) self.highest_x = point.pos.x; + if (point.pos.y > self.highest_y) self.highest_y = point.pos.y; + return self.insertAt(point, 0); + } + + /// Insert at position + pub fn insertAt(self: *Self, point: Point, node_idx: u32) bool { + if (!self.nodes.items[node_idx].boundary.containsPosition(point.pos)) { + return false; + } + + if (self.nodes.items[node_idx].total_elements < config.capacity and !self.nodes.items[node_idx].divided) { + self.nodes.items[node_idx].points[self.nodes.items[node_idx].total_elements] = point; + self.nodes.items[node_idx].total_elements += 1; + return true; + } + + if (!self.nodes.items[node_idx].divided) { + self.subdivide(node_idx) catch |err| { + std.log.err("Error subdividing: {}\n", .{err}); + return false; + }; + } + + if (self.insertAt(point, self.nodes.items[node_idx].nw)) return true; + if (self.insertAt(point, self.nodes.items[node_idx].ne)) return true; + if (self.insertAt(point, self.nodes.items[node_idx].sw)) return true; + if (self.insertAt(point, self.nodes.items[node_idx].se)) return true; + + return false; + } + + fn subdivide(self: *Self, node_idx: u32) !void { + const boundary = self.nodes.items[node_idx].boundary; + const half_width = boundary.width / 2.0; + const half_height = boundary.height / 2.0; + const x = boundary.x; + const y = boundary.y; + + self.nodes.items[node_idx].nw = @intCast(self.nodes.items.len); + try self.nodes.append(self.allocator, .{ .boundary = .{ .x = x, .y = y, .width = half_width, .height = half_height } }); + + self.nodes.items[node_idx].ne = @intCast(self.nodes.items.len); + try self.nodes.append(self.allocator, .{ .boundary = .{ .x = x + half_width, .y = y, .width = half_width, .height = half_height } }); + + self.nodes.items[node_idx].sw = @intCast(self.nodes.items.len); + try self.nodes.append(self.allocator, .{ .boundary = .{ .x = x, .y = y + half_height, .width = half_width, .height = half_height } }); + + self.nodes.items[node_idx].se = @intCast(self.nodes.items.len); + try self.nodes.append(self.allocator, .{ .boundary = .{ .x = x + half_width, .y = y + half_height, .width = half_width, .height = half_height } }); + + self.nodes.items[node_idx].divided = true; + } + + /// Query all points within a rectangle + pub fn queryRect(self: *Self, range: Rectangle, buffer: *std.ArrayListUnmanaged(Point)) !void { + try self.queryRectAt(range, buffer, 0); + } + + fn queryRectAt(self: *Self, range: Rectangle, buffer: *std.ArrayListUnmanaged(Point), node_idx: u32) !void { + if (!self.nodes.items[node_idx].boundary.intersects(range)) { + return; + } + + for (0..self.nodes.items[node_idx].total_elements) |i| { + const p = self.nodes.items[node_idx].points[i]; + if (range.containsPosition(p.pos)) { + try buffer.append(self.allocator, p); + } + } + + if (self.nodes.items[node_idx].divided) { + try self.queryRectAt(range, buffer, self.nodes.items[node_idx].nw); + try self.queryRectAt(range, buffer, self.nodes.items[node_idx].ne); + try self.queryRectAt(range, buffer, self.nodes.items[node_idx].sw); + try self.queryRectAt(range, buffer, self.nodes.items[node_idx].se); + } + } + + /// Query all points within a radius of a center position + pub fn queryRadius(self: *Self, center: Position, radius: f32, buffer: *std.ArrayListUnmanaged(Point)) !void { + const range = Rectangle{ + .x = center.x - radius, + .y = center.y - radius, + .width = radius * 2, + .height = radius * 2, + }; + try self.queryRadiusAt(range, center, radius * radius, buffer, 0); + } + + fn queryRadiusAt(self: *Self, range: Rectangle, center: Position, radius_sq: f32, buffer: *std.ArrayListUnmanaged(Point), node_idx: u32) !void { + if (!self.nodes.items[node_idx].boundary.intersects(range)) { + return; + } + + for (0..self.nodes.items[node_idx].total_elements) |i| { + const p = self.nodes.items[node_idx].points[i]; + if (p.pos.distanceSquared(center) <= radius_sq) { + try buffer.append(self.allocator, p); + } + } + + if (self.nodes.items[node_idx].divided) { + try self.queryRadiusAt(range, center, radius_sq, buffer, self.nodes.items[node_idx].nw); + try self.queryRadiusAt(range, center, radius_sq, buffer, self.nodes.items[node_idx].ne); + try self.queryRadiusAt(range, center, radius_sq, buffer, self.nodes.items[node_idx].sw); + try self.queryRadiusAt(range, center, radius_sq, buffer, self.nodes.items[node_idx].se); + } + } + + /// Find the nearest point to a position within max_distance + pub fn queryNearest(self: *Self, pos: Position, max_distance: f32) ?Point { + var nearest: ?Point = null; + var nearest_dist: f32 = max_distance; + self.findNearestAt(pos, &nearest, &nearest_dist, 0); + return nearest; + } + + fn findNearestAt(self: *Self, pos: Position, nearest: *?Point, nearest_dist: *f32, node_idx: u32) void { + // Check points in this node + for (0..self.nodes.items[node_idx].total_elements) |i| { + const p = self.nodes.items[node_idx].points[i]; + const dist = p.pos.distance(pos); + if (dist < nearest_dist.*) { + nearest_dist.* = dist; + nearest.* = p; + } + } + + // Check children with pruning + if (self.nodes.items[node_idx].divided) { + const children = [4]u32{ + self.nodes.items[node_idx].nw, + self.nodes.items[node_idx].ne, + self.nodes.items[node_idx].sw, + self.nodes.items[node_idx].se, + }; + for (children) |child_idx| { + if (self.minDistToBounds(pos, child_idx) < nearest_dist.*) { + self.findNearestAt(pos, nearest, nearest_dist, child_idx); + } + } + } + } + + fn minDistToBounds(self: *Self, pos: Position, node_idx: u32) f32 { + const bounds = self.nodes.items[node_idx].boundary; + const closest_x = std.math.clamp(pos.x, bounds.x, bounds.x + bounds.width); + const closest_y = std.math.clamp(pos.y, bounds.y, bounds.y + bounds.height); + const dx = closest_x - pos.x; + const dy = closest_y - pos.y; + return @sqrt(dx * dx + dy * dy); + } + + /// Check if any point exists within a rectangle + pub fn hasPointInRect(self: *Self, range: Rectangle) bool { + return self.hasPointInRectAt(range, 0); + } + + fn hasPointInRectAt(self: *Self, range: Rectangle, node_idx: u32) bool { + if (!self.nodes.items[node_idx].boundary.intersects(range)) { + return false; + } + + for (0..self.nodes.items[node_idx].total_elements) |i| { + const p = self.nodes.items[node_idx].points[i]; + if (range.containsPosition(p.pos)) { + return true; + } + } + + if (self.nodes.items[node_idx].divided) { + if (self.hasPointInRectAt(range, self.nodes.items[node_idx].nw)) return true; + if (self.hasPointInRectAt(range, self.nodes.items[node_idx].ne)) return true; + if (self.hasPointInRectAt(range, self.nodes.items[node_idx].sw)) return true; + if (self.hasPointInRectAt(range, self.nodes.items[node_idx].se)) return true; + } + + return false; + } + + /// Remove a point by ID (searches entire tree) + pub fn remove(self: *Self, id: T) bool { + return self.removeAt(id, 0); + } + + fn removeAt(self: *Self, id: T, node_idx: u32) bool { + var i: u32 = 0; + while (i < self.nodes.items[node_idx].total_elements) { + if (std.meta.eql(self.nodes.items[node_idx].points[i].id, id)) { + self.nodes.items[node_idx].total_elements -= 1; + if (i < self.nodes.items[node_idx].total_elements) { + self.nodes.items[node_idx].points[i] = self.nodes.items[node_idx].points[self.nodes.items[node_idx].total_elements]; + } + return true; + } + i += 1; + } + + if (self.nodes.items[node_idx].divided) { + if (self.removeAt(id, self.nodes.items[node_idx].nw)) return true; + if (self.removeAt(id, self.nodes.items[node_idx].ne)) return true; + if (self.removeAt(id, self.nodes.items[node_idx].sw)) return true; + if (self.removeAt(id, self.nodes.items[node_idx].se)) return true; + } + + return false; + } + + /// Update position of an existing point + pub fn update(self: *Self, id: T, new_pos: Position) bool { + if (self.remove(id)) { + return self.insert(.{ .id = id, .pos = new_pos }); + } + return false; + } + + /// Count total points in the tree + pub fn count(self: *Self) usize { + return self.countAt(0); + } + + fn countAt(self: *Self, node_idx: u32) usize { + var total: usize = self.nodes.items[node_idx].total_elements; + + if (self.nodes.items[node_idx].divided) { + total += self.countAt(self.nodes.items[node_idx].nw); + total += self.countAt(self.nodes.items[node_idx].ne); + total += self.countAt(self.nodes.items[node_idx].sw); + total += self.countAt(self.nodes.items[node_idx].se); + } + + return total; + } + }; +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/root.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/root.zig new file mode 100644 index 0000000..583a700 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/root.zig @@ -0,0 +1,55 @@ +// zig-utils - Standalone utilities for Zig +// No external dependencies, only std + +pub const vector = @import("vector.zig"); +pub const Position = vector.Position; +pub const PositionI = vector.PositionI; + +pub const quad_tree = @import("quad_tree.zig"); +pub const QuadTree = quad_tree.QuadTree; +pub const QuadTreeConfig = quad_tree.QuadTreeConfig; +pub const EntityPoint = quad_tree.EntityPoint; +pub const Rectangle = quad_tree.Rectangle; + +pub const sweep_and_prune = @import("sweep_and_prune.zig"); +pub const SweepAndPrune = sweep_and_prune.SweepAndPrune; +pub const AABB = sweep_and_prune.AABB; +pub const CollisionPair = sweep_and_prune.CollisionPair; +pub const sweepAndPruneSimple = sweep_and_prune.sweepAndPrune; + +pub const sparse_set = @import("sparse_set.zig"); +pub const SparseSet = sparse_set.SparseSet; + +pub const z_index_buckets = @import("z_index_buckets.zig"); +pub const ZIndexBuckets = z_index_buckets.ZIndexBuckets; + +pub const zon = @import("zon_coercion.zig"); +pub const coerceValue = zon.coerceValue; +pub const buildStruct = zon.buildStruct; +pub const tupleToSlice = zon.tupleToSlice; +pub const mergeStructs = zon.mergeStructs; + +pub const hooks = @import("hook_dispatcher.zig"); +pub const HookDispatcher = hooks.HookDispatcher; +pub const EmptyDispatcher = hooks.EmptyDispatcher; +pub const MergeHooks = hooks.MergeHooks; + +// Graph algorithms +pub const floyd_warshall = @import("floyd_warshall.zig"); +pub const FloydWarshall = floyd_warshall.FloydWarshall; + +pub const floyd_warshall_optimized = @import("floyd_warshall_optimized.zig"); +pub const FloydWarshallOptimized = floyd_warshall_optimized.FloydWarshallOptimized; +pub const FloydWarshallConfig = floyd_warshall_optimized.Config; +pub const FloydWarshallParallel = floyd_warshall_optimized.FloydWarshallParallel; +pub const FloydWarshallSimd = floyd_warshall_optimized.FloydWarshallSimd; +pub const FloydWarshallScalar = floyd_warshall_optimized.FloydWarshallScalar; + +pub const a_star = @import("a_star.zig"); +pub const AStar = a_star.AStar; + +pub const heuristics = @import("heuristics.zig"); +pub const Heuristic = heuristics.Heuristic; + +// Backwards compatibility alias (deprecated) +pub const Vector2 = Position; diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/sparse_set.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/sparse_set.zig new file mode 100644 index 0000000..a50faaa --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/sparse_set.zig @@ -0,0 +1,190 @@ +//! Sparse Set +//! +//! O(1) lookup, insert, remove with cache-friendly iteration. +//! Used for entity -> physics body mappings. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Generic sparse set for mapping keys to values +/// KeyType must be an unsigned integer type (u8, u16, u32, u64, usize) +pub fn SparseSet(comptime KeyType: type, comptime T: type) type { + comptime { + const info = @typeInfo(KeyType); + if (info != .int or info.int.signedness != .unsigned) { + @compileError("KeyType must be an unsigned integer type"); + } + } + + return struct { + const Self = @This(); + + allocator: Allocator, + sparse: []?u32, // key -> dense_index + dense_keys: []KeyType, // dense_index -> key + dense_values: []T, // dense_index -> value + count: usize, + capacity: usize, + max_key: usize, + + pub fn init(allocator: Allocator, max_keys: usize, initial_capacity: usize) !Self { + const sparse = try allocator.alloc(?u32, max_keys); + errdefer allocator.free(sparse); + @memset(sparse, null); + + const dense_keys = try allocator.alloc(KeyType, initial_capacity); + errdefer allocator.free(dense_keys); + + const dense_values = try allocator.alloc(T, initial_capacity); + errdefer allocator.free(dense_values); + + return Self{ + .allocator = allocator, + .sparse = sparse, + .dense_keys = dense_keys, + .dense_values = dense_values, + .count = 0, + .capacity = initial_capacity, + .max_key = max_keys, + }; + } + + pub fn deinit(self: *Self) void { + self.allocator.free(self.sparse); + self.allocator.free(self.dense_keys); + self.allocator.free(self.dense_values); + } + + /// Insert or update a key-value pair + pub fn put(self: *Self, key: KeyType, value: T) !void { + if (key >= self.max_key) return error.KeyOutOfRange; + + // Update existing + if (self.sparse[key]) |idx| { + self.dense_values[idx] = value; + return; + } + + // Grow if needed + if (self.count >= self.capacity) { + const new_cap = if (self.capacity == 0) 4 else self.capacity * 2; + try self.grow(new_cap); + } + + if (self.count >= std.math.maxInt(u32)) return error.CapacityExceeded; + const idx: u32 = @intCast(self.count); + self.sparse[key] = idx; + self.dense_keys[idx] = key; + self.dense_values[idx] = value; + self.count += 1; + } + + /// Atomically grow both dense arrays + fn grow(self: *Self, new_cap: usize) !void { + // Allocate new arrays first (no state change yet) + const new_keys = try self.allocator.alloc(KeyType, new_cap); + errdefer self.allocator.free(new_keys); + + const new_values = try self.allocator.alloc(T, new_cap); + + // Copy existing data + @memcpy(new_keys[0..self.count], self.dense_keys[0..self.count]); + @memcpy(new_values[0..self.count], self.dense_values[0..self.count]); + + // Free old arrays + self.allocator.free(self.dense_keys); + self.allocator.free(self.dense_values); + + // Update state atomically + self.dense_keys = new_keys; + self.dense_values = new_values; + self.capacity = new_cap; + } + + /// Get value for key + pub fn get(self: *const Self, key: KeyType) ?T { + if (key >= self.max_key) return null; + const idx = self.sparse[key] orelse return null; + return self.dense_values[idx]; + } + + /// Get pointer to value for key + pub fn getPtr(self: *Self, key: KeyType) ?*T { + if (key >= self.max_key) return null; + const idx = self.sparse[key] orelse return null; + return &self.dense_values[idx]; + } + + /// Check if key exists + pub fn contains(self: *const Self, key: KeyType) bool { + if (key >= self.max_key) return false; + return self.sparse[key] != null; + } + + /// Remove key-value pair + pub fn remove(self: *Self, key: KeyType) void { + if (key >= self.max_key) return; + const idx = self.sparse[key] orelse return; + + // Swap with last element + const last_idx = self.count - 1; + if (idx != last_idx) { + const last_key = self.dense_keys[last_idx]; + self.dense_keys[idx] = last_key; + self.dense_values[idx] = self.dense_values[last_idx]; + self.sparse[last_key] = idx; + } + + self.sparse[key] = null; + self.count -= 1; + } + + /// Clear all entries + pub fn clear(self: *Self) void { + for (self.dense_keys[0..self.count]) |key| { + self.sparse[key] = null; + } + self.count = 0; + } + + /// Iterate over all values (cache-friendly) + pub fn values(self: *const Self) []const T { + return self.dense_values[0..self.count]; + } + + /// Iterate over all keys + pub fn keys(self: *const Self) []const KeyType { + return self.dense_keys[0..self.count]; + } + + /// Get key-value pairs for iteration + pub const Entry = struct { + key: KeyType, + value: *T, + }; + + pub fn iterator(self: *Self) Iterator { + return .{ .set = self, .index = 0 }; + } + + pub const Iterator = struct { + set: *Self, + index: usize, + + pub fn next(self: *Iterator) ?Entry { + if (self.index >= self.set.count) return null; + const entry = Entry{ + .key = self.set.dense_keys[self.index], + .value = &self.set.dense_values[self.index], + }; + self.index += 1; + return entry; + } + }; + + /// Number of entries + pub fn len(self: *const Self) usize { + return self.count; + } + }; +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/sweep_and_prune.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/sweep_and_prune.zig new file mode 100644 index 0000000..c96e504 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/sweep_and_prune.zig @@ -0,0 +1,283 @@ +//! Sweep and Prune (Sort and Sweep) broad-phase collision detection +//! +//! Efficient O(n log n) algorithm for finding overlapping axis-aligned bounding boxes. +//! Best suited for scenes with many objects where most don't overlap. + +const std = @import("std"); +const vector = @import("vector.zig"); +const Position = vector.Position; + +/// Axis-aligned bounding box for sweep and prune +pub const AABB = struct { + id: u32, + pos: Position, + half_width: f32, + half_height: f32, + + pub fn init(id: u32, center: Position, half_width: f32, half_height: f32) AABB { + return .{ + .id = id, + .pos = center, + .half_width = half_width, + .half_height = half_height, + }; + } + + pub fn minX(self: AABB) f32 { + return self.pos.x - self.half_width; + } + + pub fn maxX(self: AABB) f32 { + return self.pos.x + self.half_width; + } + + pub fn minY(self: AABB) f32 { + return self.pos.y - self.half_height; + } + + pub fn maxY(self: AABB) f32 { + return self.pos.y + self.half_height; + } + + pub fn overlaps(self: AABB, other: AABB) bool { + return self.minX() < other.maxX() and + self.maxX() > other.minX() and + self.minY() < other.maxY() and + self.maxY() > other.minY(); + } +}; + +/// A collision pair +pub const CollisionPair = struct { + a: u32, + b: u32, + + pub fn init(id_a: u32, id_b: u32) CollisionPair { + // Ensure consistent ordering for deduplication + return if (id_a < id_b) + .{ .a = id_a, .b = id_b } + else + .{ .a = id_b, .b = id_a }; + } +}; + +/// Sweep and Prune collision detection system +pub fn SweepAndPrune(comptime T: type) type { + return struct { + const Self = @This(); + + /// Entity with bounding box + pub const Entity = struct { + id: T, + pos: Position, + half_width: f32, + half_height: f32, + + pub fn minX(self: Entity) f32 { + return self.pos.x - self.half_width; + } + + pub fn maxX(self: Entity) f32 { + return self.pos.x + self.half_width; + } + + pub fn minY(self: Entity) f32 { + return self.pos.y - self.half_height; + } + + pub fn maxY(self: Entity) f32 { + return self.pos.y + self.half_height; + } + + pub fn overlaps(self: Entity, other: Entity) bool { + return self.minX() < other.maxX() and + self.maxX() > other.minX() and + self.minY() < other.maxY() and + self.maxY() > other.minY(); + } + }; + + /// Collision pair with generic ID + pub const Pair = struct { + a: T, + b: T, + }; + + entities: std.ArrayListUnmanaged(Entity), + sorted_indices: std.ArrayListUnmanaged(usize), + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .entities = .empty, + .sorted_indices = .empty, + .allocator = allocator, + }; + } + + pub fn deinit(self: *Self) void { + self.entities.deinit(self.allocator); + self.sorted_indices.deinit(self.allocator); + } + + /// Clear all entities + pub fn clear(self: *Self) void { + self.entities.clearRetainingCapacity(); + self.sorted_indices.clearRetainingCapacity(); + } + + /// Add an entity to the system + pub fn add(self: *Self, id: T, pos: Position, half_width: f32, half_height: f32) !void { + try self.entities.append(self.allocator, .{ + .id = id, + .pos = pos, + .half_width = half_width, + .half_height = half_height, + }); + } + + /// Update an entity's position + pub fn updatePosition(self: *Self, id: T, new_pos: Position) void { + for (self.entities.items) |*entity| { + if (std.meta.eql(entity.id, id)) { + entity.pos = new_pos; + return; + } + } + } + + /// Remove an entity by ID + pub fn remove(self: *Self, id: T) bool { + for (self.entities.items, 0..) |entity, i| { + if (std.meta.eql(entity.id, id)) { + _ = self.entities.swapRemove(i); + return true; + } + } + return false; + } + + /// Find all collision pairs using sweep and prune + pub fn findCollisions(self: *Self, pairs: *std.ArrayListUnmanaged(Pair)) !void { + const n = self.entities.items.len; + if (n < 2) return; + + // Build sorted indices by minX + self.sorted_indices.clearRetainingCapacity(); + try self.sorted_indices.ensureTotalCapacity(self.allocator, n); + for (0..n) |i| { + try self.sorted_indices.append(self.allocator, i); + } + + // Sort by minX + const entities = self.entities.items; + std.mem.sort(usize, self.sorted_indices.items, entities, struct { + fn lessThan(ents: []const Entity, a: usize, b: usize) bool { + return ents[a].minX() < ents[b].minX(); + } + }.lessThan); + + // Sweep + for (self.sorted_indices.items, 0..) |i, idx| { + const entity_a = entities[i]; + const max_x_a = entity_a.maxX(); + + // Check all entities that could potentially overlap + for (self.sorted_indices.items[idx + 1 ..]) |j| { + const entity_b = entities[j]; + + // If entity_b starts after entity_a ends, no more overlaps possible + if (entity_b.minX() >= max_x_a) break; + + // Check Y overlap + if (entity_a.overlaps(entity_b)) { + try pairs.append(self.allocator, .{ + .a = entity_a.id, + .b = entity_b.id, + }); + } + } + } + } + + /// Find all entities overlapping with a given AABB + pub fn queryRect(self: *Self, center: Position, half_width: f32, half_height: f32, results: *std.ArrayListUnmanaged(T)) !void { + const query = Entity{ + .id = undefined, + .pos = center, + .half_width = half_width, + .half_height = half_height, + }; + + for (self.entities.items) |entity| { + if (query.overlaps(entity)) { + try results.append(self.allocator, entity.id); + } + } + } + + /// Find all entities within a radius of a position + pub fn queryRadius(self: *Self, center: Position, radius: f32, results: *std.ArrayListUnmanaged(T)) !void { + const radius_sq = radius * radius; + + for (self.entities.items) |entity| { + // Quick AABB check first + const dx = @abs(entity.pos.x - center.x); + const dy = @abs(entity.pos.y - center.y); + + if (dx > radius + entity.half_width or dy > radius + entity.half_height) { + continue; + } + + // More precise check: distance from center to nearest point on AABB + const closest_x = std.math.clamp(center.x, entity.minX(), entity.maxX()); + const closest_y = std.math.clamp(center.y, entity.minY(), entity.maxY()); + + const dist_sq = (closest_x - center.x) * (closest_x - center.x) + + (closest_y - center.y) * (closest_y - center.y); + + if (dist_sq <= radius_sq) { + try results.append(self.allocator, entity.id); + } + } + } + }; +} + +/// Simple sweep and prune for AABBs (non-generic version) +pub fn sweepAndPrune( + boxes: []const AABB, + pairs: *std.ArrayListUnmanaged(CollisionPair), + allocator: std.mem.Allocator, +) !void { + const n = boxes.len; + if (n < 2) return; + + // Create sorted indices + var indices = try allocator.alloc(usize, n); + defer allocator.free(indices); + for (0..n) |i| indices[i] = i; + + // Sort by minX + std.mem.sort(usize, indices, boxes, struct { + fn lessThan(b: []const AABB, a: usize, c: usize) bool { + return b[a].minX() < b[c].minX(); + } + }.lessThan); + + // Sweep + for (indices, 0..) |i, idx| { + const box_a = boxes[i]; + const max_x_a = box_a.maxX(); + + for (indices[idx + 1 ..]) |j| { + const box_b = boxes[j]; + + if (box_b.minX() >= max_x_a) break; + + if (box_a.overlaps(box_b)) { + try pairs.append(allocator, CollisionPair.init(box_a.id, box_b.id)); + } + } + } +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/vector.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/vector.zig new file mode 100644 index 0000000..a085b87 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/vector.zig @@ -0,0 +1,216 @@ +// Position - standalone vector math utilities for Zig +// No external dependencies, only std + +const std = @import("std"); + +pub const Position = struct { + x: f32 = 0, + y: f32 = 0, + + pub const zero = Position{ .x = 0, .y = 0 }; + pub const one = Position{ .x = 1, .y = 1 }; + + pub fn add(self: Position, other: Position) Position { + return .{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn move(self: *Position, dx: f32, dy: f32) void { + self.x += dx; + self.y += dy; + } + + pub fn sub(self: Position, other: Position) Position { + return .{ .x = self.x - other.x, .y = self.y - other.y }; + } + + pub fn scale(self: Position, scalar: f32) Position { + return .{ .x = self.x * scalar, .y = self.y * scalar }; + } + + pub fn mul(self: Position, other: Position) Position { + return .{ .x = self.x * other.x, .y = self.y * other.y }; + } + + pub fn div(self: Position, other: Position) Position { + return .{ .x = self.x / other.x, .y = self.y / other.y }; + } + + pub fn negate(self: Position) Position { + return .{ .x = -self.x, .y = -self.y }; + } + + pub fn length(self: Position) f32 { + return @sqrt(self.x * self.x + self.y * self.y); + } + + pub fn lengthSquared(self: Position) f32 { + return self.x * self.x + self.y * self.y; + } + + pub fn normalize(self: Position) Position { + const len = self.length(); + if (len == 0) return self; + return self.scale(1.0 / len); + } + + pub fn distance(self: Position, other: Position) f32 { + return self.sub(other).length(); + } + + pub fn distanceSquared(self: Position, other: Position) f32 { + return self.sub(other).lengthSquared(); + } + + pub fn dot(self: Position, other: Position) f32 { + return self.x * other.x + self.y * other.y; + } + + pub fn cross(self: Position, other: Position) f32 { + return self.x * other.y - self.y * other.x; + } + + pub fn lerp(self: Position, other: Position, t: f32) Position { + return .{ + .x = self.x + (other.x - self.x) * t, + .y = self.y + (other.y - self.y) * t, + }; + } + + pub fn rotate(self: Position, radians: f32) Position { + const cos_a = @cos(radians); + const sin_a = @sin(radians); + return .{ + .x = self.x * cos_a - self.y * sin_a, + .y = self.x * sin_a + self.y * cos_a, + }; + } + + pub fn perpendicular(self: Position) Position { + return .{ .x = -self.y, .y = self.x }; + } + + pub fn angle(self: Position) f32 { + return std.math.atan2(self.y, self.x); + } + + pub fn angleTo(self: Position, other: Position) f32 { + return other.sub(self).angle(); + } + + pub fn clamp(self: Position, min: Position, max: Position) Position { + return .{ + .x = std.math.clamp(self.x, min.x, max.x), + .y = std.math.clamp(self.y, min.y, max.y), + }; + } + + pub fn clampLength(self: Position, min_len: f32, max_len: f32) Position { + const len = self.length(); + if (len == 0) return self; + const clamped = std.math.clamp(len, min_len, max_len); + return self.scale(clamped / len); + } + + pub fn abs(self: Position) Position { + return .{ .x = @abs(self.x), .y = @abs(self.y) }; + } + + pub fn floor(self: Position) Position { + return .{ .x = @floor(self.x), .y = @floor(self.y) }; + } + + pub fn ceil(self: Position) Position { + return .{ .x = @ceil(self.x), .y = @ceil(self.y) }; + } + + pub fn round(self: Position) Position { + return .{ .x = @round(self.x), .y = @round(self.y) }; + } + + pub fn eql(self: Position, other: Position) bool { + return self.x == other.x and self.y == other.y; + } + + pub fn eqlApprox(self: Position, other: Position, epsilon: f32) bool { + return @abs(self.x - other.x) <= epsilon and @abs(self.y - other.y) <= epsilon; + } + + pub fn format( + self: Position, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + try writer.print("Position({d:.2}, {d:.2})", .{ self.x, self.y }); + } +}; + +/// Integer position for pixel-perfect positioning +pub const PositionI = struct { + x: i32 = 0, + y: i32 = 0, + + pub const zero = PositionI{ .x = 0, .y = 0 }; + pub const one = PositionI{ .x = 1, .y = 1 }; + + pub fn add(self: PositionI, other: PositionI) PositionI { + return .{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn sub(self: PositionI, other: PositionI) PositionI { + return .{ .x = self.x - other.x, .y = self.y - other.y }; + } + + pub fn scale(self: PositionI, scalar: i32) PositionI { + return .{ .x = self.x * scalar, .y = self.y * scalar }; + } + + pub fn negate(self: PositionI) PositionI { + return .{ .x = -self.x, .y = -self.y }; + } + + pub fn mul(self: PositionI, other: PositionI) PositionI { + return .{ .x = self.x * other.x, .y = self.y * other.y }; + } + + pub fn eql(self: PositionI, other: PositionI) bool { + return self.x == other.x and self.y == other.y; + } + + pub fn lengthSquared(self: PositionI) i64 { + const x: i64 = self.x; + const y: i64 = self.y; + return x * x + y * y; + } + + pub fn distanceSquared(self: PositionI, other: PositionI) i64 { + return self.sub(other).lengthSquared(); + } + + pub fn toPosition(self: PositionI) Position { + return .{ + .x = @floatFromInt(self.x), + .y = @floatFromInt(self.y), + }; + } + + pub fn fromPosition(pos: Position) PositionI { + return .{ + .x = @intFromFloat(@round(pos.x)), + .y = @intFromFloat(@round(pos.y)), + }; + } + + pub fn format( + self: PositionI, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + try writer.print("PositionI({d}, {d})", .{ self.x, self.y }); + } +}; diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/z_index_buckets.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/z_index_buckets.zig new file mode 100644 index 0000000..7cd0bb2 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/z_index_buckets.zig @@ -0,0 +1,183 @@ +//! Z-Index Bucket Storage +//! +//! Maintains items sorted by z-index using configurable buckets. +//! This eliminates the need to re-sort the entire list when z-indices change. +//! +//! Complexity: +//! - Insert: O(1) amortized +//! - Remove: O(bucket_size) - typically small due to clustered z-indices +//! - Change z-index: O(bucket_size) +//! - Iteration: O(bucket_count + n) ≈ O(n) + +const std = @import("std"); + +/// Z-index bucket storage for efficient ordered iteration. +/// Generic over: +/// - T: item type +/// - ZIndexType: unsigned integer type for z-index (u8, u16, etc.) +/// +/// The number of buckets is determined by the max value of ZIndexType + 1. +/// For u8: 256 buckets, for u4: 16 buckets, etc. +pub fn ZIndexBuckets(comptime T: type, comptime ZIndexType: type) type { + comptime { + const info = @typeInfo(ZIndexType); + if (info != .int or info.int.signedness != .unsigned) { + @compileError("ZIndexType must be an unsigned integer type"); + } + } + + const bucket_count = std.math.maxInt(ZIndexType) + 1; + + return struct { + const Self = @This(); + const Bucket = std.ArrayListUnmanaged(T); + + buckets: [bucket_count]Bucket, + allocator: std.mem.Allocator, + total_count: usize, + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .buckets = [_]Bucket{.empty} ** bucket_count, + .allocator = allocator, + .total_count = 0, + }; + } + + pub fn deinit(self: *Self) void { + for (&self.buckets) |*bucket| { + bucket.deinit(self.allocator); + } + } + + /// Insert an item at the given z-index + pub fn insert(self: *Self, item: T, z: ZIndexType) !void { + try self.buckets[z].append(self.allocator, item); + self.total_count += 1; + } + + /// Remove an item from the given z-index bucket using equality comparison. + /// Returns true if the item was found and removed. + pub fn remove(self: *Self, item: T, z: ZIndexType) bool { + const bucket = &self.buckets[z]; + for (bucket.items, 0..) |existing, i| { + if (eql(existing, item)) { + _ = bucket.swapRemove(i); + self.total_count -= 1; + return true; + } + } + return false; + } + + /// Change an item's z-index from old_z to new_z + /// Returns error if the item was not found at old_z or allocation fails + pub fn changeZIndex(self: *Self, item: T, old_z: ZIndexType, new_z: ZIndexType) !void { + if (old_z == new_z) return; + + // First verify item exists at old_z before making any changes + const bucket = &self.buckets[old_z]; + var found_index: ?usize = null; + for (bucket.items, 0..) |existing, i| { + if (eql(existing, item)) { + found_index = i; + break; + } + } + if (found_index == null) { + return error.ItemNotFound; + } + + // Insert to new bucket first - if this fails, no state has changed + try self.buckets[new_z].append(self.allocator, item); + + // Now safe to remove from old bucket (insert succeeded) + _ = bucket.swapRemove(found_index.?); + // total_count stays the same (removed one, added one) + } + + /// Get total number of items across all buckets + pub fn count(self: *const Self) usize { + return self.total_count; + } + + /// Clear all buckets + pub fn clear(self: *Self) void { + for (&self.buckets) |*bucket| { + bucket.clearRetainingCapacity(); + } + self.total_count = 0; + } + + /// Iterator that yields items in z-index order (0 to max) + pub fn iterator(self: *const Self) Iterator { + return Iterator.init(self); + } + + pub const Iterator = struct { + buckets: *const [bucket_count]Bucket, + z: usize, + idx: usize, + + pub fn init(storage: *const Self) Iterator { + var iter = Iterator{ + .buckets = &storage.buckets, + .z = 0, + .idx = 0, + }; + iter.skipEmptyBuckets(); + return iter; + } + + pub fn next(self: *Iterator) ?T { + while (self.z < bucket_count) { + const bucket = &self.buckets[self.z]; + if (self.idx < bucket.items.len) { + const item = bucket.items[self.idx]; + self.idx += 1; + return item; + } + self.z += 1; + self.idx = 0; + } + return null; + } + + fn skipEmptyBuckets(self: *Iterator) void { + while (self.z < bucket_count and self.buckets[self.z].items.len == 0) { + self.z += 1; + } + } + + pub fn reset(self: *Iterator) void { + self.z = 0; + self.idx = 0; + self.skipEmptyBuckets(); + } + }; + + /// Collect all items into a slice in z-index order. + /// The caller must provide a buffer of at least `count()` size. + pub fn collectInto(self: *const Self, buffer: []T) []T { + var i: usize = 0; + var iter = self.iterator(); + while (iter.next()) |item| { + if (i >= buffer.len) break; + buffer[i] = item; + i += 1; + } + return buffer[0..i]; + } + + /// Equality comparison for items + fn eql(a: T, b: T) bool { + // Use eql method if available (for structs), otherwise use std.meta.eql + const info = @typeInfo(T); + if (info == .@"struct" and @hasDecl(T, "eql")) { + return a.eql(b); + } else { + return std.meta.eql(a, b); + } + } + }; +} diff --git a/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/zon_coercion.zig b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/zon_coercion.zig new file mode 100644 index 0000000..eb3bd98 --- /dev/null +++ b/zig-pkg/zig_utils-0.7.3-dUo89yXUAQCzJKSJ4bvzP7jwoTv5ndBCM9PHUM0NkUD5/src/zon_coercion.zig @@ -0,0 +1,298 @@ +//! Comptime ZON coercion utilities +//! +//! Converts anonymous structs from .zon files to typed structs at comptime. +//! Handles nested struct coercion, tuple-to-slice conversion, and union coercion. +//! +//! Usage: +//! const MyType = zon.buildStruct(TargetType, zon_data); +//! const field_val = zon.coerceValue(FieldType, zon_value); + +const std = @import("std"); + +/// Coerce a comptime ZON value to the expected field type. +/// Handles nested struct coercion and tuple-to-slice conversion. +pub fn coerceValue(comptime FieldType: type, comptime data_value: anytype) FieldType { + const DataType = @TypeOf(data_value); + const field_info = @typeInfo(FieldType); + + // Handle optional types - unwrap and coerce the child type + if (field_info == .optional) { + const ChildType = field_info.optional.child; + // Check for null + if (DataType == @TypeOf(null)) { + return null; + } + // Coerce to the child type and wrap in optional + return coerceValue(ChildType, data_value); + } + + // Handle slice types + if (field_info == .pointer) { + const ptr_info = field_info.pointer; + if (ptr_info.size == .slice) { + const ChildType = ptr_info.child; + const data_info = @typeInfo(DataType); + + // If data is a tuple, convert to slice + if (data_info == .@"struct" and data_info.@"struct".is_tuple) { + return tupleToSlice(ChildType, data_value); + } + } + } + + // Handle fixed-size array coercion (tuple to array) + if (field_info == .array) { + const arr_info = field_info.array; + const data_info = @typeInfo(DataType); + if (data_info == .@"struct" and data_info.@"struct".is_tuple) { + const tuple_len = data_info.@"struct".fields.len; + if (tuple_len != arr_info.len) { + @compileError(std.fmt.comptimePrint( + "Array size mismatch: expected {d} elements, got {d}", + .{ arr_info.len, tuple_len }, + )); + } + var array: [arr_info.len]arr_info.child = undefined; + inline for (0..arr_info.len) |i| { + array[i] = coerceValue(arr_info.child, data_value[i]); + } + return array; + } + } + + // Handle tagged union coercion from anonymous struct + // Example: .{ .box = .{ .width = 50, .height = 50 } } -> Shape union + if (field_info == .@"union") { + return coerceToUnion(FieldType, data_value); + } + + // Handle nested struct coercion + if (field_info == .@"struct" and @typeInfo(DataType) == .@"struct") { + return buildStruct(FieldType, data_value); + } + + // Direct assignment for compatible types + return data_value; +} + +/// Coerce a comptime value to a tagged union type. +/// Supports: +/// - Single-field anonymous struct: .{ .box = .{ .width = 50 } } -> Union.box +/// - Enum literal for void payloads: .idle -> State.idle +/// - Payload-matching struct: .{ .width = 50, .height = 50 } -> Container.explicit (if fields match) +fn coerceToUnion(comptime UnionType: type, comptime data_value: anytype) UnionType { + const DataType = @TypeOf(data_value); + const data_info = @typeInfo(DataType); + const union_info = @typeInfo(UnionType).@"union"; + + // Case 1: Enum literal for void payload variants + // Example: .idle -> State.idle (where State = union(enum) { idle, running, ... }) + if (data_info == .enum_literal) { + const tag_name = @tagName(data_value); + + inline for (union_info.fields) |union_field| { + if (comptime std.mem.eql(u8, union_field.name, tag_name)) { + // Verify the payload is void + if (union_field.type != void) { + @compileError("Cannot use enum literal for union variant '" ++ tag_name ++ + "' with non-void payload. Use .{ ." ++ tag_name ++ " = ... } syntax instead."); + } + return @unionInit(UnionType, tag_name, {}); + } + } + @compileError("No union variant named '" ++ tag_name ++ "' in " ++ @typeName(UnionType)); + } + + // Case 2: Anonymous struct - could be variant selector or direct payload + if (data_info == .@"struct") { + const data_fields = data_info.@"struct".fields; + + // Case 2a: Single-field struct where field name matches a union variant + // Example: .{ .box = .{ .width = 50, .height = 50 } } -> Shape.box + if (data_fields.len == 1) { + const field_name = data_fields[0].name; + + // Check if this field name matches a union variant + inline for (union_info.fields) |union_field| { + if (comptime std.mem.eql(u8, union_field.name, field_name)) { + // This is the variant selector pattern + const variant_value = @field(data_value, field_name); + const coerced_payload = coerceValue(union_field.type, variant_value); + return @unionInit(UnionType, field_name, coerced_payload); + } + } + } + + // Case 2b: Multi-field struct that matches a union variant's payload type + // Example: .{ .width = 400, .height = 300 } -> Container.explicit + inline for (union_info.fields) |union_field| { + const payload_info = @typeInfo(union_field.type); + if (payload_info == .@"struct") { + // Check if data fields are compatible with this payload struct + if (comptime structFieldsCompatible(DataType, union_field.type)) { + const coerced_payload = buildStruct(union_field.type, data_value); + return @unionInit(UnionType, union_field.name, coerced_payload); + } + } + } + + @compileError("Cannot coerce struct to union type " ++ @typeName(UnionType) ++ + ". Use .{ .variant_name = payload } syntax or ensure struct fields match a variant's payload."); + } + + // Case 3: Direct assignment if types match + if (DataType == UnionType) { + return data_value; + } + + @compileError("Cannot coerce " ++ @typeName(DataType) ++ " to union type " ++ @typeName(UnionType) ++ + ". Use .{ .variant_name = payload } or .variant_name (for void payloads)."); +} + +/// Check if all fields in DataType exist in TargetType (for struct compatibility) +fn structFieldsCompatible(comptime DataType: type, comptime TargetType: type) bool { + const data_fields = @typeInfo(DataType).@"struct".fields; + const target_fields = @typeInfo(TargetType).@"struct".fields; + + // All data fields must exist in target + for (data_fields) |df| { + var found = false; + for (target_fields) |tf| { + if (std.mem.eql(u8, df.name, tf.name)) { + found = true; + break; + } + } + if (!found) return false; + } + + return data_fields.len > 0; +} + +/// Build a struct from comptime anonymous struct data. +/// Recursively coerces nested fields. +/// Raises compile error for missing required fields (fields without defaults). +pub fn buildStruct(comptime StructType: type, comptime data: anytype) StructType { + return buildStructWithContext(StructType, data, "struct"); +} + +/// Build a struct with a custom context string for error messages. +fn buildStructWithContext(comptime StructType: type, comptime data: anytype, comptime context: []const u8) StructType { + const fields = std.meta.fields(StructType); + var result: StructType = undefined; + + inline for (fields) |field| { + if (@hasField(@TypeOf(data), field.name)) { + const data_value = @field(data, field.name); + @field(result, field.name) = coerceValue(field.type, data_value); + } else if (field.default_value_ptr) |ptr| { + const default_ptr: *const field.type = @ptrCast(@alignCast(ptr)); + @field(result, field.name) = default_ptr.*; + } else { + @compileError("Missing required field '" ++ field.name ++ "' for " ++ context ++ " '" ++ @typeName(StructType) ++ "'"); + } + } + + return result; +} + +/// Convert a tuple to a slice at comptime. +/// Recursively coerces each element. +pub fn tupleToSlice(comptime ChildType: type, comptime tuple: anytype) []const ChildType { + const tuple_info = @typeInfo(@TypeOf(tuple)).@"struct"; + const len = tuple_info.fields.len; + + const array = comptime blk: { + var arr: [len]ChildType = undefined; + for (0..len) |i| { + arr[i] = coerceValue(ChildType, tuple[i]); + } + break :blk arr; + }; + + return &array; +} + +/// Merge two comptime structs, with overrides taking precedence. +/// Returns a new anonymous struct with all fields from base, plus any +/// fields from overrides (which override base values). +/// +/// Example: +/// base = .{ .x = 10, .y = 20, .color = .red } +/// overrides = .{ .color = .blue } +/// result = .{ .x = 10, .y = 20, .color = .blue } +pub fn mergeStructs(comptime base: anytype, comptime overrides: anytype) MergedStructType(@TypeOf(base), @TypeOf(overrides)) { + const BaseType = @TypeOf(base); + const OverridesType = @TypeOf(overrides); + + var result: MergedStructType(BaseType, OverridesType) = undefined; + + // Copy all fields from base + inline for (std.meta.fields(BaseType)) |field| { + if (@hasField(OverridesType, field.name)) { + @field(result, field.name) = @field(overrides, field.name); + } else { + @field(result, field.name) = @field(base, field.name); + } + } + + // Add fields that exist only in overrides (not in base) + inline for (std.meta.fields(OverridesType)) |field| { + if (!@hasField(BaseType, field.name)) { + @field(result, field.name) = @field(overrides, field.name); + } + } + + return result; +} + +/// Compute the merged struct type from two struct types. +fn MergedStructType(comptime BaseType: type, comptime OverridesType: type) type { + const base_fields = std.meta.fields(BaseType); + const override_fields = std.meta.fields(OverridesType); + + comptime var field_count = base_fields.len; + inline for (override_fields) |of| { + if (!@hasField(BaseType, of.name)) { + field_count += 1; + } + } + + comptime var field_names: [field_count][]const u8 = undefined; + comptime var field_types: [field_count]type = undefined; + comptime var field_attrs: [field_count]std.builtin.Type.StructField.Attributes = undefined; + comptime var i = 0; + + inline for (base_fields) |bf| { + if (@hasField(OverridesType, bf.name)) { + inline for (override_fields) |of| { + if (comptime std.mem.eql(u8, of.name, bf.name)) { + field_names[i] = bf.name; + field_types[i] = of.type; + field_attrs[i] = .{ .@"align" = @alignOf(of.type) }; + } + } + } else { + field_names[i] = bf.name; + field_types[i] = bf.type; + field_attrs[i] = .{ .@"align" = @alignOf(bf.type) }; + } + i += 1; + } + + inline for (override_fields) |of| { + if (!@hasField(BaseType, of.name)) { + field_names[i] = of.name; + field_types[i] = of.type; + field_attrs[i] = .{ .@"align" = @alignOf(of.type) }; + i += 1; + } + } + + return @Struct(.auto, null, &field_names, &field_types, &field_attrs); +} + +/// Check if a struct type has any fields +pub fn hasFields(comptime T: type) bool { + return std.meta.fields(T).len > 0; +}