Tables in Lua

Posted by Nick Gammon on Wed 24 Nov 2004 03:05 AM — 29 posts, 142,026 views.

Australia Forum Administrator #0
Tables are a fundamental part of the Lua design. You can make your own very easily, and store anything you like in them (including other tables).

For example, say you want to keep track of some mobs:


mobs = {}  -- create mobs table


The above line creates a table, which you can then put data into, in various ways. Let's make an individual mob another table, and then store relevant parts keyed by name:


mobs.kobold = {
  name = 'killer',
  hp = 22,
  gold = 5,
  location = 'city square'
  }

-- and another one ...

mobs.worm = {
  name = 'gordon',
  hp = 4,
  gold = 15,
  location = 'underground'
  }



Now we can start to look at this data. First, we'll print the mobs table:


print (mobs)  --> table: 00629490


You can type these examples into the MUSHclient command window (prefixed by the scripting prefix "/"), or into the Immediate scripting window.

It is all very well knowing we have a table, but what is in it? A useful feature of Lua is the "table.foreach" instruction which lets you iterate over a table. eg.


table.foreach (mobs, print)

-- output:

worm table: 006303E0
kobold table: 00630560


This is printing (ie. doing a world.Note) each entry in the table. First you see the key, and then the data. The data in this case is another table.

An alternative way of doing this is with a loop:


for k, v in pairs (mobs) do 
  print ("key = ", k, " value = ", v)
end

-- output:

key =  worm  value =  table: 006303E0
key =  kobold  value =  table: 00630560


But what is the value for the kobold? Well, we just repeat the exercise for the sub-table:


table.foreach (mobs.kobold, print)

-- output:

name killer
gold 5
location city square
hp 22


We can also access individual fields by delimiting them with dots, like this:


print (mobs.kobold.hp)  --> 22





Recursive table printer

If you are playing with tables a lot, a recursive printing function might be helpful. Here is an example:


function tprint (t, indent, done)
  done = done or {}
  indent = indent or 0
  for key, value in pairs (t) do
    Tell (string.rep (" ", indent)) -- indent it
    if type (value) == "table" and not done [value] then
      done [value] = true
      Note (tostring (key), ":");
      tprint (value, indent + 2, done)
    else
      Tell (tostring (key), "=")
      print (value)
    end
  end
end


To use this, just call it with a table, eg.


tprint (mobs)

-- output:

worm:
  name=gordon
  gold=15
  location=underground
  hp=4
kobold:
  name=killer
  gold=5
  location=city square
  hp=22


That is neat - now we can see the table, and any sub-tables, in a single command.
Amended on Sat 07 Apr 2007 06:56 AM by Nick Gammon
#1
Where would you place the table? Obviously it wouldn't be in a trigger.... Also how to I create a Lua script file?

I used to have myscript.vbs, but with the new language, not sure how to create one.
Australia Forum Administrator #2
You put the table where you put other global variables. Normally that would be in your script file, so it is in the "global script spaces".

The Lua script file is just a text file. I would call it something like myscript.lua.
#3
So make a text file called whatever.lua and load it like I would a whatever.vbs?

Seems simple enough, though you mentioned something about the Lua in the global preferences. Is that the same as loading a .lua script file i nthe scripting options in game>configure>scripting?

If so wouldn't there be a conflict having 2 script files?

Additionally, is loading a script file necessary if you can simply use the global preferance file?
Australia Forum Administrator #4
Quote:

So make a text file called whatever.lua and load it like I would a whatever.vbs?


Yes.


Quote:

If so wouldn't there be a conflict having 2 script files?


No, they both execute in the global script space for that world, so effectively you have one set of commands followed by another.

However the global preferences was intended for setting up a secure sandbox, not to have all your scripts in it. For one thing, it is kept in the registry, and if you have thousands of lines of script in it, it will bloat the registry. Also, if you lose the registry (eg, reinstall Windows) then you have lost all your scripts.

If you must do that, put all your scripting into a file, and then in the global preferences, just do:

dofile "path_to_my_script_file.lua"

However I think the preferable thing is to simply have a script file like you do for VB and quote that file's name in the scripting preferences.
#5
Hi! I've been looking for a function that has exactly the same functionality as the php function print_r (recursive printing of anything, tables, values, what ever...)

