-- 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 = {} function yl_speak_up.init_mob_table() return false end 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 -- when just stopping editing: don't reset the fs_version if(reset_fs_version) then yl_speak_up.fs_version[pname] = nil end end minetest.register_on_leaveplayer( function(player) yl_speak_up.reset_vars_for_player(player:get_player_name(), true) end ) minetest.register_on_joinplayer( function(player) yl_speak_up.reset_vars_for_player(player:get_player_name(), true) end ) --### -- Debug --### yl_speak_up.debug = true --### -- Helpers --### yl_speak_up.get_number_from_id = function(any_id) if(any_id == "d_got_item") 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 >= 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) 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) 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 = "", 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.") return future_d_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 minetest.chat_send_player(pname, "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!") errors_found = true end end end end end return errors_found end -- Make the NPC talk function yl_speak_up.talk(self, clicker) if not clicker and not clicker:is_player() then return end if not self then return end if not self.yl_speak_up or not self.yl_speak_up.id then return end local npc_id = self.yl_speak_up.id local n_id = "n_" .. 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 -- 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 "This NPC").. " [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, "This NPC 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 = nil end 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 yl_speak_up.show_fs(clicker, "talk", {n_id = n_id}) local dialog = yl_speak_up.speak_to[pname].dialog if(not(dialog.trades)) then dialog.trades = {} end 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 "..npc.." will shut up at pos ".. minetest.pos_to_string(obj:get_pos(),0).." on command of "..p_name) minetest.log("action","[MOD] yl_speak_up: NPC with ID n_"..npc.. " will shut up at pos "..minetest.pos_to_string(obj:get_pos(),0).. " on command of "..p_name) 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 "..npc.." will resume speech at pos ".. minetest.pos_to_string(obj:get_pos(),0).." on command of "..p_name) minetest.log("action","[MOD] yl_speak_up: NPC with ID n_"..npc.. " will resume speech at pos "..minetest.pos_to_string(obj:get_pos(),0).. " on command of "..p_name) 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 -- log changes done by players or admins to NPCs yl_speak_up.log_change = function(pname, n_id, text) -- make sure all variables are defined if(not(pname)) then pname = "- unkown player -" end if(not(n_id)) then n_id = "- unknown NPC -" end if(not(text)) then text = "- no text given -" end -- we don't want newlines in the texts text = string.gsub(text, "\n", "\\n") local log_text = "<"..tostring(n_id).."> ["..tostring(pname).."]: "..text -- log in general logfile minetest.log("yl_speak_up "..log_text) -- log with timestamp local log_text = tostring(os.time())..log_text -- TODO: log in a file for each npc and show it on demand? end