How to make equivalent of Instance:GetFullName for tables

I have a PlayerData table with all the players data, when they join, a Remote Event sends a replica of that table to the Client.

Then when any player data updates on the server I send an update to the client with the information so I can update the replica table.

Obviously the Client and the Server don’t share the same memory (which is why it’s a replica table in the first place) so I would send the path of the element being updated as well as the updated value of that element.

--So lets say banana equaled good but now equals super good, I would send to the client

local exampleTable = {
	fruit = {
		banana = "super good",
		apple = "good",
	},
}

local banana = exampleTable.fruit["banana"]

local path_string = "exampleTable.fruit.banana"
local new_value = banana -- value of banana

RandomRemoteEvent:FireClient(randomplayer,path_string,new_value)

-- the client can pick it up and update the replica table

The thing in question is how to get
path_string = "exampleTable.fruit.banana"

I have looked everywhere for the answer but could not find. I know it’s possible because Roblox instances have :GetFullName which is what I am trying to mimic.

I also looked into Linked Lists, which I don’t fully understand yet, but it seems you store references to different parts of the table in each part of the table, which is how Instances have things like .Parent. Not sure if that has anything to do with it.

1 Like

You can dissect the string using the character . as a divider. String manipulation perhaps helps in this case. Then the tuple you just dissected can be used in order from left to right as per usual, by setting them in variables depending on maximum depth.

The question is how to get the path into a string, once it’s a string I can easily dissect it and put it in an array then do

local pointer = replica_table

for i = 1, #path_array do
      pointer = pointer[path_array[i]]
end

also what does tuple mean? I see that a lot but don’t know what it means.

If you want to find the path in a table to a string, the for loop includes the index:

for i, v in next, tableOfChoice do
    print(i)
end

Nest in several for loops for the depths, then combine every iteration value together in a .. "." .. b .. "." .. c. It’s quite tedious writing that way and I believe with string manipulation, you can write that easier for the all-case scenario if there will be more depths than planned on the table.

A tuple is basically a set of values that can contain theoretically infinite amount of elements(until computer can’t handle it haha). You can set tuple in functions:

local function functionWithTuple(...)
    local a, b, c = ...
    print(a, b, c)
end

functionWithTuple("lol", "hi", "1337248294", "no")
functionWithTuple("There is no 'b' and 'c'.")

I won’t necessarily know how deep the element will be. And how will I get the right index if a table has other keys like:

exampleTable = {
     banana = {},
     apple = {},
     orange = {},
}

Also I see you used next, table loop, I’ve seen that before but I don’t know how it works. How does it compare to in pairs/in ipairs?

pairs uses next and are identical in functionality. ipairs is far different, works better in numeric tables, where it goes from lowest number up to the last. All of them are iterators.

Speaking of multiple tables, you’re still going to use the standard for loop in for loop, because each index is different after each table you go in-depth. If you match the correct key in the end, you will use the first index of the first for loop, followed by second and so on to build the path.

I have also thought about using recursive function for this, but might be cumbersome.

-- maybe this will work, hahaha
local function recursiveGetPathInTableFromValue(t, matchValue)
	local path = ""
	
	for key, value in next, t do
		if type(value) == "table" then
			path = path .. "." .. recursiveGetPathFromTable(value)
		end
		
		if value == matchValue then
			return key
		end
	end
	return path -- this path is missing the main table's name, but that can be fixed after you find the path and add the string of the original table's name
end

I’m trying this out now, I’ll let you know the results. Also I want to be able to find things by key not value so I switched the matchValue to matchKey and the if statement to if key == matchKey.

1 Like

This does not work, because this

if key = matchKey then
    return key
end

won’t run until the last layer where the key is found, I would need to put in the key at every stage which I don’t know.

I think I would have to implement some sort of Linked List so that an element can access it’s parent/ancestors (mimicking Roblox Instances)

1 Like
local FruitsTable = {
	fruit = {
		banana = "good"
	}
}

function MakePath(NameOfTable,Table,Index)
	local Path = ""
	
	for i, v in next, Table do
		if type(v) == "table" then
            Path = NameOfTable.."."..GetIndex(Table,v)
			for k, b in pairs(v) do
				if GetIndex(v,b) == Index then
					Path = Path.."."..Index
				end
			end
		else
			if GetIndex(Table,v) == Index then
				Path = NameOfTable.."."..Index
			end
		end
	end
	return Path
end

function GetIndex(Table,Value)
	for i, v in pairs(Table) do
		if v == Value then
			return i
		end
	end

	return nil
end

local Path = MakePath("FruitsTable",FruitsTable,"banana")  --Change these parameters if your table is different
print(Path) --prints out "FruitsTable.fruit.banana"

Ok so I figured it out, this script should work.

This is good but it won’t work if the Index is more than 1 table deep.

if GetIndex(v,b) ~= Index it will just leave and not look any deeper.

Is there anyway we can add a recursion to this? I’m working on it now let me know if you’ve figured it out.

Ok try this now:

local FruitsTable = {
	fruit = {
		banana = "good"
	}
}

local Indexes = {}
function MakePath(NameOfTable,Table,Index)
	local Path = NameOfTable
    Indexes = {}

	for i, v in next, Table do
		GetRecursiveTable(Table,Index)
	end
	for i, v in ipairs(Indexes) do
		Path = Path.."."..v
	end
	return Path
end

function GetRecursiveTable(tab,Index)
	for i, v in pairs(tab) do
		if type(v) == "table" then
			table.insert(Indexes,GetIndex(tab,v))
			GetRecursiveTable(v,Index)
		else
			if GetIndex(tab,v) == Index then
				table.insert(Indexes,GetIndex(tab,v))
			end
		end
	end
