How to get instance's path and use it elsewhere

I’m trying to create a script that loops through a folder, gets an instances path and then uses that path to find an instance in a different folder. I’m currently using Instance:GetFullName() to get the instance’s path, however I do not know how to use this to find the instance in the folder in my script. Help greatly appreciated, thank you!

I, for whatever, found myself having a hard time trying to give a generic answer. The truth is, it’s implementation defined, i.e., I’d need to know what the parent-child hierarchy looks like, and what might be the best solution.

To be real, there are three steps involved:

  1. Obtain a relative path, as I’d like to call it. (basically a path that is valid when appended to the folder’s)
  2. Append that relative path to the folder’s.
  3. Canonicalize the result, i.e., convert the string path into an Instance.

Depending on the implementation, we could skip steps 2 and 3 altogether, by doing them all at the same time (in some way or another).

Here’s an example implementation:

local function relativeTo(instance: Instance, path: {string}): Instance
    for _, name in ipairs(path) do
        instance = instance[name]
    end

    return instance
end

local function cutEnds(path: string, at: number): {string}
    local path = path:split(".")
    local final = {}

    for i = #path - at + 1, #path do
        table.insert(final, path[i])
    end

    return final
end

local absolute = workspace["MyFolder"]--["MyModules"]["Module"]
local relative = workspace["OtherThing"]["MyModules"]["Module"]

relativeTo(absolute, cutEnds(relative:GetFullName(), 2)) 
--> ["MyFolder"]["MyModules"]["Module"]

1 Like

I plan to loop through a folder and I need to find every script in this folder. The parent-child hierarchy is different for each instance I need to replace. I then need to replace this script with the script from my folder.

Below is my interpertation of this code and some questions.

What does this code do?
absolute is the new folder that the path from the old folder will be used on.
relative is the path from the old folder to be used on the new folder (absolute)
The cutEnds() function loops through the path provided and converts it to a table, with each new entry being an instance in the path.
the relativeTo() function loops through the new path converted to a table and converts it to a string, with each new path in the string coming after the previous.

Implementation:

for i,v in workspace.MyFolder:GetDescendants() do
	if v:IsA("ModuleScript") then
		local absolute = workspace.MyFolder
		local relative = v
		local path = relativeTo(absolute, cutEnds(relative:GetFullName(), 2)) 
		if script.ClientSidedObjects[path] then
			script.ClientSidedObjects[v.Parent.Name][v.Name]:Clone().Parent = v.Parent
			v:Remove()
		end
	end
end

Questions:
Does this implementation look correct to you?
Why is there a 2 at the end of the function? I couldn’t find where it is used in the function.
Was my interpertation + interpertation of this correct?
Sorry for the wall of text and bombardment of questions.

Off-topic, but your awesome-roblox repo on your GitHub, is incredibly useful.

:white_check_mark:

:white_check_mark:

The cutEnds function returns the last n elements of a path, separated by a dot. E.g., cutEnds("hello.world.me.you", 2) would return { "me", "you" }. If the parameter was 3, it would return { "world", "me", "you" }.

In order to extract "MyModules" and "Module" from the relative path, I used two, since they were the last two elements separated by a dot.

Oh, and by the way, I edited my previous reply because there were some minor bugs. Should work now, in theory.

That makes it a tad bit more difficult. This is why I said it was “implementation defined”. If I understood correctly, the hierarchy will have a variable hierarchy depth. This means using 2 as a constant will be no good.

What we have to do is adapt the previous solution. Since we know for sure that any child of folder 1 is guaranteed to exist in folder 2:

local function relativeTo(
		to: Instance, 
		ancestor: Instance, 
		nestedInstance: Instance
	)
	-- just for debugging, remove later
	--assert(ancestor:IsAncestorOf(nestedInstance))
	
	local function cutEnds(path: {string}, at: number): {string}
		local final = {}

		for i = #path - at + 1, #path do
			table.insert(final, path[i])
		end

		return final
	end
	
	local function relativeTo(instance: Instance, path: {string}): Instance
		for _, name in ipairs(path) do
			instance = instance[name]
		end

		return instance
	end

	
	local nestedPath = nestedInstance:GetFullName():split(".")
	local ancestorPath = ancestor:GetFullName():split(".")
	
	local relativePathSize = #nestedPath - #ancestorPath
	local relativePath = cutEnds(nestedPath, relativePathSize)
	
	return relativeTo(to, relativePath)
end

In this case, the function will calculate the depth beforehand, as long as we are aware of ancestor (which is a given).

I genuinely don’t know why I didn’t do this earlier. My bad.

You can even use an adapted version on the Luau demo. Feel free to ignore the code below.

local function print_tbl(tbl)
	for k, v in pairs(tbl) do
		print(`['{k}'] = '{v}'`);
	end
end

local function relativeTo(
		to: Instance, 
		ancestor: Instance, 
		nestedInstance: Instance
	)
	-- just for debugging, remove later
	--assert(ancestor:IsAncestorOf(nestedInstance))
	
	local function cutEnds(path: {string}, at: number): {string}
		local final = {}

		for i = #path - at + 1, #path do
			table.insert(final, path[i])
		end

		return final
	end
	
	local function relativeTo(instance: Instance, path: {string}): Instance
		for _, name in ipairs(path) do
			instance = instance[name]
		end

		return instance
	end

	-- path is hardcoded because we can't use :GetFullName
	local nestedPath = ("workspace.OtherThing.MyModules.Module"):split(".")
	local ancestorPath = ("workspace.OtherThing"):split(".")
	
	local relativePathSize = #nestedPath - #ancestorPath
	local relativePath = cutEnds(nestedPath, relativePathSize)
	
	return relativeTo(to, relativePath)
end

