diff --git a/data/lua/wml-tags.lua b/data/lua/wml-tags.lua index 04b896e8cb4..29b1c895053 100644 --- a/data/lua/wml-tags.lua +++ b/data/lua/wml-tags.lua @@ -275,9 +275,11 @@ function wml_actions.music(cfg) wesnoth.set_music(cfg) end -wml_actions.command = utils.handle_event_commands +function wml_actions.command(cfg) + utils.handle_event_commands(cfg, "plain") +end --- since if and while are Lua keywords, we can't create functions with such names +-- we can't create functions with names that are Lua keywords (eg if, while) -- instead, we store the following anonymous functions directly into -- the table, using the [] operator, rather than by using the point syntax -- the same is true of for and repeat @@ -289,7 +291,8 @@ wml_actions["if"] = function(cfg) if wesnoth.eval_conditional(cfg) then -- evaluate [if] tag for then_child in helper.child_range(cfg, "then") do - utils.handle_event_commands(then_child) + local action = utils.handle_event_commands(then_child, "conditional") + if action ~= "none" then break end end return -- stop after executing [then] tags end @@ -297,15 +300,18 @@ wml_actions["if"] = function(cfg) for elseif_child in helper.child_range(cfg, "elseif") do if wesnoth.eval_conditional(elseif_child) then -- we'll evaluate the [elseif] tags one by one for then_tag in helper.child_range(elseif_child, "then") do - utils.handle_event_commands(then_tag) + local action = utils.handle_event_commands(then_tag, "conditional") + if action ~= "none" then goto exit end end return -- stop on first matched condition end end + ::exit:: -- no matched condition, try the [else] tags for else_child in helper.child_range(cfg, "else") do - utils.handle_event_commands(else_child) + local action = utils.handle_event_commands(else_child, "conditional") + if action ~= "none" then break end end end @@ -314,10 +320,32 @@ wml_actions["while"] = function( cfg ) for i = 1, 65536 do if wesnoth.eval_conditional( cfg ) then for do_child in helper.child_range( cfg, "do" ) do - utils.handle_event_commands( do_child ) + local action = utils.handle_event_commands(do_child, "loop") + if action == "break" then + utils.set_exiting("none") + goto exit + elseif action == "continue" then + utils.set_exiting("none") + break + elseif action ~= "none" then + goto exit + end end else return end end + ::exit:: +end + +wml_actions["break"] = function(cfg) + utils.set_exiting("break") +end + +wml_actions["return"] = function(cfg) + utils.set_exiting("return") +end + +function wml_actions.continue(cfg) + utils.set_exiting("continue") end wesnoth.wml_actions["for"] = function(cfg) @@ -401,17 +429,20 @@ function wml_actions.switch(cfg) for v in helper.child_range(cfg, "case") do for w in utils.split(v.value) do if w == tostring(var_value) then - utils.handle_event_commands(v) + local action = utils.handle_event_commands(v, "switch") found = true - break + if action ~= "none" then goto exit end + break end end end + ::exit:: -- Otherwise execute [else] statements. if not found then for v in helper.child_range(cfg, "else") do - utils.handle_event_commands(v) + local action = utils.handle_event_commands(v, "switch") + if action ~= "none" then break end end end end diff --git a/data/lua/wml-utils.lua b/data/lua/wml-utils.lua index fba8bdd4ddd..8f57f985603 100644 --- a/data/lua/wml-utils.lua +++ b/data/lua/wml-utils.lua @@ -71,11 +71,34 @@ function utils.optional_side_filter(cfg, key_name, filter_name) return false end -function utils.handle_event_commands(cfg) +local current_exit = "none" +local scope_stack = { + push = table.insert, + pop = table.remove, +} + +--[[ Possible exit types: + - none - ordinary execution + - break - exiting a loop scope + - return - immediate termination (exit all scopes) + - continue - jumping to the end of a loop scope +]] +function utils.set_exiting(exit_type) + current_exit = exit_type +end + +--[[ Possible scope types: + - plain - ordinary scope, no special features; eg [command] or [event] + - conditional - scope that's executing because of a condition, eg [then] or [else] + - switch - scope that's part of a switch statement, eg [case] or [else] + - loop - scope that's part of a loop, eg [do] +Currently, only "loop" has any special effects. ]] +function utils.handle_event_commands(cfg, scope_type) -- The WML might be modifying the currently executed WML by mixing -- [insert_tag] with [set_variables] and [clear_variable], so we -- have to be careful not to get confused by tags vanishing during -- the execution, hence the manual handling of [insert_tag]. + scope_stack:push(scope_type) local cmds = helper.shallow_literal(cfg) for i = 1,#cmds do local v = cmds[i] @@ -104,6 +127,7 @@ function utils.handle_event_commands(cfg) local j = 0 repeat cmd(arg) + if current_exit ~= "none" then break end j = j + 1 if j >= wesnoth.get_variable(insert_from .. ".length") then break end arg = wesnoth.tovconfig(wesnoth.get_variable(string.format("%s[%d]", insert_from, j))) @@ -112,9 +136,18 @@ function utils.handle_event_commands(cfg) cmd(arg) end end + if current_exit ~= "none" then break end + end + scope_stack:pop() + if #scope_stack == 0 then + if current_exit == "continue" and scope_type ~= "loop" then + helper.wml_error("[continue] found outside a loop scope!") + end + current_exit = "none" end -- Apply music alterations once all the commands have been processed. wesnoth.set_music() + return current_exit end -- Splits the string argument on commas, excepting those commas that occur diff --git a/data/lua/wml/message.lua b/data/lua/wml/message.lua index 86bfe5e94cd..ea19b3b556e 100644 --- a/data/lua/wml/message.lua +++ b/data/lua/wml/message.lua @@ -214,7 +214,8 @@ function wesnoth.wml_actions.message(cfg) end for i, cmd in ipairs(option_events[option_chosen]) do - utils.handle_event_commands(cmd) + local action = utils.handle_event_commands(cmd, "plain") + if action ~= "none" then break end end end end diff --git a/data/lua/wml/object.lua b/data/lua/wml/object.lua index 1deadca7557..63ebd70583c 100644 --- a/data/lua/wml/object.lua +++ b/data/lua/wml/object.lua @@ -55,7 +55,8 @@ function wml_actions.object(cfg) end for cmd in helper.child_range(cfg, command_type) do - utils.handle_event_commands(cmd) + local action = utils.handle_event_commands(cmd, "conditional") + if action ~= "none" then break end end end diff --git a/data/test/scenarios/interrupts.cfg b/data/test/scenarios/interrupts.cfg new file mode 100644 index 00000000000..21549a0107b --- /dev/null +++ b/data/test/scenarios/interrupts.cfg @@ -0,0 +1,114 @@ + +{GENERIC_UNIT_TEST check_interrupts_break ( + [event] + name=start + {VARIABLE x 0} + [while] + [true][/true] + [do] + [if] + {VARIABLE_CONDITIONAL x greater_than 5} + [then] + [break][/break] + [/then] + [/if] + {VARIABLE_OP x add 1} + [/do] + [/while] + {RETURN ({VARIABLE_CONDITIONAL x equals 6})} + [/event] +)} + +{GENERIC_UNIT_TEST check_interrupts_return ( + [event] + name=start + {VARIABLE x 0} + [while] + [true][/true] + [do] + [if] + {VARIABLE_CONDITIONAL x greater_than 5} + [then] + [return][/return] + [/then] + [/if] + {VARIABLE_OP x add 1} + [/do] + [/while] + {RETURN ([false][/false])} + [/event] + [event] + name=start + {RETURN ({VARIABLE_CONDITIONAL x equals 6})} + [/event] +)} + +{GENERIC_UNIT_TEST check_interrupts_continue ( + [event] + name=start + {VARIABLE x 0} + [while] + {VARIABLE_CONDITIONAL x less_than 1} + [do] + {VARIABLE_OP x add 1} + [continue][/continue] + {RETURN ([false][/false])} + [/do] + [/while] + {RETURN ([true][/true])} + [/event] +)} + +{GENERIC_UNIT_TEST check_interrupts_break_global ( + [event] + name=start + [break][/break] + {RETURN ([false][/false])} + [/event] + [event] + name=start + {RETURN ([true][/true])} + [/event] +)} + +{GENERIC_UNIT_TEST check_interrupts_continue_global ( + [event] + name=start + [lua] + code=<< + local H = wesnoth.require "lua/helper.lua" + local A = H.set_wml_action_metatable{} + local function continue() + A.continue{} + end + -- Use pcall() to trap the WML error raised by continue in global scope + local err, res = pcall(continue) + if err then wesnoth.fire_event "success" + else wesnoth.fire_event "fail" end + >> + [/lua] + [/event] + [event] + name=success + {RETURN ([true][/true])} + [/event] + [event] + name=fail + {RETURN ([false][/false])} + [/event] +)} + +{GENERIC_UNIT_TEST check_interrupts_return_nested ( + [event] + name=start + [command] + [return][/return] + {RETURN ([false][/false])} + [/command] + {RETURN ([false][/false])} + [/event] + [event] + name=start + {RETURN ([true][/true])} + [/event] +)} diff --git a/wml_test_schedule b/wml_test_schedule index 1657333d202..0e2cca3ded3 100644 --- a/wml_test_schedule +++ b/wml_test_schedule @@ -147,3 +147,10 @@ 0 filter_this_unit_wml 0 filter_this_unit_tl 0 filter_this_unit_fai +# Interrupt tag tests +0 check_interrupts_break +0 check_interrupts_return +0 check_interrupts_continue +0 check_interrupts_break_global +0 check_interrupts_return_nested +0 check_interrupts_continue_global