Introducing: AttributeUtil!

With Attributes fully released Attributes - Unlocking New Possibilities

Enhancements to the Attribute API:

  1. Instance as an Attribute: Introducing the ability to use instances as attributes, providing more flexibility and convenience in storing complex data within attributes.
  2. SetAttributes(instance, attributes): A new method allowing you to set multiple attributes for a given instance in one operation, simplifying attribute assignment and reducing redundancy.
  3. SetInstancesAttribute(instances, key, value): A convenient way to set a common attribute value for multiple instances at once, streamlining the process of assigning attributes to related objects.
  4. GetChildrenWithAttribute(parent, attribute): This new function enables you to retrieve the children of a parent object that have a specific attribute, helping you filter and locate instances more efficiently.
  5. GetDescendantsWithAttribute(ancestor, attribute): A powerful function that allows you to obtain all descendants of a given ancestor object that possess a specified attribute, simplifying complex searches and traversal operations.
  6. ClearAllAttributes(instance): A handy method for quickly removing all attributes associated with a particular instance, facilitating attribute cleanup and management.
  7. FindFirstChildWithAttribute(parent, attribute, recursive): This function enables you to find the first child of a parent object that has a particular attribute, providing a convenient way to search for specific instances within a hierarchy.
  8. WaitForChildWithAttribute(parent, attribute, timeOut): A wait function that waits for a child with the specified attribute to appear under the given parent, allowing you to synchronize actions based on the presence of specific instances.
  9. IsSupportedType(value): A utility function that helps you determine if a given value is a supported attribute type, enabling you to validate attribute values before assigning them.
  10. IsSpecialType(value): Another utility function that assists in identifying whether a value is a special attribute type, helping you handle attribute values with specialized behavior.

Check out the source code on GitHub

Planned Additions:

  • Documentation: Creating comprehensive documentation for the AttributeService, providing clear explanations, usage examples, and guidelines to assist developers in utilizing the API effectively.
  • IncrementAttribute: Adding a method that allows you to increment the value of a numeric attribute by a specified amount, simplifying operations such as score tracking or progress updates.
  • UpdateAttribute: Introducing a function that enables you to update the value of an existing attribute, providing flexibility in modifying attribute values without the need to remove and reassign them.
  • RemoveAttribute: Implementing a method to remove a specific attribute from an instance, giving you more control over attribute management and cleanup.

Possible Future Additions:

  • SetInstancesAttributes: Considering the addition of a function that allows you to set multiple attributes for multiple instances simultaneously, further streamlining attribute assignment when dealing with groups of objects.
  • GetAddedAttribute and GetRemovedAttribute: Exploring the possibility of functions that return the attributes added or removed during attribute assignments or removals, providing useful information for tracking changes.

Feel free to suggest additional features or improvements to the AttributeService! Your feedback is valuable in shaping the future development of this API.


License

Notes

I personally use Attributes for read-only purposes. Instead, I store values in tables and reflect them onto attributes. This approach allows me to utilize Attributes for viewing player data, similar to printing, while keeping dependencies manageable. However, I understand that setting instances as attributes can be useful in certain scenarios and would consider utilizing it once Roblox fully supports tables and instances.

For sending complex data over the network, I recommend using Remotes as they offer optimized and efficient performance. Attributes perform similarly to ValueBases and Properties, so Remotes are the preferred method when dealing with complex data transfer.

Thank you for your interest and support!

29 Likes

Very basic but will save people some time, nice work!

You should make something like allow tables using HTTPService:JSONEncode() and Decode to save them as a string; but the module it self would KNOW that it was supposed to be a table and would convert them into a table using JSONDecode(), I was gonna make a module for that but just didn’t want to anymore; (don’t try detecting it because there’s {}, try to have an ID on the name or something similar if you were to do that;)

I really like your module though;

1 Like

Update!

