StendhalScripting/Lua: Difference between revisions

From Arianne
Jump to navigation Jump to search
Content deleted Content added
imported>AntumDeluge
Reserve page for Lua scripting instructions
 
imported>AntumDeluge
add categories
 
(27 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{Navigation for Stendhal Top|Playing}}
TODO

<span style="color: red; font-style: italic;">this page is a work-in progress</span>

Stendhal supports [https://www.lua.org/ Lua scripting] via the [https://sourceforge.net/projects/luaj/ LuaJ library].

Lua scripts end in the <code>.lua</code> extension & are stored in the <code>data/script</code> directory.

= Lua Basics =

For more detailed information, see the [https://www.lua.org/docs.html Lua reference manual].

== Comments ==

Lua uses double dashes (<code>--</code>) for single line comments & double dashes followed by double square brackets (<code>[[</code>) & closed with double square brackets (<code>]]</code>) for multi-line comments:
<pre>
-- a single line comment

--[[
a multi-line comment
]]
</pre>

== Variables ==

By default, Lua variables are set in [https://en.wikipedia.org/wiki/Global_variable '''global''' scope] (meaning it is exposed to the entire Lua engine). To create a variable in [https://en.wikipedia.org/wiki/Local_variable '''local''' scope], the <code>local</code> keyword must be used:
<pre>
-- a global variable
var1 = "Hello world!"

-- a local variable
local var2 = "Hello world!"
</pre>

== Data Types ==

Some common data types in Lua are ''string'', ''integer'', ''boolean'', & ''table''. Type names do not need to be declared when setting variables.

Examples:
<pre>
-- string variable
local var1 = "Hello world!"

-- integer variable
local var2 = 11

-- boolean variable
local var3 = true

-- table variable
local var4 = {}
</pre>

=== Strings ===

==== String Concatenation ====

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

Example:
<pre>
-- create a string variable
local var = "Hello"

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

print(var) -- prints "Hello world!"
</pre>

=== Tables ===

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

''(<span style="color:red;">IMPORTANT NOTE: Lua table indexes begin at 1, not 0</span>)''

==== Creating Tables ====

An empty table is initialized with a pair of curly braces (<code>{}</code>):
<pre>
local mytable = {}
</pre>

You can add values to indexed tables at initialization or with the <code>table.insert</code> method:
<pre>
-- create a table with values
local mytable = {"foo"}

-- add value
table.insert(mytable, "bar")
</pre>

To create a key=value table, any of the following methods can be used to add values:
<pre>
-- 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"
</pre>

==== Accessing Table Values ====

Square brackets (<code>[]</code>) enclosing an index number are used to access values in indexed tables (''remember that Lua table indexes start at "1" not "0"''):
<pre>
local mytable = {"foo", "bar"}

print(mytable[1]) -- prints "foo"
print(mytable[2]) -- prints "bar"
</pre>

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 <code>.</code>:
<pre>
local mytable = {foo="bar"}

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

-- using concatenated member
print(mytable.foo) -- prints "bar"
</pre>

==== Iterating Tables ====

Tables can be iterated in a <code>for</code> loop using the <code>pairs</code> or <code>ipairs</code> iterators. Loops are terminated with the <code>end</code> keyword:
<pre>
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
</pre>

Output:
<pre>
indexes:
1
2

values:
foo
bar
</pre>

Using a key=value table:
<pre>
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
</pre>

Output:
<pre>
keys:
foo
bar

values:
hello
world!
</pre>

See also: [http://lua-users.org/wiki/TablesTutorial Lua Tables Tutorial]

=== Functions ===

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

There are two ways to define functions with the <code>function</code> keyword:
<pre>
local function myFunction()
print("Hello world!")
end
</pre>

or
<pre>
local myFunction = function()
print("Hello world!")
end
</pre>

Functions can also be members of a table:
<pre>
local myTable = {}
function myTable.myFunction()
print("Hello world!")
end
</pre>

or
<pre>
local myTable = {}
myTable.myFunction = function()
print("Hello world!")
end
</pre>

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

-- execute with
myTable.myFunction()
</pre>

== Comparison Operators ==

{| class="wikitable"
|+ Logical Operators
! Operator !! Description !! Java Equivalent
|-
| and || logical ''and'' || &&
|-
| or || logical ''or'' || <nowiki>||</nowiki>
|-
| not || logical ''opposite'' || !
|}

{| class="wikitable"
|+ 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 the zone to work with, use the <code>game</code> object:

<pre>
game:setZone("0_semos_city")
</pre>

=== Create New Zone ===

It is recommended to create new zones in the XML configurations in {{StendhalFile|master|data/conf/zones|data/conf/zones}}.

Currently creating new zones via Lua is not supported.

=== Add Zone Music ===

Music can be added to zones with the <code>game:setMusic</code> function. It supports the following arguments:
* <span style="color:darkgreen; font-style:italic;>filename:</span> Basename of the OGG audio file to use stored in {{StendhalFile|master|data/music|data/music}}.
* <span style="color:darkgreen; font-style:italic;>args:</span> A table of key=value integers.
* Valid keys:
** <span style="color:darkblue; font-style:italic;">volume:</span> Volume level (default: 100).
** <span style="color:darkblue; font-style:italic;">x:</span> The horizontal point for the source of the music (default: 1).
** <span style="color:darkblue; font-style:italic;">y:</span> The vertical point for the source of the music (default: 1).
** <span style="color:darkblue; font-style:italic;">radius:</span> The radial range at which the music can be heard (default: 10000).

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

== Adding Entities ==

=== Signs ===

Signs can be created with <code>entities:createSign</code> and <code>entities:createShopSign</code>:

<pre>
local zone = "0_semos_city"
if game:setZone(zone) then
-- create the sign instance
local sign = entities:createSign()
sign:setEntityClass("signpost")
sign:setPosition(12, 55)
sign:setText("Meet Lua!")

-- Add it to the world
game:add(sign)
else
logger:error("Could not set zone: " .. zone)
end
</pre>

=== NPCs ===

Use the <code>entities:createSpeakerNPC</code> method to create an interactive NPC:

<pre>
local zone = "0_semos_city"
if game:setZone(zone) then
-- Use helper object to create a new NPC
local npc = entities:createSpeakerNPC("Lua")
npc:setEntityClass("littlegirlnpc")
npc:setPosition(10, 55)
npc:setBaseSpeed(0.1)
npc:setCollisionAction(CollisionAction.STOP)

local nodes = {
{10, 55},
{11, 55},
{11, 56},
{10, 56},
}

npc:setPath(nodes)

-- 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
</pre>

==== Adding Transitions ====

A simple example of adding a chat transition can be done without any special functionality:
<pre>
local frank = entities:createSpeakerNPC("Frank")
frank:add(ConversationStates.IDLE,
ConversationPhrases.GREETING_MESSAGES,
nil,
ConversationStates.ATTENDING,
"Hello.",
nil)
</pre>

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

For more complicated behavior, we need to use some helper methods. If we want to check a condition we use the <code>conditions:create</code> 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:
<pre>
frank:add(ConversationStates.IDLE,
ConversationPhrases.GREETING_MESSAGES,
conditions:create("PlayerHasItemWithHimCondition", {"money"}),
ConversationStates.ATTENDING,
"Hello.",
nil)
</pre>

In this scenario, the NPC will only respond if the player is carrying <item>money</item>.

A NotCondition instance can be created with the <code>actions:notCondition</code> method:

Example usage:
<pre>
local condition = conditions.notCondition(conditions:create("PlayerHasItemWithHimCondition", {"money"})
</pre>

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

Example:
<pre>
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}))
</pre>

Lua tables can be used to add multiple conditions or actions:
<pre>
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}),
})
</pre>

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

