--### -- Init --### -- self (the npc as such) is rarely passed on to any functions; in order to be able to check if -- the player really owns the npc, we need to have that data available; -- format: yl_speak_up.npc_owner[ npc_id ] = owner_name yl_speak_up.npc_owner = {} -- store the current trade between player and npc in case it gets edited in the meantime yl_speak_up.trade = {} -- store what the player last entered in an text_input action yl_speak_up.last_text_input = {} yl_speak_up.reset_vars_for_player = function(pname, reset_fs_version) yl_speak_up.speak_to[pname] = nil yl_speak_up.last_text_input[pname] = nil -- when just stopping editing: don't reset the fs_version if(reset_fs_version) then yl_speak_up.fs_version[pname] = nil end end --### -- Debug --### yl_speak_up.debug = true --### -- Helpers --### yl_speak_up.get_number_from_id = function(any_id) if(not(any_id) or any_id == "d_got_item" or any_id == "d_end" or any_id == "d_dynamic") then return "0" end return string.split(any_id, "_")[2] end local function save_path(n_id) return yl_speak_up.worldpath .. yl_speak_up.path .. DIR_DELIM .. n_id .. ".json" end yl_speak_up.get_error_message = function() local formspec = { "size[13.4,8.5]", "bgcolor[#FF0000]", "label[0.2,0.35;Please save a NPC file first]", "button_exit[0.2,7.7;3,0.75;button_back;Back]" } return table.concat(formspec, "") end yl_speak_up.find_next_id = function(t) local start_id = 1 if t == nil then return start_id end local keynum = 1 for k, _ in pairs(t) do local keynum = tonumber(yl_speak_up.get_number_from_id(k)) if keynum and keynum >= start_id then start_id = keynum + 1 end end return start_id end yl_speak_up.sanitize_sort = function(options, value) local retval = value if value == "" or value == nil or tonumber(value) == nil then local temp = 0 for k, v in pairs(options) do if v.o_sort ~= nil then if tonumber(v.o_sort) > temp then temp = tonumber(v.o_sort) end end end retval = tostring(temp + 1) end return retval end --### --Load and Save --### -- we can't really log changes here in this function because we don't know *what* has been changed yl_speak_up.save_dialog = function(n_id, dialog) if type(n_id) ~= "string" or type(dialog) ~= "table" then return false end local p = save_path(n_id) -- save some data (in particular usage of quest variables) yl_speak_up.update_stored_npc_data(n_id, dialog) -- make sure we never store any automaticly added generic dialogs dialog = yl_speak_up.strip_generic_dialogs(dialog) -- never store d_dynamic dialogs if(dialog.n_dialogs and dialog.n_dialogs["d_dynamic"]) then dialog.n_dialogs["d_dynamic"] = nil end local content = minetest.write_json(dialog) return minetest.safe_file_write(p, content) end -- if a player is supplied: include generic dialogs yl_speak_up.load_dialog = function(n_id, player) -- returns the saved dialog local p = save_path(n_id) -- note: add_generic_dialogs will also add an empty d_dynamic dialog local file, err = io.open(p, "r") if err then return yl_speak_up.add_generic_dialogs({}, n_id, player) end io.input(file) local content = io.read() local dialog = minetest.parse_json(content) io.close(file) if type(dialog) ~= "table" then dialog = {} end return yl_speak_up.add_generic_dialogs(dialog, n_id, player) end -- used by staff and input_inital_config yl_speak_up.fields_to_dialog = function(pname, fields) local n_id = yl_speak_up.speak_to[pname].n_id local dialog = yl_speak_up.load_dialog(n_id, false) local save_d_id = "" if next(dialog) == nil then -- No file found. Let's create the basic values dialog = {} dialog.n_dialogs = {} end if dialog.n_dialogs == nil or next(dialog.n_dialogs) == nil then --No dialogs found. Let's make a table dialog.n_dialogs = {} end if fields.d_text ~= "" then -- If there is dialog text, then save new or old dialog if fields.d_id == yl_speak_up.text_new_dialog_id then --New dialog -- -- Find highest d_id and increase by 1 save_d_id = "d_" .. yl_speak_up.find_next_id(dialog.n_dialogs) -- Initialize empty dialog dialog.n_dialogs[save_d_id] = {} else -- Already existing dialog save_d_id = fields.d_id end -- Change dialog dialog.n_dialogs[save_d_id].d_id = save_d_id dialog.n_dialogs[save_d_id].d_type = "text" dialog.n_dialogs[save_d_id].d_text = fields.d_text dialog.n_dialogs[save_d_id].d_sort = fields.d_sort end --Context yl_speak_up.speak_to[pname].d_id = save_d_id -- Just in case the NPC vlaues where changed or set dialog.n_id = n_id dialog.n_description = fields.n_description dialog.n_npc = fields.n_npc dialog.npc_owner = fields.npc_owner return dialog end yl_speak_up.delete_dialog = function(n_id, d_id) if d_id == yl_speak_up.text_new_dialog_id then return false end -- We don't delete "New dialog" local dialog = yl_speak_up.load_dialog(n_id, false) dialog.n_dialogs[d_id] = nil yl_speak_up.save_dialog(n_id, dialog) end --### --Formspecs --### -- get formspecs -- talk -- receive fields -- talk -- helper function -- the option to override next_id and provide a value is needed when a new dialog was -- added, then edited, and then discarded; it's still needed after that, but has to -- be reset to empty state (wasn't stored before) yl_speak_up.add_new_dialog = function(dialog, pname, next_id, dialog_text) if(not(next_id)) then next_id = yl_speak_up.find_next_id(dialog.n_dialogs) end local future_d_id = "d_" .. next_id -- Initialize empty dialog dialog.n_dialogs[future_d_id] = { d_id = future_d_id, d_type = "text", d_text = (dialog_text or ""), d_sort = next_id } -- store that there have been changes to this npc -- (better ask only when the new dialog is changed) -- table.insert(yl_speak_up.npc_was_changed[ yl_speak_up.edit_mode[pname] ], -- "Dialog "..future_d_id..": New dialog added.") -- add an option for going back to the start of the dialog; -- this is an option which the player can delete and change according to needs, -- not a fixed button which may not always fit if(not(dialog_text)) then -- we want to go back to the start from here local target_dialog = yl_speak_up.get_start_dialog_id(dialog) -- this text will be used for the button local option_text = "Let's go back to the start of our talk." -- we just created this dialog - this will be the first option yl_speak_up.add_new_option(dialog, pname, "1", future_d_id, option_text, target_dialog) end return future_d_id end -- add a new option/answer to dialog d_id with option_text (or default "") -- option_text (optional) the text that shall be shown as option/answer -- target_dialog (optional) the target dialog where the player will end up when choosing -- this option/answer yl_speak_up.add_new_option = function(dialog, pname, next_id, d_id, option_text, target_dialog) if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs[d_id])) then return nil end if dialog.n_dialogs[d_id].d_options == nil then -- make sure d_options exists dialog.n_dialogs[d_id].d_options = {} else -- we don't want an infinite amount of answers per dialog local sorted_list = yl_speak_up.get_sorted_options(dialog.n_dialogs[d_id].d_options, "o_sort") local anz_options = #sorted_list if(anz_options >= yl_speak_up.max_number_of_options_per_dialog) then -- nothing added return nil end end if(not(next_id)) then next_id = yl_speak_up.find_next_id(dialog.n_dialogs[d_id].d_options) end local future_o_id = "o_" .. next_id dialog.n_dialogs[d_id].d_options[future_o_id] = { o_id = future_o_id, o_hide_when_prerequisites_not_met = "false", o_grey_when_prerequisites_not_met = "false", o_sort = -1, o_text_when_prerequisites_not_met = "", o_text_when_prerequisites_met = (option_text or ""), } -- necessary in order for it to work local s = yl_speak_up.sanitize_sort(dialog.n_dialogs[d_id].d_options, yl_speak_up.speak_to[pname].o_sort) dialog.n_dialogs[d_id].d_options[future_o_id].o_sort = s -- log only in edit mode local n_id = yl_speak_up.speak_to[pname].n_id -- would be too difficult to add an exception for edit_mode here; thus, we do it directly here: if(yl_speak_up.npc_was_changed and yl_speak_up.npc_was_changed[n_id]) then table.insert(yl_speak_up.npc_was_changed[ n_id ], "Dialog "..d_id..": Added new option/answer "..future_o_id..".") end -- letting d_got_item point back to itself is not a good idea because the -- NPC will then end up in a loop; plus the d_got_item dialog is intended for -- automatic processing, not for showing to the player if(d_id == "d_got_item") then -- unless the player specifies something better, we go back to the start dialog -- (that is where d_got_item got called from anyway) target_dialog = yl_speak_up.get_start_dialog_id(dialog) -- ...and this option needs to be selected automaticly dialog.n_dialogs[d_id].d_options[future_o_id].o_autoanswer = 1 elseif(d_id == "d_trade") then -- we really don't want to go to another dialog from here target_dialog = "d_trade" -- ...and this option needs to be selected automaticly dialog.n_dialogs[d_id].d_options[future_o_id].o_autoanswer = 1 end local future_r_id = nil -- create a fitting dialog result automaticly if possible: -- give this new dialog a dialog result that leads back to this dialog -- (which is more helpful than creating tons of empty dialogs) if(target_dialog and (dialog.n_dialogs[target_dialog] or target_dialog == "d_end")) then future_r_id = yl_speak_up.add_new_result(dialog, d_id, future_o_id) -- actually store the new result dialog.n_dialogs[d_id].d_options[future_o_id].o_results = {} dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id] = { r_id = future_r_id, r_type = "dialog", r_value = target_dialog} end -- the d_got_item dialog is special; players can easily forget to add the -- necessary preconditions and effects, so we do that manually here if(d_id == "d_got_item") then -- we also need a precondition so that the o_autoanswer can actually get called dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites = {} -- we just added this option; this is the first and for now only precondition for it; -- the player still has to adjust it, but at least it is a reasonable default dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites["p_1"] = { p_id = "p_1", p_type = "player_offered_item", p_item_stack_size = tostring(next_id), p_match_stack_size = "exactly", -- this is just a simple example item and ought to be changed after adding p_value = "default:stick "..tostring(next_id)} -- we need to show the player that his action was successful dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id].alternate_text = "Thank you for the "..tostring(next_id).." stick(s)! ".. "Never can't have enough sticks.\n$TEXT$" -- we need an effect for accepting the item; -- taking all that was offered and putting it into the NPC's inventory is a good default future_r_id = yl_speak_up.add_new_result(dialog, d_id, future_o_id) dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id] = { r_id = future_r_id, r_type = "deal_with_offered_item", r_value = "take_all"} -- the trade dialog is equally special elseif(d_id == "d_trade") then dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites = {} -- this is just an example dialog.n_dialogs[d_id].d_options[future_o_id].o_prerequisites["p_1"] = { p_id = "p_1", p_type = "npc_inv", p_value = "inv_does_not_contain", p_inv_list_name = "npc_main", p_itemstack = "default:stick "..tostring(100-next_id)} future_r_id = yl_speak_up.add_new_result(dialog, d_id, future_o_id) -- example craft dialog.n_dialogs[d_id].d_options[future_o_id].o_results[future_r_id] = { r_id = future_r_id, r_type = "craft", r_value = "default:stick 4", o_sort = "1", r_craft_grid = {"default:wood", "", "", "", "", "", "", "", ""}} end return future_o_id end -- add a new result to option o_id of dialog d_id yl_speak_up.add_new_result = function(dialog, d_id, o_id) if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs[d_id]) or not(dialog.n_dialogs[d_id].d_options) or not(dialog.n_dialogs[d_id].d_options[o_id])) then return end -- create a new result (first the id, then the actual result) local future_r_id = "r_" .. yl_speak_up.find_next_id(dialog.n_dialogs[d_id].d_options[o_id].o_results) if future_r_id == "r_1" then dialog.n_dialogs[d_id].d_options[o_id].o_results = {} end dialog.n_dialogs[d_id].d_options[o_id].o_results[future_r_id] = {} return future_r_id end -- this is useful for result types that can exist only once per option -- (apart from editing with the staff); -- examples: "dialog" and "trade"; -- returns tue r_id or nil if no result of that type has been found yl_speak_up.get_result_id_by_type = function(dialog, d_id, o_id, result_type) if(not(dialog) or not(dialog.n_dialogs) or not(dialog.n_dialogs[d_id]) or not(dialog.n_dialogs[d_id].d_options) or not(dialog.n_dialogs[d_id].d_options[o_id])) then return end local results = dialog.n_dialogs[d_id].d_options[o_id].o_results if(not(results)) then return end for k, v in pairs(results) do if(v.r_type == result_type) then return k end end end -- helper function for sorting options/answers using options[o_id].o_sort -- (or dialogs by d_sort) yl_speak_up.get_sorted_options = function(options, sort_by) local sorted_list = {} for k,v in pairs(options) do table.insert(sorted_list, k) end table.sort(sorted_list, function(a,b) if(not(options[a][sort_by])) then return false elseif(not(options[b][sort_by])) then return true -- sadly not all entries are numeric elseif(tonumber(options[a][sort_by]) and tonumber(options[b][sort_by])) then return (tonumber(options[a][sort_by]) < tonumber(options[b][sort_by])) -- numbers have a higher priority elseif(tonumber(options[a][sort_by])) then return true elseif(tonumber(options[b][sort_by])) then return false -- if the value is the same: sort by index elseif(options[a][sort_by] == options[b][sort_by]) then return (a < b) else return (options[a][sort_by] < options[b][sort_by]) end end ) return sorted_list end -- simple sort of keys of a table numericly; -- this is not efficient - but that doesn't matter: the lists are small and -- it is only executed when configuring an NPC -- simple: if the parameter is true, the keys will just be sorted (i.e. player names) - which is -- not enough for d_, o_ etc. (which need more care when sorting) yl_speak_up.sort_keys = function(t, simple) local keys = {} for k, v in pairs(t) do -- add a prefix so that p_2 ends up before p_10 if(not(simple) and string.len(k) == 3) then k = "a"..k end table.insert(keys, k) end table.sort(keys) if(simple) then return keys end for i,k in ipairs(keys) do -- avoid cutting the single a from a_1 (action 1) if(k and string.sub(k, 1, 1) == "a" and string.sub(k, 2, 2) ~= "_") then -- remove the leading blank keys[i] = string.sub(k, 2) end end return keys end -- identify multiple results that lead to target dialogs yl_speak_up.check_for_disambigous_results = function(n_id, pname) local errors_found = false -- this is only checked when trying to edit this npc; -- let's stick to check the dialogs of this one without generic dialogs local dialog = yl_speak_up.load_dialog(n_id, false) -- nothing defined yet - nothing to repair if(not(dialog.n_dialogs)) then return end -- iterate over all dialogs for d_id, d in pairs(dialog.n_dialogs) do if(d_id and d and d.d_options) then -- iterate over all options for o_id, o in pairs(d.d_options) do if(o_id and o and o.o_results) then local dialog_results = {} -- iterate over all results for r_id, r in pairs(o.o_results) do if(r.r_type == "dialog") then table.insert(dialog_results, r_id) end end if(#dialog_results>1) then local msg = "ERROR: Dialog ".. tostring(d_id)..", option "..tostring(o_id).. ", has multiple results of type dialog: ".. minetest.serialize(dialog_results)..". Please ".. "let someone with npc_master priv fix that first!" yl_speak_up.log_change(pname, n_id, msg, "error") if(pname) then minetest.chat_send_player(pname, msg) end errors_found = true end end end end end return errors_found end -- Make the NPC talk -- assign n_ID -- usually this happens when talking to the NPC for the first time; -- but if you want to you can call this function earlier (on spawn) -- so that logging of spawning with the ID is possible yl_speak_up.initialize_npc = function(self) -- already configured? if(not(self) or (self.yl_speak_up and self.yl_speak_up.id)) then return self end local m_talk = yl_speak_up.talk_after_spawn or true local m_id = yl_speak_up.number_of_npcs + 1 yl_speak_up.number_of_npcs = m_id yl_speak_up.modstorage:set_int("amount", m_id) self.yl_speak_up = { talk = m_talk, id = m_id, textures = self.textures } return self end function yl_speak_up.talk(self, clicker) if not clicker and not clicker:is_player() then return end if not self then return end local id_prefix = "n" -- we are not dealing with an NPC but with a position/block on the map if(self.is_block) then id_prefix = "p" local owner = "- unknown -" local talk_name = "- unknown -" if(self.pos and self.pos and self.pos.x) then local meta = minetest.get_meta(self.pos) if(meta) then owner = meta:get_string("owner") or "" talk_name = meta:get_string("talk_name") or "" end end self.yl_speak_up = { is_block = true, talk = true, id = minetest.pos_to_string(self.pos, 0), textures = {}, owner = owner, npc_name = talk_name, object = nil, -- blocks don't have an object } -- TODO: remember somewhere that this block is relevant -- initialize the mob if necessary; this happens at the time of first talk, not at spawn time! elseif(not(self.yl_speak_up) or not(self.yl_speak_up.id)) then self = yl_speak_up.initialize_npc(self) end -- TODO: load inventory only when the npc actually uses one? -- create a detached inventory for the npc and load its inventory yl_speak_up.load_npc_inventory(id_prefix.."_"..tostring(self.yl_speak_up.id)) local npc_id = self.yl_speak_up.id local n_id = id_prefix.."_" .. npc_id -- remember whom the npc belongs to (as long as we still have self.owner available for easy access) yl_speak_up.npc_owner[ n_id ] = self.owner local pname = clicker:get_player_name() if not self.yl_speak_up or not self.yl_speak_up.talk or self.yl_speak_up.talk~=true then local was = "This NPC" if(id_prefix ~= "n") then was = "This block" end -- show a formspec to other players that this NPC is busy if(not(yl_speak_up.may_edit_npc(clicker, n_id))) then -- show a formspec so that the player knows that he may come back later yl_speak_up.show_fs(player, "msg", {input_to = "yl_spaek_up:ignore", formspec = "size[6,2]".. "label[1.2,0.0;"..minetest.formspec_escape((self.yl_speak_up.npc_name or was).. " [muted]").."]".. "label[0.2,0.5;Sorry! I'm currently busy learning new things.]".. "label[0.2,1.0;Please come back later.]".. "button_exit[2.5,1.5;1,0.9;ok;Ok]"}) return end -- allow the owner to edit (and subsequently unmute) the npc minetest.chat_send_player(pname, was.." is muted. It will only talk to you.") end yl_speak_up.speak_to[pname] = {} yl_speak_up.speak_to[pname].n_id = n_id -- Memorize which player talks to which NPC yl_speak_up.speak_to[pname].textures = self.yl_speak_up.textures yl_speak_up.speak_to[pname].option_index = 1 -- the object itself may be needed in load_dialog for adding generic dialogs yl_speak_up.speak_to[pname].obj = self.object -- Load the dialog and see what we can do with it -- this inculdes generic dialog parts; yl_speak_up.speak_to[pname].dialog = yl_speak_up.load_dialog(n_id, clicker) -- is this player explicitly allowed to edit this npc? if(yl_speak_up.speak_to[pname].dialog and yl_speak_up.speak_to[pname].dialog.n_may_edit and yl_speak_up.speak_to[pname].dialog.n_may_edit[pname] and minetest.check_player_privs(clicker, {npc_talk_owner=true})) then yl_speak_up.speak_to[pname].may_edit_this_npc = true end local dialog = yl_speak_up.speak_to[pname].dialog if(not(dialog.trades)) then dialog.trades = {} end -- some NPC may have reset the animation; at least set it to the desired -- value whenever we talk to the NPC if self.yl_speak_up and self.yl_speak_up.animation then self.object:set_animation(self.yl_speak_up.animation) end -- maintain a list of existing NPC, but do not force saving yl_speak_up.update_npc_data(self, dialog, false) yl_speak_up.show_fs(clicker, "talk", {n_id = n_id}) end -- mute the npc; either via the appropriate staff or via talking to him yl_speak_up.set_muted = function(p_name, obj, set_muted) if(not(obj)) then return end local luaentity = obj:get_luaentity() if(not(luaentity)) then return end local npc = luaentity.yl_speak_up.id local npc_name = luaentity.yl_speak_up.npc_name -- fallback if(not(npc_name)) then npc_name = npc end if(set_muted and luaentity.yl_speak_up.talk) then -- the npc is willing to talk luaentity.yl_speak_up.talk = false yl_speak_up.update_nametag(luaentity) -- minetest.chat_send_player(p_name,"NPC with ID n_"..npc.." will shut up at pos ".. -- minetest.pos_to_string(obj:get_pos(),0).." on command of "..p_name) minetest.chat_send_player(p_name, "NPC n_"..tostring(npc).." is now muted and will ".. "only talk to those who can edit the NPC.") yl_speak_up.log_change(p_name, "n_"..npc, "muted - NPC stops talking") elseif(not(set_muted) and not(luaentity.yl_speak_up.talk)) then -- mute the npc luaentity.yl_speak_up.talk = true yl_speak_up.update_nametag(luaentity) minetest.chat_send_player(p_name, "NPC n_"..tostring(npc).." is no longer muted and ".. "will talk with any player who right-clicks the NPC.") -- minetest.chat_send_player(p_name,"NPC with ID n_"..npc.." will resume speech at pos ".. -- minetest.pos_to_string(obj:get_pos(),0).." on command of "..p_name) yl_speak_up.log_change(p_name, "n_"..npc, "unmuted - NPC talks again") end end -- checks if dialog contains d_id and o_id yl_speak_up.check_if_dialog_has_option = function(dialog, d_id, o_id) return (dialog and d_id and o_id and dialog.n_dialogs and dialog.n_dialogs[d_id] and dialog.n_dialogs[d_id].d_options and dialog.n_dialogs[d_id].d_options[o_id]) end -- has the player the right privs? -- this is used for the "I am your master" talk based configuration; *NOT* for the staffs! yl_speak_up.may_edit_npc = function(player, n_id) if(not(player)) then return false end local pname = player:get_player_name() -- is the player allowed to edit this npc? return ((yl_speak_up.npc_owner[ n_id ] == pname and minetest.check_player_privs(player, {npc_talk_owner=true})) or minetest.check_player_privs(player, {npc_talk_master=true}) or minetest.check_player_privs(player, {npc_master=true}) or (yl_speak_up.speak_to[pname] and yl_speak_up.speak_to[pname].may_edit_this_npc)) end