Better FindFirstChild, FindFirstAncestor, FindFirstDescendant

Tree

Tree.lua is a tool to navigate between instances using their properties and attributes, these functions are to extend the functionality of the Roblox Engine’s built in navigation tools.

Example Usage:

EG 1: Look for the first child inside the parent with the Emotion attribute and a value of happy.

print(Tree.FindFirstChild(script.Parent, {
    {"Attribute", "Emotion", "happy"}
}))

EG 2: Find the first ancestor called CarHOLDER.

print(Tree.FindFirstAncestor(script, {
    {"Property", "Name", "CarHOLDER"}
}))

EG 3: Find the first ancestor with a script child called MyParentIsTheHolder.

print(Tree.FindFirstAncestor(script, {
    {"Tree", "FindFirstChild", {
        {"Property", "Name", "MyParentIsTheHolder"};
        {"Property", "ClassName", "Script"};
    }};
}))

EG 4: Find the first ancestor which is a model with a module script called A-Chassis Tune, this module script must have a Plugins folder inside it. … This may look complex, but just to prove it’s possible.

print(Tree.FindFirstAncestor(script, {
    {"Property", "ClassName", "Model"};
    {"Tree", "FindFirstChild", {
        {"Property", "Name", "A-Chassis Tune"};
        {"Property", "ClassName", "ModuleScript"};
        {"Tree", "FindFirstChild", {
            {"Property", "Name", "Plugins"};
            {"Property", "ClassName", "Folder"};
        }};
    }}
}))

GET MODULE:

HOW TO USE / DOCS:

6 Likes

it’s funny how your name is like a function commonly used in DataStores


I like the module as it filters by properties and attributes, I’ll give it a star in github and a like in this post!

5 Likes

Thank you :grinning:
Very proud of my username lol
Appreciate the like.

1 Like

Hey this module is so useful for my game, I also added WaitForChild, WaitForDescendant and WaitForAncestor

--!strict

local Terms = {}
local Tree = {}

function Terms.Property<V>(object: Instance, name: string, value: V): boolean
    local success: boolean, foundProperty: V? = pcall(function()
        return (object :: any)[name]
    end)
    if success and (not value or foundProperty == value) then
        return true
    end
    return false
end

function Terms.Attribute<V>(object: Instance, name: string, value: V): boolean
    local foundProperty = object:GetAttribute(name)
    if not value or foundProperty == value then
        return true
    end 
    return false
end

function Terms.Function(object: Instance, callback: (object: Instance) -> boolean?): boolean?
    return callback(object)
end

function Terms.Tree(object: Instance, treeFunction: string, ...: any)
    return Tree[treeFunction](object, ...)
end

function Tree.ObjectMeetsTerms(object: Instance, termArguments: {{any}}): boolean
    for _, term in ipairs(termArguments) do
        local termCloned = table.clone(term)
        local termName = termCloned[1]
        table.remove(termCloned, 1)

        if not Terms[termName](object, table.unpack(termCloned)) then
            return false
        end
    end
    return true
end

function Tree.FindFirstElementOfList(list: {Instance}, terms: {{any}}): Instance?
    for _, applicant in ipairs(list) do
        if Tree.ObjectMeetsTerms(applicant, terms) then
            return applicant
        end
    end
    return nil
end

function Tree.FindFirstChild(object: Instance, terms: {{any}}): Instance?
    return Tree.FindFirstElementOfList(object:GetChildren(), terms)
end

function Tree.FindFirstDescendant(object: Instance, terms: {{any}}): Instance?
    return Tree.FindFirstElementOfList(object:GetDescendants(), Terms)
end

function Tree.FindFirstAncestor(object: Instance, terms: {{any}}): Instance?
    local parent = object.Parent
    while parent do
        if Tree.ObjectMeetsTerms(parent, terms) then
            return parent
        end
        parent = parent.Parent
    end
    return nil
end