Nested tables are supported as well:
<pre>
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}),
})
</pre>

==== Adding Merchant Behavior ====

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

Example of adding seller behavior to an NPC:
<pre>
if game:setZone("0_semos_city") then
local frank = entities.createSpeakerNPC("Frank")
merchants:addSeller(frank, merchants.shops:get("shopname"), true)

game:add(frank)
end
</pre>

To create a custom shop list, you can use a Lua table (there are multiple ways to add elements to a Lua table):

Method 1:
<pre>
local priceList = {
meat = 50,
["ham"] = 70,
}
</pre>

Method 2:
<pre>
local priceList = {}
priceList.meat = 50
priceList["ham"] = 70
</pre>

The helper methods have special handling for underscore characters as well (the following are all the same):
<pre>
local priceList = {
smoked_ham = 100,
["smoked ham"] = 100,
}
priceList.smoked_ham = 100
priceList["smoked ham"] = 100
</pre>

Then add the seller behavior using the custom list:
<pre>
merchants:addSeller(frank, priceList, true)
</pre>

== System Properties ==

Java's system properties are exposed to Lua with the <code>properties</code> object.

Examples:
<pre>
-- 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
</pre>

== 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 is quite simple.

<pre>
-- "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")
</pre>

= See Also =

* [[StendhalScripting/LuaAPI|Lua API]]


