Merge pull request #493 from CelticMinstrel/foreach

Enhanced looping tags
This commit is contained in:
CelticMinstrel 2015-09-23 00:50:30 -04:00
commit f07e463882
29 changed files with 444 additions and 54 deletions

View file

@ -104,6 +104,8 @@ Version 1.13.1+dev:
These operators are applied after the existing '-' operator that takes the
opposite direction.
* Adjacency filters in abilities and weapon specials now support count= and is_enemy=
* Add new looping tags: [for], [foreach], [repeat]
* Add new flow control tags: [break], [continue], [return]
* Editor:
* Added Category field and color sliders to the Edit Label panel.
* Miscellaneous and bug fixes:

View file

@ -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
@ -288,7 +290,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
@ -296,7 +299,8 @@ 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 break end
end
return -- stop on first matched condition
end
@ -304,7 +308,8 @@ wml_actions["if"] = function(cfg)
-- 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
@ -313,10 +318,146 @@ 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)
local first, last, step
if cfg.array then
first = 0
last = wesnoth.get_variable(cfg.array .. ".length") - 1
step = 1
if cfg.reverse == "yes" then
first, last = last, first
step = -1
end
else
first = cfg.start or 0
last = cfg["end"] or first
step = cfg.step or ((last - first) / math.abs(last - first))
end
local i_var = cfg.variable or "i"
local save_i = utils.start_var_scope(i_var)
wesnoth.set_variable(i_var, first)
while wesnoth.get_variable(i_var) <= last do
for do_child in helper.child_range( cfg, "do" ) do
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
wesnoth.set_variable(i_var, wesnoth.get_variable(i_var) + 1)
end
::exit::
utils.end_var_scope(i_var, save_i)
end
wml_actions["repeat"] = function(cfg)
local times = cfg.times or 1
for i = 1, times do
for do_child in helper.child_range( cfg, "do" ) do
local action = utils.handle_event_commands(do_child, "loop")
if action == "break" then
utils.set_exiting("none")
return
elseif action == "continue" then
utils.set_exiting("none")
break
elseif action ~= "none" then
return
end
end
end
end
function wml_actions.foreach(cfg)
local array_name = cfg.variable or helper.wml_error "[foreach] missing required variable= attribute"
local array = helper.get_variable_array(array_name)
if #array == 0 then return end -- empty and scalars unwanted
local item_name = cfg.item_var or "this_item"
local this_item = utils.start_var_scope(item_name) -- if this_item is already set
local i_name = cfg.index_var or "i"
local i = utils.start_var_scope(i_name) -- if i is already set
local array_length = wesnoth.get_variable(array_name .. ".length")
for index, value in ipairs(array) do
-- Some protection against external modification
-- It's not perfect, though - it'd be nice if *any* change could be detected
if array_length ~= wesnoth.get_variable(array_name .. ".length") then
helper.wml_error("WML array length changed during [foreach] iteration")
end
wesnoth.set_variable(item_name, value)
-- set index variable
wesnoth.set_variable(i_name, index-1) -- here -1, because of WML array
-- perform actions
for do_child in helper.child_range(cfg, "do") do
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
-- set back the content, in case the author made some modifications
if not cfg.readonly then
array[index] = wesnoth.get_variable(item_name)
end
end
::exit::
-- house cleaning
utils.end_var_scope(item_name)
utils.end_var_scope(i)
--[[
This forces the readonly key to be taken literally.
If readonly=yes, then this line guarantees that the array
is unchanged after the [foreach] loop ends.
If readonly=no, then this line updates the array with any
changes the user has applied through the $this_item
variable (or whatever variable was given in item_var).
Note that altering the array via indexing (with the index_var)
is not supported; any such changes will be reverted by this line.
]]
helper.set_variable_array(array_name, array)
end
function wml_actions.switch(cfg)
@ -327,17 +468,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

View file

@ -71,11 +71,35 @@ 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_type = scope_type or "plain"
scope_stack:push(scope_type)
local cmds = helper.shallow_literal(cfg)
for i = 1,#cmds do
local v = cmds[i]
@ -104,6 +128,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 +137,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

View file

@ -220,7 +220,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

