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