yl_speak_up/fs_edit_preconditions.lua
2021-07-12 00:35:33 +02:00

547 lines
21 KiB
Lua

-- TODO: check inscription of a sign?
-- TODO: invlist as dropdown of inventory lists at detected position
-- Which diffrent types of preconditions are available?
-- -> The following fields are part of a precondition:
-- p_id the ID/key of the precondition/prerequirement
-- p_type selected from values_what; the staffs allow to use other
-- types like "function" or "has_item" - but that is not
-- supported here (cannot be edited or created; only be shown)
-- p_value used to store the subtype of p_type
--
-- a state/variable:
-- p_variable name of a variable the player has read access to;
-- dropdown list with allowed options
-- p_operator selected from values_operator
-- p_var_cmp_value can be set freely by the player
--
-- a block in the world:
-- p_pos a position in the world; determined by asking the player
-- to punch the block
-- p_node (follows from p_pos)
-- p_param2 (follows from p_pos)
--
-- a trade defined as an action: no variables needed (buy and pay stack follow
-- from the trade set as action)
--
-- an inventory:
-- p_itemstack an itemstack; needs to be a minetest.registered_item[..];
-- size/count is also checked
-- some helper lists for creating the formspecs and evaulating
-- the player's answers:
-- general direction of what a prerequirement may be about
local check_what = {
"- please select -",
"an internal state (i.e. of a quest)", -- 2
"a block somewhere", -- 3
"a trade", -- 4
"the inventory of the player", -- 5
"the inventory of the NPC", -- 6
"execute Lua code (requires npc_master priv)", -- 7
"Call custom functions that are supposed to be overridden by the server.", -- 8
"The preconditions of another dialog option are fulfilled/not fulfilled.", -- 9
}
-- how to store these as p_type in the precondition:
local values_what = {"", "state", "block", "trade", "player_inv", "npc_inv",
-- requires npc_master priv
"function",
-- custom function (does not require npc_master priv)
"custom",
-- depends on the preconditions of another option
"other"}
-- options for "a trade"
local check_trade = {
"- please select -",
"The NPC has the item(s) he wants to sell in his inventory.", -- 2
"The player has the item(s) needed to pay the price.", -- 3
"The NPC ran out of stock.", -- 4
"The player cannot afford the price.", -- 5
}
-- how to store these as p_value:
local values_trade = {"", "npc_can_sell", "player_can_buy", "npc_is_out_of_stock", "player_has_not_enough"}
-- options for "the inventory of " (either player or NPC; perhaps blocks later on)
local check_inv = {
"- please select -",
"The inventory contains the following item:",
"The inventory *does not* contain the following item:",
"There is room for the following item in the inventory:",
"The inventory is empty.",
}
-- how to store these as p_value (the actual itemstack gets stored as p_itemstack):
local values_inv = {"", "inv_contains", "inv_does_not_contain", "has_room_for", "inv_is_empty"}
local check_block = {
"- please select -",
"The block is as it is now.",
"There shall be air instead of this block.",
"The block is diffrent from how it is now.",
"I can't punch it. The block is as the block *above* the one I punched.",
}
-- how to store these as p_value (the actual node data gets stored as p_node, p_param2 and p_pos):
-- Note: "node_is_like" occours twice because it is used to cover blocks that
-- cannot be punched as well as normal blocks.
local values_block = {"", "node_is_like", "node_is_air", "node_is_diffrent_from", "node_is_like"}
-- comparison operators for variables
local check_operator = {
"- please select -", -- 1
"== (is equal)", -- 2
"~= (is not equal)", -- 3
">= (is greater or equal)", -- 4
"> (is greater)", -- 5
"<= (is smaller or equal)", -- 6
"< (is smaller)", -- 7
"not (logically invert)", -- 8
"is_set (has a value)", -- 9
"is_unset (has no value)", -- 10
"more than x seconds ago", -- 11
"less than x seconds ago", -- 12
"has completed quest step", -- 13
"quest step *not* completed", -- 14
}
-- how to store these as p_value (the actual variable is stored in p_variable, and the value in p_cmp_value):
local values_operator = {"", "==", "~=", ">=", ">", "<=", "<", "not", "is_set", "is_unset",
"more_than_x_seconds_ago","less_than_x_seconds_ago",
"quest_step_done", "quest_step_not_done"}
-- some internal ones...
local check_variable = {
-- "- please select -", -- this is automaticly added to the var list
"(internal) hour of ingame day", -- 2
"(internal) player's health points", -- 3
}
-- get the list of variables the player has read access to
yl_speak_up.get_sorted_player_var_list_read_access = function(pname)
local var_list = {}
-- copy the values from check_variable
for i, v in ipairs(check_variable) do
table.insert(var_list, v)
end
-- get the list of variables the player can read
local tmp = yl_speak_up.get_quest_variables_with_read_access(pname)
-- sort that list (the dropdown formspec element returns just an index)
table.sort(tmp)
for i, v in ipairs(tmp) do
table.insert(var_list, v)
end
return var_list
end
-- returns a human-readable text as description of the precondition
-- (as shown in the edit options dialog and in the edit precondition formspec)
yl_speak_up.show_precondition = function(p, pname)
if(not(p.p_type) or p.p_type == "") then
return "(nothing): Always true."
elseif(p.p_type == "item") then
return "item: The player has \""..tostring(p.p_value).."\" in his inventory."
elseif(p.p_type == "quest") then
return "quest: Always false."
elseif(p.p_type == "auto") then
return "auto: Always true."
elseif(p.p_type == "function") then
return "function: evaluate "..tostring(p.p_value)
elseif(p.p_type == "state") then
local var_name = "VALUE_OF[ - ? - ]"
if(p.p_variable) then
var_name = "VALUE_OF[ "..tostring(
yl_speak_up.strip_pname_from_var(p.p_variable, pname)).." ]"
end
if(not(p.p_operator)) then
return "Error: Operator not defined."
elseif(p.p_operator == "not") then
return "not( "..var_name.." )"
elseif(p.p_operator == "is_set") then
return var_name.." ~= nil (is_set)"
elseif(p.p_operator == "is_unset") then
return var_name.." == nil (is_unset)"
elseif(p.p_operator == "more_than_x_seconds_ago") then
return var_name.." was set to current time "..
"*more* than "..tostring(p.p_var_cmp_value).." seconds ago"
elseif(p.p_operator == "less_than_x_seconds_ago") then
return var_name.." was set to current time "..
"*less* than "..tostring(p.p_var_cmp_value).." seconds ago"
elseif(p.p_operator == "quest_step_done") then
return var_name.." shows: player completed quest step \""..
tostring(p.p_var_cmp_value).."\" successfully"
elseif(p.p_operator == "quest_step_not_done") then
return var_name.." shows: player has not yet completed quest step \""..
tostring(p.p_var_cmp_value).."\""
end
if(p.p_var_cmp_value == "") then
return var_name.." "..tostring(p.p_operator).." \"\""
end
return var_name.." "..tostring(p.p_operator).." "..
tostring(p.p_var_cmp_value)
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 "ERROR: p.p_pos is "..minetest.serialize(p.p_pos)
elseif(p.p_value == "node_is_like") then
return "The block at "..minetest.pos_to_string(p.p_pos).." is \""..
tostring(p.p_node).."\" with param2: "..tostring(p.p_param2).."."
elseif(p.p_value == "node_is_air") then
return "There is no block at "..minetest.pos_to_string(p.p_pos).."."
elseif(p.p_value == "node_is_diffrent_from") then
return "There is another block than \""..tostring(p.p_node).."\" at "..
minetest.pos_to_string(p.p_pos)..", or it is at least "..
"rotated diffrently (param2 is not "..tostring(p.p_param2)..")."
end
elseif(p.p_type == "trade") then
local nr = table.indexof(values_trade, p.p_value)
if(nr and check_trade[ nr ]) then
return check_trade[ nr ]
end
elseif(p.p_type == "player_inv" or p.p_type == "npc_inv") then
local who = "The player"
if(p.p_type == "npc_inv") then
who = "The NPC"
end
if(p.p_value == "inv_contains") then
return who.." has \""..tostring(p.p_itemstack).."\" in his inventory."
elseif(p.p_value == "inv_does_not_contain") then
return who.." does not have \""..tostring(p.p_itemstack).."\" in his inventory."
elseif(p.p_value == "has_room_for") then
return who.." has room for \""..tostring(p.p_itemstack).."\" in his inventory."
elseif(p.p_value == "inv_is_empty") then
return who.." has an empty inventory."
end
elseif(p.p_type == "custom") then
return "Call custom function with param: \""..tostring(p.p_value).."\"."
elseif(p.p_type == "other") then
local fulfilled = "fulfilled"
if(not(p.p_fulfilled) or p.p_fulfilled ~= "true") then
fulfilled = "*not* fulfilled"
end
return "The preconditions for dialog option \""..tostring(p.p_value).."\" are "..
fulfilled.."."
end
-- fallback
return tostring(p.p_value)
end
-- this is called directly in yl_speak_up.get_fs_talkdialog
-- it returns a list of options whose preconditions are fulfilled
yl_speak_up.calculate_displayable_options = function(pname, d_options)
-- 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)
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)
local pname = player:get_player_name()
local n_id = yl_speak_up.speak_to[pname].n_id
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..")
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))) 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
-- 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)
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 == "function") then
-- 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) hour of ingame day", -- 2
elseif(p.p_variable == check_variable[2]) then
-- timeofday is between 0..1; translate to 24 hours
var_val = math.floor((minetest.get_timeofday() * 24)+0.5)
-- "(internal) player's health points", -- 3
elseif(p.p_variable == check_variable[3]) then
var_val = player:get_hp()
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
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) 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) 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) 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) 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) 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) 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) 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
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") then
local inv = nil
local inv_name = "main"
-- determine the right inventory
if(p.p_type == "player_inv") then
inv = player:get_inventory()
else
inv = minetest.get_inventory({type="detached",
name="yl_speak_up_npc_"..tostring(n_id)})
inv_name = "npc_main"
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 == "custom") then
-- execute the custom function
return yl_speak_up.precondition_custom(player, p.p_value)
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))
end
-- fallback - unknown type
return false
end
-- these are only wrapper functions for those in fs_edit_general.lua
yl_speak_up.input_fs_edit_preconditions = function(player, formname, fields)
return yl_speak_up.input_fs_edit_option_related(player, formname, fields,
"p_", "o_prerequisites", yl_speak_up.max_prerequirements,
"pre(C)ondition", "tmp_prereq",
"Please punch the block you want to check in your precondition!",
values_what, values_operator, values_block, values_trade, values_inv,
check_what, check_operator, check_block, check_trade, check_inv,
-- player variables with read access
yl_speak_up.get_sorted_player_var_list_read_access,
"edit_preconditions"
)
end
yl_speak_up.get_fs_edit_preconditions = function(player, table_click_result)
return yl_speak_up.get_fs_edit_option_related(player, table_click_result,
"p_", "o_prerequisites", yl_speak_up.max_prerequirements,
"pre(C)ondition", "tmp_prereq",
"What do you want to check in this precondition?",
values_what, values_operator, values_block, values_trade, values_inv,
check_what, check_operator, check_block, check_trade, check_inv,
-- player variables with read access
yl_speak_up.get_sorted_player_var_list_read_access,
-- show one precondition element
yl_speak_up.show_precondition,
"table_of_preconditions",
"The following expression shall be true:", "Operator:", "Value to compare with:",
"The following shall be true about the block:"
)
end