Introduction to Lua


Contents


About

this page is a work-in progress

Stendhal supports Lua scripting via the LuaJ library.

Lua scripts end with the .lua extension & are stored in the data/script directory.


Lua Basics

For more detailed information, see the Lua reference manual.

Comments

Lua uses double dashes (--) for single line comments & double dashes followed by double square brackets (--[[) & closed with double square brackets (]]) for multi-line comments:

-- a single line comment

--[[
a multi-line comment
]]

Variables

By default, variables are set in global scope (meaning it is exposed to the entire Lua engine). To create a variable in local scope, the local keyword must be used:

-- a global variable
var1 = "Hello world!"

-- a local variable
local var2 = "Hello world!"

Data Types

Some of the common data types are nil, string, number, boolean, & table. Lua is a dynamically typed language, so declaring the type is not required when setting variables.

Examples:

-- string variable
local var_s = "Hello world!"

-- number variable
local var_n = 11

-- the number type also encompasses floating-point or double-precision values
local var_f = 5.112

-- boolean variable
local var_b = true

-- table variable
local var_t = {}

By default all variables are nil. This means that a variable can be accessed before it is declared without throwing an error.

-- print value of undeclared variable
print(foo) -- output: nil

-- print value of declared variable without value
foo = nil
print(foo) -- output: nil

-- print value of declared variable with value
foo = "bar"
print(foo) -- output: bar

Strings

String Concatenation

String concatenation is simple, much like Java uses a plus operator (+) to join strings, Lua uses two periods (..).

Example:

-- create a string variable
local var = "Hello"

-- append another string
var = var .. " world!"

print(var) -- output: "Hello world!"

Tables

A Lua table is a data type similar to a Java list or map. Tables can be indexed or use key=value pairs.

Lua table indexes begin at 1, not 0

Creating Tables

An empty table is initialized with a pair of curly braces ({}):

local mytable = {}

You can add values to indexed tables at initialization or with the table.insert method:

-- create a table with values
local mytable = {"foo"}

-- add value
table.insert(mytable, "bar")

To create a key=value table, any of the following methods can be used to add values:

-- all of these do the same thing, that is, assigning "bar" to mytable.foo
local mytable {
    foo = "bar",
    ["foo"] = "bar",
}
mytable.foo = "bar"
mytable["foo"] = "bar"

Accessing Table Values

Square brackets ([]) enclosing an index number are used to access values in indexed tables (remember that Lua table indexes start at "1" not "0"):

local mytable = {"foo", "bar"}

print(mytable[1]) -- output: "foo"
print(mytable[2]) -- output: "bar"

In a key=value table, values can be accessed by either enclosing the key string in square brackets or concatenating the key member using a dot (.):

local mytable = {foo="bar"}

-- using square brackets
print(mytable["foo"]) -- output: "bar"

-- using concatenated member
print(mytable.foo) -- output: "bar"

Iterating Tables

Tables can be iterated in a for loop using the pairs or ipairs iterators. Loops are terminated with the end keyword:

local mytable = {"foo", "bar"}

print("indexes:")
for idx in pairs(mytable) do
    print(idx)
end

print("\nvalues:")
for idx, value in pairs(mytable) do
    print(value)
end

Output:

indexes:
1
2

values:
foo
bar

Using a key=value table:

local mytable = {
    ["foo"] = "hello",
    ["bar"] = " world!",
}

print("keys:")
for key in pairs(mytable) do
    print(key)
end

print("\nvalues:")
for key, value in pairs(mytable) do
    print(value)
end

Output:

keys:
foo
bar

values:
hello
 world!

See also: Lua Tables Tutorial

Functions

Like normal variables, functions can be declared as global or local & must be terminated with the end keyword.

There are two ways to define functions with the function keyword:

local function myFunction()
    print("Hello world!")
end

or

local myFunction = function()
    print("Hello world!")
end

Functions can also be members of a table:

local myTable = {}
function myTable.myFunction()
    print("Hello world!")
end

or

local myTable = {}
myTable.myFunction = function()
    print("Hello world!")
end

or

local myTable = {
    myFunction = function()
        print("Hello world!")
    end,
}

-- execute with
myTable.myFunction()

Userdata

Userdata is a special type that allows Java data to be stored in Lua variables. This allows access to Java objects & methods. Userdata methods are accessed using a colon (:). One example is Stendhal's grammar parser class. It is exposed to Lua as the grammar global variable.

print(type(grammar)) -- ouput: "userdata"
print(grammar:itthem(5)) -- output: "them"

A dot (.) can be used just like accessing table values, but the userdata object itself must then be passed as the first argument. In other words grammar:itthem(5) is the same as grammar.itthem(grammar, 5).

Comparison Operators

Logical Operators

Operator Description Java Equivalent
and logical and &&
or logical or ||
not logical inverse !

Relational Operators

Operator Description Java Equivalent
< less than <
> greater than >
<= less than or equal to <=
>= greater than or equal to >=
== equal to ==
~= not equal to !=

Stendhal Application

Zones

Setting Zone

To set a zone to work with, use the game:setZone method:

game:setZone("0_semos_city")

Create New Zone

For creating a permanent zone it is recommended to use the XML configurations in data/conf/zones.

Currently creating new zones via Lua is not supported.

Add Zone Music

Music can be added to zones with the game:setMusic method.

Example:

if game:setZone("0_semos_plains_n") then
    game:setMusic("pleasant_creek_loop", {volume=85, radius=100})
end

Adding Entities

Entities can be added to the game using the entities object.

Adding Signs

Signs can be created with the "Sign", "Reader", & "ShopSign" types:

