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