View file

@ -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

View file

@ -56,3 +56,11 @@
{CONTENT}
[/test]
#enddef
#define FAIL
{RETURN ([false][/false])}
#enddef
#define SUCCEED
{RETURN ([true][/true])}
#enddef

View file

@ -56,7 +56,7 @@
[/event]
[event]
name = side 2 turn 60
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[event]
name = side 2 turn refresh

View file

@ -343,7 +343,7 @@
[/event]
[event]
name = side 1 turn refresh
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[/test]
#enddef
@ -442,7 +442,7 @@
[/event]
[event]
name = side 1 turn refresh
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[/test]
#enddef

View file

@ -16,7 +16,7 @@
[false][/false]
[/not]
[then]
{RETURN [true][/true]}
{SUCCEED}
[/then]
[/if]
[/event]
@ -35,7 +35,7 @@
[false][/false]
[/or]
[then]
{RETURN [true][/true]}
{SUCCEED}
[/then]
[/if]
[/event]

View file

@ -42,7 +42,7 @@
[/event]
[event]
name = side 1 turn 7
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[event]
name = side turn

View file

@ -157,6 +157,6 @@
{TEST_FEEDING bob 1}
{TEST_FEEDING bob 1}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -162,6 +162,6 @@
{assert_test_false (side=4) (side=2)}
{assert_test_false () (side=5)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -116,6 +116,6 @@
[/have_unit]
[/not]
)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -0,0 +1,191 @@
{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]
{FAIL}
[/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 5}
[do]
{VARIABLE_OP x add 5}
[continue][/continue]
{FAIL}
[/do]
[/while]
{RETURN ({VARIABLE_CONDITIONAL x equals 5})}
[/event]
)}
{GENERIC_UNIT_TEST check_interrupts_break_global (
[event]
name=start
[break][/break]
{FAIL}
[/event]
[event]
name=start
{SUCCEED}
[/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
{SUCCEED}
[/event]
[event]
name=fail
{FAIL}
[/event]
)}
{GENERIC_UNIT_TEST check_interrupts_return_nested (
[event]
name=start
[command]
[return][/return]
{FAIL}
[/command]
{FAIL}
[/event]
[event]
name=start
{SUCCEED}
[/event]
)}
{GENERIC_UNIT_TEST check_interrupts_elseif (
[event]
name=start
{VARIABLE x 9}
[if]
{VARIABLE_CONDITIONAL x greater_than 10}
[then]
{FAIL}
[/then]
[elseif]
{VARIABLE_CONDITIONAL x less_than 10}
[then]
[return][/return]
[/then]
[/elseif]
[else]
[wml_message]
message="Reached the [else] block!"
logger=error
[/wml_message]
{FAIL}
[/else]
[/if]
[wml_message]
message="Passed the [if] block!"
logger=error
[/wml_message]
{FAIL}
[/event]
[event]
name=start
{SUCCEED}
[/event]
)}
{GENERIC_UNIT_TEST check_interrupts_case (
[event]
name=start
{VARIABLE x 0}
[switch]
variable=x
[case]
value=1,3,5,7,9
{FAIL}
[/case]
[case]
value=0,2,4,6,8
[return][/return]
[/case]
[case]
value=0
[wml_message]
message="Reached next [case] block!"
logger=error
[/wml_message]
{FAIL}
[/case]
[else]
[wml_message]
message="Reached the [else] block!"
logger=error
[/wml_message]
{FAIL}
[/else]
[/switch]
[wml_message]
message="Passed the [switch] block!"
logger=error
[/wml_message]
{FAIL}
[/event]
[event]
name=start
{SUCCEED}
[/event]
)}

View file

@ -63,11 +63,11 @@
[/event]
[event]
name=side 1 turn 1
{RETURN ([false][/false])}
{FAIL}
[/event]
[event]
name=side 1 turn 42
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
@ -80,11 +80,11 @@
[/event]
[event]
name=side 1 turn 1 refresh
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[event]
name=side 1 turn 42
{RETURN ([false][/false])}
{FAIL}
[/event]
)}
@ -102,15 +102,15 @@
[/event]
[event]
name=side 1 turn 1 end
{RETURN ([false][/false])}
{FAIL}
[/event]
[event]
name=side 2 turn 42
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[event]
name=side 1 turn 43
{RETURN ([false][/false])}
{FAIL}
[/event]
)}

