ShiVa Lua unlocked Pt.3: API dump, Types, Assert, Globals and Unpack – ShiVa Engine

ShiVa Lua unlocked Pt.3: API dump, Types, Assert, Globals and Unpack

Up until now, we only had a look at the new Lua 5.2.3 features included in ShiVa 2.0 and EditorLua. Today, we will have a look at hidden RuntimeLua functions and look at some handy hacks that could make your coding life easier, but also much more dangerous! Proceed at your own risk.

A fair warning

All methods described in this tutorial make use of Lua 5.0.x language features, which will not translate into C++. If you decide to use any of them, you will not be able to make use of ShiVa’s C++ translator and waive potential performance benefits.
Furthermore, you will greatly confuse the ShiVa syntax highlighter and “compiler” (syntax checker) in both ShiVa 1.9.2 and ShiVa 2.0. Any undocumented code will not be colored in properly, and your log will be full of warnings and errors which you must consciously ignore. This can make debugging “regular” code more complicated.
Last but not least, remember that ShiVa is designed with self-contained, Class-like AIModels for a reason. If you make the decision to ignore this structure and write extensive spaghetti code with global variable tables, you do so at your own risk.

API dump

Lua stores all its API inside a global table called _G. To see what functions are included, make use of the table knowledge you have learned in pt.1 of this tutorial series, and perform a table dump:

--------------------------------------------------------------------------------
function gtable.apidump ( )
--------------------------------------------------------------------------------
    local seen={}
    function dumpAPI(t,i)
        seen[t]=true
        local s={}
        local n=0
        for k in pairs(t) do
            n=n+1 s[n]=k
        end
        for k,v in ipairs(s) do
            log.message(i,v)
            v=t[v]
            if type(v)=="table" and not seen[v] then
                dumpAPI(v,i.."\t")
            end
        end
    end
    log.message ( "-- ShiVa Runtime API Dump Start -----------" )
    dumpAPI(_G,"")
    log.message ( "-- ShiVa Runtime API Dump End -------------" )
--------------------------------------------------------------------------------
end
--------------------------------------------------------------------------------

After removing all ShiVa API functions, you are left with the following list of “hidden” (undocumented) Lua functions:

tostring, gcinfo, getfenv, pairs, assert, tonumber, _LOADED, _G, __onError, coroutine, loadstring, xpcall, print, unpack, require, setmetatable, ipairs, rawequal, collectgarbage, newproxy, getmetatable, rawset, dofile, next, pcall, type, _VERSION, rawget, setfenv, error, loadfile

In case you are interested in a detailed description of these base Lua functions, it can be found in the Lua manual on lua.org. A few of these, like pairs(), ipairs(), or “next” we are already familiar with through Lua tables, others like tonumber() or tostring() are self-explanatory. Yet others like rawset() are not immediately obvious, but can be very powerful helpers, as we will see later in this tutorial.

type()

Lua is a dynamically typed language. Each value carries its own type. There are eight basic types in Lua: nil, boolean, number, string, userdata, function, thread, and table. The type() function gives the type name of a given value.

type("Hello world")				--> string
type(10.4*3)					--> number
type(_G)					--> table
type(log.message)				--> function
type(this.getUser())				--> userdata
type(application.getCurrentUserActiveCamera())	--> userdata
type(true)					--> boolean
type(nil)					--> nil
type(type(X))					--> string

Most of ShiVa-specific constructs like game objects or users are typed as “userdata”, while numbers, strings etc. are recognized by Lua as its own datatypes. type() itself returns a string, against which can be tested. If a variable is not initialized or not defined, its type will be nil:

local d = 5
if type(d) == "number" then log.message ( "Type test: d is a number." ) end
log.message ( type(d2) ) -- nil, because not defined

Type checking is usually performed on user or developer input, before you pass on that input to a function of your own. A math function for instance that is fed with a string type would crash:

function abs(x)
	return x >= 0 and x or -x
end
log.message(abs("hello")) -- Error: attempt to compare number with string

a type-checked abs() function would look something like this:

function abs(x)
	if type(x) ~= "number" then return nil end
	return x >= 0 and x or -x
end

Be aware though that type checking this way introduces additional runtime overhead and decrease performance if you do it excessively. The lua-users WIKI has a nice collection of examples for type checking, have a look on lua-users.org.

assert() and pcall()

Closely related to type() checking are assert() and pcall(). While assert() is used to prematurely end a program if something goes wrong, pcall() does the exact opposite, silences errors and makes the program continue regardless. Going back to the abs() example from the previous paragraph, you could also write the function like this:

function abs(x)
  assert(type(x) == "number", "abs expects a number")
  return x >= 0 and x or -x
