[solved] Storing a test condition as a string

Discussion of Lua and LuaWML support, development, and ideas.

Moderator: Forum Moderators

Post Reply
white_haired_uncle
Posts: 1202
Joined: August 26th, 2018, 11:46 pm
Location: A country place, far outside the Wire

[solved] Storing a test condition as a string

Post by white_haired_uncle »

I have a table of preferences. For some preferences, I only want them to be visible if certain game events have occurred. So I'm looking to store my test condition as a string in the table for later execution. For example, the discovered field for the blood_rain item:

Code: Select all

table.insert(new_prefs,{ id = "show_map5", default = true , menu = "chapter5", discovered = true, type = "bool", value = true,
                description = _"Show world map on entering scenario (chapter 5)"})
table.insert(new_prefs,{ id = "show_map9", default = true , menu = "chapter9", discovered = true, type = "bool", value = true,
                description = _"Show world map on entering scenario (chapter 9)" })
table.insert(new_prefs,{ id = "blood_rain", default = true , menu = "chapter9", discovered = "return wml.variables.quests.seen_blood_rain == 1", type = "bool", value = true,
                        description = _"Show effects of blood rain" })
I'm not in love with that syntax, it's just what I have almost working, while under the assumption the "load" is the tool I want to use here.

Let's assume I want to determine whether or not to display the blood_rain preference. I can do this:

Code: Select all

a=(load(new_prefs[3].discovered))
if(a()) then ...
or

Code: Select all

status,value = pcall(load(new_prefs[3].discovered))
if status and value then ...
But what I can't figure out is how to do that in one line. Just execute the block and check the return value without the intermediate step of assigning said value to something first. I'm probably just being stubborn here, but I think I'm also displaying a fundamental lack of understanding of lua functions here and I'd like to fix that.
Last edited by white_haired_uncle on March 13th, 2024, 3:27 am, edited 1 time in total.
Speak softly, and carry Doombringer.
User avatar
Celtic_Minstrel
Developer
Posts: 2233
Joined: August 3rd, 2012, 11:26 pm
Location: Canada
Contact:

Re: Storing a test condition as a string

Post by Celtic_Minstrel »

I'll answer your direct question in a moment, but before that I want to question why you're storing the condition as a string of Lua code. Is new_prefs a general Lua table, or does it need to be valid WML?

Note: All these will assume you set the $quests.seen_blood_rain variable to yes instead of 1, which you probably should do anyway if it's meant to be a boolean value. That's not required for the examples to work, you just need to adjust them slightly if you have a good reason for it to be a number.
  1. First, let me suppose it doesn't have to be WML. In that case, you're better off just putting a function in the table, like this:

    Code: Select all

    table.insert(new_prefs, {
    	id = "blood_rain",
    	default = true,
    	menu = "chapter9",
    	discovered = function() return wml.variables.quests.seen_blood_rain end,
    	type = "bool",
    	value = true
    })
    
    And then when using it you can just call it:

    Code: Select all

    local discovered = new_prefs[3].discovered
    if type(discovered) == 'function' then discovered = discovered() end
    
  2. But, maybe it does have to be valid WML. If you need to serialize this data into the saved game value, it certainly needs to be WML-compatible, which means it can't contain functions. In that case, my personal recommendation would be to use WFL for the condition. The table would then look like this:

    Code: Select all

    table.insert(new_prefs, {
    	id = "blood_rain",
    	default = true,
    	menu = "chapter9",
    	-- Enclosing parenthesis not technically required, but I recommend them
    	-- as an easy way to distinguish a formula from a yes/no
    	discovered = "(quests.seen_blood_rain)"
    	type = "bool",
    	value = true
    })
    
    When using this version, you would need to evaluate the formula:

    Code: Select all

    local discovered = new_prefs[3].discovered
    if type(discovered) == 'string' then discovered = wesnoth.eval_formula(discovered, wml.all_variables) end
    
  3. There is another option you could take if it's required to be WML – you could use ConditionalWML. That method would look like this:

    Code: Select all

    local always_discovered = wml.tag.discovered{wml.tag['true']{}}
    table.insert(new_prefs,{ id = "show_map5", default = true , menu = "chapter5", always_discovered, type = "bool", value = true})
    table.insert(new_prefs, {
    	id = "blood_rain",
    	default = true,
    	menu = "chapter9",
    	wml.tag.discovered{ wml.tag.variable{ name="quest.seen_blood_rain", boolean_equals=true }}
    	type = "bool",
    	value = true
    })
    
    When using this version, you would need to evaluate the conditional:

    Code: Select all

    local discovered = wml.get_child(new_prefs[3], "discovered")
    if discovered then discovered = wml.eval_conditional(discovered) end
    
  4. Lastly, the answer to your question, as best I understand. As far as I can, your first example is, in fact, correct, but it will fail for items 1 and 2 because true is not a valid Lua statement. To make the table structure you showed work, calling it like this should be fine:

    Code: Select all

    local discovered = new_prefs[3].discovered
    if type(discovered) == 'string' then discovered = load(discovered)() end
    
    Though, that will raise an error if the string has a syntax error. If you'd prefer to ignore syntax errors and move on, something like this should work:

    Code: Select all

    local discovered = new_prefs[3].discovered
    if type(discovered) == 'string' then
    	local f, error = load(discovered)
    	if type(f) == 'function' then
    		discovered = f()
    		-- or, slightly safer:
    		local status, result = pcall(f)
    		if stats then
    			discovered = result
    		else
    			-- at this point, the result variable contains what went wrong, in case you want to do something with it
    			discovered = false
    		end
    	else
    		-- at this point, the error variable contains what went wrong, in case you want to do something with it
    		discovered = false
    	end
    end
    
    white_haired_uncle wrote: March 13th, 2024, 1:23 am But what I can't figure out is how to do that in one line.
    I can't think of any way to make a one-liner that will never throw an error. If you really want a one-liner though, the following should work:

    Code: Select all

    local discovered = load(new_prefs[3].discovered)()
    
    However, for that code to work, all your cases of discovered = true would need to be changed to discovered = "return true".
