mirror of
https://gitea.your-land.de/Sokomine/yl_speak_up.git
synced 2025-06-16 13:48:06 +02:00
541 lines
16 KiB
Lua
541 lines
16 KiB
Lua
-- 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_<nr>, o_<nr> 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
|