diff --git a/README.md b/README.md index 7144cf9..0e32114 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,45 @@ This library is still very much work-in-progress and not ready for use. It's ava [Zig]: https://ziglang.org/ [Flecs]: https://github.com/SanderMertens/flecs + +## Example + +```zig +const std = @import("std"); +const flecs = @import("flecs-zig-ble"); +const Context = flecs.Context(void); +const World = Context.World; +const Iter = Context.Iter; + +const Position = struct { x: f32, y: f32 }; +const Velocity = struct { x: f32, y: f32 }; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + flecs.init(gpa.allocator()); + + const world = try World.init(); + defer world.deinit(); + + _ = try world.component(Position); + _ = try world.component(Velocity); + + _ = try world.system("Move", move, flecs.c.EcsOnUpdate, "Position, Velocity"); + + const e = world.entity(); + e.set(Position, .{ .x = 10, .y = 20 }); + e.set(Velocity, .{ .x = 1, .y = 2 }); + + while (world.progress(0.0)) { } +} + +fn move(it: Iter) void { + const pos_field = it.field(Position, 1); + const vel_field = it.field(Velocity, 2); + + for (pos_field, vel_field) |*p, v| { + p.x += v.x; + p.y += v.y; + } +} +``` diff --git a/build.zig b/build.zig index b3c890f..7509c2e 100644 --- a/build.zig +++ b/build.zig @@ -4,10 +4,9 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const module = b.createModule(.{ - .source_file = .{ .path = "src/main.zig" }, + _ = b.addModule("flecs-zig-ble", .{ + .root_source_file = .{ .path = "src/main.zig" }, }); - try b.modules.put(b.dupe("flecs-zig-ble"), module); const lib = b.addStaticLibrary(.{ .name = "flecs-zig-ble", @@ -20,9 +19,7 @@ pub fn build(b: *std.Build) !void { b.installArtifact(lib); const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "test/main.zig" }, - // Has to be specified so tests can run autodoc tests from src/. - .main_pkg_path = .{ .path = "." }, + .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, }); @@ -32,13 +29,14 @@ pub fn build(b: *std.Build) !void { test_step.dependOn(&run_main_tests.step); } -fn setupFlecs(step: *std.Build.CompileStep) void { +fn setupFlecs(step: *std.Build.Step.Compile) void { step.linkLibC(); step.addIncludePath(.{ .path = "libs/flecs" }); step.addCSourceFile(.{ .file = .{ .path = "libs/flecs/flecs.c" }, .flags = &.{"-fno-sanitize=undefined"}, }); - if (step.target.isWindows()) - step.linkSystemLibraryName("ws2_32"); + if (step.rootModuleTarget().os.tag == .windows) { + step.linkSystemLibrary("ws2_32"); + } } diff --git a/build.zig.zon b/build.zig.zon index f0b2b56..c3b93de 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,4 +1,16 @@ .{ .name = "flecs-zig-ble", .version = "0.1.0", + .paths = .{ + "src", + "build.zig", + "build.zig.zon", + "README.md", + "UNLICENSE.txt", + + // Flecs dependency. + "libs/flecs/flecs.c", + "libs/flecs/flecs.h", + "libs/flecs/LICENSE", + }, } diff --git a/libs/flecs b/libs/flecs index 74a7f74..198607d 160000 --- a/libs/flecs +++ b/libs/flecs @@ -1 +1 @@ -Subproject commit 74a7f74a2835d3946f05502ef44ba1d5d8b48eff +Subproject commit 198607d10ab8f8fc44540043271d6e3be019250b diff --git a/src/entity.zig b/src/entity.zig index d1ed9a4..f7a9c79 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -2,14 +2,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const flecs = @import("./main.zig"); +const Path = flecs.Path; const c = flecs.c; -const err = @import("./error.zig"); -const util = @import("./util.zig"); - -const Id = @import("./id.zig").Id; -const Lookup = @import("./main.zig").Lookup; -const Path = @import("./path.zig").Path; -const World = @import("./world.zig").World; pub const EntityError = error{ /// Id is `0`. @@ -42,17 +36,21 @@ pub const EntityError = error{ /// the id isn't an entity), and back again with `asId()` (always succeeds). pub fn Entity(comptime ctx: anytype) type { return struct { - world: *World(ctx), - raw: c.ecs_entity_t, - const Self = @This(); + const Context = flecs.Context(ctx); + const World = Context.World; + const Id = Context.Id; + + world: *World, + raw: c.ecs_entity_t, + /// Returns an `Entity` for the specified world and raw entity id. /// /// No safety checks are done, so if you pass an invalid id, an /// invalid `Entity` might be returned that could cause panics if /// passed to other API functions without ensuring their validity. - pub fn fromRaw(world: *World(ctx), raw: c.ecs_entity_t) Self { + pub fn fromRaw(world: *World, raw: c.ecs_entity_t) Self { return .{ .world = world, .raw = raw }; } @@ -85,7 +83,7 @@ pub fn Entity(comptime ctx: anytype) type { /// When adding components, they are just default-initialized. /// To set their values you'll need to use `set(...)` on the /// `Entity` returned from this function. - pub fn init(world: *World(ctx), config: Config, ids: anytype) !Self { + pub fn init(world: *World, config: Config, ids: anytype) !Self { const meta = @typeInfo(@TypeOf(ids)); if (meta != .Struct or (meta.Struct.is_tuple == false and meta.Struct.fields.len > 0)) @compileError("Expected tuple or empty struct, got '" ++ @typeName(@TypeOf(ids)) ++ "'"); @@ -134,7 +132,7 @@ pub fn Entity(comptime ctx: anytype) type { var desc = std.mem.zeroInit(c.ecs_entity_desc_t, .{ .sep = "".ptr, .name = n.ptr }); desc.add[0] = c.ecs_pair(c.EcsChildOf, parent); scope = c.ecs_entity_init(world.raw, &desc); - if (scope == 0) return err.getLastErrorOrUnknown(); + if (scope == 0) return flecs.getLastErrorOrUnknown(); } else scope = found; }, } @@ -155,7 +153,7 @@ pub fn Entity(comptime ctx: anytype) type { }); inline for (ids, 0..) |a, i| - desc.add[i] = util.anyToId(ctx, a); + desc.add[i] = Context.anyToId(a); for (desc.add[0..ids.len]) |i| if (c.ecs_id_is_pair(i) and c.ECS_PAIR_FIRST(i) == c.EcsChildOf) @@ -163,7 +161,7 @@ pub fn Entity(comptime ctx: anytype) type { return error.FoundChildOf; const result = c.ecs_entity_init(world.raw, &desc); - if (result == 0) return err.getLastErrorOrUnknown(); + if (result == 0) return flecs.getLastErrorOrUnknown(); return Self.fromRaw(world, result); } @@ -198,7 +196,7 @@ pub fn Entity(comptime ctx: anytype) type { if (!c.ecs_is_alive(self.world.raw, self.raw)) return EntityError.IsNotAlive; } - pub fn asId(self: Self) Id(ctx) { + pub fn asId(self: Self) Id { return .{ .world = self.world, .raw = self.raw }; } @@ -261,25 +259,25 @@ pub fn Entity(comptime ctx: anytype) type { } /// Returns an iterator that yields each of this `Entity`s children. - pub fn getChildren(self: Self) World(ctx).TermIterator { + pub fn getChildren(self: Self) World.TermIterator { return self.world.term(.{ c.EcsChildOf, self }); } /// Returns an iterator that yields each of the targets of the /// specified `relation` that this `Entity` has, if any. pub fn getTargets(self: Self, relation: anytype) TargetIterator { - const rel = util.anyToEntity(ctx, relation); + const rel = Context.anyToEntity(relation); return .{ .world = self.world, .entity = self.raw, .relation = rel.raw }; } pub const TargetIterator = struct { - world: World(ctx), + world: World, entity: c.ecs_entity_t, relation: c.ecs_entity_t, index: c_int = 0, pub fn next(self: *TargetIterator) ?Self { - var result = c.ecs_get_target(self.world.raw, self.entity, self.relation, self.index); + const result = c.ecs_get_target(self.world.raw, self.entity, self.relation, self.index); self.index += 1; return if (result != 0) Self.fromRaw(self.world, result) else null; } @@ -287,37 +285,37 @@ pub fn Entity(comptime ctx: anytype) type { /// Returns whether this `Entity` has the specified value. /// - /// `id` must be convertible to an id. See also: `util.anyToId(...)`. + /// `id` must be convertible to an id. See also: `Context.anyToId(...)`. pub fn has(self: Self, id: anytype) bool { - const ecs_id = util.anyToId(ctx, id); + const ecs_id = Context.anyToId(id); return c.ecs_has_id(self.world.raw, self.raw, ecs_id); } pub fn add(self: Self, id: anytype) void { - const ecs_id = util.anyToId(ctx, id); + const ecs_id = Context.anyToId(id); c.ecs_add_id(self.world.raw, self.raw, ecs_id); } pub fn remove(self: Self, id: anytype) void { - const ecs_id = util.anyToId(ctx, id); + const ecs_id = Context.anyToId(id); c.ecs_remove_id(self.world.raw, self.raw, ecs_id); } pub fn get(self: Self, comptime T: type) ?T { - const id = Lookup(ctx, T).id; + const id = Context.lookup(T).*; const ptr = c.ecs_get_id(self.world.raw, self.raw, id); const typed_ptr: ?*const T = @alignCast(@ptrCast(ptr)); return if (typed_ptr) |p| p.* else null; } pub fn get_mut(self: Self, comptime T: type) ?*T { - const id = Lookup(ctx, T).id; + const id = Context.lookup(T).*; const ptr = c.ecs_get_mut_id(self.world.raw, self.raw, id); return @alignCast(@ptrCast(ptr)); } pub fn set(self: Self, comptime T: type, value: T) void { - const id = Lookup(ctx, T).id; + const id = Context.lookup(T).*; _ = c.ecs_set_id(self.world.raw, self.raw, id, @sizeOf(T), &value); } }; diff --git a/src/id.zig b/src/id.zig index c5d4190..3a6cf44 100644 --- a/src/id.zig +++ b/src/id.zig @@ -1,8 +1,5 @@ -const c = @import("./c.zig"); - -const Entity = @import("./entity.zig").Entity; -const Pair = @import("./pair.zig").Pair; -const World = @import("./world.zig").World; +const flecs = @import("./main.zig"); +const c = flecs.c; pub const IdError = error{ /// Id is `0`. @@ -17,12 +14,17 @@ pub const IdError = error{ /// It can be an `Entity` or `Pair`, and can have optional id flags. pub fn Id(comptime ctx: anytype) type { return struct { - world: *World(ctx), - raw: c.ecs_id_t, - const Self = @This(); - pub fn fromRaw(world: *World(ctx), raw: c.ecs_id_t) Self { + const Context = flecs.Context(ctx); + const World = Context.World; + const Entity = Context.Entity; + const Pair = Context.Pair; + + world: *World, + raw: c.ecs_id_t, + + pub fn fromRaw(world: *World, raw: c.ecs_id_t) Self { return .{ .world = world, .raw = raw }; } @@ -33,15 +35,14 @@ pub fn Id(comptime ctx: anytype) type { /// - .. it is not `0`. /// - .. it does not contain wildcards. /// - .. it does not contain invalid entities. - pub fn ensureValid(self: Self) !Self { + pub fn ensureValid(self: Self) !void { if (self.raw == 0) return IdError.IsNone; if (c.ecs_id_is_wildcard(self.raw)) return IdError.IsWildcard; if (self.asPairUnsafe()) |p| - _ = try p.ensureValid() + try p.ensureValid() else if (!self.isEntity()) if (!c.ecs_is_valid(self.raw & c.ECS_COMPONENT_MASK)) return IdError.IsInvalid; - return self; } pub fn isEntity(self: Self) bool { @@ -50,13 +51,15 @@ pub fn Id(comptime ctx: anytype) type { /// Attempts to return this `Id` as an `Entity`, or an error if it's /// not an entity, not a valid entity or not alive in the world. - pub fn asEntityAlive(self: Self) !Entity(ctx) { - self.asEntityUnsafe().ensureAlive(); + pub fn asEntityAlive(self: Self) !Entity { + const result = self.asEntityUnsafe(); + try result.ensureAlive(); + return result; } /// Returns this `Id` as an `Entity`, or `null` otherwise. /// This assumes the `Id` is valid, or an invalid result could be returned. - pub fn asEntityUnsafe(self: Self) ?Entity(ctx) { + pub fn asEntityUnsafe(self: Self) ?Entity { return if (isEntity(self)) .{ .world = self.world, .raw = self.raw } else null; } @@ -67,19 +70,23 @@ pub fn Id(comptime ctx: anytype) type { /// Attempts to return this `Id` as a `Pair`, or an error if it's not /// a pair, or either of its elements are not valid or not alive in /// the world. - pub fn asPairAlive(self: Self) !Pair(ctx) { - return @as(Pair(ctx), .{ .world = self.world, .raw = self.raw }).ensureAlive(); + pub fn asPairAlive(self: Self) !Pair { + const result = Pair{ .world = self.world, .raw = self.raw }; + try result.ensureAlive(); + return result; } /// Attempts to return this `Id` as a `Pair`, or an error if it's not /// a pair, or either of its elements are not valid. - pub fn asPairValid(self: Self) !Pair(ctx) { - return @as(Pair(ctx), .{ .world = self.world, .raw = self.raw }).ensureValid(); + pub fn asPairValid(self: Self) !Pair { + const result = Pair{ .world = self.world, .raw = self.raw }; + try result.ensureValid(); + return result; } /// Returns this `Id` as a `Pair`, or `null` otherwise. /// This assumes the `Id` is valid, or an invalid result could be returned. - pub fn asPairUnsafe(self: Self) ?Pair(ctx) { + pub fn asPairUnsafe(self: Self) ?Pair { return if (isPair(self)) .{ .world = self.world, .raw = self.raw } else null; } @@ -128,7 +135,7 @@ pub fn Id(comptime ctx: anytype) type { /// Attempts to get the component type for this `Id`. /// - /// This operation returns the entity representing the component type + /// This operation returns the entity representing the component type /// for this `Id`, if it is associated with a type. For a regular /// component with a non-zero size (an entity with the `EcsComponent` /// component) the operation will return the `Entity` itself. @@ -143,9 +150,9 @@ pub fn Id(comptime ctx: anytype) type { /// - If `.relation` has the `Tag` property, `null` is returned. /// - If `.target` is a component, it is returned. /// - Otherwise, `null` is returned. - pub fn getTypeId(self: Self) ?Entity(ctx) { + pub fn getTypeId(self: Self) ?Entity { const raw = c.ecs_get_typeid(self.world.raw, self.raw); - return if (raw != 0) Entity(ctx).fromRaw(self.world, raw); + return if (raw != 0) Entity.fromRaw(self.world, raw); } /// Returns if the provided `pattern` matches this `Id`. diff --git a/src/iter.zig b/src/iter.zig index 50dde6c..6ecff7b 100644 --- a/src/iter.zig +++ b/src/iter.zig @@ -1,24 +1,25 @@ const flecs = @import("./main.zig"); const c = flecs.c; -const Entity = @import("./entity.zig").Entity; -const Id = @import("./id.zig").Id; -const World = @import("./world.zig").World; - pub fn Iter(comptime ctx: anytype) type { return struct { - world: *World(ctx), + const Self = @This(); + + const Context = flecs.Context(ctx); + const World = Context.World; + const Entity = Context.Entity; + const Id = Context.Id; + + world: *World, raw: *c.ecs_iter_t, owned: bool, - const Self = @This(); - - pub fn fromRawPtr(world: *World(ctx), ptr: *c.ecs_iter_t) Self { + pub fn fromRawPtr(world: *World, ptr: *c.ecs_iter_t) Self { return .{ .world = world, .raw = ptr, .owned = false }; } - pub fn fromRawValue(world: *World(ctx), value: c.ecs_iter_t) !Self { - var raw = try flecs.allocator.create(c.ecs_iter_t); + pub fn fromRawValue(world: *World, value: c.ecs_iter_t) !Self { + const raw = try flecs.allocator.create(c.ecs_iter_t); raw.* = value; return .{ .world = world, .raw = raw, .owned = true }; } @@ -41,21 +42,21 @@ pub fn Iter(comptime ctx: anytype) type { } pub fn field(self: Self, comptime T: type, index: usize) []T { - var raw_ptr = c.ecs_field_w_size(self.raw, @sizeOf(T), @intCast(index)); + const raw_ptr = c.ecs_field_w_size(self.raw, @sizeOf(T), @intCast(index)); var typed_ptr: [*]T = @alignCast(@ptrCast(raw_ptr)); const is_self = c.ecs_field_is_self(self.raw, @intCast(index)); const count = if (is_self) self.getCount() else 1; return typed_ptr[0..count]; } - pub fn fieldId(self: Self, index: usize) Id(ctx) { + pub fn fieldId(self: Self, index: usize) Id { const raw = c.ecs_field_id(self.raw, @intCast(index)); - return Id(ctx).fromRaw(self.world, raw); + return Id.fromRaw(self.world, raw); } - pub fn fieldSource(self: Self, index: usize) Entity(ctx) { + pub fn fieldSource(self: Self, index: usize) Entity { const raw = c.ecs_field_src(self.raw, @intCast(index)); - return Entity(ctx).fromRaw(self.world, raw); + return Entity.fromRaw(self.world, raw); } }; } diff --git a/src/main.zig b/src/main.zig index 302a8b9..b81d1f7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,33 +1,18 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const meta = @import("./meta.zig"); + +pub const c = @import("./c.zig"); + +pub usingnamespace @import("./error.zig"); pub usingnamespace @import("./entity.zig"); pub usingnamespace @import("./id.zig"); pub usingnamespace @import("./iter.zig"); pub usingnamespace @import("./pair.zig"); -pub usingnamespace @import("./path.zig"); pub usingnamespace @import("./world.zig"); -pub const c = @import("./c.zig"); - -pub fn Context(comptime ctx: anytype) type { - return struct { - pub const Entity = @import("./entity.zig").Entity(ctx); - pub const Id = @import("./id.zig").Id(ctx); - pub const Iter = @import("./iter.zig").Iter(ctx); - pub const Pair = @import("./pair.zig").Pair(ctx); - pub const World = @import("./world.zig").World(ctx); - - pub const Path = @import("./path.zig").Path; - }; -} - -pub fn Lookup(comptime ctx: anytype, comptime T: type) type { - _ = .{ ctx, T }; // Only necessary to create a unique type. - return struct { - pub var id: c.ecs_entity_t = 0; - }; -} +pub const Path = @import("./path.zig"); pub var is_initialized = false; pub var allocator: Allocator = undefined; @@ -49,6 +34,86 @@ pub fn init(alloc: Allocator) void { c.ecs_os_api.free_ = flecsFree; } +pub fn Context(comptime ctx: anytype) type { + return struct { + pub const Entity = @import("./entity.zig").Entity(ctx); + pub const Id = @import("./id.zig").Id(ctx); + pub const Iter = @import("./iter.zig").Iter(ctx); + pub const Pair = @import("./pair.zig").Pair(ctx); + pub const World = @import("./world.zig").World(ctx); + + pub const Path = @import("./path.zig"); + + /// Returns a pointer unique to this context and the provided type + /// that holds an entity ID. Useful for associating types to entities. + pub fn lookup(comptime T: type) *c.ecs_entity_t { + _ = T; // Only necessary to create a unique type. + return &(struct { + pub var id: c.ecs_entity_t = 0; + }).id; + } + + /// Converts the specified value to an `ecs_entity_t`. + /// + /// If `null` is passed to this function, either directly or through an + /// optional type of one of the supported types, `0` is returned. It's up to + /// the caller to do any necessary testing for `0` values, and whether the + /// returned entity is valid or alive. + /// + /// The following can be converted: + /// - `Entity` and `ecs_entity_t` - Self-explanatory. + /// - `type` - Looks up the entity associated with that type. + pub fn anyToEntity(value: anytype) c.ecs_entity_t { + return switch (@TypeOf(value)) { + @TypeOf(null) => 0, + c.ecs_entity_t => value, + Entity => value.raw, + ?Entity => if (value) |v| v.raw else 0, + type => lookup(value).*, + else => @compileError("Value of type " ++ @typeName(@TypeOf(value)) ++ " can't be converted to Entity"), + }; + } + + /// Converts the specified value to an `ecs_id_t`. + /// + /// If `null` is passed to this function, either directly or through an + /// optional type of one of the supported types, `0` is returned. It's up to + /// the caller to do any necessary testing for `0` values, and whether the + /// returned id is valid. + /// + /// The following can be converted: + /// - `Id` and `ecs_id_it` - Self-explanatory. + /// - `Entity` and `ecs_entity_t` - Self-explanatory. + /// - `Pair` - Converts to the equivalent `Id`. + /// - `.{ relation, target }` - A `Pair`, converted using `anyToEntity()`. + /// - `type` - Looks up the entity associated with that type. + pub fn anyToId(value: anytype) c.ecs_id_t { + const T = @TypeOf(value); + if (comptime meta.isTuple(T)) { + if (@typeInfo(T).Struct.fields.len != 2) + @compileError("Value of type " ++ @typeName(T) ++ " must be a tuple with 2 elements, to be a Pair"); + const relation = anyToEntity(value[0]); + const target = anyToEntity(value[1]); + return c.ecs_make_pair(relation, target); + } + return switch (T) { + @TypeOf(null) => 0, + c.ecs_id_t => value, + Id => value.raw, + ?Id => if (value) |v| v.raw else 0, + Pair => value.raw, + ?Pair => if (value) |v| v.raw else 0, + // Technically same type as `ecs_id_it`. + // c.ecs_entity_t => value, + Entity => value.raw, + ?Entity => if (value) |v| v.raw else 0, + type => lookup(value).*, + else => @compileError("Value of type " ++ @typeName(T) ++ " can't be converted to Id"), + }; + } + }; +} + fn flecsMalloc(size: i32) callconv(.C) ?*anyopaque { return allocLengthEncodedSlice(size, null).ptr; } @@ -58,7 +123,7 @@ fn flecsRealloc(ptr: ?*anyopaque, size: i32) callconv(.C) ?*anyopaque { } fn flecsCalloc(size: i32) callconv(.C) ?*anyopaque { - var slice = allocLengthEncodedSlice(size, null); + const slice = allocLengthEncodedSlice(size, null); @memset(slice, 0); return slice.ptr; } @@ -97,4 +162,5 @@ fn sliceFromPtr(ptr: *anyopaque) []u8 { test { std.testing.refAllDecls(@This()); + _ = @import("./test/main.zig"); } diff --git a/src/meta.zig b/src/meta.zig new file mode 100644 index 0000000..648dfdb --- /dev/null +++ b/src/meta.zig @@ -0,0 +1,57 @@ +//! Since the `std.meta.trait` module was removed from Zig's standard +//! library, we're re-introducing some of the functions we needed. + +const std = @import("std"); + +/// Returns if the provided type is a tuple. +pub fn isTuple(comptime T: type) bool { + return @typeInfo(T) == .Struct and @typeInfo(T).Struct.is_tuple; +} + +/// Returns true if the passed type will coerce to []const u8. +/// Any of the following are considered strings: +/// ``` +/// []const u8, [:S]const u8, *const [N]u8, *const [N:S]u8, +/// []u8, [:S]u8, *[:S]u8, *[N:S]u8. +/// ``` +/// These types are not considered strings: +/// ``` +/// u8, [N]u8, [*]const u8, [*:0]const u8, +/// [*]const [N]u8, []const u16, []const i8, +/// *const u8, ?[]const u8, ?*const [N]u8. +/// ``` +pub fn isZigString(comptime T: type) bool { + return comptime blk: { + // Only pointer types can be strings, no optionals + const info = @typeInfo(T); + if (info != .Pointer) break :blk false; + + const ptr = &info.Pointer; + // Check for CV qualifiers that would prevent coerction to []const u8 + if (ptr.is_volatile or ptr.is_allowzero) break :blk false; + + // If it's already a slice, simple check. + if (ptr.size == .Slice) { + break :blk ptr.child == u8; + } + + // Otherwise check if it's an array type that coerces to slice. + if (ptr.size == .One) { + const child = @typeInfo(ptr.child); + if (child == .Array) { + const arr = &child.Array; + break :blk arr.child == u8; + } + } + + break :blk false; + }; +} + +/// Gets the simplified type name of the specified type. +/// That is, without any namespace qualifiers. +pub fn simpleTypeName(comptime T: type) [:0]const u8 { + const fullName = @typeName(T); + const index = std.mem.lastIndexOf(u8, fullName, "."); + return if (index) |i| fullName[(i + 1)..] else fullName; +} diff --git a/src/pair.zig b/src/pair.zig index e590014..105d3cd 100644 --- a/src/pair.zig +++ b/src/pair.zig @@ -1,8 +1,5 @@ -const c = @import("./c.zig"); - -const Entity = @import("./entity.zig").Entity; -const Id = @import("./id.zig").Id; -const World = @import("./world.zig").World; +const flecs = @import("./main.zig"); +const c = flecs.c; pub const PairError = error{ /// Id is `0`. @@ -25,47 +22,50 @@ pub const PairError = error{ /// can be extracted by calling `getRelation()` and `getTarget()`. pub fn Pair(comptime ctx: anytype) type { return struct { - world: *World(ctx), - raw: c.ecs_id_t, - const Self = @This(); + const Context = flecs.Context(ctx); + const World = Context.World; + const Entity = Context.Entity; + const Id = Context.Id; + + world: *World, + raw: c.ecs_id_t, + /// Build a pair from the specified `relation` and `target` entities. /// The specified entities must be alive in the world. - pub fn init(relation: Entity(ctx), target: Entity(ctx)) !Self { + pub fn init(relation: Entity, target: Entity) !Self { const raw = c.ecs_make_pair(relation.ensureAlive().raw, target.ensureAlive().raw); return .{ .world = relation.world, .raw = raw }; } /// Ensures this `Pair` is valid and its `relation` and `target` /// entities are alive in the world, returning an error otherwise. - pub fn ensureAlive(self: Self) !Self { + pub fn ensureAlive(self: Self) !void { if (self.raw == 0) return error.IsNone; if (!c.ecs_id_is_pair(self.raw)) return error.IsntPair; - _ = try self.getRelation().ensureAlive(); - _ = try self.getTarget().ensureAlive(); - return self; + try self.getRelation().ensureAlive(); + try self.getTarget().ensureAlive(); } /// Ensures this `Pair` and its elements are valid. - pub fn ensureValid(self: Self) !Self { + pub fn ensureValid(self: Self) !void { if (self.raw == 0) return error.IsNone; if (!c.ecs_id_is_pair(self.raw)) return error.IsntPair; - _ = try self.getRelation().ensureValid(); - _ = try self.getTarget().ensureValid(); - return self; + try self.getRelation().ensureValid(); + try self.getTarget().ensureValid(); } - pub fn asId(self: Self) Id(ctx) { + pub fn asId(self: Self) Id { return .{ .world = self.world, .raw = self.raw }; } - pub fn getRelation(self: Self) Entity(ctx) { - return Entity(ctx).fromRaw(self.world, c.ECS_PAIR_FIRST(self.raw)); + pub fn getRelation(self: Self) Entity { + return Entity.fromRaw(self.world, c.ECS_PAIR_FIRST(self.raw)); } - pub fn getTarget(self: Self) Entity(ctx) { - return Entity(ctx).fromRaw(self.world, c.ECS_PAIR_SECOND(self.raw)); + pub fn getTarget(self: Self) Entity { + return Entity.fromRaw(self.world, c.ECS_PAIR_SECOND(self.raw)); } // TODO: Decide whether to copy `Id` functions over? diff --git a/src/path.zig b/src/path.zig index 04e2972..40e1b53 100644 --- a/src/path.zig +++ b/src/path.zig @@ -1,349 +1,354 @@ +//! Represents the path of an `Entity`, describing its place in the world's +//! hierarchy which is constructed using `ChildOf` relationships. + const std = @import("std"); const Allocator = std.mem.Allocator; +const meta = @import("./meta.zig"); const Entity = @import("./entity.zig").Entity; +// TODO: See if we should even be using this? +const native_endian = @import("builtin").cpu.arch.endian(); + // TODO: Do something better than just `std.debug.assert`. // TODO: Offer a way to validate paths, like checking for empty parts. -/// Represents the path of an `Entity`, describing its place in the world's -/// hierarchy which is constructed using `ChildOf` relationships. -pub const Path = struct { - /// Whether the path is specified to be absolute. - /// Note that a relative path can still be interpreted as absolute. - absolute: bool, - /// The string parts that make up the path. - parts: []const EntityPart, - /// The allocator that was used to allocate `parts`. - alloc: ?Allocator = null, - /// Whether the parts outer array itself is owned by this `Path`. - owns_array: bool = false, - /// Whether the parts inner strings are owned by this `Path`. - owns_parts: bool = false, - - /// Represents an `Entity` in a `Path`, either by name or its numeric id. - pub const EntityPart = union(enum) { - id: u32, - name: [:0]const u8, - }; +const Path = @This(); + +/// Whether the path is specified to be absolute. +/// Note that a relative path can still be interpreted as absolute. +absolute: bool, +/// The string parts that make up the path. +parts: []const EntityPart, +/// The allocator that was used to allocate `parts`. +alloc: ?Allocator = null, +/// Whether the parts outer array itself is owned by this `Path`. +owns_array: bool = false, +/// Whether the parts inner strings are owned by this `Path`. +owns_parts: bool = false, + +/// Represents an `Entity` in a `Path`, either by name or its numeric id. +pub const EntityPart = union(enum) { + id: u32, + name: [:0]const u8, +}; - /// Format used to parse and stringify `Path`s. +/// Format used to parse and stringify `Path`s. +/// +/// When this type is formatted, you may use any of the `FormatOption` +/// constants in this type by name as `fmt` specifier, such as `"{unix}"`. +/// An empty `fmt` results in the `default` format being used, which can be +/// changed at runtime. +pub const FormatOptions = struct { + /// The root separator used for an absolute `Path`, if any. /// - /// When this type is formatted, you may use any of the `FormatOption` - /// constants in this type by name as `fmt` specifier, such as `"{unix}"`. - /// An empty `fmt` results in the `default` format being used, which can be - /// changed at runtime. - pub const FormatOptions = struct { - /// The root separator used for an absolute `Path`, if any. - /// - /// If set to `null`, absolute paths can't be represented using strings. - /// In this case, an absolue path and relative path with identical parts - /// will be indistinguishable. - root_sep: ?[]const u8, - /// The separator used between parts that make up a `Path`. - sep: []const u8, - - /// The format used by Flecs' C API. For example `flecs.core`. - pub const flecs_c = FormatOptions{ .root_sep = null, .sep = "." }; - /// The format used by Flecs' C++ API. For example `::flecs::core`. - pub const flecs_cpp = FormatOptions{ .root_sep = "::", .sep = "::" }; - /// Unix-like format. For example `/flecs/core`. - pub const unix = FormatOptions{ .root_sep = "/", .sep = "/" }; - - /// The default format used when none is specified. Can be changed at runtime. - pub var default = flecs_c; - }; + /// If set to `null`, absolute paths can't be represented using strings. + /// In this case, an absolue path and relative path with identical parts + /// will be indistinguishable. + root_sep: ?[]const u8, + /// The separator used between parts that make up a `Path`. + sep: []const u8, + + /// The format used by Flecs' C API. For example `flecs.core`. + pub const flecs_c = FormatOptions{ .root_sep = null, .sep = "." }; + /// The format used by Flecs' C++ API. For example `::flecs::core`. + pub const flecs_cpp = FormatOptions{ .root_sep = "::", .sep = "::" }; + /// Unix-like format. For example `/flecs/core`. + pub const unix = FormatOptions{ .root_sep = "/", .sep = "/" }; + + /// The default format used when none is specified. Can be changed at runtime. + pub var default = flecs_c; +}; - /// Creates an array of `EntityPath`s with known size equal to the number of - /// elements in the specified tuple argument. Each element of the tuple is - /// either converted to a `.name` part, or an `.id` part. - /// - /// Keep in mind the mutability and lifetime of the string elements passed - /// to this function, as they aren't cloned and ownership stays the same. - /// In many cases, the lifetime of `Path`s is relatively short. When this - /// is not the case, it's recommended to `.clone()` the path after creation. - pub fn buildParts(parts: anytype) [numElements(parts)]EntityPart { - if (comptime !std.meta.trait.isTuple(@TypeOf(parts))) - @compileError("Expected tuple, got '" ++ @typeName(@TypeOf(parts)) ++ "'"); - var result: [numElements(parts)]EntityPart = undefined; - inline for (&result, parts) |*res, part| { - res.* = if (comptime std.meta.trait.isZigString(@TypeOf(part))) - .{ .name = part } - else switch (@typeInfo(@TypeOf(part))) { - .Int, .ComptimeInt => .{ .id = part }, - else => @compileError("Expected '[:0]const u8' or 'u32', got '" ++ @typeName(@TypeOf(part)) ++ "'"), - }; - } - return result; +/// Creates an array of `EntityPath`s with known size equal to the number of +/// elements in the specified tuple argument. Each element of the tuple is +/// either converted to a `.name` part, or an `.id` part. +/// +/// Keep in mind the mutability and lifetime of the string elements passed +/// to this function, as they aren't cloned and ownership stays the same. +/// In many cases, the lifetime of `Path`s is relatively short. When this +/// is not the case, it's recommended to `.clone()` the path after creation. +pub fn buildParts(parts: anytype) [numElements(parts)]EntityPart { + if (comptime !meta.isTuple(@TypeOf(parts))) + @compileError("Expected tuple, got '" ++ @typeName(@TypeOf(parts)) ++ "'"); + var result: [numElements(parts)]EntityPart = undefined; + inline for (&result, parts) |*res, part| { + res.* = if (comptime meta.isZigString(@TypeOf(part))) + .{ .name = part } + else switch (@typeInfo(@TypeOf(part))) { + .Int, .ComptimeInt => .{ .id = part }, + else => @compileError("Expected '[:0]const u8' or 'u32', got '" ++ @typeName(@TypeOf(part)) ++ "'"), + }; } + return result; +} - /// Returns the number of elements of the specified tuple type. - fn numElements(parts: anytype) usize { - return if (comptime std.meta.trait.isTuple(@TypeOf(parts))) - @typeInfo(@TypeOf(parts)).Struct.fields.len - else - 0; - } +/// Returns the number of elements of the specified tuple type. +fn numElements(parts: anytype) usize { + return if (comptime meta.isTuple(@TypeOf(parts))) + @typeInfo(@TypeOf(parts)).Struct.fields.len + else + 0; +} - /// Creates a new `Path` from the specified parts. - /// - /// The resulting path does not own any of the given slices. - pub fn fromParts(absolute: bool, parts: []const EntityPart) Path { - std.debug.assert(parts.len > 0); - return .{ .absolute = absolute, .parts = parts }; - } +/// Creates a new `Path` from the specified parts. +/// +/// The resulting path does not own any of the given slices. +pub fn fromParts(absolute: bool, parts: []const EntityPart) Path { + std.debug.assert(parts.len > 0); + return .{ .absolute = absolute, .parts = parts }; +} - /// Parses a string as a `Path` using the `FormatOptions` specified, - /// or `FormatOptions.default` if the argument is `null`. - /// - /// If the string starts with the specified root separator (if any), the - /// resulting path will be absolute. The rest of the string will be split - /// by the specified seperator, becoming its parts. - /// - /// This function will allocate duplicate strings taken from the specified - /// source `path`, to ensure they are sentinel-terminated. The resulting - /// `Path` takes ownership of these. - pub fn fromString(path: []const u8, options: ?FormatOptions, alloc: Allocator) !Path { - if (path.len == 0) return error.MustNotBeEmpty; - const opt = options orelse FormatOptions.default; - - var remaining = path; - var absolute = false; - - // If `root_sep` is set and path starts with it, the path is absolute. - if (opt.root_sep) |p| { - if (std.mem.startsWith(u8, remaining, p)) { - remaining = remaining[p.len..]; - absolute = true; - } +/// Parses a string as a `Path` using the `FormatOptions` specified, +/// or `FormatOptions.default` if the argument is `null`. +/// +/// If the string starts with the specified root separator (if any), the +/// resulting path will be absolute. The rest of the string will be split +/// by the specified seperator, becoming its parts. +/// +/// This function will allocate duplicate strings taken from the specified +/// source `path`, to ensure they are sentinel-terminated. The resulting +/// `Path` takes ownership of these. +pub fn fromString(path: []const u8, options: ?FormatOptions, alloc: Allocator) !Path { + if (path.len == 0) return error.MustNotBeEmpty; + const opt = options orelse FormatOptions.default; + + var remaining = path; + var absolute = false; + + // If `root_sep` is set and path starts with it, the path is absolute. + if (opt.root_sep) |p| { + if (std.mem.startsWith(u8, remaining, p)) { + remaining = remaining[p.len..]; + absolute = true; } - - const parts_len = std.mem.count(u8, remaining, opt.sep) + 1; - const parts = try alloc.alloc(EntityPart, parts_len); - - var i: usize = 0; - var it = std.mem.splitSequence(u8, remaining, opt.sep); - while (it.next()) |str| : (i += 1) - parts[i] = if (parseNumericId(str)) |id| - .{ .id = id } - else - .{ .name = try alloc.dupeZ(u8, str) }; - - return .{ - .absolute = absolute, - .parts = parts, - .alloc = alloc, - .owns_array = true, - .owns_parts = true, - }; } - /// Creates a `Path` for the specified `child` entity, optionally in - /// relation to the specified `parent` entity. If `parent` is not `null`, - /// the resulting path is relative. Otherwise it will be absolute. - /// - /// This function allocates an array for the parts that make up the entity's - /// path, however each part itself is owned by Flecs and could change or be - /// invalidated ay any time, such as when an entity is renamed or removed. - pub fn fromEntity(comptime ctx: type, parent: ?Entity(ctx), child: Entity(ctx), alloc: Allocator) !Path { - std.debug.assert(child.raw != 0); - if (parent) |p| { - std.debug.assert(p.raw != 0); - std.debug.assert(p.raw != child.raw); - } - - // TODO: Use a threadlocal field with reasonable size, then clone the result. - const starting_capacity: usize = 12; - var parts = try alloc.alloc(EntityPart, starting_capacity); - errdefer alloc.free(parts); - var num_parts: usize = 0; - - // Traverse up the entity hierarchy starting from the specified child - // entity up until either the specified parent or root of the hierarchy. - var current = child; - while (true) { - std.debug.assert(num_parts == 0 or current.raw != child.raw); // Cycle detected. - parts[num_parts] = if (current.getName()) |name| - .{ .name = name } - else - .{ .id = current.getEntityId() }; - num_parts += 1; - - // Move to the parent entity, if any. - current = current.getParent() orelse - // If `parent` wasn't specified, we reached the root. Done. - // Otherwise, if the parent wasn't found, return an error. - if (parent == null) break else return error.ParentNotFound; - - // If we reached the specified `parent`, we're done here! - if (parent != null and current.raw == parent.?.raw) break; - } + const parts_len = std.mem.count(u8, remaining, opt.sep) + 1; + const parts = try alloc.alloc(EntityPart, parts_len); - parts = try alloc.realloc(parts, num_parts); - std.mem.reverse(EntityPart, parts); + var i: usize = 0; + var it = std.mem.splitSequence(u8, remaining, opt.sep); + while (it.next()) |str| : (i += 1) + parts[i] = if (parseNumericId(str)) |id| + .{ .id = id } + else + .{ .name = try alloc.dupeZ(u8, str) }; + + return .{ + .absolute = absolute, + .parts = parts, + .alloc = alloc, + .owns_array = true, + .owns_parts = true, + }; +} - return .{ - .absolute = parent != null, - .parts = parts, - .alloc = alloc, - .owns_array = true, - }; +/// Creates a `Path` for the specified `child` entity, optionally in +/// relation to the specified `parent` entity. If `parent` is not `null`, +/// the resulting path is relative. Otherwise it will be absolute. +/// +/// This function allocates an array for the parts that make up the entity's +/// path, however each part itself is owned by Flecs and could change or be +/// invalidated ay any time, such as when an entity is renamed or removed. +pub fn fromEntity(comptime ctx: type, parent: ?Entity(ctx), child: Entity(ctx), alloc: Allocator) !Path { + std.debug.assert(child.raw != 0); + if (parent) |p| { + std.debug.assert(p.raw != 0); + std.debug.assert(p.raw != child.raw); } - /// Creates a deep clone of this `Path` using the specified `Allocator`. - pub fn clone(orig: Path, alloc: Allocator) !Path { - var parts = try alloc.dupe(EntityPart, orig.parts); - errdefer alloc.free(parts); - - var num_allocated: usize = 0; - errdefer for (parts[0..num_allocated]) |part| - if (part == .name) alloc.free(part.name); + // TODO: Use a threadlocal field with reasonable size, then clone the result. + const starting_capacity: usize = 12; + var parts = try alloc.alloc(EntityPart, starting_capacity); + errdefer alloc.free(parts); + var num_parts: usize = 0; + + // Traverse up the entity hierarchy starting from the specified child + // entity up until either the specified parent or root of the hierarchy. + var current = child; + while (true) { + std.debug.assert(num_parts == 0 or current.raw != child.raw); // Cycle detected. + parts[num_parts] = if (current.getName()) |name| + .{ .name = name } + else + .{ .id = current.getEntityId() }; + num_parts += 1; - for (parts) |*part| { - if (part.* == .name) - part.* = .{ .name = try alloc.dupeZ(u8, part.name) }; - num_allocated += 1; - } + // Move to the parent entity, if any. + current = current.getParent() orelse + // If `parent` wasn't specified, we reached the root. Done. + // Otherwise, if the parent wasn't found, return an error. + if (parent == null) break else return error.ParentNotFound; - return .{ - .absolute = orig.absolute, - .parts = parts, - .alloc = alloc, - .owns_array = true, - .owns_parts = true, - }; + // If we reached the specified `parent`, we're done here! + if (parent != null and current.raw == parent.?.raw) break; } - /// Destroys any memory owned by this `Path`, if any. - pub fn deinit(self: Path) void { - if (self.owns_parts) - for (self.parts) |part| - if (part == .name) - self.alloc.?.free(part.name); - if (self.owns_array) - self.alloc.?.free(self.parts); - } + parts = try alloc.realloc(parts, num_parts); + std.mem.reverse(EntityPart, parts); - pub fn toString( - self: Path, - options: ?FormatOptions, - alloc: Allocator, - ) ![:0]const u8 { - const opt = options orelse FormatOptions.default; - const length = self.calculateLength(opt); - var result = try alloc.allocSentinel(u8, length, 0); - var stream = std.io.fixedBufferStream(result); - try write(self, opt, stream.writer()); - return result; - } + return .{ + .absolute = parent != null, + .parts = parts, + .alloc = alloc, + .owns_array = true, + }; +} - pub fn format( - self: Path, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = options; // TODO: Actually make use of this. - - const opt = if (fmt.len == 0) - FormatOptions.default - else if (@hasDecl(FormatOptions, fmt) and @TypeOf(@field(FormatOptions, fmt)) == FormatOptions) - @field(FormatOptions, fmt) - else - std.fmt.invalidFmtError(fmt, Path); +/// Creates a deep clone of this `Path` using the specified `Allocator`. +pub fn clone(orig: Path, alloc: Allocator) !Path { + var parts = try alloc.dupe(EntityPart, orig.parts); + errdefer alloc.free(parts); - try self.write(opt, writer); - } + var num_allocated: usize = 0; + errdefer for (parts[0..num_allocated]) |part| + if (part == .name) alloc.free(part.name); - /// Returns whether the contents of the specified `Path`s are equivalent. - /// - /// Path equivalency does not imply these paths are or are not referring to - /// the same `Entity`. For example, an entity that is referred to using its - /// entity id has a different path from the same entity referred to by name. - pub fn equals(first: Path, second: Path) bool { - if (first.absolute != second.absolute) return false; - if (first.parts.len != second.parts.len) return false; - for (first.parts, second.parts) |a, b| switch (a) { - .id => |a_id| if (b != .id or a_id != b.id) return false, - .name => |a_name| if (b != .name or !std.mem.eql(u8, a_name, b.name)) return false, - }; - return true; + for (parts) |*part| { + if (part.* == .name) + part.* = .{ .name = try alloc.dupeZ(u8, part.name) }; + num_allocated += 1; } - fn calculateLength(self: Path, opt: FormatOptions) usize { - // Separators. - var result = opt.sep.len * (self.parts.len - 1); - // Root separator. - if (self.absolute) { - if (opt.root_sep) |p| - result += p.len; - } - // Parts themselves. + return .{ + .absolute = orig.absolute, + .parts = parts, + .alloc = alloc, + .owns_array = true, + .owns_parts = true, + }; +} + +/// Destroys any memory owned by this `Path`, if any. +pub fn deinit(self: Path) void { + if (self.owns_parts) for (self.parts) |part| - result += switch (part) { - .id => |id| numDigits(id), - .name => |name| name.len, - }; - return result; + if (part == .name) + self.alloc.?.free(part.name); + if (self.owns_array) + self.alloc.?.free(self.parts); +} + +pub fn toString( + self: Path, + options: ?FormatOptions, + alloc: Allocator, +) ![:0]const u8 { + const opt = options orelse FormatOptions.default; + const length = self.calculateLength(opt); + const result = try alloc.allocSentinel(u8, length, 0); + var stream = std.io.fixedBufferStream(result); + try write(self, opt, stream.writer()); + return result; +} + +pub fn format( + self: Path, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = options; // TODO: Actually make use of this. + + const opt = if (fmt.len == 0) + FormatOptions.default + else if (@hasDecl(FormatOptions, fmt) and @TypeOf(@field(FormatOptions, fmt)) == FormatOptions) + @field(FormatOptions, fmt) + else + std.fmt.invalidFmtError(fmt, Path); + + try self.write(opt, writer); +} + +/// Returns whether the contents of the specified `Path`s are equivalent. +/// +/// Path equivalency does not imply these paths are or are not referring to +/// the same `Entity`. For example, an entity that is referred to using its +/// entity id has a different path from the same entity referred to by name. +pub fn equals(first: Path, second: Path) bool { + if (first.absolute != second.absolute) return false; + if (first.parts.len != second.parts.len) return false; + for (first.parts, second.parts) |a, b| switch (a) { + .id => |a_id| if (b != .id or a_id != b.id) return false, + .name => |a_name| if (b != .name or !std.mem.eql(u8, a_name, b.name)) return false, + }; + return true; +} + +fn calculateLength(self: Path, opt: FormatOptions) usize { + // Separators. + var result = opt.sep.len * (self.parts.len - 1); + // Root separator. + if (self.absolute) { + if (opt.root_sep) |p| + result += p.len; } + // Parts themselves. + for (self.parts) |part| + result += switch (part) { + .id => |id| numDigits(id), + .name => |name| name.len, + }; + return result; +} - fn write(self: Path, opt: FormatOptions, writer: anytype) !void { - // Write root separator (if applicable). - if (self.absolute) { - if (opt.root_sep) |p| - try writer.writeAll(p); - } - // Write the first part. - switch (self.parts[0]) { - .id => |id| try writer.writeIntNative(u32, id), +fn write(self: Path, opt: FormatOptions, writer: anytype) !void { + // Write root separator (if applicable). + if (self.absolute) { + if (opt.root_sep) |p| + try writer.writeAll(p); + } + // Write the first part. + switch (self.parts[0]) { + .id => |id| try writer.writeInt(u32, id, native_endian), + .name => |name| try writer.writeAll(name), + } + // Write the remaining parts, each preceeded bu separator. + for (self.parts[1..]) |part| { + try writer.writeAll(opt.sep); + switch (part) { + .id => |id| try writer.writeInt(u32, id, native_endian), .name => |name| try writer.writeAll(name), } - // Write the remaining parts, each preceeded bu separator. - for (self.parts[1..]) |part| { - try writer.writeAll(opt.sep); - switch (part) { - .id => |id| try writer.writeIntNative(u32, id), - .name => |name| try writer.writeAll(name), - } - } } +} - /// Attempts to parse the specified string as a numeric entity id. - fn parseNumericId(str: []const u8) ?u32 { - if (str.len == 0 or str.len > 10) return null; - var result: u32 = 0; - var place: u32 = 1; - var it = std.mem.reverseIterator(str); - while (it.next()) |chr| { - const d = std.fmt.charToDigit(chr, 10) catch return null; - result = std.math.add(u32, result, d * place) catch return null; - place *%= 10; // Wrapping to avoid overflow error. - } - return result; +/// Attempts to parse the specified string as a numeric entity id. +fn parseNumericId(str: []const u8) ?u32 { + if (str.len == 0 or str.len > 10) return null; + var result: u32 = 0; + var place: u32 = 1; + var it = std.mem.reverseIterator(str); + while (it.next()) |chr| { + const d = std.fmt.charToDigit(chr, 10) catch return null; + result = std.math.add(u32, result, d * place) catch return null; + place *%= 10; // Wrapping to avoid overflow error. } + return result; +} - /// Calculates the numbers of digits of an entity id when formatted as a - /// string. Since entity ids tend to be small this is technically optimized - /// for smaller numbers but performance likely doesn't matter much. - fn numDigits(n: u32) usize { - // zig fmt: off - return if (n < 10) 1 - else if (n < 100) 2 - else if (n < 1000) 3 - else if (n < 10000) 4 - else if (n < 100000) 5 - else if (n < 1000000) 6 - else if (n < 10000000) 7 - else if (n < 100000000) 8 - else if (n < 1000000000) 9 - else 10; - // zig fmt: on - } -}; +/// Calculates the numbers of digits of an entity id when formatted as a +/// string. Since entity ids tend to be small this is technically optimized +/// for smaller numbers but performance likely doesn't matter much. +fn numDigits(n: u32) usize { + // zig fmt: off + return if (n < 10) 1 + else if (n < 100) 2 + else if (n < 1000) 3 + else if (n < 10000) 4 + else if (n < 100000) 5 + else if (n < 1000000) 6 + else if (n < 10000000) 7 + else if (n < 100000000) 8 + else if (n < 1000000000) 9 + else 10; + // zig fmt: on +} test Path { const alloc = std.testing.allocator; - const expect = @import("./expect.zig"); + const expect = @import("./test/expect.zig"); // Paths may be constructed by parsing strings. const relative = try Path.fromString("some.relative.path", null, alloc); @@ -414,6 +419,8 @@ test Path { // The default `FormatOptions` may be changed. Path.FormatOptions.default = Path.FormatOptions.unix; + // But be sure to unset them after this test so other tests can succeed. + defer Path.FormatOptions.default = Path.FormatOptions.flecs_c; // This affects functions that use them to parse or format strings. try expect.fmt("some/relative/path", "{}", .{relative}); diff --git a/src/expect.zig b/src/test/expect.zig similarity index 94% rename from src/expect.zig rename to src/test/expect.zig index cf4e49b..1c373b3 100644 --- a/src/expect.zig +++ b/src/test/expect.zig @@ -3,6 +3,7 @@ //! determines the type of `expected`, avoiding the use of `@as`. const std = @import("std"); +const meta = @import("../meta.zig"); pub fn @"true"(value: anytype) !void { return equal(true, value); @@ -21,7 +22,7 @@ pub fn err(expected: anyerror, actual: anytype) !void { } pub fn equal(expected: anytype, actual: anytype) !void { - return if (comptime std.meta.trait.isZigString(@TypeOf(expected))) + return if (comptime meta.isZigString(@TypeOf(expected))) equalStrings(expected, actual) else std.testing.expectEqual(@as(@TypeOf(actual), expected), actual); diff --git a/test/entity.zig b/src/test/flecs/entity.zig similarity index 98% rename from test/entity.zig rename to src/test/flecs/entity.zig index 7133e9c..c54ff04 100644 --- a/test/entity.zig +++ b/src/test/flecs/entity.zig @@ -3,17 +3,18 @@ const std = @import("std"); const alloc = std.testing.allocator; -const expect = @import("../src/expect.zig"); -const util = @import("./util.zig"); -const FlecsError = @import("../src/error.zig").FlecsError; -const flecs = @import("../src/main.zig"); +const expect = @import("../expect.zig"); +const util = @import("../util.zig"); + +const flecs = @import("../../main.zig"); +const FlecsError = flecs.FlecsError; const c = flecs.c; -const context = flecs.Context(void); -const Entity = context.Entity; -const Path = context.Path; -const World = context.World; +const Context = flecs.Context(void); +const Entity = Context.Entity; +const Path = Context.Path; +const World = Context.World; test "Entity_init_id" { flecs.init(alloc); diff --git a/test/world.zig b/src/test/flecs/world.zig similarity index 77% rename from test/world.zig rename to src/test/flecs/world.zig index d3667b7..19e4534 100644 --- a/test/world.zig +++ b/src/test/flecs/world.zig @@ -3,23 +3,24 @@ const std = @import("std"); const alloc = std.testing.allocator; -const expect = @import("../src/expect.zig"); -const util = @import("./util.zig"); -const flecs = @import("../src/main.zig"); +const expect = @import("./../expect.zig"); +const util = @import("./../util.zig"); + +const flecs = @import("../../main.zig"); const c = flecs.c; -const context = flecs.Context(void); -const World = context.World; -const Iter = context.Iter; -const Entity = context.Entity; +const Context = flecs.Context(void); +const World = Context.World; +const Iter = Context.Iter; +const Entity = Context.Entity; const Position = struct { x: f32, y: f32 }; const Velocity = struct { x: f32, y: f32 }; fn move(it: Iter) void { - var pos = it.field(Position, 1); - var vel = it.field(Velocity, 2); + const pos = it.field(Position, 1); + const vel = it.field(Velocity, 2); util.Probe.probeIter(it) catch unreachable; for (pos, vel) |*p, *v| { @@ -28,10 +29,6 @@ fn move(it: Iter) void { } } -fn lookup(comptime T: type) c.ecs_entity_t { - return flecs.Lookup(void, T).id; -} - test "World_progress_w_0" { flecs.init(std.testing.allocator); var world = try World.init(); @@ -45,7 +42,7 @@ test "World_progress_w_0" { const move_system = try world.system("move", move, c.EcsOnUpdate, "Position, Velocity"); var ctx = util.Probe.init(); - c.ecs_set_context(world.raw, &ctx); + c.ecs_set_ctx(world.raw, &ctx, null); e1.set(Position, .{ .x = 0, .y = 0 }); e1.set(Velocity, .{ .x = 1, .y = 2 }); @@ -59,8 +56,8 @@ test "World_progress_w_0" { try expect.equal(null, ctx.param); try expect.equal(e1.raw, ctx.e[0]); - try expect.equal(lookup(Position), ctx.c[0][0]); - try expect.equal(lookup(Velocity), ctx.c[0][1]); + try expect.equal(Context.lookup(Position).*, ctx.c[0][0]); + try expect.equal(Context.lookup(Velocity).*, ctx.c[0][1]); try expect.equal(0, ctx.s[0][0]); try expect.equal(0, ctx.s[0][1]); @@ -82,7 +79,7 @@ test "World_progress_w_t" { const move_system = try world.system("move", move, c.EcsOnUpdate, "Position, Velocity"); var ctx = util.Probe.init(); - c.ecs_set_context(world.raw, &ctx); + c.ecs_set_ctx(world.raw, &ctx, null); e1.set(Position, .{ .x = 0, .y = 0 }); e1.set(Velocity, .{ .x = 1, .y = 2 }); @@ -96,8 +93,8 @@ test "World_progress_w_t" { try expect.equal(null, ctx.param); try expect.equal(e1.raw, ctx.e[0]); - try expect.equal(lookup(Position), ctx.c[0][0]); - try expect.equal(lookup(Velocity), ctx.c[0][1]); + try expect.equal(Context.lookup(Position).*, ctx.c[0][0]); + try expect.equal(Context.lookup(Velocity).*, ctx.c[0][1]); try expect.equal(0, ctx.s[0][0]); try expect.equal(0, ctx.s[0][1]); diff --git a/src/test/main.zig b/src/test/main.zig new file mode 100644 index 0000000..531a10c --- /dev/null +++ b/src/test/main.zig @@ -0,0 +1,4 @@ +test { + _ = @import("./flecs/entity.zig"); + _ = @import("./flecs/world.zig"); +} diff --git a/test/util.zig b/src/test/util.zig similarity index 95% rename from test/util.zig rename to src/test/util.zig index 12c819e..e0da3d9 100644 --- a/test/util.zig +++ b/src/test/util.zig @@ -3,7 +3,7 @@ const expect = std.testing.expect; const expectEql = std.testing.expectEqual; const expectStrEql = std.testing.expectEqualStrings; -const flecs = @import("../src/main.zig"); +const flecs = @import("../main.zig"); const c = flecs.c; const context = flecs.Context(void); @@ -65,7 +65,7 @@ pub const Probe = struct { } pub fn probeIter(it: Iter) !void { - var ctx = c.ecs_get_context(it.world.raw); + var ctx = c.ecs_get_ctx(it.world.raw); if (ctx == null) ctx = it.raw.ctx; if (ctx) |ct| { var p: *Probe = @alignCast(@ptrCast(ct)); diff --git a/src/util.zig b/src/util.zig deleted file mode 100644 index 71b1e2a..0000000 --- a/src/util.zig +++ /dev/null @@ -1,75 +0,0 @@ -const std = @import("std"); - -const c = @import("./c.zig"); - -const Lookup = @import("./main.zig").Lookup; -const Entity = @import("./entity.zig").Entity; -const Pair = @import("./pair.zig").Pair; -const Id = @import("./id.zig").Id; - -/// Converts the specified value to an `ecs_entity_t`. -/// -/// If `null` is passed to this function, either directly or through an -/// optional type of one of the supported types, `0` is returned. It's up to -/// the caller to do any necessary testing for `0` values, and whether the -/// returned entity is valid or alive. -/// -/// The following can be converted: -/// - `Entity` and `ecs_entity_t` - Self-explanatory. -/// - `type` - Looks up the entity associated with that type. -pub fn anyToEntity(comptime ctx: anytype, value: anytype) c.ecs_entity_t { - return switch (@TypeOf(value)) { - @TypeOf(null) => 0, - c.ecs_entity_t => value, - Entity(ctx) => value.raw, - ?Entity(ctx) => if (value) |v| v.raw else 0, - type => Lookup(ctx, value).id, - else => @compileError("Value of type " ++ @typeName(@TypeOf(value)) ++ " can't be converted to Entity"), - }; -} - -/// Converts the specified value to an `ecs_id_t`. -/// -/// If `null` is passed to this function, either directly or through an -/// optional type of one of the supported types, `0` is returned. It's up to -/// the caller to do any necessary testing for `0` values, and whether the -/// returned id is valid. -/// -/// The following can be converted: -/// - `Id` and `ecs_id_it` - Self-explanatory. -/// - `Entity` and `ecs_entity_t` - Self-explanatory. -/// - `Pair` - Converts to the equivalent `Id`. -/// - `.{ relation, target }` - A `Pair`, converted using `anyToEntity()`. -/// - `type` - Looks up the entity associated with that type. -pub fn anyToId(comptime ctx: anytype, value: anytype) c.ecs_id_t { - const T = @TypeOf(value); - if (comptime std.meta.trait.isTuple(T)) { - if (@typeInfo(T).Struct.fields.len != 2) - @compileError("Value of type " ++ @typeName(T) ++ " must be a tuple with 2 elements, to be a Pair"); - const relation = anyToEntity(ctx, value[0]); - const target = anyToEntity(ctx, value[1]); - return c.ecs_make_pair(relation, target); - } - return switch (T) { - @TypeOf(null) => 0, - c.ecs_id_t => value, - Id(ctx) => value.raw, - ?Id(ctx) => if (value) |v| v.raw else 0, - Pair(ctx) => value.raw, - ?Pair(ctx) => if (value) |v| v.raw else 0, - // Technically same type as `ecs_id_it`. - // c.ecs_entity_t => value, - Entity(ctx) => value.raw, - ?Entity(ctx) => if (value) |v| v.raw else 0, - type => Lookup(ctx, value).id, - else => @compileError("Value of type " ++ @typeName(T) ++ " can't be converted to Id"), - }; -} - -/// Gets the simplified type name of the specified type. -/// That is, without any namespace qualifiers. -pub fn simpleTypeName(comptime T: type) [:0]const u8 { - const fullName = @typeName(T); - const index = std.mem.lastIndexOf(u8, fullName, "."); - return if (index) |i| fullName[(i + 1)..] else fullName; -} diff --git a/src/world.zig b/src/world.zig index b431a50..4d509cf 100644 --- a/src/world.zig +++ b/src/world.zig @@ -1,23 +1,22 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const meta = @import("./meta.zig"); const flecs = @import("./main.zig"); +const EntityError = flecs.EntityError; +const Path = flecs.Path; const c = flecs.c; -const err = @import("./error.zig"); -const util = @import("./util.zig"); - -const Lookup = @import("./main.zig").Lookup; -const Entity = @import("./entity.zig").Entity; -const EntityError = @import("./entity.zig").EntityError; -const Iter = @import("./iter.zig").Iter; -const Path = @import("./path.zig").Path; pub fn World(comptime ctx: anytype) type { return struct { - raw: *c.ecs_world_t, - const Self = @This(); + const Context = flecs.Context(ctx); + const Entity = Context.Entity; + const Iter = Context.Iter; + + raw: *c.ecs_world_t, + pub fn init() !*Self { std.debug.assert(flecs.is_initialized); var result = try flecs.allocator.create(Self); @@ -54,8 +53,8 @@ pub fn World(comptime ctx: anytype) type { /// Returns an `Entity` for the specified `ecs_entity_t` value, or an /// error if the entity is invalid or not alive in this `World`. - pub fn lookupAlive(self: *Self, id: c.ecs_entity_t) !Entity(ctx) { - const result = Entity(ctx).fromRaw(self, id); + pub fn lookupAlive(self: *Self, id: c.ecs_entity_t) !Entity { + const result = Entity.fromRaw(self, id); try result.ensureAlive(); return result; } @@ -66,7 +65,7 @@ pub fn World(comptime ctx: anytype) type { /// Returns the `Entity` at the specified path, or an error if the /// entity does not exist. If the path is not absolute, the operation /// will use the current scope, or the world root. - pub fn lookupByPath(self: *Self, path: Path) !Entity(ctx) { + pub fn lookupByPath(self: *Self, path: Path) !Entity { var parent = if (path.absolute) 0 else c.ecs_get_scope(self.raw); var current: c.ecs_entity_t = undefined; for (path.parts) |part| { @@ -80,43 +79,43 @@ pub fn World(comptime ctx: anytype) type { if (part == .id and parent != c.ecs_get_parent(self.raw, current)) return error.ParentMismatch; parent = current; } - return Entity(ctx).fromRaw(self, current); + return Entity.fromRaw(self, current); } /// Returns the component `Entity` registered for the specified /// type `T`, or an error if an association has not been made. - pub fn lookupByType(self: *Self, comptime T: type) !Entity(ctx) { - return lookupAlive(self, Lookup(ctx, T).id); + pub fn lookupByType(self: *Self, comptime T: type) !Entity { + return lookupAlive(self, Context.lookup(T).*); } /// Creates or modifies an `Entity` in this `World`. /// See `Entity.init(...)` for more information. - pub fn entity(self: *Self, config: Entity(ctx).Config, add: anytype) !Entity(ctx) { - return Entity(ctx).init(self, config, add); + pub fn entity(self: *Self, config: Entity.Config, add: anytype) !Entity { + return Entity.init(self, config, add); } - pub fn tag(self: *Self, comptime T: type) !Entity(ctx) { + pub fn tag(self: *Self, comptime T: type) !Entity { if (@sizeOf(T) > 0) @compileError("'" ++ @typeName(T) ++ "' must be a zero-sized type"); - const name = util.simpleTypeName(T); + const name = meta.simpleTypeName(T); const result = try self.entity(.{ .name = name, .symbol = name }, .{}); - Lookup(ctx, T).id = result.raw; + Context.lookup(T).* = result.raw; return result; } - pub fn component(self: *Self, comptime T: type) !Entity(ctx) { + pub fn component(self: *Self, comptime T: type) !Entity { if (@sizeOf(T) == 0) @compileError("'" ++ @typeName(T) ++ "' must not be a zero-sized type"); - const name = util.simpleTypeName(T); - const entity2 = try self.entity(.{ .name = name, .symbol = name, .use_low_id = true }, .{}); + const name = meta.simpleTypeName(T); + const entity_ = try self.entity(.{ .name = name, .symbol = name, .use_low_id = true }, .{}); const desc = std.mem.zeroInit(c.ecs_component_desc_t, .{ - .entity = entity2.raw, + .entity = entity_.raw, .type = .{ .size = @sizeOf(T), .alignment = @alignOf(T) }, }); - var result = c.ecs_component_init(self.raw, &desc); - if (result == 0) return err.getLastErrorOrUnknown(); - Lookup(ctx, T).id = result; - return Entity(ctx).fromRaw(self, result); + const result = c.ecs_component_init(self.raw, &desc); + if (result == 0) return flecs.getLastErrorOrUnknown(); + Context.lookup(T).* = result; + return Entity.fromRaw(self, result); } /// Registers a singleton component of type `T` with the specified value. @@ -129,7 +128,7 @@ pub fn World(comptime ctx: anytype) type { /// Use `get()` and `set()` to get and set the value of this singleton. /// /// Returns the created component entity. - pub fn singleton(self: *Self, comptime T: type, value: T) !Entity(ctx) { + pub fn singleton(self: *Self, comptime T: type, value: T) !Entity { const single = try component(self, T); single.set(T, value); return single; @@ -160,16 +159,16 @@ pub fn World(comptime ctx: anytype) type { callback: SystemCallback, phase: anytype, expr: [:0]const u8, - ) !Entity(ctx) { - const phase2 = util.anyToEntity(ctx, phase); - const entity2 = try if (phase2 != 0) - self.entity(.{ .name = name }, .{ .{ c.EcsDependsOn, phase2 }, phase2 }) + ) !Entity { + const phase_ = Context.anyToEntity(phase); + const entity_ = try if (phase_ != 0) + self.entity(.{ .name = name }, .{ .{ c.EcsDependsOn, phase_ }, phase_ }) else self.entity(.{ .name = name }, .{}); - var context = try SystemCallbackContext.init(self, callback); + const context = try SystemCallbackContext.init(self, callback); var desc = std.mem.zeroInit(c.ecs_system_desc_t, .{ - .entity = entity2.raw, + .entity = entity_.raw, .callback = &SystemCallbackContext.invoke, .binding_ctx = context, .binding_ctx_free = &SystemCallbackContext.free, @@ -177,11 +176,11 @@ pub fn World(comptime ctx: anytype) type { desc.query.filter.expr = expr; const result = c.ecs_system_init(self.raw, &desc); - if (result == 0) return err.getLastErrorOrUnknown(); - return Entity(ctx).fromRaw(self, result); + if (result == 0) return flecs.getLastErrorOrUnknown(); + return Entity.fromRaw(self, result); } - const SystemCallback = *const fn (Iter(ctx)) void; + const SystemCallback = *const fn (Iter) void; const SystemCallbackContext = struct { world: *Self, func: SystemCallback, @@ -204,7 +203,7 @@ pub fn World(comptime ctx: anytype) type { fn invoke(it2: *anyopaque) callconv(.C) void { const it: ?*c.ecs_iter_t = @alignCast(@ptrCast(it2)); const context: *SystemCallbackContext = @alignCast(@ptrCast(it.?.binding_ctx)); - var iter = Iter(ctx).fromRawPtr(context.world, it.?); + const iter = Iter.fromRawPtr(context.world, it.?); context.func(iter); } }; @@ -212,7 +211,7 @@ pub fn World(comptime ctx: anytype) type { /// Creates a term iterator which allows querying for all entities /// that have a specific `Id`. This function supports wildcards. pub fn term(self: *Self, id: anytype) TermIterator { - const id_ = util.anyToId(ctx, id); + const id_ = Context.anyToId(id); var term_ = std.mem.zeroInit(c.ecs_term_t, .{ .id = id_ }); const iter = c.ecs_term_iter(self.raw, &term_); return .{ .world = self, .iter = iter }; @@ -221,18 +220,18 @@ pub fn World(comptime ctx: anytype) type { // TODO: Move this logic to `Iter`, add `TermIter` type? // TODO: Rename `Iter` to `Iterator`? pub const TermIterator = struct { - world: *World(ctx), + world: *World, iter: c.ecs_iter_t, index: i32 = 0, - pub fn next(self: *TermIterator) ?Entity(ctx) { + pub fn next(self: *TermIterator) ?Entity { if (self.index >= self.iter.count) if (!c.ecs_term_next(&self.iter)) return null; const result = self.iter.entities[@intCast(self.index)]; self.index += 1; - return Entity(ctx).fromRaw(self.world, result); + return Entity.fromRaw(self.world, result); } // TODO: Check where else in the codebase it would make sense to have `deinit` take a pointer? @@ -245,9 +244,9 @@ pub fn World(comptime ctx: anytype) type { }; /// Gets the current scope of this `World`. See also: `setScope()`. - pub fn getScope(self: *Self) ?Entity(ctx) { + pub fn getScope(self: *Self) ?Entity { const result = c.ecs_get_scope(self.raw); - return if (result != 0) Entity(ctx).fromRaw(self, result) else null; + return if (result != 0) Entity.fromRaw(self, result) else null; } /// Sets the current scope of this `World` to the specified entity. @@ -259,9 +258,9 @@ pub fn World(comptime ctx: anytype) type { /// Returns the previously set scope, if any. It's recommended to set /// the scope back to the previous value after you're done operating /// in the desired scope. - pub fn setScope(self: *Self, value: anytype) ?Entity(ctx) { - const result = c.ecs_set_scope(self.raw, util.anyToEntity(ctx, value)); - return if (result != 0) Entity(ctx).fromRaw(self, result) else null; + pub fn setScope(self: *Self, value: anytype) ?Entity { + const result = c.ecs_set_scope(self.raw, Context.anyToEntity(value)); + return if (result != 0) Entity.fromRaw(self, result) else null; } }; } diff --git a/test/main.zig b/test/main.zig deleted file mode 100644 index 682e66f..0000000 --- a/test/main.zig +++ /dev/null @@ -1,5 +0,0 @@ -test { - _ = @import("../src/main.zig"); - _ = @import("./entity.zig"); - _ = @import("./world.zig"); -}