Author of The Black Cross of Aleron campaign and Default++ era.
Former maintainer of Steelhive.
white_haired_uncle
Posts: 1202
Joined: August 26th, 2018, 11:46 pm
Location: A country place, far outside the Wire

Re: Storing a test condition as a string

Post by white_haired_uncle »

Code: Select all

load(new_prefs[3].discovered)()
Thank you, that's the syntax I was looking for. As I rather suspected, it may not be the right way to accomplish the task, but it's the piece that was eluding me.

I stored the condition as lua code because that's what I could get to work. I'd prefer to just store the condition itself.

I assume this needs to be WML, since prefs.value at least will be accessed by WML and it will need to be saved between games (and perhaps even persist?).

quests.seen_blood_rain is 1, not yes, and that is more or less out of my control, unfortunately. Working around that should be the easiest part.

Solution #2 looks like the cleanest, though I'm unclear how it would handle errors. Solution #4 has my attention, mostly because the other day I was looking for a try/catch solution for lua and read about pcall and now I feel like I should be doing a lot more to trap errors as a habit (or I found a new toy and I'm looking for an excuse to play with it).

Thanks
Speak softly, and carry Doombringer.
User avatar
Celtic_Minstrel
Developer
Posts: 2233
Joined: August 3rd, 2012, 11:26 pm
Location: Canada
Contact:

Re: Storing a test condition as a string

Post by Celtic_Minstrel »

white_haired_uncle wrote: March 13th, 2024, 3:26 am it will need to be saved between games (and perhaps even persist?).
This means it will need to be WML, yes, so version #1 will not work for you.
white_haired_uncle wrote: March 13th, 2024, 3:26 am Solution #2 looks like the cleanest, though I'm unclear how it would handle errors.
Broadly speaking, it more or less doesn't, but it shouldn't crash in the event of an error. If there's an error in the formula string, you'll get nil back as the result, which would effectively mean that the item is never discovered.
white_haired_uncle wrote: March 13th, 2024, 3:26 am Solution #4 has my attention, mostly because the other day I was looking for a try/catch solution for lua and read about pcall and now I feel like I should be doing a lot more to trap errors as a habit (or I found a new toy and I'm looking for an excuse to play with it).
Indeed, pcall is Lua's try-catch equivalent. I would say that the closest way to attain a try-catch idiom in Lua would be like this:

Code: Select all

local status, error = pcall(function()
	-- do some stuff here
end)
if not status then
	-- do stuff with error here
end
Though more commonly pcall is used to call a function and get back its results if it's successful. If the function is defined like this:

Code: Select all

function f(a, b, c, d) return a + b, c .. d end
then the safe way to call it is like this:

Code: Select all

local status, result1, result2 = pcall(f, a, b, 123, 'stuff')
if status then
	-- do stuff with result1 and result2
else
	local error = result1
	-- do stuff with error
end
Side note: It's a bad idea to call your variables error in Lua, since that's the name of a function (and functions as Lua's throw-equivalent, in fact). Oh well.
Author of The Black Cross of Aleron campaign and Default++ era.
Former maintainer of Steelhive.
gnombat
Posts: 710
Joined: June 10th, 2010, 8:49 pm

Re: [solved] Storing a test condition as a string

Post by gnombat »

I think there's a 5th solution that could be used here. You would need to create a global function:

Code: Select all

-- This function needs to be global (we will see why later).
function blood_rain_discovered()
    return wml.variables.quests.seen_blood_rain == 1
end
Then store the name of this function (as a string) in the preferences table:

Code: Select all

table.insert(new_prefs,{ id = "blood_rain", default = true , menu = "chapter9", discovered = "blood_rain_discovered", type = "bool", value = true,
                        description = _"Show effects of blood rain" })
Finally, when you want to determine whether or not to display the blood_rain preference:

Code: Select all

-- We are looking for the function in the global variables table _G (this is why the function has to be global).
if _G[new_prefs[3].discovered]() then
    ...
I haven't tested any of this, so it's possible this might not work for some reason, but it seems like this would be better than using load()...
User avatar
Celtic_Minstrel
Developer
Posts: 2233
Joined: August 3rd, 2012, 11:26 pm
Location: Canada
Contact:

Re: [solved] Storing a test condition as a string

Post by Celtic_Minstrel »

That's another good solution, yes. However, instead of a global function, I'd recommend namespacing it:

Code: Select all

-- I'm starting the global variable with a capital letter only because the linting systems I use
-- consider lowercase names to be a probable error
PrefFunctions = {}
function PrefFunctions.blood_rain_discovered()
	return wml.variables.quests.seen_blood_rain == 1
end
Store it the same as what gnombat posted, then use it like this:

Code: Select all

if PrefFunctions[new_prefs[3].discovered]() then
Author of The Black Cross of Aleron campaign and Default++ era.
Former maintainer of Steelhive.
Post Reply