How can I implement a read-only table in lua? - lua

I wrote an example.
function readOnly(t)
local newTable = {}
local metaTable = {}
metaTable.__index = t
metaTable.__newindex = function(tbl, key, value) error("Data cannot be changed!") end
setmetatable(newTable, metaTable)
return newTable
end
local tbl = {
sex = {
male = 1,
female = 1,
},
identity = {
police = 1,
student = 2,
doctor = {
physician = 1,
oculist = 2,
}
}
}
local hold = readOnly(tbl)
print(hold.sex)
hold.sex = 2 --error
It means that I can give access to the field of the table "tbl" but at the same time, I cannot change the value related to the field.
Now, the problem is that I wanna let all the nested tables own this read-only
property.How can I improve the "readOnly" method?

You just have to apply your readOnly function recursively to the inner table fields as well. You can do so on-access in the __index metamethod. You should also cache the readonly proxy tables that you create, otherwise any read access to inner tables (e.g. hold.sex) will create a new proxy table.
-- remember mappings from original table to proxy table
local proxies = setmetatable( {}, { __mode = "k" } )
function readOnly( t )
if type( t ) == "table" then
-- check whether we already have a readonly proxy for this table
local p = proxies[ t ]
if not p then
-- create new proxy table for t
p = setmetatable( {}, {
__index = function( _, k )
-- apply `readonly` recursively to field `t[k]`
return readOnly( t[ k ] )
end,
__newindex = function()
error( "table is readonly", 2 )
end,
} )
proxies[ t ] = p
end
return p
else
-- non-tables are returned as is
return t
end
end

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.

function only returning one value instead of both in lua

function GetCharacterName(source)
local xPlayer = QBCore.Functions.GetPlayer(source)
local name = xPlayer.PlayerData.charinfo.lastname
local name2 = xPlayer.PlayerData.charinfo.firstname
if xPlayer then
return name, name2
end
end
I'm trying to return the values of name and name2, however the return is only giving the first value. I'm newer to lua, and code in general, so just trying to learn
Additional places in which this function is called:
AddEventHandler("mdt:attachToCall", function(index)
local usource = source
local charname = GetCharacterName(usource)
local xPlayers = QBCore.Functions.GetPlayers()
for i= 1, #xPlayers do
local source = xPlayers[i]
local xPlayer = QBCore.Functions.GetPlayer(source)
if xPlayer.PlayerData.job.name == 'police' then
TriggerClientEvent("mdt:newCallAttach", source, index, charname)
end
end
TriggerClientEvent("mdt:sendNotification", usource, "You have attached to this call.")
end)
RegisterServerEvent("mdt:performVehicleSearchInFront")
AddEventHandler("mdt:performVehicleSearchInFront", function(query)
local usource = source
local xPlayer = QBCore.Functions.GetPlayer(usource)
if xPlayer.job.name == 'police' then
exports['ghmattimysql']:execute("SELECT * FROM (SELECT * FROM `mdt_reports` ORDER BY `id` DESC LIMIT 3) sub ORDER BY `id` DESC", {}, function(reports)
for r = 1, #reports do
reports[r].charges = json.decode(reports[r].charges)
end
exports['ghmattimysql']:execute("SELECT * FROM (SELECT * FROM `mdt_warrants` ORDER BY `id` DESC LIMIT 3) sub ORDER BY `id` DESC", {}, function(warrants)
for w = 1, #warrants do
warrants[w].charges = json.decode(warrants[w].charges)
end
exports['ghmattimysql']:execute("SELECT * FROM `player_vehicles` WHERE `plate` = #query", {
['#query'] = query
}, function(result)
local officer = GetCharacterName(usource)
TriggerClientEvent('mdt:toggleVisibilty', usource, reports, warrants, officer, xPlayer.job.name)
TriggerClientEvent("mdt:returnVehicleSearchInFront", usource, result, query)
end)
end)
end)
end
end)
There are two possible reasons.
name2 is nil
you're using the function call in a way that adjusts the number of return values to 1. This happens GetCharacterName(source) is not the last in a list of expressions or if you put that function call into parenthesis or you assign it to a single variable.
Edit:
Now that you've added examples of how you use that function:
local charname = GetCharacterName(usource)
In this case the result list of GetCharacterName is ajdusted to 1 value (name) as you only assingn to a single variable. If you want both return values you need two variables.
Either do it like so:
local lastName, firstName = GetCharactername(usource)
local charName = lastName .. ", " .. firstname
Then charName is "Trump, Donald" for example.
Or you return that name as a single string:
function GetCharacterName(source)
local xPlayer = QBCore.Functions.GetPlayer(source)
local name = xPlayer.PlayerData.charinfo.lastname
local name2 = xPlayer.PlayerData.charinfo.firstname
if xPlayer then
return name .. ", " .. name2
end
end
Then local charname = GetCharacterName(usource) will work
local name1, name2 = GetCharactersName()

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