View file

@ -74,7 +74,7 @@
[/event]
[event]
name=side 1 turn 2
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
@ -114,7 +114,7 @@
[/event]
[event]
name=side 1 turn 4 refresh
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
@ -193,7 +193,7 @@ Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg"
{RECRUIT_AND_CHECK 8 ("se") (7,4)}
[event]
name=side 1 turn 9 refresh
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
{RECRUIT_TEST recruit_facing_center (
@ -210,6 +210,6 @@ Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg, Gg"
{RECALL_AND_CHECK 8 ("nw") (7,4)}
[event]
name=side 1 turn 9 refresh
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -126,7 +126,7 @@
#If we got this far without triggering sighted, we fail the test.
[event]
name = side 2 turn 2
{RETURN ([false][/false])}
{FAIL}
[/event]
#This makes the sides pass their turns, when the other events have taken place.

View file

@ -77,7 +77,7 @@
[/event]
[event]
name=side 2 turn 1
{RETURN ([false][/false])}
{FAIL}
[/event]
#enddef

View file

@ -13,6 +13,6 @@
{ASSERT ({VARIABLE_CONDITIONAL b equals 2})}
{ASSERT ({VARIABLE_CONDITIONAL c equals 3})}
{ASSERT ({VARIABLE_CONDITIONAL d equals 4})}
{RETURN [true][/true]}
{SUCCEED}
[/event]
)}

View file

@ -8,6 +8,6 @@
[/event]
[event]
name = side 2 turn 1
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -79,7 +79,7 @@
#If we got this far without failing an assertion, we pass the test.
[event]
name = side 2 turn 6
{RETURN ([true][/true])}
{SUCCEED}
[/event]
#This makes all sides pass their turns, when the other events have taken place.
@ -160,7 +160,7 @@
[/time_area]
{TEST_GRUNT_DAMAGE test3 1 6 "$(11*2)"}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[/test]
@ -210,6 +210,6 @@
{TEST_GRUNT_DAMAGE test3 1 6 "$(11*2)"}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[/test]

View file

@ -72,6 +72,6 @@
[/and]
[/not])}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -21,7 +21,7 @@
{ASSERT {VARIABLE_CONDITIONAL mx equals 3}}
{ASSERT {VARIABLE_CONDITIONAL my equals 3}}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -54,7 +54,7 @@
y={PY}
[/move]
[/do_command]
{RETURN ([true][/true])}
{SUCCEED}
[/event]
[/test]
#enddef

View file

@ -111,6 +111,6 @@
[/have_unit]
)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -37,7 +37,7 @@
[/have_unit]
)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
@ -71,7 +71,7 @@
[/have_unit]
)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
@ -92,7 +92,7 @@
[/have_unit]
)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}
@ -127,6 +127,6 @@
[/have_unit]
)}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -138,6 +138,6 @@
[/do_command]
{ASSERT_YES_9_5}
{RETURN ([true][/true])}
{SUCCEED}
[/event]
)}

View file

@ -102,7 +102,7 @@
[event]
name= side turn end
{RETURN [false][/false]}
{FAIL}
[/event]
)}
@ -130,7 +130,7 @@
[event]
name= side turn end
{RETURN [false][/false]}
{FAIL}
[/event]
)}
@ -159,7 +159,7 @@
[event]
name= side turn end
{RETURN [false][/false]}
{FAIL}
[/event]
)}
@ -190,7 +190,7 @@
[/event]
[event]
name= side 2 turn end
{RETURN [false][/false]}
{FAIL}
[/event]
)}
@ -221,7 +221,7 @@
[/event]
[event]
name= side 2 turn end
{RETURN [false][/false]}
{FAIL}
[/event]
)}
@ -253,6 +253,6 @@
[/event]
[event]
name= side 2 turn end
{RETURN [false][/false]}
{FAIL}
[/event]
)}

View file

@ -147,3 +147,12 @@
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
0 check_interrupts_elseif
0 check_interrupts_case