I have added these methods to AttributeService

  • SetInstancesAttribute(instances, key, value)
  • FindFirstChildWithAttribute(parent, attribute, recursive)
  • WaitForChildWithAttribute(parent, attribute, timeOut)

please let me know if I have done FindFirstChild and WaitForChild correctly~ thanks!


reply to @LucasTutoriaisSaimo

@LucasTutoriaisSaimo, thank you for the suggestion however, Roblox might support tables (instances, CFrame, etc) later on so we don’t have to make our own Codec for it.

4 Likes

While I think your WaitForChildWithAttribute function would work, I do think there is room for improvement and optimization.

function AttributeService:WaitForChildWithAttribute(parent, attribute, timeOut)
	assert(typeof(parent) == "Instance")
	assert(typeof(attribute) == "string")
	assert(typeof(timeOut) == "number" or timeOut == nil)
	local Children = parent:GetChildren()
	for _,Child in pairs(Children) do
		if Child:GetAttribute(attribute) then
			return Child
		end
	end
	
	-- if the child already exists, no need to run any of this other code.

	local START_TIME = tick()
	local YIELD_UNTIL = (timeOut and timeOut + START_TIME or math.huge)
	local YIELD_WARN = timeOut == nil and START_TIME + 10 or nil
	
	-- YIELD_WARN provides a 10 second wait before it warns the user about infinite
	-- possible yield, given that timeOut is nil.


	local AttributeChangedConnections = {}
	-- Table of connections so we can disconnect them later
	
	local FOUND_CHILD
	
	for _,Child in pairs(Children) do
		local connection = child.AttributeChanged:Connect(function(newAttribute)
			if newAttribute == attribute then
				FOUND_CHILD = Child
			end
		end)
		table.insert(AttributeChangedConnections,connection)
	end
	-- This creates the list of connections that wait for the attribute to be added.
	-- I considered adding a :GetAttribute() call to check if it's nil, but that should
	-- never be the case since we've already checked if they had it, so a change
	-- would have to be a non nill value

	local ChildAdded = parent.ChildAdded:Connect(function(child)
		if child:GetAttribute(attribute) then
			FOUND_CHILD = child
		else
			local connection = child.AttributeChanged:Connect(function(newAttribute)
				if newAttribute == attribute then
					FOUND_CHILD = Child
				end
			end)
			table.insert(AttributeChangedConnections,connection)
			-- same as above, if the new child has no value for the attribute, it waits
			-- for a change to occur.
		end
	end)
	-- Instead of running parent:GetChildren() every loop, instead we check if the child was
	-- found by ChildAdded, saving resources that way.
	-- I also want to point out that I'm using :Connect() instead of :Wait() because a 
	-- :Wait() call can't be canceled, meaning you can't specify how long it'll wait for.

	while not FOUND_CHILD and tick() =< YIELD_UNTIL do
	-- this will break out when child is found, or if it yields too long
		if YIELD_WARN and tick() => YIELD_WARN then
			warn("Infinite yield possible on WaitForChildWithAttribute:", parent.Name, attribute)
			YIELD_WARN = nil
			-- from what I could tell, the original function would never actually warn the user, 
			-- so I did this to remedy that.

		end
		Heartbeat:Wait()
	end
	ChildAdded:Disconnect()
	for i,v in pairs(AttributeChangedConnections) do
		v:Disconnect()
	end
	-- Disconnecting the ChildAdded connection, to preventing a memory leak.
	-- Also the for loop disconnects the AttributeChanged functions
	return FOUND_CHILD -- Return child, if found.
end

I may have messed something up, or there may be a faster way to pull this off, but I’m pretty sure this will work. Though I’d like to point out that the “GetAttribute returns 0 values instead of nil when no value has been assigned” bug will break this and all other “find child with with attribute” function until it’s fixed.

1 Like

Not sure if you’re accepting contributions or if this is even considered useful but I created a function that returns every instance in the game with a certain attribute, similar to :GetTagged with CollectionService.