local zone = "0_semos_city"
if game:setZone(zone) then
    -- create the sign instance
    local sign = entities:create({
        type = "Sign",
        class = "signpost",
        pos = {12, 55},
        text = "Meet Lua!"
    })

    -- add to the world
    game:add(sign)
else
    logger:error("Could not set zone: " .. zone)
end

Adding NPCs

Use the "SpeakerNPC" type to create an interactive NPC:

local zone = "0_semos_city"
if game:setZone(zone) then
    -- create the NPC instance
    local npc = entities:create({
        type = "SpeakerNPC",
        name = "Lua",
        class = "littlegirlnpc",
        pos = {10, 55},
        path = {
            nodes = {
                {10, 55},
                {11, 55},
                {11, 56},
                {10, 56},
            },
            collisionAction = CollisionAction.STOP
        },
        speed = 0.1
    })

    -- dialogue
    npc:addJob("Actually, I am jobless.")
    npc:addGoodbye();

    -- add to the world
    game:add(npc)
else
    logger:error("Could not set zone: " .. zone)
end

Adding Transitions

A simple example of adding a chat transition can be done without any special functionality:

local frank = entities:create({
    type = "SpeakerNPC",
    name = "Frank"
})

frank:add(ConversationStates.IDLE,
    ConversationPhrases.GREETING_MESSAGES,
    nil,
    ConversationStates.ATTENDING,
    "Hello.",
    nil)

This simply adds a response to saying "hello" & sets the NPC to attend to the player (equivalent of frank:addGreeting("Hello")).

For more advanced behavior, we need to use some helper methods. If we want to check a condition we use the conditions:create method. The first parameter is the string name of the ChatCondition we want to instantiate. The second parameter is a table that contains the values that should be passed to the ChatCondition constructor.

Example:

frank:add(ConversationStates.IDLE,
    ConversationPhrases.GREETING_MESSAGES,
    conditions:create("PlayerHasItemWithHimCondition", {"money"}),
    ConversationStates.ATTENDING,
    "Hello.",
    nil)

In this scenario, the NPC will only respond if the player is carrying money.

A NotCondition instance can be created with the actions:notCondition method:

Example usage:

local condition = conditions.notCondition(conditions:create("PlayerHasItemWithHimCondition", {"money"})

To add a ChatAction, we use the actions:create method. Its usage is identical to conditions:create.

Example:

frank:add(ConversationStates.IDLE,
    ConversationPhrases.GREETING_MESSAGES,
    conditions:create("PlayerHasItemWithHimCondition", {"money"}),
    ConversationStates.ATTENDING,
    "Hello.",
    actions:create("NPCEmoteAction", {"looks greedily at your pouch of money.", false}))

Lua tables can be used to add multiple conditions or actions (this only works for NPCs created from Lua or instances of LuaSpeakerNPC):

add global helper methods for managing SpeakerNPC instances not created in Lua

frank:add(ConversationStates.IDLE,
    ConversationPhrases.GREETING_MESSAGES,
    {
        conditions:create("PlayerHasItemWithHimCondition", {"money"}),
        conditions:notCondition(conditions:create("NakedCondition")),
    },
    ConversationStates.ATTENDING,
    nil,
    {
        actions:create("SayTextAction", {"Hello."}),
        actions:create("NPCEmoteAction", {"looks greedily at your pouch of money.", false}),
    })

In this scenario, the NPC will respond if the player has money & is not naked.

Nested tables are supported as well:

local conditions = {
    conditions:create("PlayerHasItemWithHimCondition", {"money"}),
    {
        conditions:notCondition(conditions:create("NakedCondition")),
    },
}

frank:add(ConversationStates.IDLE,
    ConversationPhrases.GREETING_MESSAGES,
    conditions,
    ConversationStates.ATTENDING,
    nil,
    {
        actions:create("SayTextAction", {"Hello."}),
        actions:create("NPCEmoteAction", {"looks greedily at your pouch of money.", false}),
    })

Adding Merchant Behavior

The merchants object is used for adding merchant behavior (buying/selling) to an NPC.

should use ShopType enum

Example of adding seller behavior to an NPC:

if game:setZone("0_semos_city") then
    local frank = entities.create({
        type = "SpeakerNPC",
        name = "Frank"
    })
    merchants:addSeller(frank, merchants.shops:get("shopname"), true)

    game:add(frank)
end

To create a custom shop list, you can use a Lua table:

local priceList = {
    meat = 50,
    ham = 70,
}

The helper methods have special handling for underscore characters as well (the following are all the same):

local priceList = {
    smoked_ham = 100,
    ["smoked ham"] = 100,
}
priceList.smoked_ham = 100
priceList["smoked ham"] = 100

Then add the seller behavior using the custom list:

merchants:addSeller(frank, priceList, true)

System Properties

Java's system properties are exposed to Lua with the properties object.

Examples:

-- property state
if properties:enabled("stendhal.testserver") then
    print("Test server enabled")
    if properties:equals("stendhal.testserver", "junk") then
        print("Junk enabled")
    else
        print("Junk disabled")
    end
else
    print("Test server disabled")
end

-- property value
local prop = properties:getValue("stendhal.testserver")
if prop ~= nil then
    print("Test server enabled")
    if prop == "junk" then
        print("Junk enabled")
    else
        print("Junk disabled")
    end
else
    print("Test server disabled")
end

Misc

Typecasting

Lua does not support typecasting (as far as I know), but if the class you want to cast to has a copy constructor, achieving the same functionality possible.

-- "entities:getItem" returns an instance of Item
local bestiary = entities:getItem("bestiary")

-- in order to use the bestiary's "setOwner" method, we must convert it to an "OwnedItem" instance
-- by calling its copy constructor
bestiary = luajava.newInstance("games.stendhal.server.entity.item.OwnedItem", bestiary)
bestiary:setOwner("Ted")