 Useful CS2D Lua Functions: Part III

So according to the results of the poll on UnrealSoftware, 6 out of 10 people wanted to see a save system. In this post I’m going to describe a quite sophisticated universal system for saving data in an external file.

So, let’s begin. First, we need a plan of how the whole thing will work. Let’s think…

• We need the script to save data to a file
• We need the script to then read the data from a file
• Data is usually stored in tables, therefore we need to find a way to write tables to a file
• If possible, we need the save file to be readable by humans

A general plan is done. Let’s now think about the specifics…

• The code has to be as compact as possible
• The code shouldn’t be extremely difficult
• The code shouldn’t use too much memory

All right, now let’s get to writing the thing. As stated in our initial plan, we need a way to write tables into a file.

[lua]
function table.print(tbl)
for k,v in pairs(tbl) do
print("["..k.."] = "..v)
end
end
[/lua]

All right, that will print consecutively everything within the table. Problems:

1. It only prints the values out, doesn’t actually make them available to us
2. It only prints values that can be concatenated to a string – a number or another string
3. It doesn’t support nested tables

Okay, that’s not as good. Let’s get to fixing each of these points.

[lua]
function table.print(tbl)
local tblt="{"
for k,v in pairs(tbl) do
tblt=tblt.."["..k.."] = "..v..","
end
tblt=tblt.."}"
return tblt
end
[/lua]

All right, first point done – the function now returns the table as a string. Second point:

[lua]
function table.print(tbl)
local tblt="{"
for k,v in pairs(tbl) do
tblt=tblt.."["..k.."] = "..tostring(v)..","
end
tblt=tblt.."}"
return tblt
end
[/lua]

Somewhat fixed. Now, the most difficult part about the whole function – nested tables.

[lua]
function table.print(tbl)
local tblt="{"
for k,v in pairs(tbl) do
tblt=tblt.."["..k.."] = "
if type(v)=="table" then
tblt=tblt..table.print(v)
else
tblt=tblt..v
end
end
tblt=tblt.."}"
return tblt
end
[/lua]

Smart, isn’t it? We call the function in itself, thus adding support to a theoretically infinite amount of nested tables.

All right, now a point in our original thought plan was to make the table printout readable by humans. Right now, the printout of our function for a table of {1,2,3,{2,3}} would look like this:

[lua]
{=1,=2,=3,={=2,=3},}
[/lua]

Which is readable, but with considerable difficulty. Let’s add some line breaks and tabulation.

[lua highlight=”1-3,5,7-9,17-19″]
function table.print(tbl,i)
if not i then i=1 end
local fi=i-1
if type(tbl)=="table" then
local tblt="{\n"
for k,v in ipairs(tbl) do
for n=1,i do
tblt=tblt.." "
end
tblt=tblt.."["..k.."] = "
if type(v)=="table" then
tblt=tblt..table.print(v,i+1)..",\n"
else
tblt=tblt..tostring(v)..",\n"
end
end
for n=1,fi do
tblt=tblt.." "
end
tblt=tblt.."}"
return tblt
end
end
[/lua]

You can see changes on the highlighted lines – there’s now a new argument i, which is the relative level of the supplied table. There’s a local variable fi, which corresponds to the level of the table minus one (used for closing curly brackets). You can also see that there’s now a for loop that inserts tabbing. The final minor change was support for non-table values – a simple if statement checking for the type of the supplied argument will suffice.

And so now, with this function, the printout for a table of {{1,2,3,{7,8}},{4,5,6}} would look like this:

[lua]
{
 = {
 = 1,
 = 2,
 = 3,
 = {
 = 7,
 = 8,
},
},
 = {
 = 4,
 = 5,
 = 6,
},
}
[/lua]

Better, isn’t it? But now, we need a function to actually save things to a file, as the first point of our initial wireframe stated. Let’s make it universal by

1. Adding the possibility to specify the save file each time it’s called
2. Adding the possibility to save as many tables as we want

