StendhalScripting/Lua: Difference between revisions

From Arianne
Jump to navigation Jump to search
Content deleted Content added
imported>AntumDeluge
imported>AntumDeluge
Stendhal Application: update for changes to Lua engine
Line 224: Line 224:


= Stendhal Application =
= Stendhal Application =

== Objects and Functions ==

The following objects & functions are exposed to the Lua engine:

=== luajava ===

This is an object of the LuajavaLib library. It can be used to coerce Java static objects to Lua or create new Java object instances.

Example of exposing a static object & enums to Lua:
<pre>
-- store a Java enum in a Lua global variable
ConversationStates = luajava.bindClass("games.stendhal.server.entity.npc.ConversationStates")

-- access the enum values like so
ConversationStates.IDLE
</pre>

Example of creating an object instance:
<pre>
-- store instance in local variable
local dog = luajava.newInstance("games.stendhal.server.entity.npc.SilentNPC")
-- access object methods like so
dog:setEntityClass("animal/puppy")
dog:setPosition(2, 5)

-- class with constructor using parameters
local speaker = luajava.newInstance("games.stendhal.server.entity.npc.SpeakerNPC", "Frank")
speaker:setOutfit("body=0,head=0,eyes=0,hair=5,dress=5")
speaker:setPosition(2, 6)
</pre>

To make scripting easier, Stendhal employs a {{StendhalFile|master|src/games/stendhal/server/core/scripting/lua/init.lua|master script}} & some helper objects & methods to handle the functionality mentioned above. An explanation of these objects & methods follows.

=== game ===

The main object that handles setting zone & adding entities to game.

Methods:
* <code>game:add(object)</code> - Adds an object to the current zone.
* <code>game:setZone(name)</code> - Sets the current zone.
* <code>game:createSign(visible)</code> - Creates a new {{StendhalFile|master|src/games/stendhal/server/entity/mapstuff/sign/Sign.java|Sign}} instance.
* <code>game:createShopSign(name, title, caption, seller)</code> - Creates a new {{StendhalFile|master|src/games/stendhal/server/entity/mapstuff/sign/ShopSign.java|ShopSign}} instance.

=== npcHelper ===

This object helps to create instances of {{StendhalFile|master|src/games/stendhal/server/entity/npc/SpeakerNPC.java|SpeakerNPC}} & {{StendhalFile|master|src/games/stendhal/server/entity/npc/SilentNPC.java|SilentNPC}} classes.

Methods:
* <code>npcHelper:createSpeakerNPC(name)</code> - Creates a new SpeakerNPC.
* <code>npcHelper:createSilentNPC()</code> - Creates a new SilentNPC.
* <code>npcHelper:setPath(npc, path, loop)</code> - Sets the path for the specified NPC.
* <code>npcHelper:setPathAndPosition(npc, path, loop)</code> - Sets the path & starting position of the specified NPC.
* <code>npcHelper:addMerchant(merchantType, npc, items, offer)</code> - Adds merchant behavior to <code>npc</code> of either a buyer or seller defined by <code>merchantType</code>.
* <code>npcHelper:addSeller(npc, items, offer)</code> - Adds seller merchant behavior to <code>npc</code>.
* <code>npcHelper:addBuyer(npc, items, offer)</code> - Adds buyer merchant behavior to <code>npc</code>.


== Zones ==
== Zones ==
Line 289: Line 233:
<pre>
<pre>
game:setZone("0_semos_city")
game:setZone("0_semos_city")
</pre>

The logger is exposed to Lua via the <code>logger</code> object:

<pre>
local zone = "0_semos_city"
if game:setZone(zone) then
-- do something
else
logger:error("Could not set zone: " .. zone)
end
</pre>
</pre>


Line 328: Line 261:
=== Signs ===
=== Signs ===


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


<pre>
<pre>
Line 334: Line 267:
if game:setZone(zone) then
if game:setZone(zone) then
-- create the sign instance
-- create the sign instance
local sign = game:createSign()
local sign = entities:createSign()
sign:setEntityClass("signpost")
sign:setEntityClass("signpost")
sign:setPosition(12, 55)
sign:setPosition(12, 55)
Line 348: Line 281:
=== NPCs ===
=== NPCs ===


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


<pre>
<pre>
Line 354: Line 287:
if game:setZone(zone) then
if game:setZone(zone) then
-- Use helper object to create a new NPC
-- Use helper object to create a new NPC
local npc = npcHelper:createSpeakerNPC("Lua")
local npc = entities:createSpeakerNPC("Lua")
npc:setEntityClass("littlegirlnpc")
npc:setEntityClass("littlegirlnpc")
npc:setPosition(10, 55)
npc:setPosition(10, 55)
Line 368: Line 301:


-- Use helper object to create NPC path
-- Use helper object to create NPC path
npcHelper:setPath(npc, nodes)
entities:setPath(npc, nodes)


