Lua table not accessible (attempt to index a nil value) - lua

I have a table that looks like this:
{
block_0 = {
hash = "98d1a61c4e3d6394b2970a2a5c44ec2caf172ad5c6844b114867b31fa528220e",
index = 0
}
}
Shouldn't I be able to access the index and hash values of block_0 by saying chain["block_0"]["hash"]? It is not working. When I use this line, I get the error attempt to index a nil value (field 'block_0'). How can I properly access hash and index?
EDIT: Here is some more context:
function add_thing()
block_name = "block_0"
block = { }
block[block_name] = { }
block[block_name]["hash"] = ""
block[block_name]["index"] = ""
block[block_name]["hash"] = "this is a test hash"
block[block_name]["index"] = 10
return block
end
chain = { }
table.insert(chain, add_thing())
require 'pl.pretty'.dump(chain)

You are inserting the return value of add_thing into chain. Thus chain is now a table of tables. To index the correct field you have to index chain first, i.e. chain[1]["block_0"]["hash"]. I rather suspect that this is not the intended behaviour and you want to do the following
local function add_thing(chain)
local block_name = "block_0"
chain[block_name] = {
hash = "this is a test hash",
index = 10
}
end
local chain = {}
add_thing(chain)
print(chain["block_0"]["hash"]) -- this is a test hash
Live on Wandbox
This works as expected because tables are reference types.

Related

Retrieve an attribute from the same table in LUA

