LuaWML
Contents
The [lua] tag
This tag is a subtag of the [event] tag. It makes it possible to write actions with the Lua 5.1 language.
The tag supports only the code key, which is a string containing the Lua scripts. Since Lua makes usage of the quotes and the { and } symbols, it is certainly wise to enclose the script between stronger quotes, as they prevent the preprocessor from performing macro expansion and tokenization.
[lua] code = << wesnoth.message "Hello World!" >> [/lua]
The Lua kernel can also be accessed from the command mode:
:lua local u = wesnoth.get_units({ id = "Konrad" })[1]; u.moves = 5
The [args] sub-tag can be used to pass a WML object to the script via its variadic local variable "...".
[lua] code = << local t = ...; wesnoth.message(tostring(t.text)) >> [args] text = _ "Hello world!" [/args] [/lua]
Global environment
All the Lua scripts of a scenario share the same global environment (aka Lua state). For instance, a function defined in an event can be used in all the events that happen after it.
[event] name = preload first_time_only = no [lua] code = << function narrator(t) -- Behave like the [message] tag. wesnoth.fire("message", { speaker = "narrator", message = t.sentence }) end >> [/lua] [/event] [event] name = turn 1 [lua] code = << narrator(...) >> [args] sentence = _ "Hello world!" [/args] [/lua] [lua] code = << narrator(...) >> [args] sentence = _ "How are you today?" [/args] [/lua] [/event]
In the example above, the redundant structure could be hidden behind macros. But it may be better to simply define a new WML tag.
[event] name = preload first_time_only = no [lua] code = << -- The function is now local, since its name does not have to be -- visible outside this Lua scripts. local function handler(t) -- Behave like the [message] tag. wesnoth.fire("message", { speaker = "narrator", message = t.sentence }) end -- Create a new tag named [narrator]. wesnoth.register_wml_action("narrator", handler) >> [/lua] [/event] [event] name = turn 1 [narrator] sentence = _ "Hello world!" [/narrator] [narrator] sentence = _ "How are you today?" [/narrator] [/event]
The global environment is not preserved over save/load cycles. Therefore, storing values in the global environment is generally a bad idea (unless it has been redirected to WML variables, see helper.set_wml_var_metatable). The only time assigning global variables (including function definitions) makes sense is during a preload event, as this event is always run. Therefore, helper functions defined at that time will be available to all the later scripts.
The global environment initially contains the following modules: basic (no name), string, table, and math. A wesnoth module is also available, it provides access to the C++ engine.
At the start of a script, the variadic local variable ... (three dots) is a proxy table representing WML data. This table is the content of the [args] sub-tag of the [lua] tag, if any. The table also provides the fields x1, y1, x2, and y2, containing map locations, and the sub-tables weapon and second_weapon containing attacks, if relevant for the current event.
Examples
The following WML event is taken from Wesnoth' tutorial. It will serve as an example to present how Lua scripts are embedded into Wesnoth. The event is fired whenever a unit from side 1 (that is, the hero controlled by the user) moves to a tile that is not the one set in the WML variable target_hex.
# General catch for them moving to the wrong place. [event] name=moveto first_time_only=no [allow_undo][/allow_undo] [filter] side=1 [/filter] [if] [variable] name=target_hex.is_set equals=yes [/variable] [then] [if] [variable] name=x1 equals=$target_hex.x [/variable] [variable] name=y1 equals=$target_hex.y [/variable] [then] [/then] [else] [redraw][/redraw] [message] speaker=narrator message=_ "*Oops! You moved to the wrong place! After this message, you can press 'u' to undo, then try again." + _ " *Left click or press spacebar to continue..." [/message] [/else] [/if] [/then] [/if] [/event]
A Lua script that performs the same action is presented below.
[event] name=moveto first_time_only=no [allow_undo][/allow_undo] [filter] side=1 [/filter] [lua] code = << local args = ... if target_hex.is_set and (args.x1 ~= target_hex.x or args.y1 ~= target_hex.y) then W.redraw() narrator_says(_ "*Oops!\nYou moved to the wrong place! After this message, you can press 'u' to undo, then try again.") end >> [/lua] [/event]
Here is a more detailed explanation of the Lua code. Its first line
local args = ...
loads the parameter of the script into the args local variable. Since it is a moveto event, the args table contains the destination of the unit in the x1 and y1 fields.
The next two lines then test
if target_hex.is_set and (args.x1 ~= target_hex.x or args.y1 ~= target_hex.y)
whether the variable target_hex matches the event parameters. Since target_hex is not a local variable, it is taken from the global environment (a table implicitly named _G, so it is actually _G.target_hex). The global environment is not persistent, so it cannot be used to store data. In order to make it useful, it was mapped to the storage of WML variables by the following preload event.
[event] name=preload first_time_only=no [lua] code = << H = wesnoth.require "lua/helper.lua" -- skipping some other initializations -- ... H.set_wml_var_metatable(_G) >> [/lua] [/event]
Without a prelude redirecting _G, the conditional would have been written
if wesnoth.get_variable("target_hex.is_set") and (args.x1 ~= wesnoth.get_variable("target_hex.x") or args.y1 ~= wesnoth.get_variable("target_hex.y")
The body of the conditional then performs the [redraw] action.
W.redraw()
Again, this short syntax is made possible by a line of the prelude that makes W a proxy for performing WML actions.
W = H.set_wml_action_metatable {}
Without this shortcut, the first statement would have been written
wesnoth.fire("redraw")
Finally the script displays a message by
narrator_says(_ "*Oops!\nYou moved to the wrong place! After this message, you can press 'u' to undo, then try again.")
The narrator_says function is defined in the prelude too, since the construct behind it occurs several times in the tutorial. In plain WML, macros would have been used instead. The definition of the function is
function narrator_says(m) W.message { speaker="narrator", message = m .. _ "\n*Left click or press spacebar to continue..." } end
The function fires a [message] action and passes a WML object containing the usual two fields to it. The second field is initialized by concatenating the function argument with another string. Both strings are prefixed by the _ symbol to mark them as translatable. (Note that _ is just a unary function, not a keyword.) Again, this is made possible by a specific line of the prelude:
_ = wesnoth.textdomain "wesnoth-tutorial"
A longer translation of the tutorial is available at [1].
Interface to the C++ engine
Functionalities of the game engine are available through the functions of the wesnoth global table. Some of these functions return proxy tables. Writes to fields marked "read-only" are ignored. The __cfg fields return plain tables; in particular, writes do not modify the original object, and reads return the values from the time the dump was performed.
dofile
Replaces basic.dofile for loading files. Loads the given filename (relative to the content directory) and executes it in an unprotected environment (that is, exceptions propagate to the caller). Returns the values returned by the executed script.
wesnoth.dofile "~add-ons/MyCampaign/lua/scenario-utils.lua"
It may be helpful to put as many Lua code as possible in specific files instead of embedding it into WML files, so as to not confuse text editors. Then a scenario only needs to contain the following event:
[event] name = preload first_time_only = no [lua] code = << wesnoth.dofile "~add-ons/MyCampaign/lua/scenario-utils.lua" >> [/lua] [/event]
If the same files need to be loaded for all the scenarios, the [lua] tag above can be directly put inside the _main.cfg file (or equivalent file). The Lua code will then be executed at the start of each scenario.
require
Loads the given filename (relative to the content directory) and executes it in a protected environment. If the file has already been executed once, then loading, compilation, and execution, are skipped and the return value of its previous run is returned again.
helper = wesnoth.require "lua/helper.lua"
This function is helpful in writing libraries of functions that can be accessed from various places. So the return value of the file is supposed to be a table containing the methods provided by the library. Such a library would look like:
local library = {} function library.do_something(a) ... end function library.go_somewhere(x, y) ... end return library
It can also be helpful when writing eras, since you may not have the opportunity to call wesnoth.dofile during a preload event:
[unit_type] id = phoenix [event] name = last breath [lua] code = << wesnoth.require("~add-ons/MyEra/lua/unit-utils.lua").resurrect(...) >> [/lua] [/event] [/unit_type]
get_units
Returns an array of all the units matching the WML filter passed as the first argument.
local leaders_on_side_two = get_units({ side = 2, canrecruit = true }) local name_of_leader = leaders_on_side_two[1].name
Units are proxy tables with the following fields:
- x, y: integers (read only)
- side: integer (read/write)
- id: string (read only)
- name: translatable string (read only)
- hitpoints, max_hitpoints, experience, max_experience, max_moves: integers (read only)
- moves: integer (read/write)
- resting: boolean (read/write)
- petrified, canrecruit: booleans (read only)
- role, facing: strings (read/write)
- __cfg: WML table (dump)
The metatable of these proxy tables appears as "unit".
put_unit
Places a unit described by a table on the map. Coordinates can be passed as the first two arguments, otherwise the table is expected two fields x and y, where the unit will be placed. If the function is called with coordinates but no table, the unit on the map at given coordinates is removed instead.
-- create a unit with random traits, then erase it wesnoth.put_unit(17, 42, { type = "Elvish Lady" }) wesnoth.put_unit(17, 42)
-- give back all its move points to a unit local u = wesnoth.get_units({ x = 16, y = 41 })[1].__cfg u.moves = u.max_moves wesnoth.put_unit(u)
get_side
Returns the team with given number.
local team = wesnoth.get_side(1) team.gold = team.gold + 50
Teams are proxy tables with the following fields:
- gold, village_gold, base_income: integers (read/write)
- total_income: integer (read only)
- objectives, user_team_name: translatable strings (read/write)
- objectives_changed: boolean (read/write)
- team_name: string (read/write)
- __cfg: WML table (dump)
The metatable of these proxy tables appears as "side".
get_unit_type_ids
Returns an array containing all the unit type IDs the engine knows about.
local unit_types = wesnoth.get_unit_type_ids() wesnoth.message(string.format("%d unit types registered. First one is %s.", #unit_types, unit_types[1]))
get_unit_type
Returns the unit type with the corresponding ID.
local lich_cost = wesnoth.get_unit_type("Ancient Lich").cost
Unit types are proxy tables with the following fields:
- id: string
- name: translatable string (read only)
- max_moves, max_experience, max_hitpoints, level, cost: integers (read only)
- __cfg: WML table (dump)
The metatable of these proxy tables appears as "unit type".
find_path
Returns the shortest path from one location to another. The source location is given either by coordinates as two arguments x and y, or by a unit as a single argument (as returned by get_units). There must be a unit at the source location. The second location is given by coordinates. The last argument is an optional table that can be used to parametrize the pathfinder. Its options are:
- max_cost: if set, the pathfinder will ignore paths longer than its value
- ignore_units: if set, the path will go through units and ignore zones of control
- ignore_teleport: if set, the teleport ability of the unit is ignored
- viewing_side: if set to a valid side number, fog and shroud for this side will be taken into account; if set to an invalid number (e.g. 0), fog and shroud will be ignored; if left unset, the viewing side will be the unit side
The path is returned as a table of coordinate pairs. It contains both the source and destination tile if a path was found. The total cost of the path is also available as a second return value, if needed.
-- Display some items along the path from (x1,y1) to (x2,y2). local u = wesnoth.get_units({ x = x1, y = y1 })[1] local path, cost = wesnoth.find_path(u, x2, y2, { ignore_units = true, viewing_side = 0 }) if cost > u.moves then wesnoth.message("That's too far!") else for i, loc in ipairs(path) do wesnoth.fire("item", { x = loc[1], y = loc[2], image = "items/buckler.png" }) end end
Instead of a parameter table, a cost function can be passed to the pathfinder. It will be called for all the tiles the computed path may possibly go through. It receives three arguments. The first two are the coordinates of the tile, the last one is the current cost for reaching that tile. The function should return a floating-point value that is the cost for entering the given tile. This cost should be greater or equal to one.
-- Count how many turns it would take, assuming the worst case (3 movement points per tile) local max_moves = wesnoth.get_units({ x = x1, y = y1 })[1].max_moves local path, cost = wesnoth.find_path(x1, y2, x2, y2, function(x, y, current_cost) local remaining_moves = max_moves - (current_cost % max_moves) if remaining_moves < 3 then current_cost = current_cost + remaining_moves end return current_cost + 3 end) wesnoth.message(string.format("It would take %d turns.", math.ceil(cost / 3)))
find_vacant_tile
Returns the two coordinates of an empty tile the closest to the tile passed by coordinates. An optional unit can be passed as a third argument; if so, the returned tile is reachable by the unit from the original tile.
function teleport(src_x, src_y, dst_x, dst_y) local u = wesnoth.get_units({x = src_x, y = src_y })[1] local ut = u.__cfg dst_x, dst_y = wesnoth.find_vacant_tile(dst_x, dst_y, u) wesnoth.put_unit(src_x, src_y) wesnoth.put_unit(dst_x, dst_y, ut) end
game_config
Contrarily to the other values of the wesnoth table, game_config is simply a proxy table. Its fields offer an interface to the global settings of Wesnoth:
- version: string (read only)
- base_income: integer (read/write)
- village_income: integer (read/write)
- poison_amount: integer (read/write)
- rest_heal_amount: integer (read/write)
- recall_cost: integer (read/write)
- kill_experience: integer (read/write)
- debug: boolean (read only)
-- Healing, poison, and regeneration, are a bit weak? Let's boost them! wesnoth.game_config.poison_amount = 15
current
As with game_config, current is a proxy table. Its fields are getter for game-related properties:
- side: integer (read only)
- turn: integer (read only)
- event_context: WML table with attributes x1, y1, x2, y2, and children weapon, second_weapon, describing the trigger for the current event.
wesnoth.message(string.format("Turn %d, side %d is playing.", wesnoth.current.turn, wesnoth.current.side))
Encoding WML objects into Lua tables
Function wesnoth.fire expects a table representing a WML object as its second argument (if needed). Function wesnoth.set_variable allows to modify whole WML objects, again by passing it a table. Function wesnoth.get_variable transforms a WML object into a table, if its second argument is not set to true. All these tables have the same format.
Scalar fields are transformed into WML attributes. For instance, the following Lua table
{ a_bool = true, an_int = 42, a_float = 1.25, a_string = "scout", a_translation = _ "Hello World!" }
is equivalent to the content of the following WML object
[dummy] a_bool = "yes" an_int = "42" a_float = "1.25" a_string = "scout" a_translation = _ "Hello World!" [/dummy]
WML child objects are not stored as Lua named fields, since several of them can have the same tag. Moreover, their tags can conflict with the attribute keys. So child objects are stored as pairs string + table in the unnamed fields in definition order. For instance, the following Lua table
{ foo = 42, { "bar", { v = 1, w = 2 } }, { "foo", { x = false } }, { "bar", { y = "foo" } }, { "foobar", { { "barfoo", {} } } } }
is equivalent to the content of the following WML object
[dummy] foo = 42 [bar] v = 1 w = 2 [/bar] [foo] x = no [/foo] [bar] y = foo [bar] [foobar] [barfoo] [/barfoo] [/foobar] [/dummy]
So assuming cfg contains the above WML object, the following accesses are possible:
a_int = cfg.foo -- "dummy.foo", 42 a_string = cfg[3][2].y -- "dummy.bar[1].y", "foo" a_table = cfg[4][2] -- "dummy.foobar", { { "barfoo", {} } }
Consider using the helper.get_child and helper.child_range to ease the access to subtags.
Functions registered by wesnoth.register_wml_action receive their data in a userdata object which has the exact same structure as above. It is read-only however. Accessing fields or children performs variable substitution on the fly. Its __parsed and __literal fields provide translations to plain tables (therefore writable). __literal returns the original text of the data (including dollar symbols), while __parsed performs a last global variable substitution.
For instance, if you cannot stand any longer the fact that first_time_only is set to yes by default for the [event] tag, you can redefine it. But we have to be careful not to cause variable substitution, since the engine would perform a second variable substitution afterwards.
local old_event_handler old_event_handler = register_wml_action("event", function(cfg) -- Get the plain text from the user. local new_cfg = cfg.__literal -- The expression below is equivalent to cfg.__parsed.first_time_only, -- only faster. It is needed, since the first_time_only attribute may -- reference variables. local first = cfg.first_time_only -- Modify the default behavior of first_time_only. if first == nil then first = false end new_cfg.first_time_only = first -- Call the engine handler. old_event_handler(new_cfg) end )
Note that, since the object is a userdata and not a table, pairs and ipairs are unfortunately not usable on it. So scripts have to work at a lower level. For instance, the following function returns the first sub-tag with a given name and it works both on WML tables and WML userdata:
function get_child(cfg, name) for i = 1, #cfg do local v = cfg[i] if v[1] == name then return v[2] end end end
Another approach for handling userdata and tables in the same way, would be to convert the former into the latter beforehand:
if getmetatable(cfg) == "wml object" then cfg = cfg.__parsed end
Helper functions
The following functions are provided by the lua/helper.lua library. They are stored inside a table that is returned when loading the library with wesnoth.require.
helper = wesnoth.require "lua/helper.lua"
set_wml_tag_metatable
Sets the metable of a table so that it can be used to create subtags with less brackets. Returns the table. The fields of the table are simple wrappers around table constructors.
T = helper.set_wml_tag_metatable {} W.event { name = "new turn", T.message { speaker = "narrator", message = "?" } }
all_teams
Returns an iterator over teams that can be used in a for-in loop.
for team in helper.all_teams() do team.gold = 200 end
modify_unit
Modifies all the units satisfying the given filter (argument 1) with some WML attributes/objects (argument 2). This is a Lua implementation of the MODIFY_UNIT macro.
helper.modify_unit({ id="Delfador" }, { moves=0 })
move_unit_fake
Fakes the move of a unit satisfying the given filter (argument 1) to the given position (argument 2). This is a Lua implementation of the MOVE_UNIT macro.
helper.move_unit_fake({ id="Delfador" }, 14, 8)
distance_between
Returns the distance between two tiles given by their coordinates.
local d = distance_between(x1, y1, x2, y2)
Skeleton of a preload event
The following event is a skeleton for a prelude enabling Lua in your WML events. It creates a table H containing the functions from helper.lua and a table W that serves as a proxy for firing WML actions. It also sets up the global environment so that any access to an undefined global variable is redirected to the persistent WML storage.
[event] name=preload first_time_only=no [lua] code = << H = wesnoth.require "lua/helper.lua" W = H.set_wml_action_metatable {} _ = wesnoth.textdomain "my-campaign" -- Define your global constants here. -- ... H.set_wml_var_metatable(_G) -- Define your global functions here. -- ... >> [/lua] [/event]
It may be worth putting all the Lua code above inside a separate file and having the preload event only load it:
[event] name=preload first_time_only=no [lua] code = << wesnoth.dofile "~add-ons/Whatever/file.lua" >> [/lua] [/event]