-- Dialogue
-- Dialogue
Line 385: Line 318:
A simple example of adding a chat transition can be done without any special functionality:
A simple example of adding a chat transition can be done without any special functionality:
<pre>
<pre>
local frank = npcHelper:createSpeakerNPC("Frank")
local frank = entities:createSpeakerNPC("Frank")
frank:add(ConversationStates.IDLE,
frank:add(ConversationStates.IDLE,
ConversationPhrases.GREETING_MESSAGES,
ConversationPhrases.GREETING_MESSAGES,
Line 394: Line 327:
</pre>
</pre>


This simply adds a response to saying "hello" & sets the NPC to attend to the player.
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>newCondition</code> global function:
For more complicated behavior, we need to use some helper methods. If we want to check a condition we use the <code>newCondition</code> global function:
Line 408: Line 341:
In this scenario, the NPC will only respond if the player is carrying <item>money</item>.
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>newNotCondition</code> global function:
A NotCondition instance can be created with the <code>newNotCondition</code> global function or using the <code>conditions.not</code> method:

Example usage:
<pre>
<pre>
-- using newNotCondition
newNotCondition(newCondition("PlayerHasItemWithHimCondition", "money"))
local condition = newNotCondition("PlayerHasItemWithHimCondition", "money")

-- using conditions.not
local condition = conditions.not(newCondition("PlayerHasItemWithHimCondition", "money")
</pre>
</pre>


Line 464: Line 403:
==== Adding Merchant Behavior ====
==== Adding Merchant Behavior ====


The <code>merchants</code> object is used for adding merchant behavior (buying/selling) to an NPC.
Merchant behavior ''(buying/selling)'' can be set with one of the following helper functions:
* ''npcHelper:addMerchant(merchantType, npc, prices, addOffer)''
* ''npcHelper:addBuyer(npc, prices, addOffer)''
* ''npcHelper:addSeller(npc, prices, addOffer)''
** Arguments:
*** ''merchantType:'' (string) If set to "buyer", will add buyer behavior, otherwise will be "seller" (may change type to boolean in future).
*** ''npc:'' (SpeakerNPC) The NPC to add the behavior to.
*** ''prices:'' (Map<String, Integer> or LuaTable) List of items & their prices.
*** ''addOffer:'' (boolean) If <code>true</code>, will add default replies for "offer".


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


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


== System Properties ==
== System Properties ==


Java's system properties are exposed to Lua with the <code>game:getProperty</code>, <code>game:propertyEnabled</code>, and <code>game:propertyEquals</code> methods.
Java's system properties are exposed to Lua with the <code>properties</code> object.


Examples:
Examples:
<pre>
<pre>
-- property state
-- property state
if game:propertyEnabled("stendhal.testserver") then
if properties:enabled("stendhal.testserver") then
print("Test server enabled")
print("Test server enabled")
if game:propertyEquals("stendhal.testserver", "junk") then
if properties:equals("stendhal.testserver", "junk") then
print("Junk enabled")
print("Junk enabled")
else
else
Line 535: Line 466:


-- property value
-- property value
local prop = game:getProperty("stendhal.testserver")
local prop = properties:getValue("stendhal.testserver")
if prop ~= nil then
if prop ~= nil then
print("Test server enabled")
print("Test server enabled")

Revision as of 22:25, 10 April 2020


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 = {}

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)

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:

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

mytable.foo = "bar"
mytable["foo"] = "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 declare functions:

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

or

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

Functions can also be values in 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 setZoneMusic global function. It supports the following arguments:

  • filename: Basename of the OGG audio file to use stored in data/music.
  • volume: (optional) Volume level.
  • x: (optional) The horizontal point for the source of the music.
  • y: (optional) The vertical point for the source of the music.
  • radius: (optional) The radial range at which the music can be heard.

Example:

if game:setZone("0_semos_plains_n") then
	setZoneMusic("pleasant_creek_loop", 85)
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},
	}

	-- Use helper object to create NPC path
	entities:setPath(npc, 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 newCondition global function:

frank:add(ConversationStates.IDLE,
	ConversationPhrases.GREETING_MESSAGES,
	newCondition("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 newNotCondition global function or using the conditions.not method:

Example usage:

-- using newNotCondition
local condition = newNotCondition("PlayerHasItemWithHimCondition", "money")

-- using conditions.not
local condition = conditions.not(newCondition("PlayerHasItemWithHimCondition", "money")

To add a ChatAction, we use the newAction global function:

frank:add(ConversationStates.IDLE,
	ConversationPhrases.GREETING_MESSAGES,
	newCondition("PlayerHasItemWithHimCondition", "money"),
	ConversationStates.ATTENDING,
	"Hello.",
	newAction("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,
	{
		newCondition("PlayerHasItemWithHimCondition", "money"),
		newNotCondition(newCondition("NakedCondition")),
	},
	ConversationStates.ATTENDING,
	nil,
	{
		newAction("SayTextAction", "Hello."),
		newAction("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 = {
	newCondition("PlayerHasItemWithHimCondition", "money"),
	{
		newNotCondition(newCondition("NakedCondition")),
	},
}

frank:add(ConversationStates.IDLE,
	ConversationPhrases.GREETING_MESSAGES,
	conditions,
	ConversationStates.ATTENDING,
	nil,
	{
		newAction("SayTextAction", "Hello."),
		newAction("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