Support [foreach]array=this_item, nested loops using the default name

Since a0ee38a49, the inner this_item was still in scope when the inner loop
writes changed data back to the outer loop's variable, which meant that
changes were silently ignored.

A lot of the changes in wml-flow.lua are just indentation because of the
extra block.
This commit is contained in:
Steve Cotton 2023-01-23 17:52:49 +01:00 committed by Steve Cotton
parent 49ebbb9709
commit 0a0263e54f
3 changed files with 92 additions and 27 deletions

View file

@ -179,36 +179,51 @@ function wml_actions.foreach(cfg)
local array = wml.array_variables[array_name]
if #array == 0 then return end -- empty and scalars unwanted
local item_name = cfg.variable or "this_item"
local this_item <close> = utils.scoped_var(item_name) -- if this_item is already set
local i_name = cfg.index_var or "i"
local i <close> = utils.scoped_var(i_name) -- if i is already set
local array_length = wml.variables[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 ~= wml.variables[array_name .. ".length"] then
wml.error("WML array length changed during [foreach] iteration")
end
wml.variables[item_name] = value
-- set index variable
wml.variables[i_name] = index-1 -- here -1, because of WML array
-- perform actions
for do_child in wml.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
-- Some protection against unsupported modification of the array while iterating. Changes to
-- via the WML variable named array_name will be ignored during the loop, and then overwritten
-- by the contents of "array" afterwards. The check_for_modifications is just trying to add an
-- error message instead of silently ignoring those changes.
local array_length = wml.variables[array_name .. ".length"]
local check_for_modifications = true
if array_name:find("^" .. item_name) then
-- Disable the check for WML such as [for]array=this_item or [for]array=this_item.abilities,
-- where the current item is shadowing the variable of the same name outside the loop.
check_for_modifications = false
end
do
local this_item <close> = utils.scoped_var(item_name)
local i <close> = utils.scoped_var(i_name)
for index, value in ipairs(array) do
wml.variables[item_name] = value
-- set index variable
wml.variables[i_name] = index-1 -- here -1, because of WML array
-- perform actions
for do_child in wml.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
-- Update the copy which will eventually be written back to the
-- array, in case the author made some modifications.
if not cfg.readonly then
array[index] = wml.variables[item_name]
end
if check_for_modifications and array_length ~= wml.variables[array_name .. ".length"] then
wml.error("WML array length changed during [foreach] iteration")
end
end
-- set back the content, in case the author made some modifications
if not cfg.readonly then
array[index] = wml.variables[item_name]
end
end
::exit::

View file

@ -0,0 +1,49 @@
# wmllint: no translatables
#####
# API(s) being tested: [foreach],[foreach]array=this_item
#
# The [foreach] loop implements local-scoping for the variables this_item and i.
# This tests the behavior when looping over an array that is itself called this_item.
##
# Actions:
# Simulate storing a unit with heals+4 to a variable.
# Boost the strength of the healing ability.
##
# Expected end state:
# The unit's healing ability is heals+8.
#####
{GENERIC_UNIT_TEST foreach_mutate_nested (
[event]
name=start
# A small subset of the result of storing a unit to a variable
{VARIABLE u.abilities[0].regenerate.value 4}
{VARIABLE u.abilities[1].heals.value 4}
# This makes the length of the inner this_item different to that of
# the outer this_item, to test that the sanity check for external
# modification isn't triggered by the matching names.
{VARIABLE u.abilities[1].heals.test_attribute 4}
[foreach]
array=u.abilities
[do]
[foreach]
array=this_item.heals
[do]
[set_variable]
name=this_item.value
value=8
[/set_variable]
[/do]
[/foreach]
[/do]
[/foreach]
{ASSERT ({VARIABLE_CONDITIONAL u.abilities[0].regenerate.value equals 4})}
{ASSERT ({VARIABLE_CONDITIONAL u.abilities[1].heals.value equals 8})}
{SUCCEED}
[/event]
)}

View file

@ -401,6 +401,7 @@
0 for_end2_step2
0 for_end-2
0 for_end-2_step-2
0 foreach_mutate_nested
# AI Config Parsing tests
0 test_basic_simplified_aspect
0 test_basic_abbreviated_aspect