I would like to know if it was possible to retrieve the attribute of a table in the same array. For example here I want to retrieve the "list.index" attribute of my array, how to do?
{
category_title = "Weapon",
description = nil,
type = "list",
list = {items = {"Give", "Remove"}, index = 1},
style = {},
action = {
onSelected = function()
if list.index == 1 then
-- it's 1
else
-- it's 2
end
end
},
It's not possible to use an entry in another entry when the table is being created.
But since you're defining a function, you can do this:
onSelected = function(self)
if self.list.index == 1 then
-- it's 1
else
-- it's 2
end
end
Just make sure you call onSelected with the table as an argument.
Alternatively, you may set the function after constructing the table in order to be able to access the table as an upvalue (as opposed to leveraging table constructors):
local self = {
categoryTitle = "Weapon",
description = nil,
type = "list",
list = {items = {"Give", "Remove"}, index = 1},
style = {},
action = {}
}
function self.action.onSelected()
if self.list.index == 1 then
-- it's 1
else
-- it's 2
end
end
That way, you get self as an upvalue and don't need to pass it as an argument.

Reference Dynamic multidimensional table

I have a data structure of nested tables that can be N deep. for example
local data = {
[1] = {
[1] = { "stuff" },
[2] = {
[1] = { "stuff" },
[2] = { "more stuff" },
[3] = {
[1] = "deeper stuff"
}
}
}
Now I can reference "deeper stuff" via data[2][3][1] But is there a way that I can store the 2-3-1 as a key so I can reference this data[key] ?
I am storing a set of actions that are transformed and looped over in a denormalised table. I want to be able to reference that this particular action came from a specific point in the original data table. As this is n levels deep is there a dynamic way of writing [2][3][1]...[n]?
You cannot have a single multi-dimensional key. The only way to achieve something like that would be to have some string like "2-3-1" which you then use in an __index metamethod that tranlates it to the separate keys.
setmetatable(data, {
__index = function(t, k)
for index in k:gmatch("%d+") do
-- insert fancier error handling here
if not t then error("attempt to index nil") end
t = rawget(t, tonumber(index))
end
return t
end
})
print(data["2-3-1"]
Alternatively you use a table as key
setmetatable(data, {
__index = function(t, k)
for i,v in ipairs(k) do
if not t then error("attempt to index nil") end
t = rawget(t, v)
end
return t
end
})
print(data[{2,3,1}]
There are of course more ways to implement the table access in __index.
If data is global you could also use load and do something like
local k = string.gsub("1-1-1", "(%d+)%-?", "[%1]")
local element = load("return data .. k")()
But please don't make data global just so you can do this.
Or you write a function that does that without using metamethods as Egor suggested...

dictionary help/DataStore

The Issue is I have a dictionary that holds all my data and its supposed to be able to turn into a directorys in replicated storage with all the values being strings then turn back into a dictionary with all the keys when the player leave. However, I cant figure out how to turn into a dictionary(With keys).
ive sat for a few hours testing things but after the first layer of values I cant get figure out a way to get the deeper values and keys into the table
local DataTable =
{
["DontSave_Values"] =
{
["Stamina"] = 100;
};
["DontSave_Debounces"] =
{
};
["TestData"] = 1;
["Ship"] =
{
["Hull"] = "Large_Ship";
["Mast"] = "Iron_Tall";
["Crew"] =
{
["Joe One"] =
{
["Shirt"] = "Blue";
["Pants"] = "Green"
};
["Joe Two"] =
{
["Shirt"] = "Silver";
["Pants"] = "Brown";
["Kids"] =
{
["Joe Mama1"] =
{
["Age"] = 5
};
["Joe Mama2"]=
{
["Age"] = 6
};
}
};
}
};
["Level"] =
{
};
["Exp"] =
{
};
}
------Test to see if its an array
function isArray(Variable)
local Test = pcall(function()
local VarBreak = (Variable.." ")
end)
if Test == false then
return true
else
return false
end
end
------TURNS INTO FOLDERS
function CreateGameDirectory(Player, Data)
local mainFolder = Instance.new("Folder")
mainFolder.Parent = game.ReplicatedStorage
mainFolder.Name = Player.UserId
local function IterateDictionary(Array, mainFolder)
local CurrentDirectory = mainFolder
for i,v in pairs(Array) do
if isArray(v) then
CurrentDirectory = Instance.new("Folder", mainFolder)
CurrentDirectory.Name = i
for o,p in pairs(v) do
if isArray(p) then
local TemporaryDir = Instance.new("Folder", CurrentDirectory)
TemporaryDir.Name = o
IterateDictionary(p, TemporaryDir)
else
local NewValue = Instance.new("StringValue", CurrentDirectory)
NewValue.Name = o
NewValue.Value = p
end
end
else
local value = Instance.new("StringValue", mainFolder)
value.Name = i
value.Value = v
end
end
end
IterateDictionary(Data, mainFolder)
end
------To turn it back into a table
function CreateTable(Player)
local NewDataTable = {}
local Data = RS:FindFirstChild(Player.UserId)
local function DigDeep(newData, pData, ...)
local CurrentDir = newData
for i,v in pairs(pData:GetChildren()) do
if string.sub(v.Name,1,8) ~= "DontSave" then
end
end
end
DigDeep(NewDataTable, Data)
return NewDataTable
end
I expected to when the player leaves run createtable function and turn all the instances in replicated storage back into a dictionary with keys.
Why not just store extra information in your data table to help make it easy to convert back and forth. As an example, why not have your data look like this :
local ExampleData = {
-- hold onto your special "DON'T SAVE" values as simple keys in the table.
DONTSAVE_Values = {
Stamina = 0,
},
-- but every element under ReplicatedStorage will be used to represent an actual Instance.
ReplicatedStorage = {
-- store an array of Child elements rather than another map.
-- This is because Roblox allows you to have multiple children with the same name.
Name = "ReplicatedStorage",
Class = "ReplicatedStorage",
Properties = {},
Children = {
{
Name = "Level",
Class = "NumberValue",
Properties = {
Value = 0,
},
Children = {},
},
{
Name = "Ship",
Class = "Model",
Properties = {},
Children = {
{
-- add all of the other instances following the same pattern :
-- Name, Class, Properties, Children
},
},
},
}, -- end list of Children
}, -- end ReplicatedStorage element
};
You can create this table with a simple recursive function :
-- when given a Roblox instance, generate the dataTable for that element
local function getElementData(targetInstance)
local element = {
Name = targetInstance.Name,
Class = targetInstance.ClassName,
Properties = {},
Children = {},
}
-- add special case logic to pull out specific properties for certain classes
local c = targetInstance.ClassName
if c == "StringValue" then
element.Properties = { Value = targetInstance.Value }
-- elseif c == "ADD MORE CASES HERE" then
else
warn(string.format("Not sure how to parse information for %s", c))
end
-- iterate over the children and populate their data
for i, childInstance in ipairs(targetInstance:GetChildren()) do
table.insert( element.Children, getElementData(childInstance))
end
-- give the data back to the caller
return element
end
-- populate the table
local Data = {
ReplicatedStorage = getElementData(game.ReplicatedStorage)
}
Now Data.ReplicatedStorage.Children should have a data representation of the entire folder. You could even save this entire table as a string by passing it to HttpService:JSONEncode() if you wanted to.
When you're ready to convert these back into instances, use the data you've stored to give you enough information on how to recreate the elements :
local function recreateElement(tableData, parent)
-- special case ReplicatedStorage
if tableData.Class == "ReplicatedStorage" then
-- skip right to the children
for i, child in ipairs(tableData.Children) do
recreateElement(child, parent)
end
-- quick escape from this node
return
end
-- otherwise, just create elements from their data
local element = Instance.new(tableData.Class)
element.Name = tableData.Name
-- set all the saved properties
for k, v in pairs(tableData.Properties) do
element[k] = v
end
-- recreate all of the children of this element
for i, child in ipairs(tableData.Children) do
recreateElement(child, element)
end
-- put the element into the workspace
element.Parent = parent
end
-- populate the ReplicatedStorage from the stored data
recreateElement( Data.ReplicatedStorage, game.ReplicatedStorage)
You should be careful about how and when you choose to save this data. If you are playing a multiplayer game, you should be careful that this kind of logic only updates ReplicatedStorage for the first player to join the server. Otherwise, you run the risk of a player joining and overwriting everything that everyone else has been doing.
Since there isn't a way to iterate over properties of Roblox Instances, you'll have to manually update the getElementData function to properly store the information you care about for each type of object. Hopefully this helps!
If anyone else has this problem the solution I used was just to convert the instances strait into JSON format(I used the names as keys). so that way I can save it, and then when the player rejoins I just used JSONDecode to turn it into the dictionary I needed.
function DirToJSON(Player)
local NewData = RS:FindFirstChild(Player.UserId)
local JSONstring="{"
local function Recurse(Data)
for i, v in pairs(Data:GetChildren()) do
if v:IsA("Folder") then
if #v:GetChildren() < 1 then
if i == #Data:GetChildren()then
JSONstring=JSONstring..'"'..v.Name..'":[]'
else
JSONstring=JSONstring..'"'..v.Name..'":[],'
end
else
JSONstring=JSONstring..'"'..v.Name..'":{'
Recurse(v)
if i == #Data:GetChildren()then
JSONstring=JSONstring..'}'
else
JSONstring=JSONstring..'},'
end
end
else
if i == #Data:GetChildren()then
JSONstring=JSONstring..'"'..v.Name..'":"'..v.Value..'"'
else
JSONstring=JSONstring..'"'..v.Name..'":"'..v.Value..'",'
end
end
end
end
Recurse(NewData)
JSONstring = JSONstring.."}"
return(JSONstring)
end

Lua Table Access with Location Given as a String

I have a table and I'm trying to access a specific location that is passed in as a String. What is the easiest way to use the string to access the correct location?
Example, if the table looks like this:
a.b1 = true
a.b2.c1 = true
a.b2.c2 = false
a.b3 = true
How can I change a.b2.c2 to true given a location 'a.b2.c2' as a string.
If you have just a single level, you can use square-brace indexing:
function setSingle(obj, key, value)
obj[key] = value
end
setSingle(a, "b1", "foo")
print(a.b1) --> foo
If you have multiple, you need to do several iterations of this indexing. You can use a loop to do that:
function setMultiple(obj, keys, value)
for i = 1, #keys - 1 do
obj = obj[keys[i]]
end
-- Merely "obj = value" would affect only this local variable
-- (as above in the loop), rather than modify the table.
-- So the last index has to be done separately from the loop:
obj[keys[#keys]] = value
end
setMultiple(a, {"b2", "c1"}, "foo")
print(a.b2.c1) --> foo
You can use string.gmatch to parse a properly formatted list of keys. [^.]+ will match "words" made of non-period symbols:
function parseDots(str)
local keys = {}
for key in str:gmatch "[^.]+" do
table.insert(keys, key)
end
return keys
end
Putting this all together,
setMultiple(a, parseDots("b2.c2"), "foo")
print(a.b2.c2) --> foo
One issue you may run into is that you cannot create new tables with this function; you will have to create the containing table before you can create any keys in it. For example, beforing ading "b4.c3" you would have to add "b4".
You can use loadstring to build the statement you want to execute as a string.
a = { b2 = {} }
a.b1 = true
a.b2.c1 = true
a.b2.c2 = false
a.b3 = true
str = "a.b2.c2"
loadstring(str .. " = true")()
print(a.b2.c2)

I don't get Lua weak tables

Okay so I want to implement a cache generator, which returns a handle to me, but doesn't give me the actual data.
local module = {}
cache = {}
setmetatable(cache, { __mode = "k" })
function getItem(item)
local result = cache[item] or cacheItem(item)
return item
end
function cacheItem(item)
print("Caching item")
cache[item] = true
return cache[item]
end
function printCache()
for key, value in pairs(cache) do
print(key.name .. " and " .. value)
end
end
module.printCache = printCache
module.getItem = getItem
return module
But when I try to use it:
local module = require("./cache")
a = {
name = "Jimmie"
}
function foo()
local b = {
name = "Bobbie"
}
local h1 = module.getCachedItem(a)
module.getCachedItem(b)
local h3 = module.getCachedItem({ name = "Lenny" })
module.printCache()
end
foo()
module.printCache()
The first time around, I expect all names to be printed.
The second time around, I only expect "Jimmie" to be printed.
However all names are printed twice. lua -v returns 5.2.4.

Resources