[[Category:Stendhal]]
[[Category:Documentation]]
[[Category:API]]
[[Category:Scripting]]
[[Category:Lua]]

Latest revision as of 21:56, 18 January 2022


this page is a work-in progress

Stendhal supports Lua scripting via the LuaJ library.

Lua scripts end in 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, Lua 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 common data types in Lua are string, integer, boolean, & table. Type names do not need to be declared when setting variables.

Examples:

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

-- integer variable
local var2 = 11

-- boolean variable
local var3 = true

-- table variable
local var4 = {}

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) -- prints "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.

(IMPORTANT NOTE: 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]) -- prints "foo"
print(mytable[2]) -- prints "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 .:

local mytable = {foo="bar"}

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

-- using concatenated member
print(mytable.foo) -- prints "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()

Comparison Operators

Logical Operators
Operator Description Java Equivalent
and logical and &&
or logical or ||
not logical opposite !
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 the zone to work with, use the game object:

game:setZone("0_semos_city")

Create New Zone

It is recommended to create new zones in 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 function. It supports the following arguments:

  • filename: Basename of the OGG audio file to use stored in data/music.
  • args: A table of key=value integers.
  • Valid keys:
    • volume: Volume level (default: 100).
    • x: The horizontal point for the source of the music (default: 1).
    • y: The vertical point for the source of the music (default: 1).
    • radius: The radial range at which the music can be heard (default: 10000).

Example:

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

Adding Entities

Signs

Signs can be created with entities:createSign and entities:createShopSign:

local zone = "0_semos_city"
if game:setZone(zone) then
	-- create the sign instance
	local sign = entities:createSign()
	sign:setEntityClass("signpost")
	sign:setPosition(12, 55)
	sign:setText("Meet Lua!")

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

NPCs

Use the entities:createSpeakerNPC method to create an interactive NPC:

local zone = "0_semos_city"
if game:setZone(zone) then
	-- Use helper object to create a new NPC
	local npc = entities:createSpeakerNPC("Lua")
	npc:setEntityClass("littlegirlnpc")
	npc:setPosition(10, 55)
	npc:setBaseSpeed(0.1)
	npc:setCollisionAction(CollisionAction.STOP)

	local nodes = {
		{10, 55},
		{11, 55},
		{11, 56},
		{10, 56},
	}

	npc:setPath(nodes)

	-- 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:createSpeakerNPC("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 complicated 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 <item>money</item>.

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:

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.

Example of adding seller behavior to an NPC:

if game:setZone("0_semos_city") then
	local frank = entities.createSpeakerNPC("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 (there are multiple ways to add elements to a Lua table):

Method 1:

local priceList = {
	meat = 50,
	["ham"] = 70,
}

Method 2:

local priceList = {}
priceList.meat = 50
priceList["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 is quite simple.

-- "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")

See Also