so i stumbled across your posting, with minor changes in your source, i managed to make quite a good clone of php's print_r which works fine in lua5 as well.

source:


function print_r (t, indent, done)
    done = done or {}
    indent = indent or 0
    if type(t) == "table" then
        for key, value in pairs (t) do
            io.write(string.rep (" ", indent)) -- indent it
            if type (value) == "table" and not done [value] then
              done [value] = true
              io.write(string.format("[%s] => table\n", tostring (key)));
              io.write(string.rep (" ", indent+4)) -- indent it
              io.write("(\n");
              print_r (value, indent + 7, done)
              io.write(string.rep (" ", indent+4)) -- indent it
              io.write(")\n");
            else
              io.write(string.format("[%s] => %s\n", tostring (key),value))
            end
        end
    else
        io.write(t .. "\n")
    end
end



maybe this helps any other php programmer that wants to use lua for some reason.

cheers
Amended on Sun 30 Oct 2005 09:22 PM by Nick Gammon
Australia Forum Administrator #6
Thanks, I amended your post to put in the [code] tag to show formatting better.
Australia Forum Administrator #7
For more information about Lua tables, see Lua tables in detail.
#8
Just starting out with LUA, I found the 2 dimension table sort topic *very* useful after searching everywhere for that info.

so a question on tables if I may...

I have a master table of players and i want to copy a sub-set of this into another table based upon a filter.

EG

mastertable[1] = {["id"]=1, ["name"]="fred", ["class"]="a"}
mastertable[2] = {["id"]=1, ["name"]="bill", ["class"]="b"}
mastertable[3] = {["id"]=1, ["name"]="john", ["class"]="a"}
mastertable[4] = {["id"]=2, ["name"]="frank", ["class"]="b"}
mastertable[5] = {["id"]=2, ["name"]="robert", ["class"]="a"}

So so way of doing a table.foreach on the mastertable, checking if the "id" == a value and then any matching records copied to a new table. If I was after "id"=2 then

copytable[1] = {["id"]=2, ["name"]="frank", ["class"]="b"}
copytable[2] = {["id"]=2, ["name"]="robert", ["class"]="a"}

The following code doesn't work although I think it's not a million miles away...

table.foreach (mastertable, function() table.insert (copytable,
function (k, v)
if v.number == id then
return k
end -- if
end -- function
end -- function
)
)

Thanks in advance.
Australia Forum Administrator #9
I believe table.foreach is being deprecated in Lua 5.1, so you could do it in a simple loop:


copytable = {}

for k, v in ipairs (mastertable) do
  if v.id == 2 then
    table.insert (copytable, v)
  end -- if
end -- for


That works OK, and is easy to read.

However you can certainly do it with table.foreach. I don't know why you had 2 functions, one is enough. Also I would use table.foreachi since your table seems to be numerically indexed:


copytable = {}

table.foreachi (mastertable, 
  function (k, v)  -- key, value
    if v.id == 2 then
      table.insert (copytable, v)
    end -- if
  end -- function
  )  -- end foreachi

Amended on Tue 28 Mar 2006 08:09 PM by Nick Gammon
Australia Forum Administrator #10
Of course, you can make a more generic "table copy" function, like this:



-- generic table filter function

function filtertable (t, f)
  assert (type (t) == "table")
  assert (type (f) == "function")
  local k, v
  local result = {}
  for k, v in ipairs (t) do
    if f (v) then
      table.insert (result, v)
    end -- if
  end -- for
  return result
end -- filtertable 


-- example

copytable = filtertable (mastertable, 
                         function (v) return v.id == 2 end 
                        )


What this does is make a function that makes a copy of the supplied table, where you simply supply a function that returns true if you want the table item to be included, and false if not.

I threw in a couple of asserts to check that the arguments are what you expect.
Amended on Tue 28 Mar 2006 08:41 PM by Nick Gammon
#11
fantastic!

I knew the code should be short and elegant but i had the blinkers on i suspect.

Also thanks for the warning on LUA 5.1, i will follow your advice.

:-)
Australia Forum Administrator #12
Also you can simplify your table definition, assuming what you did was not just an example. Instead of:


mastertable[1] = {["id"]=1, ["name"]="fred", ["class"]="a"}
mastertable[2] = {["id"]=1, ["name"]="bill", ["class"]="b"}


You can use:


