Difference between revisions of "LuaWML"
(add table iteration warning) |
(→Random Lua table iteration) |
||
Line 710: | Line 710: | ||
<syntaxhighlight lang='lua'> | <syntaxhighlight lang='lua'> | ||
local table = { ["Mage"] = true, ["Wose"] = true } | local table = { ["Mage"] = true, ["Wose"] = true } | ||
− | |||
local concat = "" | local concat = "" | ||
for k, _ in pairs(table) do -- wrong, leads to OOS | for k, _ in pairs(table) do -- wrong, leads to OOS |
Revision as of 13:54, 6 November 2017
Contents
The [lua] tag
This tag is a part of ActionWML, thus can be used inside [event] and at other places where ActionWML can be used. It makes it possible to write actions with the Lua 5.2 language.
It is also possible to put this tag inside a [scenario] ScenarioWML, those tags will be executed immediately when the lua engine loads which is even before the scenario preload event is fired.
(Version 1.13.0 and later only) [lua] is now also allowed in [era] and [modification] those [lua] tags are then copied into the [scenario]/[multiplayer] when it is played just like [event]s inside [era] or [modification]
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 placed in the wesnoth.wml_actions table
-- The tag is [narrator], same as the function name
function wesnoth.wml_actions.narrator(t)
-- Behave like the [message] tag.
wesnoth.fire("message",
{ speaker = "narrator", message = t.sentence })
end
>>
[/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. The only time assigning global variables (including function definitions) makes sense is in a [lua] block directly in [scenario] or 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. Additionally, the functions clock, date, time and difftime from the os library (keep in mind that they aren't multiplayer- and replay-safe), as well as traceback from the debug library are also available.
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.
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 event_data = wesnoth.current.event_context
if V.target_hex.is_set and
(event_data.x1 ~= V.target_hex.x or event_data.y1 ~= V.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 event_data = wesnoth.current.event_context
puts the event data into the event_data local variable. Since it is a moveto event, the event_data table contains the destination of the unit in the x1 and y1 fields.
The next two lines then test
if V.target_hex.is_set and
(event_data.x1 ~= V.target_hex.x or event_data.y1 ~= V.target_hex.y)
whether the variable V.target_hex matches the event parameters. Since V is not a local variable, it is taken from the global environment. Usually, variables from the global environment are not persistent (they get lost on reloading), so it shouldn't be used to store data. In order to have an easy way to access the usual persistent Wml variables, the global variable V 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
-- ...
V = H.set_wml_var_metatable {}
>>
[/lua]
[/event]
Without a prelude redirecting V, the conditional would have been written
if wesnoth.get_variable("target_hex.is_set") and
(event_data.x1 ~= wesnoth.get_variable("target_hex.x") or event_data.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 engine and helper functions
Functionalities of the game engine are available through the functions contained in 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.
Some helper 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"
WML variables
- wesnoth.get_variable
- wesnoth.set_variable
- wesnoth.get_all_vars (Version 1.13.0 and later only)
- helper.set_wml_var_metatable
- helper.get_child
- helper.get_nth_child (Version 1.13.2 and later only)
- helper.child_count (Version 1.13.2 and later only)
- helper.child_range
- helper.child_array (Version 1.13.2 and later only)
- helper.get_variable_array
- helper.get_variable_proxy_array
- helper.set_variable_array
Events and WML actions
- wesnoth.fire
- wesnoth.wml_actions
- wesnoth.wml_conditionals (Version 1.13.0 and later only)
- wesnoth.game_events
- wesnoth.fire_event
- wesnoth.fire_event_by_id (Version 1.13.6 and later only)
- wesnoth.add_event_handler (Version 1.13.0 and later only)
- wesnoth.remove_event_handler (Version 1.13.0 and later only)
- wesnoth.eval_conditional
- wesnoth.tovconfig
- helper.set_wml_action_metatable
- helper.wml_error
- helper.literal
- helper.parsed
- helper.shallow_literal
- helper.shallow_parsed
User interface
- wesnoth.message
- wesnoth.clear_messages
- wesnoth.textdomain
- wesnoth.delay
- wesnoth.float_label
- wesnoth.select_hex
- wesnoth.select_unit
- wesnoth.highlight_hex
- wesnoth.deselect_hex (Version 1.13.2 and later only)
- wesnoth.scroll_to_tile
- wesnoth.lock_view
- wesnoth.view_locked
- wesnoth.play_sound
- wesnoth.set_music
- wesnoth.music_list (Version 1.13.8 and later only)
- wesnoth.sound_volume (Version 1.13.8 and later only)
- wesnoth.show_message_dialog (Version 1.13.2 and later only)
- wesnoth.show_popup_dialog (Version 1.13.2 and later only)
- wesnoth.show_story (Version 1.13.8 and later only)
- wesnoth.show_dialog
- wesnoth.set_dialog_value
- wesnoth.get_dialog_value
- wesnoth.set_dialog_active
- wesnoth.set_dialog_callback
- wesnoth.set_dialog_markup
- wesnoth.set_dialog_focus (Version 1.13.2 and later only)
- wesnoth.set_dialog_visible (Version 1.13.2 and later only)
- wesnoth.set_dialog_canvas
- wesnoth.add_dialog_tree_node (Version 1.13.0 and later only)
- wesnoth.remove_dialog_item (Version 1.13.1 and later only)
- wesnoth.get_displayed_unit
- wesnoth.theme_items
- helper.get_user_choice
- wesnoth.is_skipping_messages (Version 1.13.2 and later only)
- wesnoth.skip_messages (Version 1.13.2 and later only)
- wesnoth.log (Version 1.13.5 and later only)
- wesnoth.zoom (Version 1.13.8 and later only)
Map and terrains
- wesnoth.get_map_size
- wesnoth.get_terrain
- wesnoth.set_terrain
- wesnoth.get_terrain_info
- wesnoth.get_selected_tile
- wesnoth.get_locations
- wesnoth.get_villages
- wesnoth.match_location
- wesnoth.add_tile_overlay
- wesnoth.remove_tile_overlay
- wesnoth.add_fog (Version 1.13.5 and later only)
- wesnoth.remove_fog (Version 1.13.5 and later only)
- wesnoth.add_sound_source (Version 1.13.5 and later only)
- wesnoth.remove_sound_source (Version 1.13.5 and later only)
- wesnoth.get_sound_source (Version 1.13.5 and later only)
- wesnoth.map.get_direction (Version 1.13.8 and later only)
- wesnoth.map.get_relative_dir (Version 1.13.8 and later only)
- wesnoth.map.rotate_right_around_center (Version 1.13.8 and later only)
- wesnoth.map.get_adjacent_tiles (Version 1.13.8 and later only)
- wesnoth.map.tiles_adjacent (Version 1.13.8 and later only)
- wesnoth.map.distance_between (Version 1.13.8 and later only)
- items.place_image
- items.place_halo
- items.remove
Time of day schedule
- wesnoth.get_time_of_day
- wesnoth.add_time_area (Version 1.13.0 and later only)
- wesnoth.remove_time_area (Version 1.13.0 and later only)
Units
- wesnoth.get_units
- wesnoth.get_unit
- wesnoth.match_unit
- wesnoth.put_unit
- wesnoth.erase_unit (Version 1.13.2 and later only)
- wesnoth.get_recall_units
- wesnoth.put_recall_unit
- wesnoth.create_unit
- wesnoth.copy_unit
- wesnoth.extract_unit
- wesnoth.add_modification
- wesnoth.unit_resistance
- wesnoth.unit_defense
- wesnoth.unit_movement_cost
- wesnoth.unit_vision_cost
- wesnoth.unit_jamming_cost
- wesnoth.unit_ability
- wesnoth.unit_types
- wesnoth.races
- wesnoth.get_traits
- wesnoth.simulate_combat
- wesnoth.transform_unit
- wesnoth.create_animator (Version 1.13.7 and later only)
- wesnoth.effects (Version 1.13.2 and later only)
Sides
- wesnoth.sides
- wesnoth.get_sides
- wesnoth.get_village_owner
- wesnoth.set_village_owner
- wesnoth.is_enemy
- wesnoth.match_side
- wesnoth.get_starting_location
- wesnoth.set_side_id (Version 1.13.7 and later only)
- wesnoth.place_shroud (Version 1.13.7 and later only)
- wesnoth.clear_shroud (Version 1.13.7 and later only)
- wesnoth.is_fogged (Version 1.13.7 and later only)
- wesnoth.is_shrouded (Version 1.13.7 and later only)
- wesnoth.switch_ai (Version 1.13.7 and later only)
- wesnoth.append_ai (Version 1.13.7 and later only)
- wesnoth.add_ai_component (Version 1.13.7 and later only)
- wesnoth.change_ai_component (Version 1.13.7 and later only)
- wesnoth.add_ai_component (Version 1.13.7 and later only)
- helper.all_teams
Pathfinder
- wesnoth.find_path
- wesnoth.find_vacant_tile
- wesnoth.find_reach
- wesnoth.find_cost_map
- helper.distance_between
- helper.adjacent_tiles
Lua files
- wesnoth.dofile
- wesnoth.require
- wesnoth.have_file (Version 1.13.5 and later only)
- wesnoth.read_file (Version 1.13.5 and later only)
Location sets
- location_set.create
- location_set.of_pairs
- location_set.of_wml_var
- location_set:empty
- location_set:size
- location_set:clear
- location_set:get
- location_set:insert
- location_set:remove
- location_set:of_pairs
- location_set:of_wml_var
- location_set:to_pairs
- location_set:to_stable_pairs
- location_set:to_wml_var
- location_set:union
- location_set:inter
- location_set:iter
- location_set:stable_iter
- location_set:filter
- location_set:union_merge
- location_set:inter_merge
Miscellaneous
- wesnoth.game_config
- wesnoth.get_era
- wesnoth.current
- wesnoth.synchronize_choice
- wesnoth.get_image_size
- wesnoth.compare_versions
- wesnoth.have_file
- wesnoth.debug
- wesnoth.get_time_stamp
- wesnoth.random (Version 1.13.2 and later only)
- wesnoth.eval_formula (Version 1.13.5 and later only)
- wesnoth.compile_formula (Version 1.13.5 and later only)
- wesnoth.name_generator (Version 1.13.5 and later only)
- wesnoth.set_next_scenario (Version 1.13.5 and later only)
- helper.set_wml_tag_metatable
- helper.modify_unit
- helper.move_unit_fake
- helper.rand
- helper.round
- helper.shuffle
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. This means that for every subtag appearing in the wml code there is an additional table "layer" in the corresponding WML table of the form {[1] = "tag_name", [2] = {}}
which is equivalent to {"tag_name", {}}
. [1] etc are the unnamed fields (as opposed to wml attributes). The table under [2] in this subtable then holds the wml attributes from inside the wml subtag. So every subtag other than the toplevel tag corresponds to two nested tables each. For instance, the following Lua table
{
foo = 42,
{ "bar", { v = 1, w = 2 } },
{ "foo", { x = false } },
{ "bar", { y = "foo" } },
{ "foobar", { z = 5, { "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]
z = 5
[barfoo]
[/barfoo]
[/foobar]
[/dummy]
Both tables above are also equivalent to this WML table, where all unnamed fields are displayed:
{
foo = 42,
[1] = { [1] = "bar", [2] = { v = 1, w = 2 } },
[2] = { [1] = "foo", [2] = { x = false } },
[3] = { [1] = "bar", [2] = { y = "foo" } },
[4] = { [1] = "foobar", [2] = { z = 5, [1] = { [1] = "barfoo", [2] = {} } } }
}
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", { z = 5, { "barfoo", {} } }
For creating valid wml table in lua it is usully easier to use T = helper.set_wml_tag_metatable {} asuming you did that you can create the above wml document with
{
foo = 42,
T.bar {
v = 1,
w = 1,
},
T.foo {
x = false,
},
T.bar {
y = "foo",
},
T.foobar {
z = 5,
T.barfoo {
},
},
}
Consider using the helper.get_child and helper.child_range to ease the access to subtags.
(Version 1.13.5 and later only) As a convenience, attributes with array values (tables with only integer keys) are concatenated into a string when converting a Lua table into WML. For example, the following Lua code:
{
x = {1, 2, 3, 4},
y = {7, 8, 9, 10}
}
produces the following WML table:
[dummy]
x=1,2,3,4
y=7,8,9,10
[/dummy]
Functions registered in wesnoth.wml_actions 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 in attributes and [insert_tag] children), while __parsed performs a 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 = wesnoth.wml_actions.event
function wesnoth.wml_actions.event(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: The above example will only affect nested events. Toplevel events will still default to first_time_only=yes.)
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
(Version 1.13.2 and later only) pairs and ipairs now work on vconfig objects (contrary to the above statement). However, pairs works a little differently than on plain configs (tables) - it returns only string keys (attributes in WML terms) and not integer keys (tags in WML terms).
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
The WML userdata provides two other special fields: __shallow_parsed and __shallow_literal. They return a table corresponding to the WML userdata with variable substitution performed on the attributes (or not). [insert_tag] tags have also been parsed, so the number of children is faithful. But contrarily to __parsed and __literal, the process is not recursive: all the children are still WML userdata and variable substitution can still happen for them. These shallow translators are meant as optimized versions of the deep ones, when only the toplevel attributes need to be writable.
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 sets up a table T so be used for easier creation of valid WML tables. It also sets up a table V so that any access to it is redirected to the persistent WML storage. Finally, it loads a textdomain to be accessed through the _ variable.
[event]
name=preload
first_time_only=no
[lua]
code = <<
H = wesnoth.require "lua/helper.lua"
W = H.set_wml_action_metatable {}
T = H.set_wml_tag_metatable {}
V = H.set_wml_var_metatable {}
_ = wesnoth.textdomain "my-campaign"
-- Define your global constants here.
-- ...
-- Define your global functions here.
-- ...
>>
[/lua]
[/event]
It may be worth putting the whole Lua script above inside a separate file and having the preload event load it:
[event]
name=preload
first_time_only=no
[lua]
code = << wesnoth.dofile "~add-ons/Whatever/file.lua" >>
[/lua]
[/event]
Remarks on Random Numbers
The math.random function is not safe for replays and multiplayer games, since the random values will be different each time and on all the clients. Instead, the Lua code should use the helper.rand() function to synchronize random values. It takes the same argument in the same format as [set_variable] rand=.
local random_variable = helper.rand("1,2,3")
Also available is wesnoth.random, which has the same interface as math.random but is multiplayer-safe.
Random Lua table iteration
Table iteration order is not strictly defined in Lua. If your code depends on the order of iteration, make sure you sort the table keys beforehand and iterate over the sorted array only. Otherwise you'll get OOS, as different clients will have different local views on how to iterate the table.
local table = { ["Mage"] = true, ["Wose"] = true }
local concat = ""
for k, _ in pairs(table) do -- wrong, leads to OOS
concat = concat .. k
end
local array = { "Mage", "Wose" }
local good_usage = table[1] -- correct
local concat = ""
for _, v in ipairs(array) do -- correct
concat = concat .. v
end
Lua arrays have a strictly defined order and never lead to OOS. In other words, `ipars` is always safe, while `pairs` should only be used if the order of iteration doesn't change anything.