The first point is pretty easy – add an argument for the file path, and bam, you got it. But what to do with the second? According to most materials, you can only specify so many arguments – you can’t make it support a variable amount of them! Well, yes, you actually can. This is how you do it:

[lua]
function savedata(file,…)
[/lua]

What? Three dots as an argument? The hell is that? That, my friends, is our variable amount of arguments. The arguments specified ‘within’ those dots – that is, after the first ‘file’ argument, are stored in a local table called arg, which has every argument within the dots in it, as well as a value with the key n that stores the total amount of those arguments. Knowing this, we can do it like so:

[lua]
function savedata(file,…)
local file=assert(io.open(file,"w"))
for k,v in pairs(arg) do
if k~="n" then
file:write(table.print(v))
file:write("\n\n")
end
end
file:close()
end
[/lua]

Okay, yeah, we open the file, we check for the values in the arg table, we check if it’s not the total amount and we write the data. Simple enough. That should work.

Well, no. Since we are saving the table to then load it, we need some sort of a name. Since we’re going to supply arguments to that function, the tables we give it probably have names, if they’re supplied as variables and not raw tables. But how in the world do you access the name of the table? You run it through tostring? Of course not.

Here, another intricate Lua feature comes in. It’s called the environment table. The environment table, that is called _G, stores every variable in itself, with the variable names as keys for them. Paradise! Now we just need to check if the value in the environment table corresponds with the value of the argument, and get its key to get the name of that! Jolly good. Here’s how it goes:

[lua]
function savedata(file,…)
local file=assert(io.open(file,"w"))
for k,v in pairs(arg) do
if k~="n" then
for ak,av in pairs(_G) do –cycling through environment table _G
if av==v then — if value in _G is equal to value of arg
if type(v)=="table" then — if the arg is a table
file:write(ak.." = ")
file:write(table.print(v)) — print the table out
else
file:write(ak.." = ")
if type(v)=="string" then
file:write("\""..v.."\"")
else
file:write(tostring(v))
end
end
end
end
file:write("\n\n")
end
end
file:close()
end
[/lua]

And there you go, the saving function done. Now we can write our data in an external file.

[lua]
dofile(file)
end
[/lua]

And that’s it! One line! You don’t even have to necessarily make it a separate function! Since we save our file as pretty much a simple Lua script with table definitions, we just execute it by means of the dofile function and it loads those table up! Just great.

Attention! The saving system will only save tables that have names:

[lua]
tbl={1,2,3}
savedata("sys/save.sav",tbl) –this will work

savedata("sys/save.sav",{1,2,3}) –this will not
[/lua]

The reason for that is because we save tables to later load them by names – without names we can’t exactly make it readily available for modification.

Full source code:

[lua]
function table.print(tbl,i)
if not i then i=1 end
local fi=i-1
if type(tbl)=="table" then
local tblt="{\n"
for k,v in ipairs(tbl) do
for n=1,i do
tblt=tblt.." "
end
tblt=tblt.."["..k.."] = "
if type(v)=="table" then
tblt=tblt..table.print(v,i+1)..",\n"
else
tblt=tblt..tostring(v)..",\n"
end
end
for n=1,fi do
tblt=tblt.." "
end
tblt=tblt.."}"
return tblt
end
end

function savedata(file,…)
local file=assert(io.open(file,"w"))
for k,v in pairs(arg) do
if k~="n" then
for ak,av in pairs(_G) do
if av==v then
if type(v)=="table" then
file:write(ak.." = ")
file:write(table.print(v))
else
file:write(ak.." = ")
if type(v)=="string" then
file:write("\""..v.."\"")
else
file:write(tostring(v))
end
end
end
end
file:write("\n\n")
end
end
file:close()
end

dofile(file)
end
[/lua]

I hope this lesson was interesting and useful – leave your questions or comments below and I’ll respond. Good luck! 1. kalis says: