yl_speak_up/exec_eval_preconditions.lua

479 lines
18 KiB
Lua

-- This file contains what is necessary to execute/evaluate a precondition.
--
-- You can add your own custom functions the file:
-- in custom_functions_you_can_override.lua
-- this is called directly in yl_speak_up.get_fs_talkdialog
-- it returns a list of options whose preconditions are fulfilled
-- allow_recursion may be false - we need to avoid infinite loops
yl_speak_up.calculate_displayable_options = function(pname, d_options, allow_recursion)
-- Let's go through all the options and see if we need to display them to the user
local retval = {}
local player = minetest.get_player_by_name(pname)
if d_options == nil then
return {}
end
-- sort the entries by o_sort so that preconditions referencing options earlier in the
-- list can work without causing loops or the like
local sorted_list = yl_speak_up.get_sorted_options(d_options, "o_sort")
for i, o_k in ipairs(sorted_list) do
local o_v = d_options[ o_k ]
-- Can we display this option?
retval[o_k] = yl_speak_up.eval_all_preconditions(player, o_v.o_prerequisites, o_k, retval,o_v)
-- do we need to take care of an automatic autoanswer?
if(retval[o_k] and retval[o_k] == true and o_v.o_autoanswer and o_v.o_autoanswer == 1
and allow_recursion) then
-- abort here - because we already know which option needs to be selected next
retval["autoanswer"] = o_k
return retval
end
end
return retval
end
-- called by calculate_displayable_options(..);
-- returns false if a single precondition is false
-- Important: If something cannot be determined (i.e. the node is nil),
-- *both* the condition and its inverse condition may be
-- true (or false).
yl_speak_up.eval_all_preconditions = function(player, prereq, o_id, other_options_true_or_false, d_option)
local pname = player:get_player_name()
local n_id = yl_speak_up.speak_to[pname].n_id
-- if this is a quest step: check if its preconditions are fulfilled
-- (that way there doesn't have to be a manual precondition set for quests)
if(d_option and d_option.quest_id and d_option.quest_step) then
local d_id = yl_speak_up.speak_to[pname].d_id
yl_speak_up.debug_msg(player, n_id, o_id, "..checking quest step \""..
tostring(d_option.quest_step).."\" of quest \""..
tostring(d_option.quest_id).."\".")
if(not(yl_speak_up.quest_step_possible(player, d_option.quest_step, d_option.quest_id,
n_id, d_id, o_id))) then
yl_speak_up.debug_msg(player, n_id, o_id, "Quest step not available. Aborting.")
-- no need to look any further - once we hit a false, it'll stay false
return false
else
yl_speak_up.debug_msg(player, n_id, o_id, "OK. Quest step available.")
end
end
if(not(prereq)) then
yl_speak_up.debug_msg(player, n_id, o_id, "No preconditions given.")
-- no prerequirements? then they are automaticly fulfilled
return true
end
yl_speak_up.debug_msg(player, n_id, o_id, "Checking preconditions..")
-- we need to be fast and efficient here - and the properties stay fixed for the NPC
-- during this call, so we can cache them
local properties = yl_speak_up.get_npc_properties(pname)
for k, p in pairs(prereq) do
yl_speak_up.debug_msg(player, n_id, o_id, "..checking "..
tostring(p.p_id)..": "..yl_speak_up.show_precondition(p, pname))
if(not(yl_speak_up.eval_precondition(player, n_id, p, other_options_true_or_false, properties))) then
yl_speak_up.debug_msg(player, n_id, o_id, tostring(p.p_id)..
" -> is false. Aborting.")
-- no need to look any further - once we hit a false, it'll stay false
return false
end
end
-- all preconditions are true
yl_speak_up.debug_msg(player, n_id, o_id, "OK. All preconditions true.")
return true
end
-- helper function for yl_speak_up.eval_precondition
-- (needed by "state", "property" and "evaluate")
-- Parameters:
-- p.p_operator the operator (>, <, ==, is_set, ...) from values_operator
-- p.p_var_cmp_value the value against which we compare
-- var_val the current value - that one which we want to check
yl_speak_up.eval_precondition_with_operator = function(p, var_val)
if(p.p_operator == "not") then
return not(var_val)
elseif(p.p_operator == "is_set") then
return var_val ~= nil
elseif(p.p_operator == "is_unset") then
return var_val == nil
-- for security reasons: do this manually instead of just evaluating a term
elseif(p.p_operator == "==") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
-- best do these comparisons in string form to make sure both are of same type
return tostring(var_val) == tostring(p.p_var_cmp_value)
elseif(p.p_operator == "~=") then
return tostring(var_val) ~= tostring(p.p_var_cmp_value)
elseif(p.p_operator == ">=") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
-- compare numeric if possible
if(tonumber(var_val) and tonumber(p.p_var_cmp_value)) then
return tonumber(var_val) >= tonumber(p.p_var_cmp_value)
-- fallback: compare as strings
else
return tostring(var_val) >= tostring(p.p_var_cmp_value)
end
elseif(p.p_operator == ">") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
if(tonumber(var_val) and tonumber(p.p_var_cmp_value)) then
return tonumber(var_val) > tonumber(p.p_var_cmp_value)
else
return tostring(var_val) > tostring(p.p_var_cmp_value)
end
elseif(p.p_operator == "<=") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
if(tonumber(var_val) and tonumber(p.p_var_cmp_value)) then
return tonumber(var_val) <= tonumber(p.p_var_cmp_value)
else
return tostring(var_val) <= tostring(p.p_var_cmp_value)
end
elseif(p.p_operator == "<") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
if(tonumber(var_val) and tonumber(p.p_var_cmp_value)) then
return tonumber(var_val) < tonumber(p.p_var_cmp_value)
else
return tostring(var_val) < tostring(p.p_var_cmp_value)
end
elseif(p.p_operator == "more_than_x_seconds_ago") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
if(not(tonumber(var_val)) or not(tonumber(p.p_var_cmp_value))) then
return true
end
return (tonumber(var_val) + tonumber(p.p_var_cmp_value)) <
math.floor(minetest.get_us_time()/1000000)
elseif(p.p_operator == "less_than_x_seconds_ago") then
if(p.p_var_cmp_value == nil or var_val == nil) then
return false
end
if(not(tonumber(var_val)) or not(tonumber(p.p_var_cmp_value))) then
return false
end
return (tonumber(var_val) + tonumber(p.p_var_cmp_value)) >
minetest.get_us_time()/1000000
-- this is currently equivalent to >= but may change in the future
-- TODO: quest steps may be strings in the future
elseif(p.p_operator == "quest_step_done") then
-- if the variable is not set at all, then the quest step definitely
-- has not been reached yet
if((p.p_var_cmp_value == nil) or (var_val == nil)) then
return false
end
-- compare numeric if possible
if(tonumber(var_val) and tonumber(p.p_var_cmp_value)) then
return tonumber(var_val) >= tonumber(p.p_var_cmp_value)
-- fallback: compare as strings
else
return tostring(var_val) >= tostring(p.p_var_cmp_value)
end
-- this is currently equivalent to < but may change in the future
-- TODO: quest steps may be strings in the future
elseif(p.p_operator == "quest_step_not_done") then
-- if the variable is not set at all, then the quest step definitely
-- has not been reached yet
if((p.p_var_cmp_value == nil) or (var_val == nil)) then
return true
end
if(tonumber(var_val) and tonumber(p.p_var_cmp_value)) then
return tonumber(var_val) < tonumber(p.p_var_cmp_value)
else
return tostring(var_val) < tostring(p.p_var_cmp_value)
end
end
-- unsupported operator
return false
end
-- checks if precondition p is true for the player and npc n_id
yl_speak_up.eval_precondition = function(player, n_id, p, other_options_true_or_false, properties)
if(not(p.p_type) or p.p_type == "") then
-- empty prerequirement: automaticly true (fallback)
return true
elseif(p.p_type == "item") then
-- a precondition set by using the staff;
-- aequivalent to p.p_type == "player_inv" and p.p_itemstack == "inv_contains"
return player:get_inventory():contains_item("main", p.p_value)
elseif(p.p_type == "quest") then
-- a precondition set by using the staff; intended as future quest interface?
return false
elseif(p.p_type == "auto") then
-- a precondition set by using the staff; kept for compatibility
return true
elseif(p.p_type == "true") then
-- mostly useful for generic dialogs
return true
elseif(p.p_type == "false") then
-- mostly useful for temporally disabling options
return false
elseif(p.p_type == "function") then
if(not(yl_speak_up.npc_has_priv(n_id, "precon_exec_lua", p.p_is_generic))) then
return false
end
-- a precondition set by using the staff;
-- extremly powerful (executes any lua code)
return yl_speak_up.eval_and_execute_function(player, p, "p_")
elseif(p.p_type == "state") then
local var_val = false
if(not(p.p_variable) or p.p_variable == "") then
-- broken precondition
return false
-- internal custom server functions for preconditions
elseif(table.indexof(yl_speak_up.custom_server_functions.precondition_descriptions,
p.p_variable) > -1) then
-- evaluate it
var_val = yl_speak_up.custom_server_functions.precondition_eval(
player, p.p_variable, p)
if(p.p_operator == "true_for_param") then
return var_val
elseif(p.p_operator == "false_for_param") then
return not(var_val)
end
else
local pname = player:get_player_name()
local owner = yl_speak_up.npc_owner[ n_id ]
-- get the value of the variable
-- the owner is alrady encoded in the variable name
var_val = yl_speak_up.get_quest_variable_value(pname, p.p_variable)
end
-- actually evaulate it
return yl_speak_up.eval_precondition_with_operator(p, var_val)
elseif(p.p_type == "property") then
-- fallback in case this function is called alone, without properties
if(not(properties)) then
local pname = player:get_player_name()
properties = yl_speak_up.get_npc_properties(pname)
end
return yl_speak_up.eval_precondition_with_operator(p, properties[p.p_value])
elseif(p.p_type == "evaluate") then
if(not(player) or not(p.p_value)) then
return false
end
local custom_data = yl_speak_up.custom_functions_p_[p.p_value]
if(not(custom_data) or not(custom_data.code)) then
return false
end
local fun = custom_data.code
-- actually call the function
local ret = fun(player, n_id, p)
-- compare the result with wat is expected
return yl_speak_up.eval_precondition_with_operator(p, ret)
elseif(p.p_type == "block") then
if(not(p.p_pos) or type(p.p_pos) ~= "table"
or not(p.p_pos.x) or not(p.p_pos.y) or not(p.p_pos.z)) then
return false
elseif(p.p_value == "node_is_like") then
local node = minetest.get_node_or_nil(p.p_pos)
return (node and node.name and node.name == p.p_node and node.param2 == p.p_param2)
elseif(p.p_value == "node_is_air") then
local node = minetest.get_node_or_nil(p.p_pos)
return (node and node.name and node.name == "air")
elseif(p.p_value == "node_is_diffrent_from") then
local node = minetest.get_node_or_nil(p.p_pos)
return (node and node.name and (node.name ~= p.p_node or node.param2 ~= p.p_param2))
end
-- fallback - unsupported option
return false
elseif(p.p_type == "trade") then
local pname = player:get_player_name()
local dialog = yl_speak_up.speak_to[pname].dialog
local n_id = yl_speak_up.speak_to[pname].n_id
local d_id = yl_speak_up.speak_to[pname].d_id
local o_id = yl_speak_up.speak_to[pname].o_id
-- if there is no trade, then this condition is true
if(not(dialog) or not(dialog.trades) or not(d_id) or not(o_id)) then
return true
end
local trade = dialog.trades[ tostring(d_id).." "..tostring(o_id) ]
-- something is wrong with the trade
if(not(trade)
or not(trade.pay) or not(trade.pay[1]) or not(trade.buy) or not(trade.buy[1])) then
return false
end
if( p.p_value == "npc_can_sell") then
local npc_inv = minetest.get_inventory({type="detached",
name="yl_speak_up_npc_"..tostring(n_id)})
return npc_inv:contains_item("npc_main", trade.buy[1])
elseif(p.p_value == "npc_is_out_of_stock") then
local npc_inv = minetest.get_inventory({type="detached",
name="yl_speak_up_npc_"..tostring(n_id)})
return not(npc_inv:contains_item("npc_main", trade.buy[1]))
elseif(p.p_value == "player_can_buy") then
local player_inv = player:get_inventory()
return player_inv:contains_item("main", trade.pay[1])
elseif(p.p_value == "player_has_not_enough") then
local player_inv = player:get_inventory()
return not(player_inv:contains_item("main", trade.pay[1]))
end
return false
elseif(p.p_type == "player_inv" or p.p_type == "npc_inv" or p.p_type == "block_inv") then
local inv = nil
local inv_name = "main"
-- determine the right inventory
if(p.p_type == "player_inv") then
inv = player:get_inventory()
elseif(p.p_type == "npc_inv") then
inv = minetest.get_inventory({type="detached",
name="yl_speak_up_npc_"..tostring(n_id)})
inv_name = "npc_main"
elseif(p.p_type == "block_inv") then
if(not(p.p_pos) or type(p.p_pos) ~= "table"
or not(p.p_pos.x) or not(p.p_pos.y) or not(p.p_pos.z)) then
return false
end
local meta = minetest.get_meta(p.p_pos)
if(not(meta)) then
return false
end
inv = meta:get_inventory()
if(not(inv)) then
return false
end
inv_name = p.p_inv_list_name
end
if( p.p_itemstack and p.p_value == "inv_contains") then
return inv:contains_item(inv_name, p.p_itemstack)
elseif(p.p_itemstack and p.p_value == "inv_does_not_contain") then
return not(inv:contains_item(inv_name, p.p_itemstack))
elseif(p.p_itemstack and p.p_value == "has_room_for") then
return inv:room_for_item(inv_name, p.p_itemstack)
elseif(p.p_value == "inv_is_empty") then
return inv:is_empty(inv_name)
end
return false
elseif(p.p_type == "player_offered_item") then
local pname = player:get_player_name()
local inv = minetest.get_inventory({type="detached", name="yl_speak_up_player_"..pname})
local stack_got = inv:get_stack("npc_wants",1)
if(stack_got:is_empty()) then
return false -- empty stack
end
local stack_wanted = ItemStack(p.p_value)
-- check group
if(p.p_item_group and p.p_item_group ~= "") then
local g = minetest.get_item_group(stack_got:get_name(), p.p_item_group)
if(not(g) or g == 0) then
return false -- wrong group
end
-- or: check item name
elseif(stack_got:get_name() ~= stack_wanted:get_name()) then
return false -- wrong item
end
-- independent of that: check stack size
if(p.p_match_stack_size and p.p_match_stack_size ~= "") then
local c_got = stack_got:get_count()
local c_wanted = stack_wanted:get_count()
if( p.p_match_stack_size == "exactly" and c_got ~= c_wanted) then
return false -- not exactly the same amount as the wanted one
elseif(p.p_match_stack_size == "less" and c_got >= c_wanted) then
return false -- didn't get less than the given number
elseif(p.p_match_stack_size == "more" and c_got <= c_wanted) then
return false -- didn't get more than the given number
elseif(p.p_match_stack_size == "another" and c_got == c_wanted) then
return false -- got the same than the given number
end
end
-- check quest_id
if(p.p_item_quest_id and p.p_item_quest_id ~= "") then
local meta = stack_got:get_meta()
-- we don't check here if the item was given by the right NPC;
-- only the quest id has to fit
if(meta:get_string("yl_speak_up:quest_id") ~= p.p_item_quest_id) then
return false -- wrong quest_id
end
-- was this quest item given to another player?
if(meta:get_string("yl_speak_up:quest_item_for") ~= pname) then
return false -- wrong player
end
end
-- all ok
return true
elseif(p.p_type == "other") then
-- are the preconditions of another option fulfilled?
return (p.p_value
and other_options_true_or_false
and other_options_true_or_false[ p.p_value ] ~= nil
and tostring(other_options_true_or_false[ p.p_value ]) == tostring(p.p_fulfilled))
elseif(p.p_type == "entity_type") then
local pname = player:get_player_name()
-- is the NPC type the same as the requested entity_type?
return (p.p_value
and yl_speak_up.speak_to[pname]._self
and yl_speak_up.speak_to[pname]._self.name
and yl_speak_up.speak_to[pname]._self.name == p.p_value)
end
-- fallback - unknown type
return false
end
-- helper function for yl_speak_up.eval_trade_list_preconditions
yl_speak_up.eval_precondition_npc_inv = function(p, inv, inv_name)
if(p.p_type ~= "npc_inv") then
return false
end
-- determine the right inventory
if( p.p_itemstack and p.p_value == "inv_contains") then
return inv:contains_item(inv_name, p.p_itemstack)
elseif(p.p_itemstack and p.p_value == "inv_does_not_contain") then
return not(inv:contains_item(inv_name, p.p_itemstack))
elseif(p.p_itemstack and p.p_value == "has_room_for") then
return inv:room_for_item(inv_name, p.p_itemstack)
elseif(p.p_value == "inv_is_empty") then
return inv:is_empty(inv_name)
end
return false
end
-- cheaper version of eval_all_preconditions for the trade_list (d_trade);
-- returns the ID of the first option where the precondition match or nil;
-- *only* preconditions of the type "npc_inv" are evaluated!
yl_speak_up.eval_trade_list_preconditions = function(player)
local pname = player:get_player_name()
local dialog = yl_speak_up.speak_to[pname].dialog
local n_id = yl_speak_up.speak_to[pname].n_id
if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs["d_trade"])
or not(dialog.n_dialogs["d_trade"].d_options)) then
return
end
local options = dialog.n_dialogs["d_trade"].d_options
local sorted_o_list = yl_speak_up.get_sorted_options(options, "o_sort")
local inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)})
local inv_name = "npc_main"
-- search through all options
for i, s_o_id in ipairs(sorted_o_list) do
local prereq = options[s_o_id].o_prerequisites
local all_ok = true
for k, p in pairs(prereq) do
if(not(yl_speak_up.eval_precondition_npc_inv(p, inv, inv_name))) then
all_ok = false
break
end
end
if(all_ok) then
return s_o_id
end
end
end