-- keeps a list of NPC with some data; Index: n_id -- is saved to disk yl_speak_up.npc_list = {} -- list of objects of the NPC; Index: n_id -- no point in saving this to disk yl_speak_up.npc_list_objects = {} -- file to store the list of NPC for the "/npc_talk list" command yl_speak_up.npc_list_path = minetest.get_worldpath().."/yl_speak_up_npc_list.data" -- pre-calculated lines for the NPC list (only which ones are shown and how some columns -- are colored varies per player) yl_speak_up.cache_general_npc_list_lines = {} -- we need to remember which NPC was shown to the player in which order; index: player name yl_speak_up.cache_npc_list_per_player = {} -- which table column is the player using for sorting? yl_speak_up.sort_npc_list_per_player = {} -- add/update data about NPC self with dialog dialog (optional) -- force_store ought to be true if something important has been changed yl_speak_up.update_npc_data = function(self, dialog, force_store) -- is this a properly indexed npc? if(not(self) or not(self.yl_speak_up) or not(self.yl_speak_up.id)) then return end local is_known = not(not(yl_speak_up.npc_list[self.yl_speak_up.id])) -- who else may edit this NPC - that is stored in the dialog local may_edit = {} local desc = "" local trades = {} if(dialog) then may_edit = dialog.n_may_edit desc = dialog.n_description -- maybe update this only when the trades are updated? if(dialog.trades) then for k, v in pairs(dialog.trades) do if(v.buy and v.pay and v.buy[1] and v.pay[1]) then table.insert(trades, {v.buy[1], v.pay[1]}) end end end elseif(is_known) then local old = yl_speak_up.npc_list[self.yl_speak_up.id] may_edit = old.may_edit desc = old.desc trades = old.trades end local created_at = 0 if(is_known and yl_speak_up.npc_list[self.yl_speak_up.id].created_at) then created_at = yl_speak_up.npc_list[self.yl_speak_up.id].created_at else created_at = os.time() end local pos = {} if(self.object) then pos = self.object:get_pos() if(pos) then pos = { x = math.floor(pos.x or 0), y = math.floor(pos.y or 0), z = math.floor(pos.z or 0)} else pos = { x=0, y=0, z=0} end end -- only store real, important properties local properties = {} for k, v in pairs(self.yl_speak_up.properties or {}) do if(string.sub(k, 1, 5) ~= "self.") then properties[k] = v end end -- update the information we have on the NPC yl_speak_up.npc_list[self.yl_speak_up.id] = { typ = self.name, name = self.yl_speak_up.npc_name, desc = desc, owner = self.owner, may_edit = may_edit, trades = trades, pos = pos, properties = properties, created_at = created_at, muted = self.yl_speak_up.talk, animation = self.yl_speak_up.animation, skin = self.yl_speak_up.skin, textures = self.textures, } -- the current object will change after deactivate; there is no point in storing -- it over server restart yl_speak_up.npc_list_objects[self.yl_speak_up.id] = self.object -- if we didn't know about this NPC before then by all means store the data if(not(is_known) or force_store) then yl_speak_up.npc_list_store() end end yl_speak_up.npc_list_load = function() local file,err = io.open( yl_speak_up.npc_list_path, "rb") if (file == nil) then yl_speak_up.npc_list = {} return end local data = file:read("*all") file:close() yl_speak_up.npc_list = minetest.deserialize(data) end yl_speak_up.npc_list_store = function() local file,err = io.open( yl_speak_up.npc_list_path, "wb") if (file == nil) then return end file:write(minetest.serialize(yl_speak_up.npc_list)) file:close() end -- emergency restore NPC that got lost (egg deleted, killed, ...) yl_speak_up.command_npc_force_restore_npc = function(pname, rest) if(not(pname)) then return end if(not(minetest.check_player_privs(pname, {npc_talk_admin = true}))) then minetest.chat_send_player(pname, "This command is used for restoring ".. "NPC that somehow got lost (egg destroyed, killed, ..). You ".. "lack the \"npc_talk_admin\" priv required to run this command.") return end if(not(rest) or rest == "" or rest == "help" or rest == "?") then minetest.chat_send_player(pname, "This command is used for restoring ".. "NPC that somehow got lost (egg destroyed, killed, ..).\n".. "WARNING: If the egg is found again later on, make sure that ".. "this restored NPC and the NPC from the egg are not both placed!\n".. " There can only be one NPC per ID.\n".. "Syntax: /npc_talk force_restore_npc []\n".. " is the ID (number! Without \"n_\") of the NPC to be restored.\n".. " is only needed if the NPC is not listed in ".. "\"/npc_talk list\" (=extremly old NPC).") return end local parts = string.split(rest or "", " ", false, 1) local id = tonumber(parts[1] or "") if(not(id)) then minetest.chat_send_player(pname, "Please provide the ID (number!) of the NPC ".. "you wish to restore.") return elseif(not(yl_speak_up.number_of_npcs) or yl_speak_up.number_of_npcs < 1 or id > yl_speak_up.number_of_npcs) then minetest.chat_send_player(pname, "That ID is larger than the amount of existing NPC. ".. "Restoring is for old NPC that got lost.") return elseif(id < 1) then minetest.chat_send_player(pname, "That ID is smaller than 1. Can't restore negative NPC.") return end local player = minetest.get_player_by_name(pname) if(not(player)) then return end -- if we've seen the NPC before: make sure he's not just unloaded because nobody is where he is if(yl_speak_up.npc_list[id] and yl_speak_up.npc_list[id].pos and yl_speak_up.npc_list[id].pos.x and yl_speak_up.npc_list[id].pos.y and yl_speak_up.npc_list[id].pos.z) then local v_npc = vector.new(yl_speak_up.npc_list[id].pos) local v_pl = vector.new(player:get_pos()) if(vector.distance(v_npc, v_pl) > 6) then minetest.chat_send_player(pname, "You are more than 6 m away from the last ".. "known position of this NPC at ".. minetest.pos_to_string(yl_speak_up.npc_list[id].pos).. ". Please move closer to make sure the NPC isn't just not loaded ".. "due to nobody beeing near!") return end end -- check the currently loaded mobs to make sure he wasn't loaded since last update of our list for k,v in pairs(minetest.luaentities) do if(v and v.yl_speak_up and v.yl_speak_up.id and v.yl_speak_up.id == id) then minetest.chat_send_player(pname, "An NPC with the ID "..tostring(id).. " is currently loaded. No restoring required!") return end end local data = nil -- do we need a donator NPC because we have never seen this NPC and have no data? local copy_from_id = tonumber(parts[2] or "") if(not(yl_speak_up.npc_list[id])) then if(not(copy_from_id) or not(yl_speak_up.npc_list[copy_from_id])) then minetest.chat_send_player(pname, "We have no data on NPC "..tostring(id).. ". Please provide the ID of an EXISTING NPC from which necessary ".. "data can be copied!") return end minetest.chat_send_player(pname, "Will use the data of the NPC with ID ".. tostring(copy_from_id).." to set up the new/restored NPC.") data = yl_speak_up.npc_list[copy_from_id] else data = yl_speak_up.npc_list[id] end -- ok..the NPC is not loaded. Perhaps he really got lost. minetest.chat_send_player(pname, "Will try to restore the NPC with the ID "..tostring(id)..".") if(not(data.typ) or not(minetest.registered_entities[data.typ or "?"])) then minetest.chat_send_player(pname, "Error: No NPC entity prototype found for \"".. tostring(data.name).."\". Aborting.") return end -- this is an emergency fallback restore - so it's ok to drop the NPC where the admin is standing local mob = minetest.add_entity(player:get_pos(), data.typ) local ent = mob and mob:get_luaentity() if(not(ent)) then minetest.chat_send_player(pname, "Failed to create a new NPC entity of type \"".. tostring(data.name).."\". Aborting.") return end -- set up the new NPC local npc_name = data.name local npc_desc = data.npc_description local npc_owner = data.owner -- the dialog includes the trades, n_may_edit and other data local dialog = yl_speak_up.load_dialog(id, nil) -- restore name and description from dialog if possible if(dialog and dialog.n_npc) then npc_name = dialog.n_npc npc_desc = dialog.n_description npc_owner = dialog.npc_owner or data.owner end ent.yl_speak_up = { id = id, talk = data.muted, properties = data.properties, npc_name = npc_name, npc_description = npc_desc, infotext = yl_speak_up.infotext, -- will be set automaticly later animation = data.animation, textures = data.textures, } -- This is at least useful for mobs_redo. Other mob mods may require adjustments. ent.owner = npc_owner ent.tamed = true -- update nametag, infotext etc. yl_speak_up.update_nametag(ent) if(data.animation and ent.object) then ent.object:set_animation(data.animation) end if(data.skin and ent.object) then ent.object:set_properties({textures = table.copy(data.skin)}) ent.yl_speak_up.textures = table.copy(data.skin) end -- update the NPC list yl_speak_up.update_npc_data(ent, dialog, true) minetest.chat_send_player(pname, "Placed the restored NPC ID "..tostring(id).. ", named "..tostring(data.name)..", right where you stand.") end -- provides a list of NPC the player can edit yl_speak_up.command_npc_talk_list = function(pname, rest) if(not(pname)) then return end -- check if there are any loaded entities handled by yl_speak_up that -- are *not* in the list yet local liste = {} for k,v in pairs(minetest.luaentities) do if(v and v.yl_speak_up and v.yl_speak_up.id) then if(not(yl_speak_up.npc_list[v.yl_speak_up.id])) then local dialog = yl_speak_up.load_dialog(v.yl_speak_up.id, nil) yl_speak_up.update_npc_data(v, dialog, false) else yl_speak_up.update_npc_data(v, nil, false) end end end -- store the updated list yl_speak_up.npc_list_store() -- update the information for display yl_speak_up.build_cache_general_npc_list_lines() -- clear the stored NPC list and calculate it anew yl_speak_up.cache_npc_list_per_player[pname] = {} -- Note: show_fs cannot be used here as that expects the player to be talking to an actual npc yl_speak_up.show_fs_ver(pname, "yl_speak_up:show_npc_list", yl_speak_up.get_fs_show_npc_list(pname, nil)) end -- allow to sort the npc list, display more info on one NPC etc. yl_speak_up.input_show_npc_list = function(player, formname, fields) local pname = player:get_player_name() -- teleport to NPC if(fields.teleport and fields.selected_id and yl_speak_up.cache_npc_list_per_player[pname] and minetest.check_player_privs(pname, {teleport=true})) then local id = tonumber(fields.selected_id) if(not(id) or id < 0 or not(yl_speak_up.npc_list[id]) or table.indexof(yl_speak_up.cache_npc_list_per_player[pname], id) < 1) then minetest.chat_send_player(pname, "Sorry. Cannot find that NPC.") return end -- try cached position local pos = yl_speak_up.npc_list[id].pos local obj = yl_speak_up.npc_list_objects[id] if(obj) then pos = obj:get_pos() end if(not(pos) or not(pos.x) or not(pos.y) or not(pos.z)) then pos = yl_speak_up.npc_list[id].pos end if(not(pos) or not(pos.x) or not(pos.y) or not(pos.z)) then minetest.chat_send_player(pname, "Sorry. Cannot find position of that NPC.") return end player:set_pos(pos) minetest.chat_send_player(pname, "Teleporting to NPC with ID ".. tostring(fields.selected_id)..': '.. tostring(yl_speak_up.npc_list[id].name)..'.') return end -- sort by column or select an NPC if(fields.show_npc_list) then local selected = minetest.explode_table_event(fields.show_npc_list) -- sort by column if(selected.row == 1) then local old_sort = yl_speak_up.sort_npc_list_per_player[pname] or 0 -- reverse sort if(old_sort == selected.column) then yl_speak_up.sort_npc_list_per_player[pname] = -1 * selected.column else -- sort by new col yl_speak_up.sort_npc_list_per_player[pname] = selected.column end -- show the update yl_speak_up.show_fs_ver(pname, "yl_speak_up:show_npc_list", yl_speak_up.get_fs_show_npc_list(pname, nil)) return else -- show details about a specific NPC yl_speak_up.show_fs_ver(pname, "yl_speak_up:show_npc_list", yl_speak_up.get_fs_show_npc_list(pname, selected.row)) return end end return end -- the entries for the "/npc_talk list" NPC list are generally the same for all -- - except that not all lines are shown to each player and that some -- lines might be colored diffrently yl_speak_up.build_cache_general_npc_list_lines = function() -- small helper function to suppress the display of zeros local show_if_bigger_null = function(value, do_count) if(do_count and value) then local anz = 0 for k, v in pairs(value) do anz = anz + 1 end value = anz end if(value and value > 0) then return tostring(value) else return "" end end -- the real priv names would be far too long local short_priv_name = { precon_exec_lua = 'pX', effect_exec_lua = 'eX', effect_give_item = 'eG', effect_take_item = 'eT', effect_move_player = 'eM', } yl_speak_up.cache_general_npc_list_lines = {} for k, data in pairs(yl_speak_up.npc_list) do local data = yl_speak_up.npc_list[k] local n = (data.name or "- ? -") if(data.desc and data.desc ~= "") then n = n..', '..(data.desc or "") end -- is the NPC muted? local npc_color = (yl_speak_up.nametag_color_when_not_muted or '#FFFFFF') if(data.muted ~= nil and data.muted == false) then npc_color = (yl_speak_up.nametag_color_when_muted or '#FFFFFF') end -- is the NPC loaded? local is_loaded_color = '#777777' if(yl_speak_up.npc_list_objects[k]) then is_loaded_color = '#FFFFFF' end -- is it a generic NPC? local n_id = 'n_'..tostring(k) local is_generic = '' if(yl_speak_up.generic_dialogs[n_id]) then is_generic = 'G' end -- does the NPC have extra privs? local priv_list = '' if(yl_speak_up.npc_priv_table[n_id]) then for priv, has_it in pairs(yl_speak_up.npc_priv_table[n_id]) do priv_list = priv_list..tostring(short_priv_name[priv])..' ' end end -- fallback if something went wrong with the position (or it's unknown) local pos_str = '- unknown -' if(not(data.pos) or not(data.pos.x) or not(data.pos.y) or not(data.pos.z)) then data.pos = {x=0, y=0, z=0} end pos_str = minetest.formspec_escape(minetest.pos_to_string(data.pos)) yl_speak_up.cache_general_npc_list_lines[k] = { id = k, -- keep for sorting is_loaded_color = is_loaded_color, n_id = n_id, is_generic = is_generic, npc_color = npc_color, -- muted or not -- npc_color is diffrent for each player n_name = minetest.formspec_escape(n), owner = minetest.formspec_escape(data.owner or '- ? -'), is_loaded_color = is_loaded_color, anz_trades = show_if_bigger_null(#data.trades), anz_properties = show_if_bigger_null(data.properties, true), anz_editors = show_if_bigger_null(data.may_edit, true), pos = pos_str, priv_list = priv_list, } end end -- allow to toggle between trade entries and full log -- Note: takes pname instead of player(object) as first parameter yl_speak_up.get_fs_show_npc_list = function(pname, selected_row) -- which NPC can the player edit? local level = 0 if( minetest.check_player_privs(pname, {npc_master=true}) or minetest.check_player_privs(pname, {npc_talk_master=true}) or minetest.check_player_privs(pname, {npc_talk_admin=true})) then level = 2 elseif(minetest.check_player_privs(pname, {npc_talk_owner=true})) then level = 1 end if(level < 1) then return "size[5,1]label[0,0;Error: You do not have the npc_talk_owner priv.]" end local formspec_start = 'size[18,14.7]'.. 'label[4.5,0.5;List of all NPC (that you can edit)]'.. 'tablecolumns[' .. 'color;text,align=right;'.. -- the ID 'color;text,align=center;'.. -- is the NPC a generic one? 'color;text,align=left;'.. -- the name of the NPC 'color;text,align=center;'.. -- the name of the owner of the NPC 'color;text,align=right;'.. -- number of trades offered 'color;text,align=right;'.. -- number of properties set 'color;text,align=right;'.. -- number of people who can edit NPC 'color;text,align=center;'.. -- last known position 'color;text,align=center]'.. -- does he have extra privs? 'table[0.1,1.0;17.8,9.8;show_npc_list;' -- add information about a specific NPC (selected row) local info_current_row = '' if(selected_row and selected_row > 1 and yl_speak_up.cache_npc_list_per_player[pname] and yl_speak_up.cache_npc_list_per_player[pname][selected_row-1]) then local k = yl_speak_up.cache_npc_list_per_player[pname][selected_row-1] local data = yl_speak_up.npc_list[k] local line = yl_speak_up.cache_general_npc_list_lines[k] if(data) then local edit_list = {data.owner} if(data.may_edit) then for e, t in pairs(data.may_edit or {}) do table.insert(edit_list, e) end end local n_id = 'n_'..tostring(k) local priv_list = {} if(yl_speak_up.npc_priv_table[n_id]) then for priv, has_it in pairs(yl_speak_up.npc_priv_table[n_id]) do table.insert(priv_list, priv) end else priv_list = {'- none -'} end local prop_text = 'label[3.0,2.0;- none -]' if(data.properties) then local prop_list = {} for k, v in pairs(data.properties) do table.insert(prop_list, minetest.formspec_escape( tostring(k)..' = '..tostring(v))) end if(#prop_list > 0) then prop_text = 'dropdown[3.0,1.8;8,0.6;properties;'.. table.concat(prop_list, ',')..';;]' end end local first_seen_at = '- unknown -' if(data.created_at and data.created_at ~= "") then first_seen_at = minetest.formspec_escape(os.date("%m/%d/%y", data.created_at)) end -- allow those with teleport priv to easily visit their NPC local teleport_button = '' if(minetest.check_player_privs(pname, {teleport=true})) then -- the ID of the NPC we want to visit is hidden in a field; this is unsafe, -- but the actual check needs to happen when the teleport button is pressed -- anyway teleport_button = 'field[40,40;0,0;selected_id;;'..tostring(k)..']'.. 'button_exit[12.1,1.8;5,0.6;teleport;Teleport to this NPC]' end info_current_row = 'container[0.1,11.2]'.. 'label[0.1,0.0;Name, Desc:]'.. 'label[3.0,0.0;'..tostring(line.n_name)..']'.. 'label[0.1,0.5;Typ:]'.. 'label[3.0,0.5;'.. minetest.formspec_escape(tostring(data.typ or '- ? -'))..']'.. 'label[12.1,0.5;First seen at:]'.. 'label[14.4,0.5;'.. first_seen_at..']'.. 'label[0.1,1.0;Can be edited by:]'.. 'label[3.0,1.0;'.. minetest.formspec_escape(table.concat(edit_list, ', '))..']'.. 'label[0.1,1.5;Has the privs:]'.. 'label[3.0,1.5;'.. minetest.formspec_escape(table.concat(priv_list, ', '))..']'.. 'label[0.1,2.0;Properties:]'.. prop_text.. teleport_button.. 'container_end[]' end else selected_row = 1 info_current_row = 'label[0.1,11.2;Click on a column name/header in order to sort by '.. 'that column. Click it again in order to reverse sort order.\n'.. 'Click on a row to get more information about a specific NPC.\n'.. 'Only NPC that can be edited by you are shown.\n'.. 'Legend: \"G\": is generic NPC. '.. '\"#Tr\", \"#Pr\": Number of trades or properties the NPC offers.\n'.. ' \"#Ed\": Number of players that can edit the NPC. '.. '\"Privs\": List of abbreviated names of privs the NPC has.]' end local formspec = {} -- TODO: blocks may also be talked to local tmp_liste = {} for k, v in pairs(yl_speak_up.npc_list) do if(level == 2 or (v.owner and v.owner == pname) or (v.may_edit and v.may_edit[pname])) then table.insert(tmp_liste, k) end end -- the columns with the colors count as well even though they can't be selected -- (don't sort the first column by n_ STRING - sort by NUMBER) local col_names = {"id", "id", "is_generic", "is_generic", "n_name", "n_name", "owner", "owner", "anz_trades", "anz_trades", "anz_properties", "anz_properties", "anz_editors", "anz_editors", "pos", "pos", "priv_list", "priv_list"} local sort_col = yl_speak_up.sort_npc_list_per_player[pname] if(not(sort_col) or sort_col == 0) then table.sort(tmp_liste) elseif(sort_col > 0) then -- it is often more helpful to sort in descending order local col_name = col_names[sort_col] table.sort(tmp_liste, function(a, b) return yl_speak_up.cache_general_npc_list_lines[a][col_name] > yl_speak_up.cache_general_npc_list_lines[b][col_name] end) else local col_name = col_names[sort_col * -1] table.sort(tmp_liste, function(a, b) return yl_speak_up.cache_general_npc_list_lines[a][col_name] < yl_speak_up.cache_general_npc_list_lines[b][col_name] end) end local col_headers = {'n_id', 'G', 'Name', 'Owner', '#Tr', '#Pr', '#Ed', 'Position', 'Privs'} for i, k in ipairs(col_headers) do if( sort_col and sort_col == (i * 2)) then table.insert(formspec, 'yellow') table.insert(formspec, 'v '..k..' v') elseif(sort_col and sort_col == (i * -2)) then table.insert(formspec, 'yellow') table.insert(formspec, '^ '..k..' ^') else table.insert(formspec, '#FFFFFF') table.insert(formspec, k) end end yl_speak_up.cache_npc_list_per_player[pname] = tmp_liste for i, k in ipairs(tmp_liste) do local data = yl_speak_up.npc_list[k] local line = yl_speak_up.cache_general_npc_list_lines[k] -- own NPC are colored green, others white local owner_color = '#FFFFFF' if(data.owner == pname) then owner_color = '#00FF00' elseif (data.may_edit and data.may_edit[pname]) then owner_color = '#FFFF00' end table.insert(formspec, line.is_loaded_color) table.insert(formspec, line.n_id) table.insert(formspec, 'orange') table.insert(formspec, line.is_generic) table.insert(formspec, line.npc_color) table.insert(formspec, line.n_name) table.insert(formspec, owner_color) -- diffrent for each player table.insert(formspec, line.owner) table.insert(formspec, line.is_loaded_color) table.insert(formspec, line.anz_trades) table.insert(formspec, line.is_loaded_color) table.insert(formspec, line.anz_properties) table.insert(formspec, owner_color) -- diffrent for each player table.insert(formspec, line.anz_editors) table.insert(formspec, line.is_loaded_color) table.insert(formspec, line.pos) table.insert(formspec, line.is_loaded_color) table.insert(formspec, line.priv_list) end table.insert(formspec, ";"..selected_row.."]") return table.concat({formspec_start, table.concat(formspec, ','), info_current_row, 'button_exit[0.1,14;19.6,0.6;exit;Exit]'}, '') end -- at load/reload of the mod: read the list of existing NPC yl_speak_up.npc_list_load()