1local helper = wesnoth.require "helper"
2local location_set = wesnoth.require "location_set"
3local utils = wesnoth.require "wml-utils"
4local wml_actions = wesnoth.wml_actions
5local T = wml.tag
6
7wesnoth.require "wml-conditionals"
8wesnoth.require "wml-flow"
9wesnoth.require "wml"
10
11--[[
12
13Note: When adding new WML tags, unless they're very simple, it's preferred to
14add a new file in the "data/lua/wml" directory rather than implementing it in this file.
15The file will then automatically be loaded by the above require statement.
16
17Also note: The above on_load event needs to be registered before any other on_load events.
18That means before loading the WML tags via wesnoth.require "wml".
19
20]]
21
22function wml_actions.sync_variable(cfg)
23	local names = cfg.name or helper.wml_error "[sync_variable] missing required name= attribute."
24	local result = wesnoth.synchronize_choice(
25		function()
26			local res = {}
27			for name_raw in utils.split(names) do
28				local name = utils.trim(name_raw)
29				local variable_type = string.sub(name, string.len(name)) == "]" and "indexed" or ( wml.variables[name .. ".length"] > 0 and "array" or "attribute")
30				local variable_info = { name = name, type = variable_type }
31				table.insert(res, { "variable", variable_info })
32				if variable_type == "indexed" then
33					table.insert(variable_info, { "value", wml.variables[name] } )
34				elseif variable_type == "array" then
35					for i = 1, wml.variables[name .. ".length"] do
36						table.insert(variable_info, { "value",  wml.variables[string.format("%s[%d]", name, i - 1)] } )
37					end
38				else
39					variable_info.value = wml.variables[name]
40				end
41			end
42			return res
43		end
44	)
45	for variable in wml.child_range(result, "variable") do
46		local name = variable.name
47
48		if variable.type == "indexed" then
49			wml.variables[name] = variable[1][2]
50		elseif variable.type == "array" then
51			for index, cfg_pair in ipairs(variable) do
52				wml.variables[string.format("%s[%d]", name, index - 1)] = cfg_pair[2]
53			end
54		else
55			wml.variables[name] = variable.value
56		end
57	end
58end
59
60function wml_actions.chat(cfg)
61	local side_list = wesnoth.get_sides(cfg)
62	local speaker = tostring(cfg.speaker or "WML")
63	local message = tostring(cfg.message or
64		helper.wml_error "[chat] missing required message= attribute."
65	)
66
67	for index, side in ipairs(side_list) do
68		if side.controller == "human" and side.is_local then
69			wesnoth.message(speaker, message)
70			break
71		end
72	end
73
74	local observable = cfg.observable ~= false
75
76	if observable then
77		local all_sides = wesnoth.get_sides()
78		local has_human_side = false
79		for index, side in ipairs(all_sides) do
80			if side.controller == "human" and side.is_local then
81				has_human_side = true
82				break
83			end
84		end
85
86		if not has_human_side then
87			wesnoth.message(speaker, message)
88		end
89	end
90end
91
92function wml_actions.gold(cfg)
93	local amount = tonumber(cfg.amount) or
94		helper.wml_error "[gold] missing required amount= attribute."
95	local sides = wesnoth.get_sides(cfg)
96	for index, team in ipairs(sides) do
97		team.gold = team.gold + amount
98	end
99end
100
101--note: This tag can't easily (without deprecation) be extended to store an array,
102--since the gold is stored in a scalar variable, not a container (there's no key).
103function wml_actions.store_gold(cfg)
104	local team = wesnoth.get_sides(cfg)[1]
105	if team then wml.variables[cfg.variable or "gold"] = team.gold end
106end
107
108function wml_actions.clear_variable(cfg)
109	local names = cfg.name or
110		helper.wml_error "[clear_variable] missing required name= attribute."
111	for w in utils.split(names) do
112		wml.variables[utils.trim(w)] = nil
113	end
114end
115
116function wml_actions.store_unit_type_ids(cfg)
117	local types = {}
118	for k in pairs(wesnoth.unit_types) do
119		table.insert(types, k)
120	end
121	table.sort(types)
122	types = table.concat(types, ',')
123	wml.variables[cfg.variable or "unit_type_ids"] = types
124end
125
126function wml_actions.store_unit_type(cfg)
127	local types = cfg.type or
128		helper.wml_error "[store_unit_type] missing required type= attribute."
129	local writer = utils.vwriter.init(cfg, "unit_type")
130	for w in utils.split(types) do
131		local unit_type = wesnoth.unit_types[w] or
132			helper.wml_error(string.format("Attempt to store nonexistent unit type '%s'.", w))
133		utils.vwriter.write(writer, unit_type.__cfg)
134	end
135end
136
137function wml_actions.fire_event(cfg)
138	local u1 = wml.get_child(cfg, "primary_unit")
139	u1 = u1 and wesnoth.get_units(u1)[1]
140	local x1, y1 = 0, 0
141	if u1 then x1, y1 = u1.x, u1.y end
142
143	local u2 = wml.get_child(cfg, "secondary_unit")
144	u2 = u2 and wesnoth.get_units(u2)[1]
145	local x2, y2 = 0, 0
146	if u2 then x2, y2 = u2.x, u2.y end
147
148	local w1 = wml.get_child(cfg, "primary_attack")
149	local w2 = wml.get_child(cfg, "secondary_attack")
150	if w2 then w1 = w1 or {} end
151
152	if cfg.id and cfg.id ~= "" then wesnoth.fire_event_by_id(cfg.id, x1, y1, x2, y2, w1, w2)
153	elseif cfg.name and cfg.name ~= "" then wesnoth.fire_event(cfg.name, x1, y1, x2, y2, w1, w2)
154	end
155end
156
157function wml_actions.allow_recruit(cfg)
158	local unit_types = cfg.type or helper.wml_error("[allow_recruit] missing required type= attribute")
159	for index, team in ipairs(wesnoth.get_sides(cfg)) do
160		local v = team.recruit
161		for type in utils.split(unit_types) do
162			table.insert(v, type)
163			wesnoth.add_known_unit(type)
164		end
165		team.recruit = v
166		end
167end
168
169function wml_actions.allow_extra_recruit(cfg)
170	local recruits = cfg.extra_recruit or helper.wml_error("[allow_extra_recruit] missing required extra_recruit= attribute")
171	for index, unit in ipairs(wesnoth.get_units(cfg)) do
172		local v = unit.extra_recruit
173		for recruit in utils.split(recruits) do
174			table.insert(v, recruit)
175			wesnoth.add_known_unit(recruit)
176		end
177		unit.extra_recruit = v
178	end
179end
180
181function wml_actions.disallow_recruit(cfg)
182	local unit_types = cfg.type
183	for index, team in ipairs(wesnoth.get_sides(cfg)) do
184		if unit_types then
185			local v = team.recruit
186			for w in utils.split(unit_types) do
187				for i, r in ipairs(v) do
188					if r == w then
189						table.remove(v, i)
190						break
191					end
192				end
193			end
194			team.recruit = v
195		else
196			team.recruit = nil
197		end
198	end
199end
200
201function wml_actions.disallow_extra_recruit(cfg)
202	local recruits = cfg.extra_recruit or helper.wml_error("[disallow_extra_recruit] missing required extra_recruit= attribute")
203	for index, unit in ipairs(wesnoth.get_units(cfg)) do
204		local v = unit.extra_recruit
205		for w in utils.split(recruits) do
206			for i, r in ipairs(v) do
207				if r == w then
208					table.remove(v, i)
209					break
210				end
211			end
212		end
213		unit.extra_recruit = v
214	end
215end
216
217function wml_actions.set_recruit(cfg)
218	local recruit = cfg.recruit or helper.wml_error("[set_recruit] missing required recruit= attribute")
219	for index, team in ipairs(wesnoth.get_sides(cfg)) do
220		local v = {}
221		for w in utils.split(recruit) do
222			table.insert(v, w)
223		end
224		team.recruit = v
225	end
226end
227
228function wml_actions.set_extra_recruit(cfg)
229	local recruits = cfg.extra_recruit or helper.wml_error("[set_extra_recruit] missing required extra_recruit= attribute")
230	local v = {}
231
232	for w in utils.split(recruits) do
233		table.insert(v, w)
234	end
235
236	for index, unit in ipairs(wesnoth.get_units(cfg)) do
237		unit.extra_recruit = v
238	end
239end
240
241function wml_actions.store_map_dimensions(cfg)
242	local var = cfg.variable or "map_size"
243	local w, h, b = wesnoth.get_map_size()
244	wml.variables[var .. ".width"] = w
245	wml.variables[var .. ".height"] = h
246	wml.variables[var .. ".border_size"] = b
247end
248
249function wml_actions.unit_worth(cfg)
250	local u = wesnoth.get_units(cfg)[1] or
251		helper.wml_error "[unit_worth]'s filter didn't match any unit"
252	local ut = wesnoth.unit_types[u.type]
253	local hp = u.hitpoints / u.max_hitpoints
254	local xp = u.experience / u.max_experience
255	local best_adv = ut.cost
256	for w in utils.split(ut.__cfg.advances_to) do
257		local uta = wesnoth.unit_types[w]
258		if uta and uta.cost > best_adv then best_adv = uta.cost end
259	end
260	wml.variables["cost"] = ut.cost
261	wml.variables["next_cost"] = best_adv
262	wml.variables["health"] = math.floor(hp * 100)
263	wml.variables["experience"] = math.floor(xp * 100)
264	wml.variables["recall_cost"] = ut.recall_cost
265	wml.variables["unit_worth"] = math.floor(math.max(ut.cost * hp, best_adv * xp))
266end
267
268function wml_actions.lua(cfg)
269	cfg = wml.shallow_literal(cfg)
270	local bytecode, message = load(cfg.code or "")
271	if not bytecode then error("~lua:" .. message, 0) end
272	bytecode(wml.get_child(cfg, "args"))
273end
274
275function wml_actions.music(cfg)
276	if cfg.play_once then
277		wesnoth.music_list.play(cfg.name)
278	else
279		if not cfg.append then
280			if cfg.immediate and wesnoth.music_list.current_i then
281				wesnoth.music_list.current.once = true
282			end
283			wesnoth.music_list.clear()
284		end
285		local m = #wesnoth.music_list
286		wesnoth.music_list.add(cfg.name, not not cfg.immediate, cfg.ms_before or 0, cfg.ms_after or 0)
287		local n = #wesnoth.music_list
288		if n == 0 then
289			return
290		end
291		if cfg.shuffle == false then
292			wesnoth.music_list[n].shuffle = false
293		end
294		-- Always overwrite shuffle even if the new track couldn't be added,
295		-- but title shouldn't be overwritten.
296		if cfg.title ~= nil and m ~= n then
297			wesnoth.music_list[n].title = cfg.title
298		end
299	end
300end
301
302function wml_actions.volume(cfg)
303	if cfg.music then
304		local rel = tonumber(cfg.music) or 100.0
305		wesnoth.music_list.volume = rel
306	end
307	if cfg.sound then
308		local rel = tonumber(cfg.sound) or 100.0
309		wesnoth.sound_volume(rel)
310	end
311end
312
313function wml_actions.scroll_to(cfg)
314	local loc = wesnoth.get_locations( cfg )[1]
315	if not loc then return end
316	if not utils.optional_side_filter(cfg) then return end
317	wesnoth.scroll_to_tile(loc[1], loc[2], cfg.check_fogged, cfg.immediate)
318	if cfg.highlight then
319		wesnoth.highlight_hex(loc[1], loc[2])
320		wml_actions.redraw{}
321	end
322end
323
324function wml_actions.scroll_to_unit(cfg)
325	local u = wesnoth.get_units(cfg)[1]
326	if not u then return end
327	if not utils.optional_side_filter(cfg, "for_side", "for_side") then return end
328	wesnoth.scroll_to_tile(u.x, u.y, cfg.check_fogged, cfg.immediate)
329	if cfg.highlight then
330		wesnoth.highlight_hex(u.x, u.y)
331		wml_actions.redraw{}
332	end
333end
334
335function wml_actions.lock_view(cfg)
336	wesnoth.lock_view(true)
337end
338
339function wml_actions.unlock_view(cfg)
340	wesnoth.lock_view(false)
341end
342
343function wml_actions.select_unit(cfg)
344	local u = wesnoth.get_units(cfg)[1]
345	if not u then return end
346	wesnoth.select_unit(u.x, u.y, cfg.highlight, cfg.fire_event)
347end
348
349function wml_actions.unit_overlay(cfg)
350	local img = cfg.image or helper.wml_error( "[unit_overlay] missing required image= attribute" )
351	for i,u in ipairs(wesnoth.get_units(cfg)) do
352		local ucfg = u.__cfg
353		for w in utils.split(ucfg.overlays) do
354			if w == img then ucfg = nil end
355		end
356		if ucfg then
357			ucfg.overlays = ucfg.overlays .. ',' .. img
358			wesnoth.put_unit(ucfg)
359		end
360	end
361end
362
363function wml_actions.remove_unit_overlay(cfg)
364	local img = cfg.image or helper.wml_error( "[remove_unit_overlay] missing required image= attribute" )
365
366	-- Loop through all matching units.
367	for i,u in ipairs(wesnoth.get_units(cfg)) do
368		local ucfg = u.__cfg
369		local t = utils.parenthetical_split(ucfg.overlays)
370		-- Remove the specified image from the overlays.
371		for i = #t,1,-1 do
372			if t[i] == img then table.remove(t, i) end
373		end
374		-- Reassemble the list of remaining overlays.
375		ucfg.overlays = table.concat(t, ',')
376		wesnoth.put_unit(ucfg)
377	end
378end
379
380function wml_actions.store_turns(cfg)
381	local var = cfg.variable or "turns"
382	wml.variables[var] = wesnoth.game_config.last_turn
383end
384
385function wml_actions.store_unit(cfg)
386	local filter = wml.get_child(cfg, "filter") or
387		helper.wml_error "[store_unit] missing required [filter] tag"
388	local kill_units = cfg.kill
389
390	--cache the needed units here, since the filter might reference the to-be-cleared variable(s)
391	local units = wesnoth.get_units(filter)
392	local recall_units = wesnoth.get_recall_units(filter)
393
394	local writer = utils.vwriter.init(cfg, "unit")
395
396	for i,u in ipairs(units) do
397		utils.vwriter.write(writer, u.__cfg)
398		if kill_units then u:erase() end
399	end
400
401	if (not filter.x or filter.x == "recall") and (not filter.y or filter.y == "recall") then
402		for i,u in ipairs(recall_units) do
403			local ucfg = u.__cfg
404			ucfg.x = "recall"
405			ucfg.y = "recall"
406			utils.vwriter.write(writer, ucfg)
407			if kill_units then u:erase() end
408		end
409	end
410end
411
412function wml_actions.sound(cfg)
413	local name = cfg.name or helper.wml_error("[sound] missing required name= attribute")
414	wesnoth.play_sound(name, cfg["repeat"])
415end
416
417function wml_actions.store_locations(cfg)
418	-- the variable can be mentioned in a [find_in] subtag, so it
419	-- cannot be cleared before the locations are recovered
420	local locs = wesnoth.get_locations(cfg)
421	local writer = utils.vwriter.init(cfg, "location")
422	for i, loc in ipairs(locs) do
423		local x, y = loc[1], loc[2]
424		local t = wesnoth.get_terrain(x, y)
425		local res = { x = x, y = y, terrain = t }
426		if wesnoth.get_terrain_info(t).village then
427			res.owner_side = wesnoth.get_village_owner(x, y) or 0
428		end
429		utils.vwriter.write(writer, res)
430	end
431end
432
433function wml_actions.store_reachable_locations(cfg)
434	local unit_filter = wml.get_child(cfg, "filter") or
435		helper.wml_error "[store_reachable_locations] missing required [filter] tag"
436	local location_filter = wml.get_child(cfg, "filter_location")
437	local range = cfg.range or "movement"
438	local moves = cfg.moves or "current"
439	local variable = cfg.variable or helper.wml_error "[store_reachable_locations] missing required variable= key"
440	local reach_param = { viewing_side = cfg.viewing_side or 0 }
441	if range == "vision" then
442		moves = "max"
443		reach_param.ignore_units = true
444	end
445
446	local reach = location_set.create()
447
448	for i,unit in ipairs(wesnoth.get_units(unit_filter)) do
449		local unit_reach
450		if moves == "max" then
451			local saved_moves = unit.moves
452			unit.moves = unit.max_moves
453			unit_reach = location_set.of_pairs(wesnoth.find_reach(unit, reach_param))
454			unit.moves = saved_moves
455		else
456			unit_reach = location_set.of_pairs(wesnoth.find_reach(unit, reach_param))
457		end
458
459		if range == "vision" or range == "attack" then
460			unit_reach:iter(function(x, y)
461				reach:insert(x, y)
462				for u,v in helper.adjacent_tiles(x, y) do
463					reach:insert(u, v)
464				end
465			end)
466		else
467			reach:union(unit_reach)
468		end
469	end
470
471	if location_filter then
472		reach = reach:filter(function(x, y)
473			return wesnoth.match_location(x, y, location_filter)
474		end)
475	end
476	reach:to_wml_var(variable)
477end
478
479function wml_actions.hide_unit(cfg)
480	for i,u in ipairs(wesnoth.get_units(cfg)) do
481		u.hidden = true
482	end
483	wml_actions.redraw {}
484end
485
486function wml_actions.unhide_unit(cfg)
487	for i,u in ipairs(wesnoth.get_units(cfg)) do
488		u.hidden = false
489	end
490	wml_actions.redraw {}
491end
492
493function wml_actions.capture_village(cfg)
494	local side = cfg.side
495	local filter_side = wml.get_child(cfg, "filter_side")
496	local fire_event = cfg.fire_event
497	if side then side = tonumber(side) or helper.wml_error("invalid side in [capture_village]") end
498	if filter_side then
499		if side then helper.wml_error("duplicate side information in [capture_village]") end
500		side = wesnoth.get_sides(filter_side)[1]
501		if side then side = side.side end
502	end
503	local locs = wesnoth.get_locations(cfg)
504
505	for i, loc in ipairs(locs) do
506		wesnoth.set_village_owner(loc[1], loc[2], side, fire_event)
507	end
508end
509
510function wml_actions.terrain(cfg)
511	local terrain = cfg.terrain or helper.wml_error("[terrain] missing required terrain= attribute")
512	cfg = wml.shallow_parsed(cfg)
513	cfg.terrain = nil
514	for i, loc in ipairs(wesnoth.get_locations(cfg)) do
515		wesnoth.set_terrain(loc[1], loc[2], terrain, cfg.layer, cfg.replace_if_failed)
516	end
517end
518
519function wml_actions.delay(cfg)
520	local delay = tonumber(cfg.time) or
521		helper.wml_error "[delay] missing required time= attribute."
522	local accelerate = cfg.accelerate or false
523	wesnoth.delay(delay, accelerate)
524end
525
526function wml_actions.floating_text(cfg)
527	local locs = wesnoth.get_locations(cfg)
528	local text = cfg.text or helper.wml_error("[floating_text] missing required text= attribute")
529
530	for i, loc in ipairs(locs) do
531		wesnoth.float_label(loc[1], loc[2], text, cfg.color)
532	end
533end
534
535function wml_actions.petrify(cfg)
536	for index, unit in ipairs(wesnoth.get_units(cfg)) do
537		unit.status.petrified = true
538		-- Extract unit and put it back to update animation (not needed for recall units)
539		unit:extract()
540		unit:to_map()
541	end
542
543	for index, unit in ipairs(wesnoth.get_recall_units(cfg)) do
544		unit.status.petrified = true
545	end
546end
547
548function wml_actions.unpetrify(cfg)
549	for index, unit in ipairs(wesnoth.get_units(cfg)) do
550		unit.status.petrified = false
551		-- Extract unit and put it back to update animation (not needed for recall units)
552		unit:extract()
553		unit:to_map()
554	end
555
556	for index, unit in ipairs(wesnoth.get_recall_units(cfg)) do
557		unit.status.petrified = false
558	end
559end
560
561function wml_actions.transform_unit(cfg)
562	local transform_to = cfg.transform_to
563
564	for index, unit in ipairs(wesnoth.get_units(cfg)) do
565
566		if transform_to then
567			wesnoth.transform_unit( unit, transform_to )
568		else
569			local hitpoints = unit.hitpoints
570			local experience = unit.experience
571			local recall_cost = unit.recall_cost
572			local status = wml.get_child( unit.__cfg, "status" )
573
574			unit.experience = unit.max_experience
575			wesnoth.advance_unit(unit, false, false)
576
577			unit.hitpoints = hitpoints
578			unit.experience = experience
579			unit.recall_cost = recall_cost
580
581			for key, value in pairs(status) do unit.status[key] = value end
582			if unit.status.unpoisonable then unit.status.poisoned = nil end
583		end
584	end
585
586	wml_actions.redraw {}
587end
588
589function wml_actions.store_side(cfg)
590	local writer = utils.vwriter.init(cfg, "side")
591	for t, side_number in helper.get_sides(cfg) do
592		local container = t.__cfg
593		-- set values not properly handled by the __cfg
594		container.income = t.total_income
595		container.net_income = t.net_income
596		container.base_income = t.base_income
597		container.expenses = t.expenses
598		container.total_upkeep = t.total_upkeep
599		container.num_units = t.num_units
600		container.num_villages = t.num_villages
601		container.side = side_number
602		utils.vwriter.write(writer, container)
603	end
604end
605
606function wml_actions.add_ai_behavior(cfg)
607	local unit = wesnoth.get_units(wml.get_child(cfg, "filter"))[1] or
608		helper.wml_error("[add_ai_behavior]: no unit specified")
609
610	local side = cfg.side or
611		helper.wml_error("[add_ai_behavior]: no side attribute given")
612
613	local sticky = cfg.sticky or false
614	local loop_id = cfg.loop_id or "main_loop"
615	local eval = cfg.evaluation
616	local exec = cfg.execution
617	local id = cfg.bca_id or ("bca-" .. unit.__cfg.underlying_id)
618
619	local ux = unit.x -- @note: did I get it right that coordinates in C++ differ by 1 from thos in-game(and in Lua)?
620	local uy = unit.y
621
622	if not (eval and exec) then
623		helper.wml_error("[add_ai_behavior]: invalid execution/evaluation handler(s)")
624	end
625
626	local path = "stage[" .. loop_id .. "].candidate_action[" .. id .. "]" -- bca: behavior candidate action
627
628	wesnoth.wml_actions.modify_ai {
629		action = "add",
630		engine = "lua",
631		path = path,
632		side = side,
633
634		T.candidate_action {
635			id = id,
636			name = id,
637			engine = "lua",
638			sticky = sticky,
639			unit_x = ux,
640			unit_y = uy,
641			evaluation = eval,
642			execution = exec
643		},
644	}
645end
646
647function wml_actions.store_starting_location(cfg)
648	local writer = utils.vwriter.init(cfg, "location")
649	for _, side in ipairs(wesnoth.get_sides(cfg)) do
650		local loc = wesnoth.get_starting_location(side.side)
651		if loc then
652			local terrain = wesnoth.get_terrain(loc[1], loc[2])
653			local result = { x = loc[1], y = loc[2], terrain = terrain }
654			if wesnoth.get_terrain_info(terrain).village then
655				result.owner_side = wesnoth.get_village_owner(loc[1], loc[2]) or 0
656			end
657			utils.vwriter.write(writer, result)
658		end
659	end
660end
661
662function wml_actions.store_villages( cfg )
663	local villages = wesnoth.get_villages( cfg )
664	local writer = utils.vwriter.init(cfg, "location")
665	for index, village in ipairs( villages ) do
666		utils.vwriter.write(writer, {
667			x = village[1],
668			y = village[2],
669			terrain = wesnoth.get_terrain( village[1], village[2] ),
670			owner_side = wesnoth.get_village_owner( village[1], village[2] ) or 0
671		})
672	end
673end
674
675function wml_actions.put_to_recall_list(cfg)
676	local units = wesnoth.get_units(cfg)
677
678	for i, unit in ipairs(units) do
679		if cfg.heal then
680			unit.hitpoints = unit.max_hitpoints
681			unit.moves = unit.max_moves
682			unit.attacks_left = unit.max_attacks
683			unit.status.poisoned = false
684			unit.status.slowed = false
685		end
686		wesnoth.put_recall_unit(unit, unit.side)
687	end
688end
689
690function wml_actions.allow_undo(cfg)
691	wesnoth.allow_undo()
692end
693
694function wml_actions.allow_end_turn(cfg)
695	wesnoth.allow_end_turn(true)
696end
697
698function wml_actions.disallow_end_turn(cfg)
699	wesnoth.allow_end_turn(false)
700end
701
702function wml_actions.clear_menu_item(cfg)
703	wesnoth.clear_menu_item(cfg.id)
704end
705
706function wml_actions.set_menu_item(cfg)
707	wesnoth.set_menu_item(cfg.id, cfg)
708end
709
710function wml_actions.place_shroud(cfg)
711	local sides = utils.get_sides(cfg)
712	local tiles = wesnoth.get_locations(cfg)
713	for i,side in ipairs(sides) do
714		wesnoth.place_shroud(side.side, tiles)
715	end
716end
717
718function wml_actions.remove_shroud(cfg)
719	local sides = utils.get_sides(cfg)
720	local tiles = wesnoth.get_locations(cfg)
721	for i,side in ipairs(sides) do
722		wesnoth.remove_shroud(side.side, tiles)
723	end
724end
725
726function wml_actions.time_area(cfg)
727	if cfg.remove then
728		wml_actions.remove_time_area(cfg)
729	else
730		wesnoth.add_time_area(cfg)
731	end
732end
733
734function wml_actions.remove_time_area(cfg)
735	local id = cfg.id or helper.wml_error("[remove_time_area] missing required id= key")
736
737	for w in utils.split(id) do
738		wesnoth.remove_time_area(w)
739	end
740end
741
742function wml_actions.replace_schedule(cfg)
743	wesnoth.replace_schedule(cfg)
744end
745
746function wml_actions.scroll(cfg)
747	local sides = utils.get_sides(cfg)
748	local have_human = false
749	for i, side in ipairs(sides) do
750		if side.controller == 'human' and side.is_local then
751			have_human = true
752		end
753	end
754	if have_human or #sides == 0 then
755		wesnoth.scroll(cfg.x or 0, cfg.y or 0)
756	end
757end
758
759function wml_actions.color_adjust(cfg)
760	wesnoth.color_adjust(cfg)
761end
762
763function wml_actions.end_turn(cfg)
764	wesnoth.end_turn()
765end
766
767function wml_actions.event(cfg)
768	if cfg.remove then
769		wml_actions.remove_event(cfg)
770	else
771		wesnoth.add_event_handler(cfg)
772	end
773end
774
775function wml_actions.remove_event(cfg)
776	local id = cfg.id or helper.wml_error("[remove_event] missing required id= key")
777
778	for w in utils.split(id) do
779		wesnoth.remove_event_handler(w)
780	end
781end
782
783function wml_actions.inspect(cfg)
784	wesnoth.gamestate_inspector(cfg)
785end
786
787function wml_actions.label( cfg )
788	local new_cfg = wml.parsed( cfg )
789	for index, location in ipairs( wesnoth.get_locations( cfg ) ) do
790		new_cfg.x, new_cfg.y = location[1], location[2]
791		wesnoth.label( new_cfg )
792	end
793end
794
795function wml_actions.open_help(cfg)
796	wesnoth.open_help(cfg.topic)
797end
798
799function wml_actions.redraw(cfg)
800	local clear_shroud = cfg.clear_shroud
801
802	-- Backwards compat, the behavior of the tag was to clear shroud in case that side= is given.
803	if cfg.clear_shroud == nil and cfg.side ~= nil then
804		clear_shroud = true
805	end
806
807	wesnoth.redraw(cfg, clear_shroud)
808end
809
810function wml_actions.print(cfg)
811	wesnoth.print(cfg)
812end
813
814function wml_actions.unsynced(cfg)
815	wesnoth.unsynced(function ()
816		wml_actions.command(cfg)
817	end)
818end
819
820local function on_board(x, y)
821	if type(x) ~= "number" or type(y) ~= "number" then
822		return false
823	end
824	local w, h = wesnoth.get_map_size()
825	return x >= 1 and y >= 1 and x <= w and y <= h
826end
827
828wml_actions.unstore_unit = function(cfg)
829	local variable = cfg.variable or helper.wml_error("[unstore_unit] missing required 'variable' attribute")
830	local unit_cfg = wml.variables[variable] or helper.wml_error("[unstore_unit]: variable '" .. variable .. "' doesn't exist")
831	if type(unit_cfg) ~= "table" or unit_cfg.type == nil then
832		helper.wml_error("[unstore_unit]: variable '" .. variable .. "' doesn't contain unit data")
833	end
834	local unit = wesnoth.create_unit(unit_cfg)
835	local advance = cfg.advance ~= false
836	local animate = cfg.animate ~= false
837	local check_passability = cfg.check_passability ~= false or nil
838	local x = cfg.x or unit.x or -1
839	local y = cfg.y or unit.y or -1
840	wesnoth.add_known_unit(unit.type)
841	if on_board(x, y) then
842		if cfg.find_vacant then
843			x,y = wesnoth.find_vacant_tile(x, y, check_passability and unit)
844		end
845		unit:to_map(x, y, cfg.fire_event)
846		local text
847		if unit_cfg.gender == "female" then
848			text = cfg.female_text or cfg.text
849		else
850			text = cfg.male_text or cfg.text
851		end
852		local color = cfg.color
853		if color == nil and cfg.red and cfg.blue and cfg.green then
854			color = cfg.red .. "," .. cfg.green .. "," .. cfg.blue
855		end
856		if text ~= nil and not wesnoth.is_skipping_messages() then
857			wesnoth.float_label(x, y, text, color)
858		end
859		if advance then
860			wesnoth.advance_unit(unit, animate, cfg.fire_event)
861		end
862	else
863		unit:to_recall()
864	end
865end
866
867wml_actions.teleport = function(cfg)
868	local context = wesnoth.current.event_context
869	local filter = wml.get_child(cfg, "filter") or { x = context.x1, y = context.y1 }
870	local unit = wesnoth.get_units(filter)[1]
871	if not unit then
872		-- No error if no unit matches.
873		return
874	end
875	wesnoth.teleport(unit, cfg.x, cfg.y, cfg.check_passability == false, cfg.clear_shroud ~= false, cfg.animate)
876end
877
878function wml_actions.remove_sound_source(cfg)
879	wesnoth.remove_sound_source(cfg.id)
880end
881
882function wml_actions.sound_source(cfg)
883	wesnoth.add_sound_source(cfg)
884end
885
886function wml_actions.deprecated_message(cfg)
887	if type(cfg.level) ~= "number" or cfg.level < 1 or cfg.level > 4 then
888		local _ = wesnoth.textdomain "wesnoth"
889		helper.wml_error(_"Invalid deprecation level (should be 1-4)")
890	end
891	wesnoth.deprecated_message(cfg.what, cfg.level, cfg.version, cfg.message or '')
892end
893
894function wml_actions.wml_message(cfg)
895	wesnoth.log(cfg.logger, cfg.message, cfg.to_chat)
896end
897
898local function parse_fog_cfg(cfg)
899	-- Side filter
900	local ssf = wml.get_child(cfg, "filter_side")
901	local sides = wesnoth.get_sides(ssf or {})
902	-- Location filter
903	local locs = wesnoth.get_locations(cfg)
904	return locs, sides
905end
906
907function wml_actions.lift_fog(cfg)
908	local locs, sides = parse_fog_cfg(cfg)
909	for i = 1, #sides do
910		wesnoth.remove_fog(sides[i].side, locs, not cfg.multiturn)
911	end
912end
913
914function wml_actions.reset_fog(cfg)
915	local locs, sides = parse_fog_cfg(cfg)
916	for i = 1, #sides do
917		wesnoth.add_fog(sides[i].side, locs, cfg.reset_view)
918	end
919end
920
921function wesnoth.wml_actions.change_theme(cfg)
922	local new_theme = cfg.theme
923
924	if new_theme == nil then
925		new_theme = ""
926	end
927
928	wesnoth.game_config.theme = new_theme
929end
930
931function wesnoth.wml_actions.zoom(cfg)
932	wesnoth.zoom(cfg.factor, cfg.relative)
933end
934
935function wesnoth.wml_actions.story(cfg)
936	local title = cfg.title or helper.wml_error "Missing title key in [story] ActionWML"
937	wesnoth.show_story(cfg, title)
938end
939
940function wesnoth.wml_actions.cancel_action(cfg)
941	wesnoth.cancel_action()
942end
943
944function wesnoth.wml_actions.store_unit_defense(cfg)
945	local unit = wesnoth.get_units(cfg)[1] or helper.wml_error "[store_unit_defense]'s filter didn't match any unit"
946	local terrain = cfg.terrain
947	local defense
948
949	if terrain then
950		defense = wesnoth.unit_defense(unit, terrain)
951	elseif cfg.loc_x and cfg.loc_y then
952		defense = wesnoth.unit_defense(unit, wesnoth.get_terrain(cfg.loc_x, cfg.loc_y))
953	else
954		defense = wesnoth.unit_defense(unit, wesnoth.get_terrain(unit.x, unit.y))
955	end
956	wml.variables[cfg.variable or "terrain_defense"] = defense
957end
958