Introducing: AttributeUtil!

With Attributes fully released I have found a few flaws that the current API doesn’t have:

Source Code | gist

Todo:

  • Documentation
  • IncrementAttribute
  • UpdateAttribute
  • RemoveAttribute

Maybe:

  • :SetInstancesAttributes
  • :GetAddedAttribute
  • :GetRemovedAttribute

feel free to suggest more features to AttributeService


License

Notes

I personally use Attributes for Read only purposes, I write values to tables instead and then those are reflected onto Attributes. (I basically use Attributes to view Player Data instead of using print)

I don’t set Instances as an Attribute because I would have dependencies everywhere, and I’d be better off using a Module for that anyways.

If you need to send complex data over the network use Remotes they are fast and very optimized, Attributes performance isn’t different from ValueBases (they aren’t different from Properties)

However when Tables and Instances are fully supported by Roblox I’d definitely utilize it!

26 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