From cf60a43e10f39b50e721d5306d47fcfc9a548f67 Mon Sep 17 00:00:00 2001 From: copygirl Date: Wed, 6 Sep 2023 13:51:29 +0200 Subject: [PATCH] Entity.init improvements - Move EntityConfig to Entity.Config - Now able to specify .parent and .path - Add "Entity_init_id_path" test --- src/entity.zig | 117 +++++++++++++++++++++++++++++++++++++++--------- src/main.zig | 2 + src/world.zig | 3 +- test/entity.zig | 18 +++++++- 4 files changed, 116 insertions(+), 24 deletions(-) diff --git a/src/entity.zig b/src/entity.zig index ec2efe3..f190e87 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -23,12 +23,6 @@ pub const EntityError = error{ IsNotAlive, }; -pub const EntityConfig = struct { - name: ?[:0]const u8 = null, - symbol: ?[:0]const u8 = null, - use_low_id: bool = false, -}; - /// An `Entity` is an `Id` which represents a "thing" within the world. This /// could be a traditional game object, but is also able to represent other /// concepts such as component types, relationship types, systems, modules, @@ -60,36 +54,117 @@ pub fn Entity(comptime ctx: anytype) type { return .{ .world = world, .raw = raw }; } - /// Creates a new `Entity` in the specified world. - /// May return an existing entity if one with the given name exists. - /// May return an error if the entity creation failed. + pub const Config = struct { + /// Set to specify the entity's name. + name: ?[]const u8 = null, + /// Set to specify the entity's symbol. + symbol: ?[]const u8 = null, + /// Whether to use a small id when generating one. + /// These are typically reserved for components. + use_low_id: bool = false, + /// Set to specify the parent to create the entity under. + /// - Asserts when used together with an absolute `path`. + parent: ?Self = null, + /// Set to specify the `Path` under which to create the entity. + /// - Asserts when used together with a `name`. + /// - Asserts when the final part of the path is an id. + /// - Asserts when any parts refer to a non-existent id. + path: ?Path = null, + }; + + /// Creates a new `Entity` in the specified world. If `config` refers + /// to an existing entity, it is modified, then returned. Returns an + /// error if the entity creation failed. /// - /// The `config` argument may be used to specify the `name` and - /// `symbol` used to create the entity with. If an entity with the - /// given name already exists, that one is modified instead. - /// Does not take ownership of any of the provided values. + /// `add` is a tuple that specifies `Id`s added to the entity. + /// For example: `.{ Position, Velocity, .{ ChildOf, parent } }`. /// - /// The `add` argument is a tuple that specifies the `Id`s the entity - /// is created with initially. For example: - /// `.{ Position, Velocity, .{ ChildOf, parent } }` + /// There are three ways to set a parent entity: Through `.parent`, + /// an absolute `.path` (resulting in no parent), or by passing a + /// `ChildOf` relationship in `add`. This function panics if these + /// methods are combined. If no parent is specified, it defaults to + /// the current world's scope. /// /// When you add components, they are default-initialized. To set /// their values you'll need to use one of the setter functions on the /// `Entity` returned from this function. - pub fn new(world: *World(ctx), config: EntityConfig, add: anytype) !Self { - var desc = std.mem.zeroes(c.ecs_entity_desc_t); - desc.name = if (config.name) |n| n.ptr else null; - desc.symbol = if (config.symbol) |s| s.ptr else null; - desc.use_low_id = config.use_low_id; + pub fn new(world: *World(ctx), config: Config, add: anytype) !Self { + if (config.name != null and config.path != null) + std.debug.panic(".name and .path can't be used together", .{}); + if (config.parent != null and config.path != null and config.path.?.absolute) + std.debug.panic(".parent and an absolute .path can't be used together", .{}); + if (config.path) |path| if (path.parts[path.parts.len - 1] == .id) + std.debug.panic(".path must not have a final part that is an id", .{}); const meta = @typeInfo(@TypeOf(add)); if (meta != .Struct or (meta.Struct.is_tuple == false and meta.Struct.fields.len > 0)) @compileError("Expected tuple or empty struct, got '" ++ @typeName(@TypeOf(add)) ++ "'"); if (meta.Struct.fields.len > c.FLECS_ID_DESC_MAX) @compileError("Adding more than FLECS_ID_DESC_MAX ids"); + + // Get name either from `.name` or last part of `.path`. + // Specifying a path with a single part is equivalent to using `.name`. + const name = if (config.path) |path| + path.parts[path.parts.len - 1].name + else if (config.name) |name| + name + else + null; + + const absolute = if (config.path) |path| path.absolute else false; + var scope: ?c.ecs_entity_t = if (config.parent) |parent| parent.raw else if (absolute) 0 else null; + + // Ensure the parent entities specified in `path` exist or create them. + if (config.path) |path| if (path.parts.len > 1) + for (path.parts[0..(path.parts.len - 1)]) |part| switch (part) { + .name => |n| { + // TODO: Use an allocator that's well-fitted for super short-lived allocations. + const nameZ = try world.allocator.dupeZ(u8, n); + defer world.allocator.free(nameZ); + const parent = scope orelse c.ecs_get_scope(world.raw); + const found = c.ecs_lookup_child(world.raw, parent, nameZ.ptr); + if (found == 0) { + var desc = std.mem.zeroInit(c.ecs_entity_desc_t, .{ .name = nameZ.ptr }); + desc.add[0] = c.ecs_pair(c.EcsChildOf, parent); + scope = c.ecs_entity_init(world.raw, &desc); + if (scope == 0) return err.getLastErrorOrUnknown(); + } else scope = found; + }, + .id => |i| { + const found = c.ecs_get_alive(world.raw, i); + if (found == 0) std.debug.panic("No entity with id '{d}' found", .{i}); + // TODO: Allow formatting entity, use it to improve this error message. + if (c.ecs_get_parent(world.raw, found) != scope.?) std.debug.panic("Entity '{d}' does not have expected parent", .{i}); + scope = found; + }, + }; + + // Set the scope to create the entity under (its parent). + // This avoids using up an id for a `ChildOf` relationship. + const previous = if (scope) |s| c.ecs_set_scope(world.raw, s) else null; + defer _ = if (previous) |s| c.ecs_set_scope(world.raw, s); + + // TODO: Use an allocator that's well-fitted for super short-lived allocations. + const nameZ = if (name) |n| try world.allocator.dupeZ(u8, n) else null; + defer if (nameZ) |n| world.allocator.free(n); + const symbolZ = if (config.symbol) |s| try world.allocator.dupeZ(u8, s) else null; + defer if (symbolZ) |s| world.allocator.free(s); + + var desc = std.mem.zeroInit(c.ecs_entity_desc_t, .{ + .sep = "".ptr, // Disable tokenization. + .name = if (nameZ) |n| n.ptr else null, + .symbol = if (symbolZ) |s| s.ptr else null, + .use_low_id = config.use_low_id, + }); + inline for (add, 0..) |a, i| desc.add[i] = util.anyToId(ctx, a); + for (desc.add[0..add.len]) |id| + if (c.ecs_id_is_pair(id) and c.ECS_PAIR_FIRST(id) == c.EcsChildOf) + if (config.parent != null or config.path != null) + std.debug.panic("Found ChildOf relationship, but .parent or .path was specified", .{}); + const result = c.ecs_entity_init(world.raw, &desc); if (result == 0) return err.getLastErrorOrUnknown(); return Self.fromRaw(world, result); diff --git a/src/main.zig b/src/main.zig index 607fe3b..d393ae1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,6 +16,8 @@ pub fn Context(comptime ctx: anytype) type { 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; }; } diff --git a/src/world.zig b/src/world.zig index c583340..43b7f2a 100644 --- a/src/world.zig +++ b/src/world.zig @@ -8,7 +8,6 @@ const util = @import("./util.zig"); const Lookup = @import("./main.zig").Lookup; const Entity = @import("./entity.zig").Entity; const EntityError = @import("./entity.zig").EntityError; -const EntityConfig = @import("./entity.zig").EntityConfig; const Iter = @import("./iter.zig").Iter; pub fn World(comptime ctx: anytype) type { @@ -64,7 +63,7 @@ pub fn World(comptime ctx: anytype) type { /// Creates a new `Entity` in this `World`. /// See `Entity.new(...)` for more information. - pub fn entity(self: *Self, config: EntityConfig, add: anytype) !Entity(ctx) { + pub fn entity(self: *Self, config: Entity(ctx).Config, add: anytype) !Entity(ctx) { return Entity(ctx).new(self, config, add); } diff --git a/test/entity.zig b/test/entity.zig index ae9296c..d13a27d 100644 --- a/test/entity.zig +++ b/test/entity.zig @@ -14,8 +14,9 @@ const flecs = @import("../src/main.zig"); const c = flecs.c; const context = flecs.Context(void); -const World = context.World; const Entity = context.Entity; +const Path = context.Path; +const World = context.World; test "Entity_init_id" { var world = try World.initMinimal(alloc); @@ -38,3 +39,18 @@ test "Entity_init_id_name" { defer path2.deinit(); try expectFmt("foo", "{}", .{path2}); } + +test "Entity_init_id_path" { + var world = try World.initMinimal(alloc); + defer world.deinit(); + + const p = try Path.fromString("parent.child", null, alloc); + defer p.deinit(); + const e = try world.entity(.{ .path = p }, .{}); + // try expect(e.raw != 0); -- Not necessary, world.entity() returns error if result would be 0. + try expectEqualStrings("child", e.getName().?); + + const path = try e.getPath(alloc); + defer path.deinit(); + try expectFmt("parent.child", "{}", .{path}); +}