mastertable[1] = {id=1, name="fred", class="a"}
mastertable[2] = {id=1, name="bill", class="b"}
#13
a follow up question!

what's the best way of emptying a table since they can't be deleted?

1. just a table.setn = 0? so the contents get's overwritten
2. do a table.remove in a foreach?
3. do a loop setting each element to null?

Thanks
Australia Forum Administrator #14

mastertable = nil


That will remove the reference to the table, and the garbage collector will collect it. Or, if you want to empty it, and keep it as a table:


mastertable = {}  -- make new, empty, table


If, for some reason, you need to keep the table the same (for example, you have references to it elsewhere), you can do this:


for k in pairs (mastertable) do
  mastertable [k] = nil
end -- for
Amended on Wed 29 Nov 2006 09:29 PM by Nick Gammon
#15
Hi Nick

Thanks for the advice so far.

I note that using table.sort doesn't produce stable/repeatable results.

Any advice on how to do this please?

Furthermore I'd like to be able to do sortation on two elements of a table (using above sample data, say primary sort on name, secondary sort on class). How would you do that using table.sort? (or would you put in full code for a sort into a function and do it that way?).

Thanks.
USA #16
How do you define stable/repeatable results? If you take a table with the same elements, the sort will always be the same, unless of course some elements are equal in which case their sort order is undefined.

To make things sort on multiple values, you could use something like this as your sort function:
function (a,b)
  if a.date < b.date
    return true -- primary sort
  elsif a.date == b.date
    return a.file < b.file -- secondary sort
  else
    return false -- a is >= b in this case (as far as the sort is concerned)
  end
end
#17
Perfect.

Thanks.
#18
Hello.

It might be that I am a bit dumb at the moment. :-)

I want to sort a table by its values, but that seems to be a real problem.

What I have is something like this:

age = {}
age.peter = 24
age.paul = 14
age.mary 34

I want to have this table sorted/printed according to their ages.

A table.sort(age) does nothing, nor does a table.sort(age,funcion (a,b) return age.a < age.b end)

What is the trick?

Regards
Thomas
Australia Forum Administrator #19
The sort function only works on numerically-indexed tables, whereas your tables has keys "peter", "paul" and "mary".

You can sort it, but you must first get a numerically-keyed table. This will do it:


age = {}
age.peter = 24
age.paul = 14
age.mary = 34


-- copy keys into numeric table
t = {}

for k in pairs (age) do
  table.insert (t, k)  -- remember key of each item
end -- for

-- show unsorted table
table.foreach (t, print)

-- sort it
table.sort (t, 
  function (a, b)
   return age [a] < age [b]
  end -- function
  )

-- show results

print "after sort..."

table.foreach (t, print)

Output

1 peter
2 mary
3 paul
after sort...
1 paul
2 peter
3 mary



What I have done here is first looped to copy the keys of your main table into a secondary (temporary) table t. You can see from the initial debugging display that they were added in the order peter, mary and paul (they are stored internally as a hash, so the order cannot really be predicted).

Now we sort the table t, using the keys to index into the age table to find which age is less than which other age.

Finally we can print the table t, which is now sorted into the correct order (paul, peter, mary).

This general technique can be used to sort anything into any sequence.
Amended on Wed 29 Nov 2006 09:14 PM by Nick Gammon
Australia Forum Administrator #20
Quote:

return age.a < age.b


Semantically this is wrong anyway. This is comparing key "a" to key "b". That is, this is the same as:


return age ["a"] < age ["b"]


In other words, you are comparing the item "a" to the item "b", if they exist, which they don't in your case.

You really wanted:


return age [a] < age [b]


However my remarks about needing the numerically-indexed table still stand.
Amended on Wed 29 Nov 2006 09:21 PM by Nick Gammon
#21
ok so i took your advice nick and fiddled around with what i already knew, learnt a bit more in the process (after 2 days work(slow learner))

Trigger: You still have to kill * * (*)

Send:
campaign = {}
mobs.mobname = {
mob = "%2", area = "%3"
}
print (campaign)

Send to: script


This is all good and well in creating a table of campaign mobs in Aardwolf but I'm really struggling to find a way to manipulate the first 2 true wildcards (the 1st is recognised as an asterisk by the 1st wildcard).

Im pretty sure my main problem is that it's sending to script and not world so i can't put something like:

Send:
campaign = {}
mobs.mobname = {
mob = "%2", area = "%3"
}
print (campaign)
table.foreach (campaign, print)

runto "%3"
where "%2"

my second problem is i dont know how to differentiate/identify ((between)) different wildcard entries on different table entries

is there a special function or command that would make the ' runto "%3" ' and then the ' where "%2" ' - send to world within the script? Can I do it using another trigger or alias or have i got completely the wrong idea? My brain is telling me that i will need to identify which is the first table entry and the string values of %2 and %3 in that entry.

I'm quite lost here as you can tell.
Australia Forum Administrator #22
To deal with the obvious problem:


runto "%3"
where "%2"


needs to be:


Send ("runto %3")
Send ("where %2")


This because, in a script, the Send function sends its arguments to the MUD.
#23
That's partly usefull because it does work however I need to be able to pull just the first set of %2 and %3 from the first table entry otherwise the script executes and it makes me run to every single area stored as the string for %3 for each entry (along with some additonal sends for 'hold portal' 'enter' 'down')

my trigger now looks like this :


Trigger: You still have to kill * * (*)

Send:
campaign = {}
mobs.mobname = {
mob = "%2", area = "%3"
}
print (campaign)
Send ("hold portal")
Send ("enter")
Send ("d")
Send ("hold candle")
Send ("runto %3")
Send ("where %2")

Send to: Script


So my question is - how do I pull/retrieve the first values and the first values ONLY for %2 and %3? - (because when the trigger executes next, the first entry of the table is the only one necessary for progressing, I believe the table is required to do this otherwise the script gets confused with all the different values of the wildcards.
Australia Forum Administrator #24
Show the whole thing please:

Template:copying
For advice on how to copy aliases, timers or triggers from within MUSHclient, and paste them into a forum message, please see Copying XML.
#25
<triggers>
<trigger
enabled="y"
match="Finish * off!"
sequence="100"
sound="C:\Aardwolf\MUSHclient\sounds\Decapitation Head Blood-SoundBible.com-310864499.wav"
>
</trigger>
<trigger
enabled="y"
match="Trigger: You still have to kill * * (*)"
send_to="12"
sequence="100"
>
<send>campaign = {}
mobs.mobname = {
mob = "%2", area = "%3"
}
print (campaign)

Send ("enter")
Send ("d")
Send ("runto %3")
Send ("where %2")
</send>
</trigger>
</triggers>
#26
err for some reason the above post copied 2 triggers at once... this is the one in question:


<triggers>
  <trigger
   enabled="y"
   match="Trigger: You still have to kill * * (*)"
   send_to="12"
   sequence="100"
  >
  <send>campaign = {}
mobs.mobname = {
mob = "%2", area = "%3"
}
print (campaign)

Send ("enter")
Send ("d")
Send ("runto %3")
Send ("where %2")
</send>
  </trigger>
</triggers>
Amended on Tue 03 Apr 2012 02:06 AM by Nick Gammon
Australia Forum Administrator #27
Why are you setting campaign to be an empty table?

Can you show some example output, describe what is actually happening, and what you expect to happen?
#28
ok so working backwards I realise I had made a couple of mistakes... this is now what my trigger looks like and is working just fine but I cant seem to be able to manipulate the key entry for 'area' (%3):


<triggers>
  <trigger
   enabled="y"
   match="You still have to kill * * (*)"
   send_to="12"
   sequence="100"
  >
  <send>campaign = {}
 -- create campaign table

campaign.mobs = {
 -- create sub table entries of campaign table

mob = "%2", area = "%3"
 -- gives the key 'mob' the value of the second wildcard and the key?! 'area' the value of the third wildcard

}
 -- close campaign table

print (campaign)
 -- prints campaign table

table.foreach (campaign.mobs, print)
 -- prints all the values of the subtables campaign.mobs


</send>
  </trigger>
</triggers>


which gives output that looks like this (for the last 4 mobs of the campaign):

table: 0x05dedd48
area The Blighted Tundra of Andarin
mob a pine tree
table: 0x05deb020
area The Labyrinth
mob a hippogriff
table: 0x05df3770
area The Wobbly Woes of Woobleville
mob An obnoxious goat

I want to be able to runto the area of the first key entry for 'area' and locate which room the mob is in by use of a trigger sending to script.