local workspace = {
	MyFolder = {
		MyModules = {
			Module = {
				Name = "bababooey"
			}
		}
	};
	OtherThing = {
		MyModules = {
			Module = {
				Name = ">:)"
			}
		}
	}
}
local to = workspace["MyFolder"]--["MyModules"]["Module"]
local ancestor = workspace["OtherThing"]
local relative = workspace["OtherThing"]["MyModules"]["Module"]

print_tbl(relativeTo(to, ancestor, relative))
--> ['Name'] = 'bababooey'
1 Like

I tried recreating my own script so I really master this and learn from this. I am currently stuck because in the FindPath() function I cannot get folder to return as a list of its parents until the chosen folder and it instead returns as a single instance, which causes the script.ClientSidedObjects[path].Parent = path.Parent line to error. Do you know how to make it return differently? I didn’t see you do something different in your code and was also curious how yours works, but mine errors.

function FindPath(
	instance, -- modulescript to update
	folder) -- folder to loop through + folder to parent new COs to (gets updated COs from script.ClientSidedObjects)
	
	local path = tostring(instance:GetFullName()):split(".")
	print(path)

	for i,v in pairs(path) do
		if i <= 3 then -- dont loop through if i is 2 or less because "Tower" and "Model" and "ClientSidedObjects" are not parts that are wanted in the path

			folder = folder[v]
		end
	end
	
	return folder
end

function UpdateScript(instance, folder)
	
	local path = FindPath(instance, folder)
	
	script.ClientSidedObjects[path].Parent = path.Parent
	instance:Remove()
	
end

There’s no need for tostring on GetFullName, because Instance:GetFullName() already returns a string.

I think you confused the operator here. <= means lower than or equal to, meaning the if statement would only execute if i was equal to or lower than 3, which is the opposite of your intended logic.

Another thing: because you’re iterating using pairs (which should’ve been ipairs, really), the loop will start at index 1, meaning it would return any element after index 4 (since anything below <= 3 is ignored), not the last n elements.

You tried to index an Instance with an Instance. I think you meant to do path.Name. Not only that, this would only work is path was a direct child of ClientSidedObjects. If it were to be a descendant (indirect child), it wouldn’t be able to find a result.

This is why I created a helper function which would be able to index descendants even (the relativeTo helper function, which is inside relativeTo (nested functions / local functions))

Instance:Remove() has been deprecated in favor of Instance:Destroy(), so use that instead.

1 Like

The problem I am having is that FindPath(instance, folder) does not return the path, only the instance. In your nested relativeTo() function, all that I can see is that it would return the instance as well and not the path. Am I incorrect? How would I get the path and use it?

What path are you looking for? Is it relativePath maybe? At the end of the day, you could call GetFullName on the result of the main relativeTo function, no? Or did I misunderstand you? (likely the case)

1 Like

Yes, I am looking for the relativePath. To better explain, the path only from the child to the folder that is the same between the folder of updated ModuleScripts and the folder with unupdated ModuleScripts.

It’s already defined in the code. What you’d want to do is add this as a second return value to the main relativeTo, so it’d look like this:

...
return relativeTo(to, relativePath), relativePath

And so:

local instance, path = relativeTo(to, ancestor, descendant)

print("instance:", instance.Name)
print("path:", path)
1 Like

Code works! What should I do about instances with the same name?

Solution: don’t use the same names :white_check_mark:

Just kidding.

Not really.

I mean, I don’t think there’s any way to avoid name collisions. It’s impossible to differentiate between two instances with the same name, unless you filter them out yourself (such as by ClassName), but the implementation would look overly complicated (you’d replace the indexing operator [] with a for loop).

I fixed it! I added this to the nested relativeTo() function:

local function relativeTo(instance: Instance, path: {string}): Instance
		for _, name in ipairs(path) do
			if instance:FindFirstChild(name) then
				instance = instance:FindFirstChild(name)
			end
		end

		return instance
	end

Also, how do I use the “path” output to refer to the instance in the script. I am currently doing this, but it’s not working. I believe part of this is is because path is a table and I don’t think I’m using the right syntax to refer to a path of instances either.

local instance, path = relativeTo(script.ClientSidedObjects, Tower.Tower.ClientSidedObjects, v)
script.ClientSidedObjects[path].Parent = instance.Parent
instance:Destroy()

Currently, it has no significant difference. Some might argue it is worse because FindFirstChild is slower than [], and they it’s not like they differ much in behavior (i.e., they would return the same instance if a collision were to occur) (AFAIK)

That’s right. Instead of using path directly, you would have to manually index each name. In fact, that’s exactly what the nested relativeTo function does.

Oh, and one more thing, since it seems you misunderstood this part: relativeTo (the main one) already does the whole job of indexing the relative path for you. You don’t need to do script.ClientSidedObjects[path] because instance is already the one existing under script.ClientSidedObjects. That’s why you provided it as the first argument.

Also, I’m pretty sure this eliminates the need to keep track of the path variable. I assumed you wanted it for some other purpose, but since that isn’t the case, keep it the way it was.

How would I manually index each name in path since the distance from the folder varies?

It should already account for such cases, since the path’s size is automatically calculated.

To update the script would I just do:

path.Parent = instance.Parent
instance:Destroy()

Why would you update it all? Remember that the relativeTo(to, ancestor, descendant) function simply returns an instance that is a descendant of to by using descendant’s path as a reference.

Not this script, I need this function to loop through a folder and replace all the ModuleScripts in it.

for _, module in Tower.Tower.ClientSidedObject:GetDescendants() do
	if module:IsA("ModuleScript") then
		local otherModule = relativeTo(
			script.ClientSidedObjects,
			Tower.Tower.ClientSidedObject,
			module)
		
		local parent = module.Parent
		module:Destroy()
		otherModule.Parent = parent
	end
end

Something kinda like this