-- Which diffrent types of effects are available? -- -> The following fields are part of an effect/result: -- r_id the ID/key of the effect/result -- r_type selected from values_what; the staffs allow to use other -- types like "function" or "give_item" etc. - but that is not -- supported here (cannot be edited or created; only be shown) -- r_value used to store the subtype of r_type -- -- a state/variable ("state"): -- r_variable name of a variable the player has *write* access to; -- dropdown list with allowed options -- r_operator selected from values_operator -- r_var_cmp_value can be set freely by the player (the variable will be -- set to this value) -- -- a block in the world ("block"): -- r_pos a position in the world; determined by asking the player -- to punch the block -- r_node (follows from r_pos) -- r_param2 (follows from r_pos) -- -- place an item into the inventory of a block (i.e. a chest; "put_into_block_inv"): -- r_pos the position of the target block -- r_inv_list_name the inventory list where the item shall be moved to (often "main") -- r_itemstack the itemstack that is to be moved -- -- take item out of the inventory of a block (i.e. a chest; "take_from_block_inv"); -- same as "put_into_block_inv" -- -- accept items the player has given to the NPC ("deal_with_offered_item"): -- r_value subtype; one of yl_speak_up.dropdown_values_deal_with_offered_item -- -- a craft receipe ("craft"): -- r_value the expected craft result -- r_craft_grid array containing the stacks in the 9 craft grid fields in string form -- -- on_failure ("on_failure"): -- r_value alternate target dialog if the previous *effect* failed -- -- chat_all ("chat_all"): -- r_value chat message sent to all players -- -- -- give item to player ("give_item"): requires npc_master priv -- r_value the itemstack that shall be added to the player's inventory -- -- take item from player's inventory ("take_item"): requires npc_master priv -- r_value the itemstack that will be removed from the player's inventory -- -- move the player to a position ("move"): requires npc_master priv -- r_value the position where the player shall be moved to -- -- execute lua code ("function"): requires npc_master priv -- r_value the lua code that shall be executed -- -- call a custom function ("custom"): -- r_value parameter that is passed on to the custom function -- -- Unlike in preconditions, trade (the trade action already happened) and -- inventory actions are not supported as effects. -- -- some helper lists for creating the formspecs and evaulating -- the player's answers: -- general direction of what could make up an effect local check_what = { "- please select -", "an internal state (i.e. of a quest)", -- 2 "a block somewhere", -- 3 "put item from the NPC's inventory into a chest etc.", -- 4 "take item from a chest etc. and put it into the NPC's inventory", -- 5 "an item the player offered to the NPC", "NPC crafts something", -- 6 "go to other dialog if the previous effect failed", -- 7 "send a chat message to all players", -- 8 "give item (created out of thin air) to player (requires npc_master priv)", -- 9 "take item from player and destroy it (requires npc_master priv)", -- 10 "move the player to a given position (requires npc_master priv)", -- 11 "execute Lua code (requires npc_master priv)", -- 12 "Call custom functions that are supposed to be overridden by the server.", -- 13 } -- how to store these as r_type in the precondition: local values_what = {"", "state", "block", -- interact with the inventory of blocks on the map "put_into_block_inv", "take_from_block_inv", -- the player gave an item to the NPC; now deal with it somehow "deal_with_offered_item", -- crafting, handling failure, send chat message to all "craft", "on_failure", "chat_all", -- the following require the npc_master priv: "give_item", "take_item", "move", "function", -- custom function (does not require npc_master priv) "custom"} -- unlike in the preconditions, the "I cannot punch it" option is -- not offered here - because the player (and later the NPC) needs -- to be able to build at this position local check_block = { "- please select -", -- 1 "If there is air: Place a block so that it looks like now.", -- 2 "If there is a block: Dig it.", -- 3 "Punch the block.", -- 4 "Right-click the block.", -- 5 } -- how to store these as p_value (the actual node data gets stored as p_node, p_param2 and p_pos): local values_block = {"", "place", "dig", "punch", "right-click"} -- comparison operators for variables local check_operator = { "- please select -", -- 1 "new value:", -- 2 "discard/unset/forget", -- 3 "current time", -- 4 "quest step completed:", -- 5 minetest.formspec_escape("max(current, new_value)"), -- 6 minetest.formspec_escape("min(current, new_value)"), -- 7 "increment by:", -- 8 "decrement by:", -- 9 } -- how to store these as r_value (the actual variable is stored in r_variable, and the value in r_new_value): local values_operator = {"", "set_to", "unset", "set_to_current_time", "quest_step", "maximum", "minimum", "increment", "decrement"} -- for "deal_with_offered_item", used i.e. in yl_speak_up.get_fs_edit_option_effect_deal_with_offered_item yl_speak_up.dropdown_list_deal_with_offered_item = { minetest.formspec_escape("- please select -"), "Take the expected part of the offered item(s) and put them in the NPC's inventory.", "Accept all of the offered item(s) and put them in the NPC's inventory.", "Refuse and give the item(s) back." } -- the values that will be stored for yl_speak_up.dropdown_list_deal_with_offered_item above yl_speak_up.dropdown_values_deal_with_offered_item = { "do_nothing", "take_as_wanted", "take_all", "refuse_all"} -- get the list of variables the player has *write* access to yl_speak_up.get_sorted_player_var_list_write_access = function(pname) local var_list = {} -- some values - like hour of day or HP of the player - can be read in -- a precondition but not be modified -- get the list of variables the player can *write* local tmp = yl_speak_up.get_quest_variables_with_write_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 -- check if a block of type node_name is blacklisted for a certain interaction -- (this is needed if a block is not prepared for interaction with NPC and -- expects to always be dealing with a player) -- Parameters: -- how how to interact with the node -- node_name the node to place -- node_there the node that can currently be found at that position yl_speak_up.check_blacklisted = function(how, node_name, node_there) return yl_speak_up.blacklist_effect_on_block_interact[ node_name ] or yl_speak_up.blacklist_effect_on_block_interact[ node_there ] or (how == "place" and yl_speak_up.blacklist_effect_on_block_place[ node_name ]) or (how == "dig" and yl_speak_up.blacklist_effect_on_block_dig[ node_there ]) or (how == "punch" and yl_speak_up.blacklist_effect_on_block_punch[ node_there ]) or (how == "right-click" and yl_speak_up.blacklist_effect_on_block_right_click[ node_there]) or (how == "put" and yl_speak_up.blacklist_effect_on_block_right_click[ node_there]) or (how == "take" and yl_speak_up.blacklist_effect_on_block_right_click[ node_there]) end -- returns a human-readable text as description of the effects -- (as shown in the edit options dialog and in the edit effect formspec) yl_speak_up.show_effect = function(r, pname) if(not(r.r_type) or r.r_type == "") then return "(nothing): Nothing to do. No effect." elseif(r.r_type == "give_item") then return "give_item: Add \""..tostring(r.r_value).."\" to the player's inventory." elseif(r.r_type == "take_item") then return "take_item: Take \""..tostring(r.r_value).."\" from the player's inventory." elseif(r.r_type == "move") then return "move: Move the player to "..tostring(r.r_value).."." elseif(r.r_type == "function") then return "function: execute \""..tostring(r.r_value).."\"." elseif(r.r_type == "trade") then return "trade: obsolete (now defined as an action)" elseif(r.r_type == "dialog") then return "Switch to dialog \""..tostring(r.r_value).."\"." elseif(r.r_type == "state") then local var_name = "VARIABLE[ - ? - ]" if(r.r_variable) then var_name = "VARIABLE[ "..tostring( yl_speak_up.strip_pname_from_var(r.r_variable, pname)).." ]" end if(not(r.r_operator)) then return "Error: Operator not defined." elseif(r.r_operator == "set_to") then return "set "..var_name.." to value \"".. tostring(r.r_var_cmp_value).."\"" elseif(r.r_operator == "unset") then return "discard "..var_name.." (unset)" elseif(r.r_operator == "set_to_current_time") then return "set "..var_name.." to the current time" elseif(r.r_operator == "quest_step") then return "store that the player has completed quest step \"".. tostring(r.r_var_cmp_value).."\"" elseif(r.r_operator == "maximum") then return "set "..var_name.." to value \"".. tostring(r.r_var_cmp_value).."\" if its current value is larger than that" elseif(r.r_operator == "minimum") then return "set "..var_name.." to value \"".. tostring(r.r_var_cmp_value).."\" if its current value is lower than that" elseif(r.r_operator == "increment") then return "increment the value of "..var_name.." by \"".. tostring(r.r_var_cmp_value).."\"" elseif(r.r_operator == "decrement") then return "decrement the value of "..var_name.." by \"".. tostring(r.r_var_cmp_value).."\"" else return "ERROR: Wrong operator \""..tostring(r.r_operator).."\" for "..var_name end elseif(r.r_type == "block") then if(not(r.r_pos) or type(r.r_pos) ~= "table" or not(r.r_pos.x) or not(r.r_pos.y) or not(r.r_pos.z)) then return "ERROR: r.r_pos is "..minetest.serialize(r.r_pos) -- we don't check here yet which node is actually there - that will be done upon execution elseif(yl_speak_up.check_blacklisted(r.r_value, r.r_node, r.r_node)) then return "ERROR: Blocks of type \""..tostring(r.r_node).."\" do not allow ".. "interaction of type \""..tostring(r.r_value).."\" for NPC." elseif(r.r_value == "place") then return "Place \""..tostring(r.r_node).."\" with param2: "..tostring(r.r_param2).. " at "..minetest.pos_to_string(r.r_pos).."." elseif(r.r_value == "dig") then return "Dig the block at "..minetest.pos_to_string(r.r_pos).."." elseif(r.r_value == "punch") then return "Punch the block at "..minetest.pos_to_string(r.r_pos).."." elseif(r.r_value == "right-click") then return "Right-click the block at "..minetest.pos_to_string(r.r_pos).."." else return "ERROR: Don't know what to do with the block at ".. minetest.pos_to_string(r.r_pos)..": \""..tostring(r.r_value).."\"?" end elseif(r.r_type == "craft") then -- this is only shown in the edit options menu and when editing an effect; -- we can afford a bit of calculation here (it's not a precondtion...) if(not(r.r_value) or not(r.r_craft_grid)) then return "ERROR: Crafting not configured correctly." end local craft_str = "Craft \""..tostring(r.r_value).."\" from ".. table.concat(r.r_craft_grid, ", ").."." -- check here if the craft receipe is broken local input = {} input.items = {} for i, v in ipairs(r.r_craft_grid) do input.items[ i ] = ItemStack(v or "") end input.method = "normal" -- normal crafting; no cooking or fuel or the like input.width = 3 local output, decremented_input = minetest.get_craft_result(input) if(output.item:is_empty()) then return "Error: Recipe changed! No output for "..craft_str end -- the craft receipe may have changed in the meantime and yield a diffrent result local expected_stack = ItemStack(r.r_value) if(output.item:get_name() ~= expected_stack:get_name() or output.item:get_count() ~= expected_stack:get_count()) then return "Error: Amount of output changed! "..craft_str end return craft_str elseif(r.r_type == "on_failure") then return "If the *previous* effect failed, go to dialog \""..tostring(r.r_value).. "\"." elseif(r.r_type == "chat_all") then return "Send chat message: \""..tostring(r.r_value).."\"" elseif(r.r_type == "custom") then return "Call custom function with param: \""..tostring(r.r_value).."\"." elseif(r.r_type == "put_into_block_inv") then if(not(r.r_pos) or type(r.r_pos) ~= "table" or not(r.r_pos.x) or not(r.r_pos.y) or not(r.r_pos.z)) then return "ERROR: r.r_pos is "..minetest.serialize(r.r_pos) end return "Put item \""..tostring(r.r_itemstack).."\" from NPC inv into block at ".. minetest.pos_to_string(r.r_pos).. " in inventory list \""..tostring(r.r_inv_list_name).."\"." elseif(r.r_type == "take_from_block_inv") then if(not(r.r_pos) or type(r.r_pos) ~= "table" or not(r.r_pos.x) or not(r.r_pos.y) or not(r.r_pos.z)) then return "ERROR: r.r_pos is "..minetest.serialize(r.r_pos) end return "Take item \""..tostring(r.r_itemstack).."\" from block at ".. minetest.pos_to_string(r.r_pos).. " out of inventory list \""..tostring(r.r_inv_list_name).. "\" and put it into the NPC's inventory." elseif(r.r_type == "deal_with_offered_item") then local nr = 1 if(r.r_value) then nr = math.max(1, table.indexof(yl_speak_up.dropdown_values_deal_with_offered_item, r.r_value)) return yl_speak_up.dropdown_list_deal_with_offered_item[ nr ] end return "ERROR: Missing subtype r.r_value: \""..tostring(r.r_value).."\"" end -- fallback return tostring(r.r_value) end -- called by yl_speak_up.input_talk(..) -- -- This function is called *after* the player has clicked on an option -- and *after* any actions (i.e. trade) have been completed either -- successfully (=action_was_succesful is true) or not. -- Unlike the preconditions, the effects are executed in ordered form, -- ordered by their r_id. -- Returns the new target dialog that is to be displayed next. This will -- usually be the one with the r_type "dialog" - unless r_type "on_failure" -- was encountered after an unsuccessful action *or* right after an -- effect that returned false. -- Note: In edit mode, effects will *not* be executed. yl_speak_up.execute_all_relevant_effects = function(player, effects, o_id, action_was_successful) local target_dialog = "" local pname = player:get_player_name() local n_id = yl_speak_up.speak_to[pname].n_id if(not(effects)) then yl_speak_up.debug_msg(player, n_id, o_id, "No effects given.") -- no effects? Then...return to the start dialog return {next_dialog = "", alternate_text = nil} end local edit_mode = (yl_speak_up.edit_mode[pname] == n_id) -- Important: the list of effects is *sorted* here. The order remains constant! local sorted_key_list = yl_speak_up.sort_keys(effects) if(not(sorted_key_list) or #sorted_key_list < 1) then yl_speak_up.debug_msg(player, n_id, o_id, "Error: No effects found. At least one of ".. "type \"dialog\" is necessary.") elseif(not(edit_mode)) then yl_speak_up.debug_msg(player, n_id, o_id, "Executing effects: ".. table.concat(sorted_key_list, ", ")..".") else yl_speak_up.debug_msg(player, n_id, o_id, "Not executing effects because in edit mode.") end -- failed actions may set an alternate text local alternate_text = nil local last_result = action_was_successful local res = true for i, k in ipairs(sorted_key_list) do local r = effects[ k ] yl_speak_up.debug_msg(player, n_id, o_id, "..executing ".. tostring(r.r_id)..": "..yl_speak_up.show_effect(r, pname)) -- do not execute effects in edit mode if(not(edit_mode)) then yl_speak_up.debug_msg(player, n_id, o_id, "Executing effect "..tostring(r.r_id)..".") res = yl_speak_up.execute_effect(player, n_id, o_id, r) if(not(res)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).. " -> Effect failed to execute.") alternate_text = r.alternate_text end else -- in edit mode: assume that the effect was successful res = true end -- "dialog" gives us the normal target_dialog if(r.r_type and r.r_type == "dialog") then target_dialog = r.r_value alternate_text = r.alternate_text -- "on_failure" gives an alternate target dialog if the action -- or last effect failed elseif(r.r_type and r.r_type == "on_failure" and r.r_value and not(last_result)) then yl_speak_up.debug_msg(player, n_id, o_id, "Aborted executing effects at ".. tostring(r.r_id)..". New target dialog: "..tostring(r.r_value)..".") -- we also stop execution here return {next_dialog = r.r_value, alternate_text = r.alternate_text} end last_result = res end -- all preconditions are true yl_speak_up.debug_msg(player, n_id, o_id, "Finished executing effects.") return {next_dialog = target_dialog, alternate_text = alternate_text} end -- executes an effect/result r for the player and npc n_id; -- returns true on success (relevant for on_failure) -- Note: In edit mode, this function does not get called. yl_speak_up.execute_effect = function(player, n_id, o_id, r) if(not(r.r_type) or r.r_type == "") then -- nothing to do return true elseif(r.r_type == "auto" or r.r_type == "trade") then -- these effects don't do anything return true elseif(r.r_type == "put_into_block_inv" or r.r_type == "take_from_block_inv") then -- get the inventory of the block if(not(r.r_pos) or type(r.r_pos) ~= "table" or not(r.r_pos.x) or not(r.r_pos.y) or not(r.r_pos.z)) then -- position not found? yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": No or incorrect position given: ".. minetest.serialize(r.rp_pos)..".") return false end local meta = minetest.get_meta(r.r_pos) if(not(meta)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": Failed to get metadata at ".. minetest.serialize(r.rp_pos)..".") return false end local inv = meta:get_inventory() if(not(inv)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": Failed to get inventory at ".. minetest.serialize(r.rp_pos)..".") return false end local inv_name = r.r_inv_list_name -- get the inventory of the npc local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)}) local npc_inv_name = "npc_main" -- for easier checking local how_to_interact = "take" if(r.r_type and r.r_type == "put_into_block_inv") then how_to_interact = "put" end local stack = ItemStack(r.r_itemstack) -- is there enough room for the item? if(how_to_interact == "put" and not(inv:room_for_item(inv_name, stack))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": No room for \"".. minetest.serialize(r.r_itemstack).."\"".. " in node at ".. minetest.serialize(r.r_pos)..", inv list \"".. minetest.serialize(inv_name).."\".") return false elseif(how_to_interact == "take" and not(npc_inv:room_for_item("npc_main", stack))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": NPC has no room for \"".. minetest.serialize(r.r_itemstack).."\".") return false end -- does the item exist? if(how_to_interact == "put" and not(npc_inv:contains_item(npc_inv_name, stack, false))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": NPC does not have \"".. minetest.serialize(r.r_itemstack).."\".") return false elseif(how_to_interact == "take" and not(inv:contains_item(inv_name, stack, false))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": Block at "..minetest.serialize(r.r_pos).. " does not contain \""..tostring(r.r_itemstack).."\" in list \"".. tostring(r.r_inv_list).."\".") return false end -- check the blacklist local node = minetest.get_node(r.r_pos) if(not(node) or not(node.name)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": No node found at "..minetest.serialize(r.r_pos)..".") return false end -- do not interact with nodes on the blacklist if(yl_speak_up.check_blacklisted(how_to_interact, node.name, node.name)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": Blocks of type \""..tostring(node.name).."\" do not allow ".. "interaction of type \""..tostring(r.r_value).."\" for NPC.") return false end -- construct a fake player local owner_name = yl_speak_up.npc_owner[ n_id ] if(not(owner_name) or owner_name == "") then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": NPC does not have an owner. Aborting.") return false end -- act in the name of the owner when accessing inventories local fake_player = { get_player_name = function() return owner_name end, is_player = function() return true end, is_fake_player = true, get_wielded_item = function(self, item) if(self._inventory and def.wield_list) then return self._inventory:get_stack(def.wield_list, self._wield_index) end return ItemStack(self._wielded_item) end, } -- TODO: get the fake player from pipeworks? local def = minetest.registered_nodes[ node.name ] if(def and def[ "allow_metadata_inventory_"..how_to_interact ]) then local res = def[ "allow_metadata_inventory_"..how_to_interact ]( r.r_pos, inv_name, 1, ItemStack(r.r_itemstack), fake_player) if(not(res) or res < ItemStack(r.r_itemstack):get_count()) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. r.r_type..": allow_metadata_inventory_"..tostring(how_to_interact).. " forbits interaction at "..minetest.serialize(r.r_pos)..".") return false end end -- all ok so far; we can proceed if(how_to_interact == "put") then local r1 = npc_inv:remove_item(npc_inv_name, stack) local r2 = inv:add_item(inv_name, r1) return true elseif(how_to_interact == "take") then local r1 = inv:remove_item(inv_name, stack) local r2 = npc_inv:add_item(npc_inv_name, r1) return true end return false elseif(r.r_type == "dialog" or r.r_type == "on_failure") then -- this needs to be handled in the calling function return true elseif(r.r_type == "function") then -- this can only be set and edited with the staff return yl_speak_up.eval_and_execute_function(player, r, "r_") -- this can only be set and edited with the staff elseif(r.r_type == "give_item") then if(not(r.r_value)) then return false end local item = ItemStack(r.r_value) if(not(minetest.registered_items[item:get_name()])) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "give_item: "..tostring(item:get_name()).." unknown.") return false end local r = player:get_inventory():add_item("main", item) if(not(r)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "give_item: "..tostring(item:get_name()).." failed.") return false end return true -- this can only be set and edited with the staff elseif(r.r_type == "take_item") then if(not(r.r_value)) then return false end local item = ItemStack(r.r_value) if(not(minetest.registered_items[item:get_name()])) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "take_item: "..tostring(item:get_name()).." unknown.") return false end local r = player:get_inventory():remove_item("main", item) if(not(r)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "take_item: "..tostring(item:get_name()).." failed.") return false end return true -- this can only be set and edited with the staff elseif(r.r_type == "move") then -- copeid/moved here from AliasAlreadyTakens code in functions.lua local target_pos = nil local target_pos_valid = false --pos like (100,20,400) if minetest.string_to_pos(r.r_value) then target_pos = minetest.string_to_pos(r.r_value) target_pos_valid = true end --pos like 100,20,400 local maybe = string.split(r.r_value, ",") if not target_pos_valid and maybe and tonumber(maybe[1]) and tonumber(maybe[2]) and tonumber(maybe[3]) and maybe[4] == nil and tonumber(maybe[1]) <= 32000 and tonumber(maybe[1]) >= -32000 and tonumber(maybe[2]) <= 32000 and tonumber(maybe[2]) >= -32000 and tonumber(maybe[3]) <= 32000 and tonumber(maybe[3]) >= -32000 then target_pos = {x=maybe[1],y=maybe[2],z=maybe[3]} target_pos_valid = true end --pos like {x=100,y=20,z=400} if not target_pos_valid and string.sub(r.r_value,1,1) == "{" and string.sub(r.r_value,-1,-1) == "}" then local might_be_pos = minetest.deserialize("return " .. r.r_value) if tonumber(might_be_pos.x) and tonumber(might_be_pos.x) <= 32000 and tonumber(might_be_pos.x) >= -32000 and tonumber(might_be_pos.y) and tonumber(might_be_pos.y) <= 32000 and tonumber(might_be_pos.y) >= -32000 and tonumber(might_be_pos.z) and tonumber(might_be_pos.z) <= 32000 and tonumber(might_be_pos.z) >= -32000 then target_pos = might_be_pos target_pos_valid = true end end if target_pos_valid == true then player:set_pos(target_pos) if vector.distance(player:get_pos(),target_pos) >= 2 then yl_speak_up.log_change(pname, n_id, tostring(r.r_id)..": ".. "Something went wrong! Player wasn't moved properly.") end end -- Debug if target_pos_valid == false then local obj = yl_speak_up.speak_to[pname].obj local n_id = yl_speak_up.speak_to[pname].n_id local npc = yl_speak_up.get_number_from_id(n_id) if obj:get_luaentity() and tonumber(npc) then yl_speak_up.log_change(pname, n_id, tostring(r.r_id)..": ".. "NPC at "..minetest.pos_to_string(obj:get_pos(),0).. " could not move player "..pname.." because the content of ".. tostring(r.r_id).." is wrong:"..dump(r.r_value)) else yl_speak_up.log_change(pname, n_id, tostring(r.r_id)..": ".. "NPC with unknown ID or without proper object ".. " could not move player "..pname.." because the content of ".. tostring(r.r_id).." is wrong:"..dump(r.r_value)) end return false end return true -- "an internal state (i.e. of a quest)", -- 2 elseif(r.r_type == "state") then if(not(r.r_variable) or r.r_variable == "") then return false end local pname = player:get_player_name() local owner = yl_speak_up.npc_owner[ n_id ] -- set the value of the variable local new_value = nil if( r.r_operator and r.r_operator == "set_to") then new_value = r.r_var_cmp_value elseif(r.r_operator and r.r_operator == "unset") then new_value = nil elseif(r.r_operator and r.r_operator == "set_to_current_time") then -- we store the time in seconds - because microseconds would just -- confuse the users and be too fine grained anyway new_value = math.floor(minetest.get_us_time()/1000000) elseif(r.r_operator and r.r_operator == "quest_step") then -- quest_step and maximum are effectively the same -- TODO: later on, quest steps may be strings local var_val = yl_speak_up.get_quest_variable_value(pname, r.r_variable) if(var_value and tonumber(var_val) and tonumber(r.r_var_cmp_value)) then new_value = math.max(tonumber(var_val), tonumber(r.r_var_cmp_value)) else new_value = r.r_var_cmp_value end elseif(r.r_operator and r.r_operator == "maximum") then local var_val = yl_speak_up.get_quest_variable_value(pname, r.r_variable) if(var_value and tonumber(var_val) and tonumber(r.r_var_cmp_value)) then new_value = math.max(tonumber(var_val), tonumber(r.r_var_cmp_value)) else new_value = r.r_var_cmp_value end elseif(r.r_operator and r.r_operator == "minimum") then local var_val = yl_speak_up.get_quest_variable_value(pname, r.r_variable) if(var_value and tonumber(var_val) and tonumber(r.r_var_cmp_value)) then new_value = math.min(tonumber(var_val), tonumber(r.r_var_cmp_value)) else new_value = r.r_var_cmp_value end elseif(r.r_operator and r.r_operator == "increment") then local var_val = yl_speak_up.get_quest_variable_value(pname, r.r_variable) if(var_value and tonumber(var_val) and tonumber(r.r_var_cmp_value)) then new_value = tonumber(var_val) + tonumber(r.r_var_cmp_value) else new_value = r.r_var_cmp_value end elseif(r.r_operator and r.r_operator == "decrement") then local var_val = yl_speak_up.get_quest_variable_value(pname, r.r_variable) if(var_value and tonumber(var_val) and tonumber(r.r_var_cmp_value)) then new_value = tonumber(var_val) + tonumber(r.r_var_cmp_value) else new_value = -1 * r.r_var_cmp_value end else yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "state: Unsupported type: "..tostring(r.r_value)..".") return false end -- the owner is already encoded in the variable name local ret = yl_speak_up.set_quest_variable_value(pname, r.r_variable, new_value) yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "state: Success: "..tostring(ret).." for setting "..tostring(r.r_variable).." to ".. tostring(new_value)..".") return ret -- "a block somewhere" -- 3 elseif(r.r_type == "block") then -- is the position given correctly? if(not(r.r_pos) or type(r.r_pos) ~= "table" or not(r.r_pos.x) or not(r.r_pos.y) or not(r.r_pos.z)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: Missing or wrong position given: ".. minetest.serialize(r.r_pos)..".") return false end -- check protection (relevant for some actions): the *owner* -- of the NPC needs to be able to build there local is_protected = minetest.is_protected(r.r_pos, yl_speak_up.npc_owner[ n_id ] or "?") -- human readable position; mostly for debug messages local pos_str = tostring(minetest.pos_to_string(r.r_pos)) -- the area has to be loaded local node = minetest.get_node_or_nil(r.r_pos) if(not(node)) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: Not loaded (nil) at pos "..pos_str..".") return false end -- do not interact with nodes on the blacklist if(yl_speak_up.check_blacklisted(r.r_value, r.r_node, node.name)) then -- construct the right text for the error message local nname = node.name if(r.r_value == "place") then nname = r.r_node end yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: Blocks of type \""..tostring(nname).."\" do not allow ".. "interaction of type \""..tostring(r.r_value).."\" for NPC.") return false end -- if node has owner set: check if owner == npc owner local meta = minetest.get_meta(r.r_pos) if(meta and meta:get_string("owner") and meta:get_string("owner") ~= "" and meta:get_string("owner") ~= yl_speak_up.npc_owner[ n_id ]) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: Blocks at "..pos_str.." is owned by "..meta:get_string("owner").. ". NPC is owned by "..tostring(yl_speak_up.npc_owner[ n_id ]).. " and thus cannot interact with it.") return false end -- "If there is air: Place a block so that it looks like now.", -- 2 if(r.r_value and r.r_value == "place") then if(is_protected) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: place - "..pos_str.." is protected. Can't place.") return false end if(not(node) or not(node.name) or node.name ~= "air") then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: place - there is already a block at pos "..pos_str.. ". Can't place.") return false end -- does the NPC have this block in his inventory? else he can't place it local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)}) if(not(npc_inv:contains_item("npc_main", tostring(r.r_node)))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: place - NPC does not have "..tostring(r.r_node).. " in his inventory for placing at "..pos_str..".") return false end -- TODO: switch to minetest.place_node in the future once the bug with placing -- on an air node is fixed -- actually place the node minetest.set_node(r.r_pos, {name=r.r_node, param2=r.r_param2}) -- consume the item npc_inv:remove_item("npc_main", tostring(r.r_node)) return true -- "If there is a block: Dig it.", -- 3 elseif(r.r_value and r.r_value == "dig") then if(is_protected) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: place - "..pos_str.." is protected. Can't place.") return false end if(not(node) or not(node.name) or node.name == "air" or not(minetest.registered_items[ node.name ])) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: dig - there is no block at pos "..pos_str..".") return false end -- TODO: use dig_node once that can put the items in the inventory -- local dig_res = minetest.dig_node(r.r_pos) if(minetest.registered_items[ node.name ].can_dig and not(minetest.registered_items[ node.name ].can_dig(r.r_pos))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: dig - Can't dig block at pos "..pos_str..".") return false end -- actually remove the node minetest.remove_node(r.r_pos) -- get node drops when digging without a tool local drop_list = minetest.get_node_drops(node, nil) local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)}) -- put the drops into the inventory of the NPC for i, d in ipairs(drop_list) do local rest = npc_inv:add_item("npc_main", ItemStack(d)) if(rest and not(rest:is_empty()) and rest:get_count()>0) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).. " block: dig (info) - NPC had no room for item drop ".. rest:to_string().." from digging at "..pos_str..".") end end yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: dig - success: "..tostring(node.name).." at pos "..pos_str..".") return true -- "Punch the block.", -- 4 elseif(r.r_value and r.r_value == "punch") then -- even air can be punched - even if that is pretty pointless minetest.punch_node(r.r_pos) return true -- "Right-click the block.", -- 5 elseif(r.r_value and r.r_value == "right-click") then if(not(node) or not(node.name) or not(minetest.registered_nodes[node.name])) then return false end -- do not right-click nodes that have a metadata formspec string local meta = minetest.get_meta(r.r_pos) if(meta and meta:get_string("formspec") and meta:get_string("formspec") ~= "") then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: right-click - The block at "..pos_str.." has a ".. "formspec set. NPC can't read these. Interaction not possible.") return false end -- do not right-click nodes that have an inventory (they most likely show a -- formspec - which the NPC can't use anyway) local inv = meta:get_inventory() for k, l in pairs(inv:get_lists()) do -- if the inventory contains any lists: abort yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: right-click - The block at "..pos_str.." has an ".. "inventory. Most likely it will show a formspec on right-click. ".. "NPC can't read these. Interaction not possible.") return false end -- is it a door? if(doors.registered_doors[node.name]) then doors.door_toggle( r.r_pos, node, nil) --, clicker) yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: Opened/closed door at "..pos_str..".") -- is it a normal trapdoor? elseif(doors.registered_trapdoors[node.name]) then doors.trapdoor_toggle(r.r_pos, node, nil) --, clicker) yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: Opened/closed trapdoor at "..pos_str..".") elseif(minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick and minetest.registered_nodes[node.name].on_rightclick(r.r_pos, node, nil)) then minetest.registered_nodes[node.name].on_rightclick(r.r_pos, node, nil) yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: right-clicked at at pos "..pos_str..".") else yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "block: right-click at at pos "..pos_str.." had no effect.") end end return false -- ""NPC crafts something", -- 4 elseif(r.r_type == "craft") then if(not(r.r_craft_grid) or not(r.r_value)) then return false end local input = {} input.items = {} -- multiple slots in the craft grid may contain the same item local sum_up = {} for i, v in ipairs(r.r_craft_grid) do if(v and v ~= "") then local stack = ItemStack(v) -- store this for later crafting input.items[ i ] = stack local name = stack:get_name() if(sum_up[ name ]) then sum_up[ name ] = sum_up[ name ] + stack:get_count() else sum_up[ name ] = stack:get_count() end else -- empty itemstack in this slot input.items[ i ] = ItemStack("") end end -- does the NPC have all these items in his inventory? local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)}) for k, v in pairs(sum_up) do if(not(npc_inv:contains_item("npc_main", k.." "..v))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "Crafting failed: NPC does not have "..tostring(k.." "..v)) return false end end -- do these input items form a valid craft recipe? input.method = "normal" -- normal crafting; no cooking or fuel or the like input.width = 3 local output, decremented_input = minetest.get_craft_result(input) if(output.item:is_empty()) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "Crafting failed: No output for that recipe.") return false end -- the craft receipe may have changed in the meantime and yield a diffrent result local expected_stack = ItemStack(r.r_value) if(output.item:get_name() ~= expected_stack:get_name() or output.item:get_count() ~= expected_stack:get_count()) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "Crafting failed: Diffrent output: "..tostring(output.item:to_string())) return false end yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "Great: Crafting is possible!") -- actually consume the items required, return the ones in decremented_input for i, v in ipairs(r.r_craft_grid) do if(v and v ~= "") then npc_inv:remove_item("npc_main", ItemStack(v)) end end -- add the craft result if(not(npc_inv:room_for_item("npc_main", output.item))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "No room for craft result "..output.item:to_string()) end npc_inv:add_item("npc_main", output.item) -- add the decremented_inputs for k,v in pairs(decremented_input.items) do if(k and not(v:is_empty())) then if(not(npc_inv:room_for_item("npc_main", v))) then yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." ".. "No room for craft decr. input "..v:to_string()) end -- actually give the decremented input to the NPC npc_inv:add_item("npc_main", v) end end return true -- "send a chat message to all players", -- 6 elseif(r.r_type == "chat_all") then local pname = player:get_player_name() local dialog = yl_speak_up.speak_to[pname].dialog local text = r.r_value -- replace $NPC_NAME$, $OWNER_NAME$, $PLAYER_NAME$ etc. text = yl_speak_up.replace_vars_in_text(text, dialog, pname) minetest.chat_send_all( yl_speak_up.chat_all_prefix.. minetest.colorize(yl_speak_up.chat_all_color, text)) -- sending a chat message always counts as successful return true -- "Call custom functions that are supposed to be overridden by the server.", -- 13 elseif(r.r_type == "custom") then -- execute the custom function return yl_speak_up.effect_custom(player, r.r_value) end -- fallback: unkown type return false end -- these are only wrapper functions for those in fs_edit_general.lua yl_speak_up.input_fs_edit_effects = function(player, formname, fields) return yl_speak_up.input_fs_edit_option_related(player, formname, fields, "r_", "o_results", yl_speak_up.max_result_effects, "(Ef)fect", "tmp_result", "Please punch the block you want to manipulate in your effect!", values_what, values_operator, values_block, {}, {}, check_what, check_operator, check_block, {}, {}, -- player variables with write access yl_speak_up.get_sorted_player_var_list_write_access, "edit_effects" ) end yl_speak_up.get_fs_edit_effects = function(player, table_click_result) return yl_speak_up.get_fs_edit_option_related(player, table_click_result, "r_", "o_results", yl_speak_up.max_result_effects, "(Ef)fect", "tmp_result", "What do you want to change with this effect?", values_what, values_operator, values_block, {}, {}, check_what, check_operator, check_block, {}, {}, -- player variables with write access yl_speak_up.get_sorted_player_var_list_write_access, yl_speak_up.show_effect, "table_of_elements", "Change the value of the following variable:", "Set variable to:", "New value:", "The NPC shall do something to the block at the following position:" ) end