-- if player has npc_talk_owner priv AND is owner of this particular npc: -- chat option: "I am your owner. I have new orders for you. -- -> enters edit mode -- when edit_mode has been enabled, the following chat options are added to the options: -- chat option: "Add new answer/option to this dialog." -- -> adds a new aswer/option -- chat option: "That was all. I'm finished with giving you new orders. Remember them!" -- -> ends edit mode -- --### -- Init --### -- store if the player is editing a particular NPC; format: yl_speak_up.edit_mode[pname] = npc_id yl_speak_up.edit_mode = {} -- changes applied in edit_mode are applied immediately - but not immediately stored to disk -- (this gives the players a chance to back off in case of unwanted changes) yl_speak_up.npc_was_changed = {} -- 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.edit_mode[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") 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) 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) 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 if(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 -- allow to enter force edit mode (useful when an NPC was broken) yl_speak_up.force_edit_mode = {} -- command to enter force edit mode yl_speak_up.command_npc_talk_force_edit = function(pname, param) if(not(pname)) then return end if(yl_speak_up.force_edit_mode[pname]) then yl_speak_up.force_edit_mode[pname] = nil minetest.chat_send_player(pname, "Ending force edit mode for NPC. From now on talks ".. "will no longer start in edit mode.") else yl_speak_up.force_edit_mode[pname] = true minetest.chat_send_player(pname, "STARTING force edit mode for NPC. From now on talks ".. "with NPC will always start in edit mode provided ".. "you are allowed to edit this NPC.\n".. "In order to end force edit mode, give the command ".. "/npc_talk_force_edit a second time.") end 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" self.yl_speak_up = { talk = true, id = minetest.pos_to_string(self.pos, 0), textures = {}, owner = "TODO_owner_name", -- TODO npc_name = "TODO_npc_name", -- TODO 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 -- 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; -- make sure this is never true in edit mode (because in edit mode we want to -- edit this particular NPC without generic parts) local player = clicker if(yl_speak_up.edit_mode[pname] == n_id) then player = false end yl_speak_up.speak_to[pname].dialog = yl_speak_up.load_dialog(n_id, player) -- 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 -- are we in force edit mode, and can the player edit this NPC? if(yl_speak_up.force_edit_mode[pname] and yl_speak_up.may_edit_npc(clicker, n_id)) then yl_speak_up.edit_mode[pname] = n_id 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 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) 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 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 -- 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