At its core, ShiVa is a C++ engine which also allows developers to use Lua directly inside
the editor for easy coding. ShiVa has an extensive API with over 5000 functions and
constants to choose from. Most of them are covered in our documentation. To make the Lua
interface possible, ShiVa ships with a complete Lua interpreter, which means a great
selection of helpful additional functions are available to you which you were probably not
aware of if you only used the ShiVa documentation and not the official Lua
documentation. This tutorial series will introduce you to a few
of those commands and hopefully encourage you to look through the Lua documentation more
often.
Lua versions and C++
ShiVa has a Runtime API and an Editor API. Both use different versions of the Lua
interpreter. The Runtime API, which you are familiar with from the 1.9.x series of ShiVa
editors, uses Lua 5.0.3, while the new ShiVa 2.0 editor is – at the time of writing
– fully Lua 5.2.x compatible. Keep that in mind, so you do not try to run code that is
incompatible with the respective versions.
Also keep in mind that all “undocumented” Lua cannot be translated into C++ by
ShiVa. Only the Lua functions in the ShiVa documentation are supported by our translator. By
using Lua, you are potentially missing out on performance, depending on the complexity of
your code of course. We recommend using “undocumented” Lua mostly inside the
ShiVa 2.0 editor to help you code editor modules, while sticking to the documented (and C++
supported) ShiVa Runtime API for your games themselves.
You can easily test your code using the ShiVa 2.0 Editor Console and Log.
Tables
Lua tables differ from the ShiVa Runtime table.* API. The new Editor API uses them
extensively, so it is important to familiarize yourself with the new concepts.
Tables are initialized in Lua with curly brackets:
-- empty table
a = {}
Lua table indices start at 1, not at 0 like in the ShiVa table API. Lua tables can contain numbers, strings, a mixture of both, …
-- number table
b = {7, 8, 9}
log.message(b[1]) --> 7
log.message(b[7]) --> nil
-- mixed table
c = {7, "eight", 9}
log.message(c[2]) --> eight
… or even other tables. Accessing them is easy as well:
-- tables in table
d = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }
log.message ( "table value at d 2,2 index: " ..d[2][2] ) --> table value at d 2,2 index: 5
Table elements can be counted using the # operator:
days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
log.message ( "How many days are there? Answer: ".. #days ) --> How many days are there? Answer: 7
Looping through tables requires the pairs() or ipairs() function, which will return a key-value pair. ipairs() is necessarily ordered, while pairs() might not be.
days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
for i,v in ipairs(days) do
log.message ( "Day "..i .." named " ..v )
end
One major caveat with tables is value assignment. There are 3 ways:
-- create table
a = {}
-- assign
a.a = "index a"
a["a"] = "string a"
a[a] = "bracketed a"
The dot syntax and [“”] are equivalent, but [] is not. The output of this code
wouild be: string a, string a, bracketed a.
You can also create key-value pairs directly inside the table initialization. Note how
indices might not always be appropriate keys, how upper/lower case matters (First/first),
and how the values are retrieved exactly the same, though their initialization looks
different:
t = {["First"] = "one", first = "two"}
--log.message("Assigned to T: " ..t[1] ..", " ..t[2]) --> error!
log.message("Assigned to T: " ..t["First"] ..", " ..t["first"]) --> one, two
t = {foo = "bar"} -- same as ["foo"]="bar" (but not [foo]="bar" , that would use the variable foo)
When you are done, you can erase a key/value pair from a table by assigning nil to the key, or nil to the entire table to clear it:
a[b] = nil
a = nil
Further reading: lua-users.org – TablesTutorial
os and io
os and io are built-in APIs that can be used to communicate with the “outside world”, meaning your operating system and other processes. This can be very handy if you want to execute a bash or bat script for instance, or launch another standalone tool.
-- double \\ to escape backslash on Windows
os.execute ("C:\\AndroidStudio\\uninstall.exe")
By default, os and popen are blocking, which means your parent process freezes as long as the child process is running. This will most likely be unwanted. use “start” and “&” to put a process in the background and unblock the parent:
-- using "start" on windows unblocks parent.
-- also note: mixed single and double quotes to escape whitespace on Windows
os.execute('start C:\\ShiVa192\\"ShiVa Editor Advanced"\\ShiVa.exe')
-- the & for Mac and Linux
os.execute("firefox &")
Sometimes, it is not necessary to include a file name, because the application is located in a system path or comes as standard with the operating system. Other times, the operating system will figure out what to do completely on its own, given the command, and launch the appropriate application:
-- launch the Windows Calculator app
os.execute("calc")
-- launch a website with the default browser on Windows
os.execute("start http://www.shiva-engine.com")
You can check whether a program is installed by testing the os.execute return value:
if os.execute("start calc") == nil then
log.warning ( "Windows Calculator is not installed!" )
else
log.message ( "Windows Calculator launched successfully." )
end
You can use os.execute to feed program parameters to the software as well. This code for instance will import an STE into ShiVa 1.9:
-- import CWD\archive.ste into ShiVa 192
os.execute('start C:\\ShiVa192\\"ShiVa Editor Advanced"\\ShiVa.exe archive.ste')
Piping is also supported, but the extent depends on the operating system.
-- write the output of "ls -l" on Mac/Linux into a file in the CWD
os.execute("ls -l > directoryListing.txt")
The output of a CLI tool can be processed with io.popen and lines(), like so:
-- log the entire directory listing of the windows folder to console
local f = assert (io.popen ("dir C:\\Windows"))
for line in f:lines() do
log.message (line)
end
f:close()
True, non-blocking IPC through pipes the POSIX way is unfortunately not possible with on-board Lua tools. A very bodgy way to do it using temporary files, however it is not recommended for reoccurring jobs in order to keep your drive alive.
-- get a temporary file name
n = os.tmpname ()
-- launch a bash shell in Windows, feed a shell script to it, and store the output in the temp file
os.execute ("bash " .. n)
-- display output
for line in io.lines (n) do
log.message (line)
end
-- remove temporary file
os.remove (n)