end

function GetIndex(Table,Value)
	for i, v in pairs(Table) do
		if v == Value then
			return i
		end
	end

	return nil
end

local Path = MakePath("FruitsTable",FruitsTable,"banana") --Change these parameters if your table is different once again
print(Path) 

This works with however many subtables.

I’m pretty sure roblox in 2019 suggested using pairs instead, as they optimized the VM, and nothing was changed to next, honestly just use pairs, it’s easier to understand as well.

1 Like

I’d recommend rethinking your program (tl: skip to the next section). There are really only two solutions similar to what you’re doing (there is a third one I recommend at the bottom):

  • You can have each table have a reference to the table it’s inside:
local exampleTable
exampleTable = {
	fruit = {
		banana = {value = "super good", parent = exampleTable.fruit},
		berries = {
			raspberry = {value = "okay", parent = exampleTable.fruit.berries}, 
			blueberry = {value = "great", parent = exampleTable.fruit.berries},
			parent = exampleTable.fruit
		}, 
		parent = exampleTable
	}
}

Obviously that’s pretty weird, and you’d need to get the key from the table with the value anyways.

  • You can do what a few other people are programming:

Basically what they are doing is they are searching through a table where each value could be a table with unknown depth and finding a value that matches an inputted one then generating a path string.


I'd recommend rethinking your problem. For starters, you already need to know when to run this function. You would do that when the value changes. To find when a value changes and to run a function to ask the server to send a message to the client, you probably already call a function like changeValue or you call another function like updateList after making changes. I'd recommend having a function that works something like this:
local function changeData(pathArray) --pathArray: {key, ... value} (last item should be the value)
	--send pathArray to client, and call changeData(pathArray) on client
	local tabl = exampleTable 
	for index, path in ipairs(pathArray) do
		if not pathArray[index + 2] then
			tabl[path] = pathArray[index + 1]
		else
			if not tabl[path] then
				tabl[path] = {}
			end
			tabl = tabl[path]
		end
	end
end
Example
local exampleTable = {
	fruit = {}
}

local function changeData(pathArray)
	--send pathArray to client
	local tabl = exampleTable 
	for index, path in ipairs(pathArray) do
		if not pathArray[index + 2] then
			tabl[path] = pathArray[index + 1]
		else
			if not tabl[path] then
				tabl[path] = {}
			end
			tabl = tabl[path]
		end
	end
end

changeData({"fruit", "apple", "good"})
print(exampleTable)

If you use this function then you can just send the array to the client.

1 Like

I think I potentially found the root of the problem

I wrote a long reply before explaining how my system works then scrapped it because I think I found the root of the problem

So to sum it up, I have a reference module in Replicated Storage with all the Items and their properties. When the client buys something, they send a Remote to the server with the ItemID, then a ShopHandler script calls :AddItem in the PlayerData Module. The PlayerData Module can see what type of item it is and manually puts it in the table accordingly.

So really right there I can manually make a path_array easily.


The way I’m saving players data is in one datastore using @lolerisProfileService, so all data is nested in Profile.Data

The root of the problem

Until now the game I’m working on is privated (it’s not ready) and I’ve already made so many changes to the Data Structure (correct terminology?)/hierarchy of nested tables.

What I’m scared of is what if I want to change the hierarchy after my game is released, and there are already profiles with the “legacy” Data Structure.

example
local Profile = { -- gets saved to DataStore via ProfileService
	Data = { -- table with all the player data 
		foods = {
			banana = "banana",
			apple = "apple",
		},
	}, 
}

Which I then want to change to this

local Profile = {
	Data = {
		
		foods =  {
			fruits = {
				apple = "apple",
				banana = "banana",
			},
			vegetables = {
				carrot = "carrot",
			},
		},
		
	}
}

Any code that assumes something is in a certain path (basically all my code handling the PlayerData, will break because the path has changed.

If I then update my code, the code won’t be able to read any “legacy profiles” that were created before the change. I could then probably make a “Legacy to New” function where I specify which old tables go in which new location but it seems like a pain.

Basically I really just want to know how the pro’s do it, how do the big games like Jailbreak manage and replicate Player Data, while making it future-proof.

You should look into the ReplicaService. It’s also made by @loleris and seems perfect for replicating player data:


Handling Different Save Versions

To handle legacy data structures, most games add a value to save the save version. I’d imagine this would then be used with a legacyToNew function that would extract useful data from the old save and create an updated save.

I’m not very experienced with large scale data stores, though I assume big games make it so their data structure is fairly flexible. For example:

local Profile = { -- gets saved to DataStore via ProfileService
	Data = { -- table with all the player data 
		foods = {
			banana = "banana",
			apple = "apple",
		},
	}, 
}

If you wanted to change the save structure of this to:

local Profile = {
	Data = {
		
		foods =  {
			fruits = {
				apple = "apple",
				banana = "banana",
			},
			vegetables = {
				carrot = "carrot",
			},
		},
		
	}
}

I would instead add module function called IsAFruit that checks if something is a fruit based on a table inside the module. There aren’t many cases were changing the structure of the data is necessary but when it is you can almost always create a function to modify it.

If you’re worried about changing the structure you should have each player dataset have a version id. You could alternatively make functions that guess what version a player’s data is. It should always be possible to convert data with a function.

I actually looked into ReplicaService, and spent some time learning how to
use it but decided I wouldn’t use it now because I wanted to learn how to do it myself, and it seems really robust and over the top for what I need currently. I’ll probably use it eventually because it seems really useful.

So I somewhat already do that with my ItemData module, Thanks for your help I’ll take these things into consideration.

1 Like