Read only iterable table in lua?

I want to have a read only table in my Lua program. If ever a key is removed or a key is associated with a new value, an error must be thrown.
function readonly(table)
local meta = { } -- metatable for proxy
local proxy = { } -- this table is always empty
meta.__index = table -- refer to table for lookups
meta.__newindex = function(t, key, value)
error("You cannot make any changes to this table!")
end
setmetatable(proxy, meta)
return proxy -- user will use proxy instead
end
It works great.
t = { }
t["Apple"] = "Red"
t[true] = "True!"
t[51] = 29
for k,v in pairs(t) do
print(v)
end
t = readonly(t)
t[51] = 30
Prints
Red
True!
29
input:7: You cannot make any changes to this table!
Problem
for k, v in pairs(t) do
print(v)
end
Will print nothing under all circumstances now. That's because the proxy table will never have anything inside of it. pairs apparently never calls index and thus cannot retrieve anything from the actual table.
What can I do to make this readonly table iterable?
I'm on Lua 5.1 and have access to these metamethods:
Lua 5.1 Manual
You can modify standard Lua function pairs to work correctly with your read-only tables.
local function readonly_newindex(t, key, value)
error("You cannot make any changes to this table!")
end
function readonly(tbl)
return
setmetatable({}, {
__index = tbl,
__newindex = readonly_newindex
})
end
local original_pairs = pairs
function pairs(tbl)
if next(tbl) == nil then
local mt = getmetatable(tbl)
if mt and mt.__newindex == readonly_newindex then
tbl = mt.__index
end
end
return original_pairs(tbl)
end
Usage:
t = { }
t["Apple"] = "Red"
t[true] = "True!"
t[51] = 29
for k,v in pairs(t) do
print(k, v)
end
t = readonly(t)
for k,v in pairs(t) do
print(k, v)
end
t[51] = 30
One solution is to create a wholly custom iterator for the table.
function readonly(table)
local meta = { } -- metatable for proxy
local proxy = { } -- this table is always empty
meta.__index = table -- refer to table for lookups
meta.__newindex = function(t, key, value)
error("You cannot make any changes to this table!")
end
local function iter()
return next, table
end
setmetatable(proxy, meta)
return proxy, iter -- user will use proxy instead
end
Usage:
t = { }
t["Apple"] = "Red"
t[true] = "True!"
t[51] = 29
for k,v in pairs(t) do
print(v)
end
t, tIter = readonly(t)
t[51] = 30
for k, v in tIter do
print(v)
end

attaching metatables within tabes

I have parser that parses a config file and produces a table.
The resulting table can look something like:
root = {
global = {
},
section1 = {
subsect1 = {
setting = 1
subsubsect2 = {
}
}
}
}
The goal is to have a table I can read settings from and if the setting doesn't exist, it'll try to grab it from it's parent. At the top level it will grab from global. If it's not in global it'll return nil.
I attach metatables to root like this:
local function attach_mt(tbl, parent)
for k,v in pairs(tbl) do
print(k, v)
if type(v) == 'table' then
attach_mt(v, tbl)
setmetatable(v, {
__index = function(t,k)
print("*****parent=", dump(parent))
if parent then
return tbl[k]
else
if rawget(tbl, k) then
return rawget(tbl, k)
end
end
print(string.format("DEBUG: Request for key: %s: not found", k))
return nil
end
})
end
end
end
attach_mt(root)
However, when requesting keys it doesn't work. What appears to be the case is that is always nil. How do I read from the parent table?
local function attach_mt(tbl, parent)
setmetatable(tbl, {__index = parent or root.global})
for k, v in pairs(tbl) do
if type(v) == 'table' then
attach_mt(v, tbl)
end
end
end
attach_mt(root)
setmetatable(root.global, nil)

Resources