From 17a830f85728bcbc76cd33fba730ea9c6ddc3fdf Mon Sep 17 00:00:00 2001 From: Emi Gutekanst Date: Sat, 8 Feb 2025 19:13:12 -0700 Subject: [PATCH] object: add ability to tag arbitrary objects with arbitrary tags and values Modules define lists of objects, e.g. a `SpriteRenderer` module may define ```zig sprites: mach.Objects(struct { // ... }), ``` Previously, the only way for another Mach module to 'attach data to a sprite object' or 'tag a sprite' would be to (ab)use the graph relations system, creating their own object and using parent/child relations to express that the sprite has some tag/data associated with it. For example: ```zig // Game.zig is_monster: mach.Object(struct{}), // empty object just to indicate 'some other object is a monster' // ... // Create a 'tag object' const is_monster_tag_obj_id = game.is_monster.new(.{}); // Add the 'tag object' as a child of our sprite sprite_renderer.sprites.addChild(my_sprite_id, is_monster_tag_obj_id); // ... ``` This usage of the API was quite ugly/usage, and importantly eliminated your ability to use the parent/child relations for _other_ things where they are more appropriate. However, it did mean that you didn't have to go and fork+modify the `SpriteRenderer` module that you e.g. imported as a reusable package. With this change, we add object _tags_ and _tags with values_. Any module can add their own tags or tags with values to any object, whether it is from their module or not. For example, the `is_monster` example above could now be written as: ```zig // Game.zig pub const mach_tags = .{ .is_monster }; // ... try sprite_renderer.sprites.setTag(sprite_id, Game, .is_monster, null); const is_monster: bool = sprite_renderer.sprites.hasTag(sprite_id, Game, .is_monster); // is_monster == true! // No longer a monster try sprite_renderer.sprites.removeTag(sprite_id, Game, .is_monster); ``` This allows for effectively tagging objects as distinct kinds, states, etc. even though they aren't our object and we can't modify their `struct {}` type to include an `is_monster: bool` field of our own. Internally, the implementation stores tags using a hashmap under the assumption that not all objects in a list will have a tag. Tags with values work almost identically, the only difference is that the last parameter to `setTag` is set to another `mach.ObjectID` which points to whatever arbitrary data you'd like to attach to the object, and `getTag` returns it. For example: ```zig // Game.zig pub const mach_tags = .{ /// Whether a sprite is a monster .is_monster, /// Whether a sprite has a friendly sprite attached to it .{ .friend, Sprite, .sprites }, }; // ... try sprite_renderer.sprites.setTag(sprite_id, Game, .friend, friendly_sprite_id); const has_friend: bool = sprite_renderer.sprites.hasTag(sprite_id, Game, .friend); // has_friend == true! // Get our friend const friend_id: mach.ObjectID = sprite_renderer.sprites.getTag(sprite_id, Game, .friend); // friend_id == friendly_sprite_id // Delete our friend try sprite_renderer.sprites.removeTag(sprite_id, Game, .friend); ``` Signed-off-by: Emi Gutekanst --- src/module.zig | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/module.zig b/src/module.zig index 75436d55..8b37160e 100644 --- a/src/module.zig +++ b/src/module.zig @@ -70,6 +70,9 @@ pub fn Objects(options: ObjectsOptions, comptime T: type) type { /// A bitset used to track per-field changes. Only used if options.track_fields == true. updated: ?std.bit_set.DynamicBitSetUnmanaged = if (options.track_fields) .{} else null, + + /// Tags storage + tags: std.AutoHashMapUnmanaged(TaggedObject, ?ObjectID) = .{}, }, pub const IsMachObjects = void; @@ -77,6 +80,11 @@ pub fn Objects(options: ObjectsOptions, comptime T: type) type { const Generation = u16; const Index = u32; + const TaggedObject = struct { + object_id: ObjectID, + tag_hash: u64, + }; + const PackedID = packed struct(u64) { type_id: ObjectTypeID, generation: Generation, @@ -275,6 +283,52 @@ pub fn Objects(options: ObjectsOptions, comptime T: type) type { if (mach.is_debug) data.set(unpacked.index, undefined); } + // TODO(objects): evaluate whether tag operations should ever return an error + + /// Sets a tag on an object + pub fn setTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M), value_id: ?ObjectID) !void { + _ = objs.validateAndUnpack(id, "setTag"); + + // TODO: validate that value_id is an object coming from the mach.Objects(T) list indicated by the tag value in M.mach_tags. + //const value_mach_objects = moduleTagValueObjects(M, tag); + + const tagged = TaggedObject{ + .object_id = id, + .tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)), + }; + try objs.internal.tags.put(objs.internal.allocator, tagged, value_id); + } + + /// Removes a tag on an object + pub fn removeTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M)) void { + _ = objs.validateAndUnpack(id, "setTag"); + const tagged = TaggedObject{ + .object_id = id, + .tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)), + }; + _ = objs.internal.tags.remove(tagged); + } + + /// Whether an object has a tag + pub fn hasTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M)) bool { + _ = objs.validateAndUnpack(id, "hasTag"); + const tagged = TaggedObject{ + .object_id = id, + .tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)), + }; + return objs.internal.tags.contains(tagged); + } + + /// Get an object's tag value, or null. + pub fn getTag(objs: *@This(), id: ObjectID, comptime M: type, tag: ModuleTagEnum(M)) ?mach.ObjectID { + _ = objs.validateAndUnpack(id, "hasTag"); + const tagged = TaggedObject{ + .object_id = id, + .tag_hash = std.hash.Wyhash.hash(0, @tagName(tag)), + }; + return objs.internal.tags.get(tagged) orelse null; + } + pub fn slice(objs: *@This()) Slice { return Slice{ .index = 0, @@ -497,6 +551,46 @@ fn ModuleFunctionName2(comptime M: type) type { }); } +/// Enum describing all mach_tags for a given comptime-known module. +fn ModuleTagEnum(comptime M: type) type { + // TODO(object): handle duplicate enum field case in mach_tags with a more clear error? + // TODO(object): improve validation error messages here + validate(M); + if (@typeInfo(@TypeOf(M.mach_tags)) != .@"struct") { + @compileError("mach: invalid module, `pub const mach_tags must be `.{ .is_monster, .{ .renderer, mach.Renderer.objects } }`, found: " ++ @typeName(@TypeOf(M.mach_tags))); + } + var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; + var i: u32 = 0; + inline for (@typeInfo(@TypeOf(M.mach_tags)).@"struct".fields, 0..) |field, field_index| { + const f = M.mach_tags[field_index]; + if (@typeInfo(field.type) == .enum_literal) { + enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(f), .value = i }}; + i += 1; + } else { + if (@typeInfo(field.type) != .@"struct") { + @compileError("mach: invalid module, mach_tags entry is not an enum literal or struct, found: " ++ @typeName(field.type)); + } + // TODO(objects): validate length of struct + const tag = f.@"0"; + const M2 = f.@"1"; + const object_list_tag = f.@"2"; + _ = object_list_tag; // autofix + validate(M2); + // TODO: validate that M2.object_list_tag is a mach objects list + enum_fields = enum_fields ++ [_]std.builtin.Type.EnumField{.{ .name = @tagName(tag), .value = i }}; + i += 1; + } + } + return @Type(.{ + .@"enum" = .{ + .tag_type = if (enum_fields.len > 0) std.math.IntFittingRange(0, enum_fields.len - 1) else u0, + .fields = enum_fields, + .decls = &[_]std.builtin.Type.Declaration{}, + .is_exhaustive = true, + }, + }); +} + pub fn Modules(module_lists: anytype) type { inline for (moduleTuple(module_lists)) |module| { validate(module);