1
2local helper = wesnoth.require "helper"
3local utils = wesnoth.require "wml-utils"
4local _ = wesnoth.textdomain "wesnoth"
5
6local function log(msg, level)
7	wesnoth.log(level, msg, true)
8end
9
10local function get_image(cfg, speaker)
11	local image = cfg.image
12	local left_side = true
13
14	if speaker and (image == nil or image == "") and (cfg.second_image == nil or cfg.second_image == "") then
15		image = speaker.portrait
16	end
17
18	if image == "none" or image == nil then
19		return "", true
20	end
21
22	-- Note: This is deprecated except for use to set default alignment in portraits
23	-- (Move it into the first if statement later, with a nil check)
24	if image:find("~RIGHT%(%)") then
25		left_side = false
26		-- The percent signs escape the parentheses for a literal match
27		image = image:gsub("~RIGHT%(%)", "")
28	end
29
30	if cfg.image_pos == 'left' then
31		left_side = true
32	elseif cfg.image_pos == 'right' then
33		left_side = false
34	elseif cfg.image_pos ~= nil then
35		helper.wml_error('Invalid [message]image_pos - should be left or right')
36	end
37
38	return image, left_side
39end
40
41local function get_caption(cfg, speaker)
42	local caption = cfg.caption
43
44	if not caption and speaker ~= nil then
45		if speaker.name ~= nil and tostring(speaker.name):len() > 0 then
46			caption = speaker.name
47		else
48			caption = speaker.__cfg.language_name
49		end
50	end
51
52	return caption
53end
54
55local function get_pango_color(color)
56	local pango_color = "#"
57
58	-- if a hex color was passed in
59	-- or if a color string was passed in - contains no non-letter characters
60	-- just use that
61	if string.sub(color, 1, 1) == "#" or not string.match(color, "%A+") then
62		pango_color = color
63	-- decimal color was passed in, convert to hex color for pango
64	else
65		for s in string.gmatch(color, "%d+") do
66			pango_color = pango_color .. tonumber(s, 16)
67		end
68	end
69
70	return pango_color
71end
72
73-- add formatting
74local function add_formatting(cfg, text)
75	-- span tag
76	local formatting = "<span"
77
78	-- if message text, add formatting
79	if text and cfg then
80		-- add font
81		if cfg.font and cfg.font ~= '' then
82			formatting = formatting .. " font='" .. cfg.font .. "'"
83		end
84
85		-- add font_family
86		if cfg.font_family and cfg.font_family ~= '' then
87			formatting = formatting .. " font_family='" .. cfg.font_family .. "'"
88		end
89
90		-- add font_size
91		if cfg.font_size and cfg.font_size ~= '' then
92			formatting = formatting .. " font_size='" .. cfg.font_size .. "'"
93		end
94
95		-- font_style
96		if cfg.font_style and cfg.font_style ~= '' then
97			formatting = formatting .. " font_style='" .. cfg.font_style .. "'"
98		end
99
100		-- font_weight
101		if cfg.font_weight and cfg.font_weight ~= '' then
102			formatting = formatting .. " font_weight='" .. cfg.font_weight .. "'"
103		end
104
105		-- font_variant
106		if cfg.font_variant and cfg.font_variant ~= '' then
107			formatting = formatting .. " font_variant='" .. cfg.font_variant .. "'"
108		end
109
110		-- font_stretch
111		if cfg.font_stretch and cfg.font_stretch ~= '' then
112			formatting = formatting .. " font_stretch='" .. cfg.font_stretch .. "'"
113		end
114
115		-- add color
116		if cfg.color and cfg.color ~= '' then
117			formatting = formatting .. " color='" .. get_pango_color(cfg.color) .. "'"
118		end
119
120		-- bgcolor
121		if cfg.bgcolor and cfg.bgcolor ~= '' then
122			formatting = formatting .. " bgcolor='" .. get_pango_color(cfg.bgcolor) .. "'"
123		end
124
125		-- underline
126		if cfg.underline and cfg.underline ~= '' then
127			formatting = formatting .. " underline='" .. cfg.underline .. "'"
128		end
129
130		-- underline_color
131		if cfg.underline_color and cfg.underline_color ~= '' then
132			formatting = formatting .. " underline_color='" .. get_pango_color(cfg.underline_color) .. "'"
133		end
134
135		-- rise
136		if cfg.rise and cfg.rise ~= '' then
137			formatting = formatting .. " rise='" .. cfg.rise .. "'"
138		end
139
140		-- strikethrough
141		if cfg.strikethrough and tostring(cfg.strikethrough) ~= '' then
142			formatting = formatting .. " strikethrough='" .. tostring(cfg.strikethrough) .. "'"
143		end
144
145		-- strikethrough_color
146		if cfg.strikethrough_color and cfg.strikethrough_color ~= '' then
147			formatting = formatting .. " strikethrough_color='" .. get_pango_color(cfg.strikethrough_color) .. "'"
148		end
149
150		-- fallback
151		if cfg.fallback and tostring(cfg.fallback) ~= '' then
152			formatting = formatting .. " fallback='" .. tostring(cfg.fallback) .. "'"
153		end
154
155		-- letter_spacing
156		if cfg.letter_spacing and cfg.letter_spacing ~= '' then
157			formatting = formatting .. " letter_spacing='" .. cfg.letter_spacing .. "'"
158		end
159
160		-- gravity
161		if cfg.gravity and cfg.gravity ~= '' then
162			formatting = formatting .. " gravity='" .. cfg.gravity .. "'"
163		end
164
165		-- gravity_hint
166		if cfg.gravity_hint and cfg.gravity_hint ~= '' then
167			formatting = formatting .. " gravity_hint='" .. cfg.gravity_hint .. "'"
168		end
169
170		-- wrap in span tags and return if a color was added
171		if formatting ~= "<span" then
172			return formatting .. ">" .. text .. "</span>"
173		end
174	end
175
176	-- or return unmodified message
177	return text
178end
179
180local function get_speaker(cfg)
181	local speaker
182	local context = wesnoth.current.event_context
183
184	if cfg.speaker == "narrator" then
185		speaker = "narrator"
186	elseif cfg.speaker == "unit" then
187		speaker = wesnoth.get_unit(context.x1 or 0, context.y1 or 0)
188	elseif cfg.speaker == "second_unit" then
189		speaker = wesnoth.get_unit(context.x2 or 0, context.y2 or 0)
190	else
191		speaker = wesnoth.get_units(cfg)[1]
192	end
193
194	return speaker
195end
196
197local function message_user_choice(cfg, speaker, options, text_input, sound, voice)
198	local image, left_side = get_image(cfg, speaker)
199	local caption = get_caption(cfg, speaker)
200
201	local msg_cfg = {
202		left_side = left_side,
203		title = caption,
204		message = cfg.message,
205		portrait = image,
206		mirror = cfg.mirror,
207		second_portrait = cfg.second_image,
208		second_mirror = cfg.second_mirror,
209	}
210
211	if speaker ~= nil then
212		if cfg.male_message ~= nil and speaker.gender == "male" then
213			msg_cfg.message = cfg.male_message
214		elseif cfg.female_message ~= nil and speaker.gender == "female" then
215			msg_cfg.message = cfg.female_message
216		end
217	end
218
219	-- add formatting
220	msg_cfg.message = add_formatting(cfg, msg_cfg.message)
221
222	-- Parse input text, if not available all fields are empty
223	if text_input then
224		local input_max_size = tonumber(text_input.max_length) or 256
225		if input_max_size > 1024 or input_max_size < 1 then
226			log("Invalid maximum size for input " .. input_max_size, "warning")
227			input_max_size = 256
228		end
229
230		-- This roundabout method is because text_input starts out
231		-- as an immutable userdata value
232		text_input = {
233			label = text_input.label or "",
234			text  = text_input.text	 or "",
235			max_length = input_max_size,
236		}
237	end
238
239	return function()
240		if sound then wesnoth.play_sound(sound) end
241		if voice then
242			local speech = {
243				id = "wml_message_speaker",
244				sounds = voice,
245				loops = 0,
246				delay = 0,
247			}
248			if speaker then
249				speech.x = speaker.x
250				speech.y = speaker.y
251			end
252			wesnoth.add_sound_source(speech)
253		end
254
255		local option_chosen, ti_content = wesnoth.show_message_dialog(msg_cfg, options, text_input)
256
257		if voice then
258			wesnoth.remove_sound_source("wml_message_speaker")
259		end
260
261		if option_chosen == -2 then -- Pressed Escape (only if no input)
262			wesnoth.skip_messages()
263		end
264
265		local result_cfg = {}
266
267		if #options > 0 then
268			result_cfg.value = option_chosen
269		end
270
271		if text_input ~= nil then
272			result_cfg.text = ti_content
273		end
274
275		return result_cfg
276	end
277end
278
279function wesnoth.wml_actions.message(cfg)
280	local show_if = wml.get_child(cfg, "show_if") or {}
281	if not wesnoth.eval_conditional(show_if) then
282		log("[message] skipped because [show_if] did not pass", "debug")
283		return
284	end
285
286	-- Only the first text_input tag is considered
287	local text_input
288	for text_input_cfg in wml.child_range(cfg, "text_input") do
289		if text_input ~= nil then
290			log("Too many [text_input] tags, only first one accepted", "warning")
291			break
292		end
293		text_input = text_input_cfg
294	end
295
296	local options, option_events = {}, {}
297	for option in wml.child_range(cfg, "option") do
298		local condition = wml.get_child(option, "show_if") or {}
299
300		if wesnoth.eval_conditional(condition) then
301			if option.message and not option.image and not option.label then
302				local message = tostring(option.message)
303				wesnoth.deprecated_message("[option]message=", 3, "1.15.0", "Use label= instead.");
304				-- Legacy format
305				table.insert(options, option.message)
306			else
307				local opt = {
308					label = option.label,
309					description = option.description,
310					image = option.image,
311					default = option.default,
312					value = option.value
313				}
314				if option.message then
315					wesnoth.deprecated_message("[option]message=", 3, "1.15.0", "Use label= instead.");
316					if not option.label then
317						-- Support either message or description
318						opt.label = option.message
319					else
320						log("[option] has both label= and message=, ignoring the latter", "warning")
321					end
322				end
323				table.insert(options, opt)
324			end
325			table.insert(option_events, {})
326
327			for cmd in wml.child_range(option, "command") do
328				table.insert(option_events[#option_events], cmd)
329			end
330		end
331	end
332
333	-- Check if there is any input to be made, if not the message may be skipped
334	local has_input = text_input ~= nil or #options > 0
335
336	if not has_input and wesnoth.is_skipping_messages() then
337		-- No input to get and the user is not interested either
338		log("Skipping [message] because user not interested", "debug")
339		return
340	end
341
342	local sides_for = cfg.side_for
343	if sides_for and not has_input then
344		local show_for_side = false
345
346		-- Sanity checks on side number and controller
347		for side in utils.split(sides_for) do
348			side = tonumber(side)
349			if side > 0 and side <= #wesnoth.sides
350				and wesnoth.sides[side].controller == "human"
351				and wesnoth.sides[side].is_local
352			then
353				show_for_side = true
354				break
355			end
356		end
357
358		if not show_for_side then
359			-- Player isn't controlling side which should see the message
360			log("Player isn't controlling side that should see [message]", "debug")
361			return
362		end
363	end
364
365	local speaker = get_speaker(cfg)
366	if not speaker then
367		-- No matching unit found, continue onto the next message
368		log("No speaker found for [message]", "debug")
369		return
370	elseif cfg.highlight == false then
371		-- Nothing to do here
372	elseif speaker == "narrator" then
373		-- Narrator, so deselect units
374		wesnoth.deselect_hex()
375		-- The speaker is expected to be either nil or a unit later
376		speaker = nil
377		wesnoth.fire("redraw")
378	else
379		-- Check ~= false, because the default if omitted should be true
380		if cfg.scroll ~= false then
381			wesnoth.scroll_to_tile(speaker.x, speaker.y, true, false, true)
382		end
383
384		wesnoth.highlight_hex(speaker.x, speaker.y)
385		wesnoth.fire("redraw")
386	end
387
388	local msg_dlg = message_user_choice(cfg, speaker, options, text_input, cfg.sound, cfg.voice)
389
390	local option_chosen
391	if not has_input then
392		-- Always show the dialog if it has no input, whether we are replaying or not
393		msg_dlg()
394	else
395		local wait_description = cfg.wait_description or _("input")
396		if type(sides_for) ~= "number" then
397			-- 0 means currently playing side.
398			sides_for = 0
399		end
400		local choice = wesnoth.synchronize_choice(wait_description, msg_dlg, sides_for)
401
402		option_chosen = tonumber(choice.value)
403
404		if text_input ~= nil then
405			-- Implement the consequences of the choice
406			wml.variables[text_input.variable or "input"] = choice.text
407		end
408	end
409
410	-- Unhilight the speaker
411	if speaker and not cfg.highlight == false then
412		wesnoth.deselect_hex()
413	end
414
415	if #options > 0 then
416		if option_chosen > #options then
417			log("invalid choice (" .. option_chosen .. ") was specified, choice 1 to " ..
418				#options .. " was expected", "debug")
419			return
420		end
421
422		if cfg.variable ~= nil then
423			if options[option_chosen].value == nil then
424				wml.variables[cfg.variable] = option_chosen
425			else
426				wml.variables[cfg.variable] = options[option_chosen].value
427			end
428		end
429
430		for i, cmd in ipairs(option_events[option_chosen]) do
431			local action = utils.handle_event_commands(cmd, "plain")
432			if action ~= "none" then break end
433		end
434	end
435end
436