I suppose it would be similar to GetDescendantsWithAttribute but with the entire game.

function AttributeService:GetAttributed(attribute)
    local services = {}
    for i,v in pairs(game:GetChildren()) do
        pcall(function()
            services[#services + 1] = game:GetService(v.Name)
        end)
    end
    local tagged = {}
    for i,service in pairs(services) do
        pcall(function()
            for i2,v2 in pairs(service:GetDescendants()) do
                local tag = v2:GetAttribute(attribute)
                if tag ~= nil then
                    tagged[#tagged + 1] = v2
                end
            end
        end)
    end
    return tagged
end

I also have one that returns every attribute in the game:

function AttributeService:GetAllAttributes(parent)
    parent = parent or game
    local services = {}
    for i,v in pairs(game:GetChildren()) do
        pcall(function()
            services[#services + 1] = game:GetService(v.Name)
        end)
    end
    if parent == game then
        local tags = {}
        for i,service in pairs(services) do
            pcall(function()
                attributesInService = self:GetAllAttributes(service)
            end)
            for i,v in pairs(attributesInService) do
                if not table.find(tags, v) then
                    tags[#tags + 1] = v
                end
            end
        end
        return tags
    else
        local attributesToReturn = {}
        for i,v in pairs(parent:GetDescendants()) do
            local currentAttributes = v:GetAttributes()
            for i,v in pairs(currentAttributes) do
                if not table.find(attributesToReturn, v) then
                    attributesToReturn[#attributesToReturn + 1] = i
                end
            end
        end
        return attributesToReturn
    end
end

just to point out, you can avoid this if statement by just doing

parent = parent or game
2 Likes

Good idea, thanks! I’ll edit the original reply.

1 Like

Update


@7z99

Hello~

Thank you both for your contributions however while features and performance is important I’d like to keep the module as clean and simple as possible


#1 Reply to NotAPorgu

@NotAPorgu your WaitForChildWithAttribute implementation wouldn’t work well because it behaves like WaitForChild which we can’t do that, it will not return if an already existing child had an Attribute added to it.

2 Likes

Oh right I forgot about that, I doubt it would be hard to update it to add an .AttributeChanged connection to it. I can write an updated version if you want me to.

EDIT: I just updated my original post to include the AttributeChanged connection

Update!

I have made changes to the module and now it includes:

  • The ability to add Instance as an Attribute
  • IsSupportedType(value)
  • IsSpecialType(value)
  • LuaU type checking
  • run time type checking using t

Instances are added to a Table and also tagged with a GUID and that GUID is also added as an Attribute, this is generally for replication and in some cases when the Instance.Parent is nil.

Although I do believe it is better to use Remotes for complex tables, I will add support for JSON valid tables for those who need it.

3 Likes

I don’t know if you quit working on this however it would be nice if you could implement a method like Debris:AddItem(instance,time) but works with attributes.

1 Like

Changelog:

  • Changed name from AttributeService to AttributeUtil
  • Added CFrame support
  • Added Table support for JSON valid values only: will automatically filter out invalid values
  • Added a new Codec
  • Added a new TestEz spec.lua file

How Instance works:

Currently all SPECIAL_TYPES are given a Header during Encoding to identify what type an Attribute is while Decoding

How Instance serialization works:

Currently all Instances in the game are given an Attribute name “GUID” with a GUID as it’s value for reference and this value is also assign as a collection tag to the Instance for identification.

Collection Tags will not replicate on some Services and Instances that are nil to the client or vice versa


@Neotrinax that’s not really in the scope of this module, you can easily program something like that yourself using task.delay to remove an Attribute

3 Likes

Changelog:

  • Fix Encoding: filter now works correctly for Nested Tables
  • Set Attributes will make sure that Attribute name is valid (Names must only use alphanumeric characters and underscore, No spaces or unique symbols are allowed)
  • Table memory address is now shown in a Table Attribute Value