function Tree.WaitForChild(object: Instance, terms: {{any}}, timeOut: number?): Instance
    local found = Tree.FindFirstChild(object, terms)
    if found then
        return found
    else
        local connections: {RBXScriptConnection} = {}
        local thread = coroutine.running()
        local isResumed = false

        local function resume(...: any)
            if not isResumed then
                isResumed = true
                for _, connection in ipairs(connections) do
                    connection:Disconnect()
                end
                table.clear(connections)
                task.spawn(thread, ...)
            end
        end

        table.insert(connections, object.ChildAdded:Connect(function(child: Instance)
            if Tree.ObjectMeetsTerms(child, terms) then
                resume(child)
            end
        end))

        table.insert(connections, object.AncestryChanged:Connect(function(_: Instance, parent: Instance?)
            if not parent then
                resume(nil)
            end
        end))

        table.insert(connections, object.Destroying:Connect(function()
            resume(nil)
        end))

        task.delay(timeOut or 60, function()
            resume(nil)
        end)

        return coroutine.yield()
    end
end

function Tree.WaitForDescendant(object: Instance, terms: {{any}}, timeOut: number?): Instance
    local found = Tree.FindFirstDescendant(object, terms)
    if found then
        return found
    else
        local connections: {RBXScriptConnection} = {}
        local thread = coroutine.running()
        local isResumed = false

        local function resume(...: any)
            if not isResumed then
                isResumed = true
                for _, connection in ipairs(connections) do
                    connection:Disconnect()
                end
                table.clear(connections)
                task.spawn(thread, ...)
            end
        end

        table.insert(connections, object.DescendantAdded:Connect(function(descendant: Instance)
            if Tree.ObjectMeetsTerms(descendant, terms) then
                resume(descendant)
            end
        end))

        table.insert(connections, object.AncestryChanged:Connect(function(_: Instance, parent: Instance?)
            if not parent then
                resume(nil)
            end
        end))

        table.insert(connections, object.Destroying:Connect(function()
            resume(nil)
        end))

        task.delay(timeOut or 60, function()
            resume(nil)
        end)

        return coroutine.yield()
    end
end

function Tree.WaitForAncestor(object: Instance, terms: {{any}}, timeOut: number?): Instance
    local found = Tree.FindFirstDescendant(object, terms)
    if found then
        return found
    else
        local connections: {RBXScriptConnection} = {}
        local thread = coroutine.running()
        local isResumed = false

        local function resume(...: any)
            if not isResumed then
                isResumed = true
                for _, connection in ipairs(connections) do
                    connection:Disconnect()
                end
                table.clear(connections)
                task.spawn(thread, ...)
            end
        end

        table.insert(connections, object.AncestryChanged:Connect(function(_: Instance, parent: Instance?)
            if parent then
                local ancestor: Instance? = parent
                while ancestor do
                    if Tree.ObjectMeetsTerms(ancestor, terms) then
                        resume(ancestor)
                        break
                    end
                    ancestor = ancestor.Parent
                end
            end
        end))

        table.insert(connections, object.Destroying:Connect(function()
            resume(nil)
        end))

        task.delay(timeOut or 60, function()
            resume(nil)
        end)

        return coroutine.yield()
    end
end

export type Tree = {
    FindFirstChild: (object: Instance, terms: {{any}}) -> Instance?,
    FindFirstDescendant: (object: Instance, terms: {{any}}) -> Instance?,
    FindFirstAncestor: (object: Instance, terms: {{any}}) -> Instance?,

    WaitForChild: (object: Instance, terms: {{any}}, timeOut: number?) -> Instance,
    WaitForDescendant: (object: Instance, terms: {{any}}, timeOut: number?) -> Instance,
    WaitForAncestor: (object: Instance, terms: {{any}}, timeOut: number?) -> Instance
}

return (table.freeze(Tree) :: any) :: Tree

Also here’s an example about Terms.Function

local rootPart = Tree.WaitForChild(character, {
    {"Property", "Name", "HumanoidRootPart"},
    {"Function", function(object: Instance)
       return object:IsA("BasePart") -- pass if the object is actually a BasePart
    end}
}) :: BasePart
2 Likes

Thanks for the contribution! May I add to the public module?

1 Like

Sure, because this your creation.

1 Like