end
log.message(abs("hello"))

this will produce a detailed ShiVa error log with line numbers and traceback:

[+ Warning ] {Scripting         }---------------------------------------------------------------
[+ Warning ] {Scripting         }AI Runtime error : [string "derp_Handler_onInit"]:16: abs expects a number
[+ Warning ] {Scripting         }---------------------------------------------------------------
[+ Warning ] {Scripting         }AI Stack Traceback :
[+ Warning ] {Scripting         }    [Internal] : Function 'assert'
[+ Warning ] {Scripting         }    [Line  16] : Handler 'onInit'
[+ Warning ] {Scripting         }    [Line  20] : Handler 'onInit'
[+ Warning ] {Scripting         }---------------------------------------------------------------

pcall() on the other hand completely silences errors. If a call is successful, pcall() will return true, otherwise false. The actual result of the tested function will be return as subsequent arguments, or the error message.

function abs(x)
  return x >= 0 and x or -x
end
local test, result = pcall(abs, "hello")
log.message ( test )	-- false
log.message ( result )	-- [o Message ] {Scripting         }[Handler] derp.onInit (line 16): attempt to compare number with string
local test, result = pcall(abs, -5)
log.message ( test )	-- true
log.message ( result )	-- 5

Globals and _G

Variables and functions you want to access from any AIModel are stored in global a Lua table named _G. By dumping the content of this table in the first paragraph, we had a look at the entire ShiVa API as well as all the undocumented functions.
This table is not static however, you can add your own entries. Storing number and string values there is possible, but not really a good idea since this “spaghetti coding” is considered bad style, obfuscates your code and makes it hard to maintain. On the other hand, you can use it to store function pointers, which allows you to write your own Lua libraries with functions that are accessible from any AIModel.
You can add to the _G table through the rawset() function. To make it easy to use, you could write a manager function like this:

--------------------------------------------------------------------------------
function gtable.declare (name, initval)
--------------------------------------------------------------------------------
    rawset(_G, name, initval or false)
--------------------------------------------------------------------------------
end
--------------------------------------------------------------------------------

By default, we initialize to false to avoid confusion with nil, since nil could mean both “no value” and “no initialized value” otherwise. To check, use the complementary function rawget():

if rawget(_G, var) == nil then
	-- `var' is undeclared
	-- ...
end

All you have to do in order to promote a function from your AI to the global table is included in the declare() function from above. Call it with the name of your function in onInit() and you are good to go:

-- declare function in this AI to be a global function
this.declare ("declare", this.declare)
-- use globally declared function to declare another global function
declare ("apidump", this.apidump)
-- now apidump() can be called from any AI in the game

To learn more about globals, have a look at the Lua docs on lua.org.

Reusable Libraries, the true ShiVa way

For comparison, the ShiVa way to reusable library code is AIModel stacking. If you wanted a reusable math library for instance, you would include this AIModel in every user and every object Ai stack. Communication between all AIs would be done through either get/setAIVariable, or better yet, sendEvent(Immediate).
aistacking

Multiple returns and unpack()

Pt.3 of this tutorial series concludes with a very handy table trick that allows you to keep better track of functions with multiple returns. Lots of functions in ShiVa have multiple return values, like scene.getFirstHitColliderEx(), which has no less than 9 return values:

local hHitObject, nHitDist, nHitSurfaceID, x, y, z, i, j, k = scene.getFirstHitColliderEx ( ... )

For simplicity reasons, let’s scale back and consider a function with only two return values, like the following one. It computes the sum and the average of two numbers:

--------------------------------------------------------------------------------
function gtable.add2average ( n1, n2 )
--------------------------------------------------------------------------------
    local s = n1 + n2
    local a = s / 2
    return s,a
--------------------------------------------------------------------------------
end
--------------------------------------------------------------------------------

Normally, you would name every variable that is returned, like so:

local r1, r2 = this.add2average ( 4,8 )

However, you can also have all variables returned at once in a single table:

local r1    = { this.add2average ( 4,8 ) }

You could now access every individual element through its index. This system can be used for instance to enable easy vec3 handling, which many developers have been missing from runtime ShiVa:

local vec = { math.vectorAdd ( 1,2,3, 4,5,6 ) }
log.message ( vec[1], " ", vec[2], " ", vec[3] ) -- 5 7 9

But it gets better, because you can use this returned table as function arguments thanks to the magic of unpack():

local r1    = { this.add2average ( 4,8 ) }
local r2,r3 = this.add2average ( unpack(r1) )
log.message ( r2, ", ", r3 )

No more messy multi variable returns!


  • slackBanner