Update Zig and Flecs, big refactor

main
copygirl 3 months ago
parent 10ce815c2a
commit 772908b3c1
  1. 42
      README.md
  2. 16
      build.zig
  3. 12
      build.zig.zon
  4. 2
      libs/flecs
  5. 52
      src/entity.zig
  6. 53
      src/id.zig
  7. 31
      src/iter.zig
  8. 110
      src/main.zig
  9. 57
      src/meta.zig
  10. 44
      src/pair.zig
  11. 611
      src/path.zig
  12. 3
      src/test/expect.zig
  13. 17
      src/test/flecs/entity.zig
  14. 35
      src/test/flecs/world.zig
  15. 4
      src/test/main.zig
  16. 4
      src/test/util.zig
  17. 75
      src/util.zig
  18. 97
      src/world.zig
  19. 5
      test/main.zig

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

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

@ -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",
},
}

@ -1 +1 @@
Subproject commit 74a7f74a2835d3946f05502ef44ba1d5d8b48eff
Subproject commit 198607d10ab8f8fc44540043271d6e3be019250b

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

@ -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`.

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

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

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

@ -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?

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

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

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

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

@ -0,0 +1,4 @@
test {
_ = @import("./flecs/entity.zig");
_ = @import("./flecs/world.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));

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

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

@ -1,5 +0,0 @@
test {
_ = @import("../src/main.zig");
_ = @import("./entity.zig");
_ = @import("./world.zig");
}
Loading…
Cancel
Save