master #4

Merged
AliasAlreadyTaken merged 56 commits from master into yl_stable 2025-03-08 04:12:44 +01:00
26 changed files with 3142 additions and 968 deletions

188
addons/action_send_mail.lua Normal file
View File

@ -0,0 +1,188 @@
-- requires mail.send from the mail mod
-- sending a mail allows to give feedback - and for players to ask the NPC owner to add more texts
-- and let the NPC answer to further questions
-- define the custom action named "send_mail"
local action_send_mail = {
-- this information is necessary for allowing to add this as an action to an option
description = "Send a mail via the mail_mod mod for feedback/ideas/etc.",
-- define the parameters that can be set when the action is added
param1_text = "To:",
param1_desc = "Who shall receive this mail? Default: $OWNER_NAME$."..
"\nNote: Leave fields empty for the default values."..
"\nNote: All parameters allow to use the usual replacements like $NPC_NAME$,"..
"\n\t$OWNER_NAME$, $PLAYER_NAME$, $VAR name_of_your_var$, $PROP name_of_prop$.",
param2_text = "From:",
param2_desc = "Who shall be listed as the sender of this mail?\n"..
"The player talking to the NPC might be best as it makes answering easier.\n"..
"Default: $PLAYER_NAME$. Also allowed: $OWNER_NAME$.",
param3_text = "cc:",
param3_desc = "(optional) Whom to send a carbon copy to?"..
"\nThis is useful if multiple players may edit this NPC."..
"\nIt is also possible to send a copy to $PLAYER_NAME$.",
param4_text = "bcc:",
param4_desc = "(optional) Who gets set in the bcc?",
param5_text = "Subject:",
param5_desc = "The subject of the mail. The player talking to the NPC\n"..
"will provide the actual text for the body of the mail.\n"..
"Default: \"$NPC_NAME$: regarding $PLAYER_NAME$\"",
}
-- this function will show a formspec whenever our custom action "send_mail" is executed
--yl_speak_up.custom_functions_a_[ "send_mail" ].code = function(player, n_id, a)
action_send_mail.code = function(player, n_id, a)
local pname = player:get_player_name()
-- sending the mail can either succeed or fail; pdata.tmp_mail_* variables store the result
local pdata = yl_speak_up.speak_to[pname]
-- just the normal dialog data from the NPC (contains name of the NPC and owner)
local dialog = yl_speak_up.speak_to[pname].dialog
local npc_name = "- (this NPC) -"
local owner_name = "- (his owner) -"
if(dialog) then
-- the NPC is the one "forwarding" the message (so that the receiver will know
-- *which* NPC was talked to)
npc_name = minetest.formspec_escape(dialog.n_npc or "- ? -")
-- usually the owner is the receiver
owner_name = minetest.formspec_escape(a.a_param1 or dialog.npc_owner or "- ? ")
end
-- the mail was already sent successful; we still return once to this formspec so that
-- the player gets this information and can finish the action successfully
if(pdata and pdata.tmp_mail_to and pdata.tmp_mail_success) then
local mail_to = minetest.formspec_escape(pdata.tmp_mail_to or "?")
-- unset temporary variables that are no longer needed
pdata.tmp_mail_success = nil
pdata.tmp_mail_error = nil
pdata.tmp_mail_to = nil
return table.concat({
"size[20,3]label[0.2,0.7;",
npc_name,
" has sent a mail containing your text to ",
mail_to,
" and awaits further instructions."..
"\nPlease be patient and wait for a reply. This may take some time as ",
mail_to,
" has to receive, read and answer the mail.]",
-- offer a button to finally complete the action successfully
"button[4,2;6.8,0.9;finished_action;Ok. I'll wait.]",
}, "")
-- we tried to send the mail - and an error occoured
elseif(pdata and pdata.tmp_mail_to and pdata.tmp_mail_error) then
local mail_to = minetest.formspec_escape(pdata.tmp_mail_to or "?")
local error_msg = minetest.formspec_escape(pdata.tmp_mail_error or "?")
-- unset temporary variables that are no longer needed
pdata.tmp_mail_success = nil
pdata.tmp_mail_error = nil
pdata.tmp_mail_to = nil
return table.concat({
"size[20,8]label[0.2,0.7;",
npc_name,
" FAILED to sent a mail containing your text to ",
mail_to,
" in order to get help!]",
"textarea[0.2,1.8;19.6,5;;The following error(s) occourd:;",
error_msg,
"]",
-- the action can no longer be completed successfully; best to back to talk
"button[7,7.0;6.0,0.9;back_to_talk;Back to talk]",
}, "")
end
-- the mail has not been sent yet; show the normal formspec asking for text input
return table.concat({"size[20,8.5]label[4,0.7;Send a message to ",
npc_name,
"]",
"button[17.8,0.2;2.0,0.9;back_to_talk;Back]",
"label[0.2,7.0;Note: ",
npc_name,
" will send a mail to ",
minetest.formspec_escape(a.a_param1 or dialog.npc_owner or "- ? "),
", requesting instructions how to respond. This may take a while.]",
"button[3.6,7.5;6.0,0.9;back_to_talk;Abort and go back]",
"button[10.2,7.5;6.0,0.9;send_mail;Send this message]",
-- read-only
"textarea[0.2,1.8;19.6,5;message_text;Write your message for ",
npc_name,
" here, and then click on \"Send this message\":;",
"",
"]",
})
end
-- whenever our formspec above for the custom action "send_mail" received input (player clicked
-- on a button), this function is called
action_send_mail.code_input_handler = function(player, n_id, a, formname, fields)
local pname = player:get_player_name()
if(not(pname) or not(yl_speak_up.speak_to[pname])) then
return fields
end
-- sending was aborted or there was no text to send
if( not(fields.message_text) or fields.message_text == ""
or not(fields.send_mail) or fields.send_mail == "") then
return fields
end
local dialog = yl_speak_up.speak_to[pname].dialog
-- prepare data/parameters for the mail we want to send
-- $PLAYER_NAME$, $OWNER_NAME$, $NPC_NAME$, $VAR *$ $PROP *$ are allowed replacements!!
local mail_to = yl_speak_up.replace_vars_in_text(a.a_param1, dialog, pname)
local mail_from = yl_speak_up.replace_vars_in_text(a.a_param2, dialog, pname)
local mail_cc = yl_speak_up.replace_vars_in_text(a.a_param3, dialog, pname)
local mail_bcc = yl_speak_up.replace_vars_in_text(a.a_param4, dialog, pname)
local mail_subject = yl_speak_up.replace_vars_in_text(a.a_param5, dialog, pname)
if(not(mail_to) or mail_to == "") then
mail_to = dialog.npc_owner
end
-- make sure the sender is not forged; we allow EITHER the name of the owner of the NPC
-- OR the name of the player currently talking to the npc
if(not(mail_from) or mail_from == "" or mail_from ~= dialog.npc_owner) then
mail_from = pname
end
if(not(mail_cc) or mail_cc == "") then
mail_cc = nil
end
if(not(mail_bcc) or mail_bcc == "") then
mail_bcc = nil
end
if(not(mail_subject) or mail_subject == "") then
mail_subject = (dialog.n_npc or "- ? -")..": regarding "..pname
end
-- actually send the mail via the mail_mod mod
local success, error_msg = mail.send({
from = mail_from,
to = mail_to,
cc = mail_cc,
bcc = mail_bcc,
subject = mail_subject,
body = "Dear "..tostring(dialog.npc_owner)..",\n\n"..tostring(pname)..
" asked me something I don't know the answer to. Hope you can help? "..
"This is the request:\n\n"..
tostring(fields.message_text or "- no message -")
})
-- Sending this mail was either successful or not. We want to display this to the player.
-- Therefore, we set fields.back_from_error_msg. This tells the calling function that it
-- needs to display the formspec generated by the function
-- yl_speak_up.custom_functions_a_[ "send_mail" ].code
-- again.
fields.back_from_error_msg = true
-- The function displaying the formspec needs to know that it has to display the result
-- of sending the mail now. We need to store these variables somewhere.
local pdata = yl_speak_up.speak_to[pname]
pdata.tmp_mail_success = success
pdata.tmp_mail_error = error_msg
pdata.tmp_mail_to = mail_to
-- the function has to return fields
return fields
end
if(minetest.global_exists("mail")
and type(mail) == "table"
and type(mail.send) == "function") then
-- only add this action if the mail mod and the mail.send function exist
yl_speak_up.custom_functions_a_[ "send_mail" ] = action_send_mail
end

View File

@ -0,0 +1,45 @@
-- hand out a preconfigured waypoint compass to the player
yl_speak_up.custom_functions_r_[ "send_coordinates" ] = {
description = "Send a chat message to the player with coordinates.",
param1_text = "X coordinate:",
param1_desc = "The target x coordinate.",
param2_text = "Y coordinate:",
param2_desc = "The target y coordinate.",
param3_text = "Z coordinate:",
param3_desc = "The target z coordinate.",
param4_text = "Name of target location:",
param4_desc = "This is how the target location is called, i.e. \"Hidden treasure chest\".",
-- the color cannot be set this way
-- param5_text = "Color code in Hex:",
-- param5_desc = "Give the color for the compass here. Example: \"FFD700\".\n"..
-- "Needs to be 6 characters long, with each character ranging\n"..
-- "from 0-9 or beeing A, B, C, D, E or F.\n"..
-- "Or just write something like yellow, orange etc.",
code = function(player, n_id, r)
local pname = player:get_player_name()
local coords = core.string_to_pos((r.r_param1 or "0")..","..
(r.r_param2 or "0")..","..
(r.r_param3 or "0"))
local town = (r.r_param4 or "- some place somewhere -")
if(not(coords)) then
minetest.chat_send_player(pname, "Sorry. There was an internal error with the "..
"coordinates. Please inform whoever is responsible for this NPC.")
return false
end
if(not(pname) or not(yl_speak_up.speak_to[pname])) then
return false
end
local dialog = yl_speak_up.speak_to[pname].dialog
minetest.chat_send_player(pname,
(dialog.n_npc or "- ? -")..": \""..
tostring(town).."\" can be found at "..core.pos_to_string(coords, 0)..".")
if(minetest.get_modpath("waypoint_compass")) then
minetest.chat_send_player(pname, "If you have a waypoint compass, right-click "..
"while wielding it. Select \"copy:\" to copy the location above into "..
"your compass.")
end
-- the function was successful (effects only return true or false)
return true
end,
}

View File

@ -0,0 +1,94 @@
-- requires mail.send from the mail mod
-- NPC can send out mails in order to sum up a quest state or complex step
-- - or just to inform their owner that they ran out of stock
-- There is also a similar action defined in another file. The action
-- allows the player that talks to the NPC to enter his/her own mailtext.
-- The *effect* here requires that the text has been configured in advance.
-- define the custom effect named "send_mail"
local effect_send_mail = {
-- this information is necessary for allowing to add this as an effect to an option
description = "Send a preconfigured mail via the mail_mod mod for quest state etc.",
-- define the parameters that can be set when the action is added
param1_text = "To:",
param1_desc = "Who shall receive this mail? Default: $PLAYER_NAME$."..
"\nNote: Leave fields empty for the default values."..
"\nNote: All parameters allow to use the usual replacements like $NPC_NAME$,"..
"\n\t$OWNER_NAME$, $PLAYER_NAME$, $VAR name_of_your_var$, $PROP name_of_prop$.",
-- the "From:" field will always be the name of the owner of the NPC
-- param2_text = "From:",
-- param2_desc = "Who shall be listed as the sender of this mail?\n"..
-- "The player talking to the NPC might be best as it makes answering easier.\n"..
-- "Default: $PLAYER_NAME$. Also allowed: $OWNER_NAME$.",
param3_text = "cc:",
param3_desc = "(optional) Whom to send a carbon copy to?"..
"\nThis is useful if multiple players may edit this NPC."..
"\nIt is also possible to send a copy to $PLAYER_NAME$.",
param4_text = "bcc:",
param4_desc = "(optional) Who gets set in the bcc?",
param5_text = "Subject:",
param5_desc = "The subject of the mail. Ought to give the player information\n"..
"which NPC sent this mail and why.\n"..
"Default: \"$NPC_NAME$ has a message from $OWNER_NAME$\"",
param6_text = "Mail text:",
param6_desc = "The actual text of the mail. Use the usual replacements to make the mail\n"..
"meaningful! You may want to use $VAR name_of_your_var$.\n"..
"Note: Use \\n to create a newline!",
}
-- the actual implementation of the function - run when the effect is executed
effect_send_mail.code = function(player, n_id, r)
local pname = player:get_player_name()
if(not(pname) or not(yl_speak_up.speak_to[pname])) then
return fields
end
local dialog = yl_speak_up.speak_to[pname].dialog
-- prepare data/parameters for the mail we want to send
-- $PLAYER_NAME$, $OWNER_NAME$, $NPC_NAME$, $VAR *$ $PROP *$ are allowed replacements!
local mail_to = yl_speak_up.replace_vars_in_text(r.r_param1, dialog, pname)
local mail_from = yl_speak_up.replace_vars_in_text(r.r_param2, dialog, pname)
local mail_cc = yl_speak_up.replace_vars_in_text(r.r_param3, dialog, pname)
local mail_bcc = yl_speak_up.replace_vars_in_text(r.r_param4, dialog, pname)
local mail_subject = yl_speak_up.replace_vars_in_text(r.r_param5, dialog, pname)
local mail_text = yl_speak_up.replace_vars_in_text(r.r_param6, dialog, pname)
-- this is in reverse of the actions: the mail is usually sent to the player the
-- NPC is talking with - e.g. as a reminder of a quest status
if(not(mail_to) or mail_to == "") then
mail_to = pname
end
-- the mail always originates from the owner of the NPC
mail_from = dialog.npc_owner
if(not(mail_cc) or mail_cc == "") then
mail_cc = nil
end
if(not(mail_bcc) or mail_bcc == "") then
mail_bcc = nil
end
if(not(mail_subject) or mail_subject == "") then
mail_subject = (dialog.n_npc or "- ? -").." has a message from "..(dialog.npc_owner or "- ? -")
end
-- actually send the mail via the mail_mod mod
local success, error_msg = mail.send({
from = mail_from,
to = mail_to,
cc = mail_cc,
bcc = mail_bcc,
subject = mail_subject,
body = "Message from "..tostring(dialog.n_npc or "- ? -")..":\n\n"..
table.concat(string.split(mail_text or "- no message -", "\\n"), "\n")
})
return success
end
if(minetest.global_exists("mail")
and type(mail) == "table"
and type(mail.send) == "function") then
-- only add this effect if the mail mod and the mail.send function exist
yl_speak_up.custom_functions_r_[ "send_mail" ] = effect_send_mail
end

20
addons/load_addons.lua Normal file
View File

@ -0,0 +1,20 @@
-- this file lods addons - actions, preconditions, effects and other things
-- - which may not be of intrest to all games
-- - and which usually require other mods to be installed in order to work
local path_addons = yl_speak_up.modpath..DIR_DELIM.."addons"..DIR_DELIM
-- the action "send_mail" requires the "mail" mod and allows to send
-- ingame mails via actions
if(minetest.global_exists("mail")
and type(mail) == "table"
and type(mail.send) == "function") then
dofile(path_addons .. "action_send_mail.lua")
dofile(path_addons .. "effect_send_mail.lua")
end
-- makes mostly sense if the waypoint_compass mod is installed
dofile(path_addons.."effect_send_coordinates.lua")

View File

@ -8,26 +8,6 @@ yl_speak_up.stop_talking = function(pname)
end
-- helper function for
-- yl_speak_up.get_fs_talkdialog and
-- yl_speak_up.check_and_add_as_generic_dialog
-- find the dialog with d_sort == 0 or lowest number
yl_speak_up.get_start_dialog_id = function(dialog)
if(not(dialog) or not(dialog.n_dialogs)) then
return nil
end
-- Find the dialog with d_sort = 0 or alternatively with the lowest number
local lowest_sort = nil
local d_id = nil
for k, v in pairs(dialog.n_dialogs) do
local nr = tonumber(v.d_sort)
if(not(lowest_sort) or (nr and nr >= 0 and nr < lowest_sort)) then
lowest_sort = nr
d_id = k
end
end
return d_id
end
-- count visits to this dialog - but *not* for generic dialogs as those are just linked and not

View File

@ -24,6 +24,30 @@ yl_speak_up.replace_vars_in_text = function(text, dialog, pname)
PLAYER_NAME = pname,
}
-- only try to replace variables if there are variables inside the text
if(string.find(text, "$VAR ")) then
local varlist = yl_speak_up.get_quest_variables(dialog.npc_owner, true)
for i,v in ipairs(varlist) do
local v_name = string.sub(v, 3)
-- only allow to replace unproblematic variable names
if(not(string.find(v_name, "[^%w^%s^_^%-^%.]"))) then
-- remove leading $ from $ var_owner_name var_name
subs["VAR "..v_name] = yl_speak_up.get_quest_variable_value(dialog.npc_owner, v) or "- not set -"
end
end
end
-- only replace properties if any properties are used inside the text
if(string.find(text, "$PROP ")) then
local properties = yl_speak_up.get_npc_properties(pname)
for k,v in pairs(properties) do
-- only allow to replace unproblematic property names
if(not(string.find(k, "[^%w^%s^_^%-^%.]"))) then
subs["PROP "..k] = v
end
end
end
local day_time_name = "day"
local day_time = minetest.get_timeofday()
if(day_time < 0.5) then
@ -41,7 +65,9 @@ yl_speak_up.replace_vars_in_text = function(text, dialog, pname)
-- substitutions in it using substring captured by "()" in
-- pattern. "[%a_]+" means one or more letter or underscore.
-- If lookup returns nil, then no substitution is made.
text = string.gsub(text or "", "%$([%a_]+)%$", subs)
-- Note: Names of variables may contain alphanumeric signs, spaces, "_", "-" and ".".
-- Variables with other names cannot be replaced.
text = string.gsub(text or "", "%$([%w%s_%-%.]+)%$", subs)
return text
end

View File

@ -48,14 +48,7 @@ yl_speak_up.command_npc_talk = function(pname, param)
-- implemented in fs_npc_list.lua:
return yl_speak_up.command_npc_force_restore_npc(pname, rest)
elseif(cmd and cmd == "privs") then
-- TODO: make this available for npc_talk_admin?
if(not(minetest.check_player_privs(pname, {privs = true}))) then
minetest.chat_send_player(pname, "This command is used for managing "..
"privs (like execute lua, teleportation, giving items...) for NPC. "..
"You lack the \"privs\" priv required to "..
"run this command.")
return
end
-- the command now checks for player privs
-- implemented in npc_privs.lua:
return yl_speak_up.command_npc_talk_privs(pname, rest)
end
@ -68,9 +61,9 @@ yl_speak_up.command_npc_talk = function(pname, param)
" version show human-readable version information\n"..
" list shows a list of NPC that you can edit\n"..
" debug debug a particular NPC\n"..
" generic [requores npc_talk_admin priv] list, add or remove NPC as generic NPC\n"..
" privs list, grant or revoke privs for your NPC\n"..
" generic [requires npc_talk_admin priv] list, add or remove NPC as generic NPC\n"..
" force_restore_npc [requires npc_talk_admin priv] restore NPC that got lost\n"..
" privs [requires privs priv] list, grant or revoke privs for an NPC\n"..
-- reload is fully handled in register_once
"Note: /npc_talk_reload [requires privs priv] reloads the code of the mod without server "..
"restart."..

View File

@ -129,15 +129,32 @@ yl_speak_up.player_vars_min_save_time = 60
------------------------------------------------------------------------------
-- Privs - usually no need to change
------------------------------------------------------------------------------
-- * set the name of the priv that allows to add, edit and change preconditions, actions and
-- effects listed in yl_speak_up.npc_priv_names in npc_privs.lua
-- * this also allows the player to use the "/npc_talk privs" command to assign these privs
-- to NPC
-- * it does *NOT* include the "precon_exec_lua" and "effect_exec_lua" priv - just
-- "effect_give_item", "effect_take_item" and "effect_move_player"
-- NPC need npc privs in order to use some preconditions, actions and effects.
--
-- Plaers need privs in order to add, edit and change preconditions, actions and
-- effects listed in yl_speak_up.npc_priv_names in npc_privs.lua.
--
-- The following player priv allows the player to use the "/npc_talk privs" command to
-- grant/revoke/see these npc privs for *all NPC - not only for those the player can edit!
-- * default: "npc_talk_admin" (but can also be set to "npc_master" or "privs" if you want)
yl_speak_up.npc_privs_priv = "npc_talk_admin"
-- depending on your server, you might want to allow /npc_talk privs to be used by players
-- who *don't* have the privs priv;
-- WANRING: "precon_exec_lua" and "effect_exec_lua" are dangerous npc privs. Only players
-- with the privs priv ought to be able to use those!
-- The privs priv is the fallback if nothing is specified here.
yl_speak_up.npc_priv_needs_player_priv = {}
-- these privs allow to create items out of thin air - similar to the "give" priv
yl_speak_up.npc_priv_needs_player_priv["effect_give_item"] = "give"
yl_speak_up.npc_priv_needs_player_priv["effect_take_item"] = "give"
-- on servers with travelnets and/or teleporters, you'd most likely want to allow every
-- player to let NPC teleport players around; the "interact" priv covers that
--yl_speak_up.npc_priv_needs_player_priv["effect_move_player"] = "interact"
-- on YourLand, travel is very restricted; only those who can teleport players around can
-- do it with NPC as well; for backward compatibility, this is set for all servers
yl_speak_up.npc_priv_needs_player_priv["effect_move_player"] = "bring"
------------------------------------------------------------------------------
-- Blacklists - not all blocks may be suitable for all effects NPC can do
------------------------------------------------------------------------------

View File

@ -177,9 +177,10 @@ yl_speak_up.execute_next_action = function(player, a_id, result_of_a_id, formnam
yl_speak_up.speak_to[pname].d_id = this_action.a_on_failure
yl_speak_up.speak_to[pname].o_id = nil
yl_speak_up.speak_to[pname].a_id = nil
yl_speak_up.show_fs(player, "talk", {n_id = n_id,
d_id = this_action.a_on_failure,
alternate_text = this_action.alternate_text})
-- allow d_end, d_trade, d_got_item etc. to work as a_on_failure
yl_speak_up.show_next_talk_fs_after_action(player, pname,
this_action.a_on_failure, formname,
dialog, d_id, n_id, this_action.alternate_text)
return
else
local this_action = actions[ sorted_key_list[ nr ]]
@ -220,6 +221,26 @@ yl_speak_up.execute_next_action = function(player, a_id, result_of_a_id, formnam
local target_dialog = res.next_dialog
yl_speak_up.speak_to[pname].o_id = nil
yl_speak_up.speak_to[pname].a_id = nil
-- the function above returns a target dialog; show that to the player
yl_speak_up.show_next_talk_fs_after_action(player, pname, target_dialog, formname,
dialog, target_dialog, n_id, res.alternate_text)
end
-- after completing the action - either successfully or if it failed:
yl_speak_up.show_next_talk_fs_after_action = function(player, pname, target_dialog, formname,
dialog, d_id, n_id, alternate_text)
-- allow to switch to d_trade from any dialog
if(target_dialog and target_dialog == "d_trade") then
yl_speak_up.show_fs(player, "trade_list")
return
end
-- allow to switch to d_got_item from any dialog
if(target_dialog and target_dialog == "d_got_item") then
yl_speak_up.show_fs(player, "player_offers_item")
return
end
-- end conversation
if(target_dialog and target_dialog == "d_end") then
yl_speak_up.stop_talking(pname)
@ -229,18 +250,16 @@ yl_speak_up.execute_next_action = function(player, a_id, result_of_a_id, formnam
end
return
end
-- the special dialogs d_trade and d_got_item have no actions or effects - thus
-- d_id cannot become d_trade or d_got_item
if(not(target_dialog)
or target_dialog == ""
or not(dialog.n_dialogs[target_dialog])) then
target_dialog = d_id
end
if(target_dialog and target_dialog == "d_trade") then
yl_speak_up.show_fs(player, "trade_list")
return
end
-- the function above returns a target dialog; show that to the player
-- actually show the next dialog to the player
yl_speak_up.show_fs(player, "talk", {n_id = n_id, d_id = target_dialog,
alternate_text = res.alternate_text})
alternate_text = alternate_text})
end
@ -293,6 +312,22 @@ yl_speak_up.get_action_by_player = function(player)
end
-- did the NPC try to give something to the player already - and the player didn't take it?
-- then give that old item back to the NPC
yl_speak_up.action_take_back_failed_npc_gives = function(trade_inv, npc_inv)
if(not(trade_inv) or not(npc_inv)) then
return
end
local last_stack = trade_inv:get_stack("npc_gives", 1)
if(not(last_stack:is_empty())) then
-- strip any metadata to avoid stacking problems
npc_inv:add_item("npc_main", last_stack:get_name().." "..last_stack:get_count())
-- clear the stack
trade_inv:set_stack("npc_gives", 1, "")
end
end
-- Create the quest item by taking a raw item (i.e. a general piece of paper) out
-- of the NPC's inventory, applying a description (if given) and quest id (if
-- given); place the quest item in the trade inv of the player in the npc_gives slot.
@ -312,6 +347,10 @@ yl_speak_up.action_quest_item_prepare = function(player)
local stack = ItemStack(a.a_value)
-- get the inventory of the NPC
local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)})
local trade_inv = minetest.get_inventory({type="detached", name="yl_speak_up_player_"..pname})
yl_speak_up.action_take_back_failed_npc_gives(trade_inv, npc_inv)
-- does the NPC have the item we are looking for?
if(not(npc_inv:contains_item("npc_main", stack))) then
local o_id = yl_speak_up.speak_to[pname].o_id
@ -341,7 +380,6 @@ yl_speak_up.action_quest_item_prepare = function(player)
-- put the stack in the npc_gives-slot of the trade inventory of the player
-- (as that slot is managed by the NPC alone we don't have to worry about
-- anything else in the slot)
local trade_inv = minetest.get_inventory({type="detached", name="yl_speak_up_player_"..pname})
-- actually put the stack in there
trade_inv:set_stack("npc_gives", 1, new_stack)
return true

View File

@ -489,6 +489,8 @@ yl_speak_up.execute_effect = function(player, n_id, o_id, r)
elseif(r.r_type == "function") then
-- this can only be set and edited with the staff
if(not(yl_speak_up.npc_has_priv(n_id, "effect_exec_lua", r.r_is_generic))) then
yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." "..
r.r_type..": The NPC does not have the \"effect_exec_lua\" priv.")
return false
end
return yl_speak_up.eval_and_execute_function(player, r, "r_")
@ -498,6 +500,8 @@ yl_speak_up.execute_effect = function(player, n_id, o_id, r)
return false
end
if(not(yl_speak_up.npc_has_priv(n_id, "effect_give_item", r.r_is_generic))) then
yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." "..
r.r_type..": The NPC does not have the \"effect_give_item\" priv.")
return false
end
local item = ItemStack(r.r_value)
@ -518,6 +522,8 @@ yl_speak_up.execute_effect = function(player, n_id, o_id, r)
-- this can only be set and edited with the staff
elseif(r.r_type == "take_item") then
if(not(yl_speak_up.npc_has_priv(n_id, "effect_take_item", r.r_is_generic))) then
yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." "..
r.r_type..": The NPC does not have the \"effect_take_item\" priv.")
return false
end
if(not(r.r_value)) then
@ -539,6 +545,8 @@ yl_speak_up.execute_effect = function(player, n_id, o_id, r)
-- this can only be set and edited with the staff
elseif(r.r_type == "move") then
if(not(yl_speak_up.npc_has_priv(n_id, "effect_move_player", r.r_is_generic))) then
yl_speak_up.debug_msg(player, n_id, o_id, tostring(r.r_id).." "..
r.r_type..": The NPC does not have the \"effect_move_player\" priv.")
return false
end
-- copeid/moved here from AliasAlreadyTakens code in functions.lua
@ -791,6 +799,7 @@ yl_speak_up.execute_effect = function(player, n_id, o_id, r)
return yl_speak_up.use_tool_on_block(r, "on_use", player, n_id, o_id)
end
-- even air can be punched - even if that is pretty pointless
-- TODO: some blocks may define their own functions and care for what the player wields (i.e. cheese mod)
minetest.punch_node(r.r_pos, nil)
return true
-- "Right-click the block.", -- 5

View File

@ -13,10 +13,15 @@ local use_d_name = true
-- in order to be able to deal with multiple NPC in ink, we use the NPC id n_id
-- plus the dialog id d_id as a name prefix; o_id, a_id and r_id are appended
-- as needed
yl_speak_up.export_to_ink.print_knot_name = function(lines, knot_name)
yl_speak_up.export_to_ink.print_knot_name = function(lines, knot_name, use_prefix, dialog_names)
if(knot_name and dialog_names[knot_name]) then
knot_name = dialog_names[knot_name]
end
knot_name = use_prefix..tostring(knot_name or "ERROR")
table.insert(lines, "\n\n=== ")
table.insert(lines, tostring(knot_name or "ERROR"))
table.insert(lines, knot_name)
table.insert(lines, " ===")
return knot_name
end
@ -55,7 +60,7 @@ end
-- choices are a bit complicated as they may contain alternate_text that is to be
-- displayed instead (in yl_speak_up) and before (in ink) shwoing the target dialog text;
-- also, the divert_to target dialog may need to be rewritten
yl_speak_up.export_to_ink.print_choice = function(lines, choice_text, n_id, start_dialog,
yl_speak_up.export_to_ink.print_choice = function(lines, choice_text, use_prefix, start_dialog,
alternate_text, divert_to, only_once, label,
precondition_list, effect_list,
dialog_names)
@ -111,19 +116,18 @@ yl_speak_up.export_to_ink.print_choice = function(lines, choice_text, n_id, star
table.insert(lines, "\n")
end
-- actually go to the dialog this option leads to
table.insert(lines, " -> ")
table.insert(lines, " -> "..use_prefix)
if(not(start_dialog) or start_dialog == "") then
start_dialog = "d_1"
end
if(not(divert_to) or divert_to == "") then
-- go back to the start dialog (the start dialog may have been changed)
divert_to = tostring(start_dialog)
elseif(divert_to == "d_end" or divert_to == tostring(n_id).."_d_end") then
elseif(divert_to == "d_end" or divert_to == use_prefix.."d_end") then
-- go back to choosing between talking to NPC and end
divert_to = tostring(n_id).."_main"
elseif(string.sub(divert_to, 1, 2) ~= "n_") then
-- make sure it is prefixed with the n_id
divert_to = tostring(n_id).."_"..tostring(divert_to)
divert_to = "d_end"
else
divert_to = tostring(divert_to)
end
if(dialog_names and dialog_names[divert_to]) then
divert_to = dialog_names[divert_to]
@ -134,12 +138,8 @@ end
-- this prints the dialog as a knot - but without choices (those are added to the lines table later)
-- d: dialog
yl_speak_up.export_to_ink.print_dialog_knot = function(lines, n_id, d_id, d)
local knot_name = tostring(n_id).."_"..tostring(d_id)
if(use_d_name) then
knot_name = (d.d_name or knot_name)
end
ink_export.print_knot_name(lines, knot_name)
yl_speak_up.export_to_ink.print_dialog_knot = function(lines, use_prefix, d_id, d, dialog_names)
local knot_name = ink_export.print_knot_name(lines, d_id, use_prefix, dialog_names)
-- many characters at the start of a line have a special meaning;
-- hopefully they will not be obstrusive later on;
@ -159,28 +159,30 @@ end
-- a knot for each action
-- Parameter:
-- a action
yl_speak_up.export_to_ink.print_action_knot = function(lines, n_id, d_id, o_id, start_dialog,
a, alternate_text_on_success, next_target, dialog_names)
local knot_name = tostring(n_id).."_"..tostring(d_id).."_"..tostring(o_id).."_"..tostring(a.a_id)
ink_export.print_knot_name(lines, knot_name)
yl_speak_up.export_to_ink.print_action_knot = function(lines, use_prefix, d_id, o_id, start_dialog,
a, alternate_text_on_success, next_target, dialog_names,
e_list_on_success)
local action_prefix = use_prefix.."action_"..tostring(a.a_id).."_"..tostring(o_id).."_"
local knot_name = ink_export.print_knot_name(lines, d_id, action_prefix, dialog_names)
table.insert(lines, "\n:action: ")
table.insert(lines, a.a_id)
table.insert(lines, " ")
table.insert(lines, yl_speak_up.show_action(a))
table.insert(lines, "A: "..minetest.serialize(a or {})..".")
ink_export.print_choice(lines, "Action was successful", n_id, start_dialog,
ink_export.print_choice(lines, "Action was successful", use_prefix, start_dialog,
alternate_text_on_success, next_target, false, nil,
nil, nil, dialog_names)
nil, e_list_on_success, dialog_names)
ink_export.print_choice(lines, "Action failed", n_id, start_dialog,
ink_export.print_choice(lines, "Action failed", use_prefix, start_dialog,
a.alternate_text, a.a_on_failure, false, nil,
nil, nil, dialog_names)
ink_export.print_choice(lines, "Back", n_id, start_dialog,
nil, tostring(n_id).."_"..tostring(d_id), false, nil,
ink_export.print_choice(lines, "Back", use_prefix, start_dialog,
nil, tostring(d_id), false, nil,
nil, nil, dialog_names)
return knot_name
return string.sub(knot_name, string.len(use_prefix)+1)
end
@ -190,11 +192,11 @@ end
-- Parameter:
-- r effect/result
-- r_prev previous effect
yl_speak_up.export_to_ink.print_effect_knot = function(lines, n_id, d_id, o_id, start_dialog,
yl_speak_up.export_to_ink.print_effect_knot = function(lines, use_prefix, d_id, o_id, start_dialog,
r, r_prev, alternate_text_on_success, next_target,
dialog_names)
local knot_name = tostring(n_id).."_"..tostring(d_id).."_"..tostring(o_id).."_"..tostring(r.r_id)
ink_export.print_knot_name(lines, knot_name)
local effect_prefix = use_prefix.."effect_"..tostring(r.r_id).."_"..tostring(o_id).."_"
local knot_name = ink_export.print_knot_name(lines, d_id, effect_prefix, dialog_names)
table.insert(lines, "\n:effect: ")
table.insert(lines, r.r_id)
@ -208,20 +210,19 @@ yl_speak_up.export_to_ink.print_effect_knot = function(lines, n_id, d_id, o_id,
-- show text of the *previous effect* - because that is the one which may have failed:
table.insert(lines, yl_speak_up.show_effect(r_prev))
ink_export.print_choice(lines, "Effect was successful", n_id, start_dialog,
ink_export.print_choice(lines, "Effect was successful", use_prefix, start_dialog,
alternate_text_on_success, next_target, false, nil,
nil, nil, dialog_names)
ink_export.print_choice(lines, "Effect failed", n_id, start_dialog,
ink_export.print_choice(lines, "Effect failed", use_prefix, start_dialog,
r.alternate_text, r.r_value, false, nil,
nil, nil, dialog_names)
return knot_name
return string.sub(knot_name, string.len(use_prefix)+1)
end
-- which variables are used by this NPC?
yl_speak_up.export_to_ink.print_variables_used = function(lines, dialog, n_id, pname)
yl_speak_up.export_to_ink.print_variables_used = function(lines, dialog)
if(not(dialog) or not(dialog.n_dialogs)) then
return
end
@ -306,7 +307,7 @@ local var_with_operator = function(liste, var_name, op, var_cmp_value, vars_used
-- "quest_step_done", "quest_step_not_done"
end
yl_speak_up.export_to_ink.translate_precondition_list = function(dialog, preconditions, vars_used, n_id,
yl_speak_up.export_to_ink.translate_precondition_list = function(dialog, preconditions, vars_used, use_prefix,
dialog_names)
-- collect preconditions that may work in ink
local liste = {}
@ -320,9 +321,9 @@ yl_speak_up.export_to_ink.translate_precondition_list = function(dialog, precond
var_with_operator(liste, p.p_value, p.p_operator, p.p_var_cmp_value, vars_used)
elseif(p and p.p_type and p.p_type == "evaluate" and p.p_value == "counted_visits_to_option") then
-- simulate the visit counter that ink has in yl_speak_up
local tmp_var_name = n_id.."_"..p.p_param1
local tmp_var_name = use_prefix..p.p_param1
if(dialog_names[tmp_var_name]) then
tmp_var_name = dialog_names[tmp_var_name].."."..tostring(p.p_param2)
tmp_var_name = use_prefix..dialog_names[tmp_var_name].."."..tostring(p.p_param2)
else
tmp_var_name = tmp_var_name.. "_"..tostring(p.p_param2)
end
@ -360,7 +361,7 @@ local set_var_to_value = function(liste, var_name_full, op, val, vars_used)
end
end
yl_speak_up.export_to_ink.translate_effect_list = function(dialog, effects, vars_used, n_id)
yl_speak_up.export_to_ink.translate_effect_list = function(dialog, effects, vars_used)
-- collect effects that may work in ink
local liste = {}
-- variables may be set in effects
@ -377,7 +378,8 @@ yl_speak_up.export_to_ink.translate_effect_list = function(dialog, effects, vars
end
yl_speak_up.export_to_ink_language = function(dialog, n_id)
-- Note: use_prefix ought to be tostring(n_id).."_" or ""
yl_speak_up.export_to_ink_language = function(dialog, use_prefix)
local start_dialog = yl_speak_up.get_start_dialog_id(dialog)
if(not(start_dialog)) then
start_dialog = "d_1"
@ -388,25 +390,47 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id)
and dialog.n_dialogs[start_dialog].d_name) then
start_dialog = dialog.n_dialogs[start_dialog].d_name
else
start_dialog = tostring(n_id).."_"..tostring(start_dialog)
start_dialog = tostring(start_dialog)
end
local main = tostring(n_id).."_main"
local tmp = {"-> ", main,
"\n=== ", main, " ===",
-- prefix all dialog names with this;
-- advantage: several NPC dialog exports can be combined into one inc game
-- where the player can talk to diffrent NPC (which can have the
-- same dialog names without conflict thanks to the prefix)
-- use_prefix = tostring(n_id).."_"
if(not(use_prefix)) then
use_prefix = ""
end
-- go to the main loop whenever the player ends the conversation with the NPC;
-- this allows to create an additional dialog in INK where the player can then
-- decide to talk to multiple NPC - or to continue his conversation with the
-- same NPC
local main_loop = use_prefix.."d_end"
local tmp = {"-> ", main_loop,
"\n=== ", main_loop, " ===",
"\nWhat do you wish to do?",
"\n+ Talk to ", tostring(dialog.n_npc), " -> ", tostring(start_dialog),
"\n+ Talk to ", tostring(dialog.n_npc or prefix or "-unknown-"), " -> ", use_prefix..tostring(start_dialog),
"\n+ End -> END"}
local vars_used = ink_export.print_variables_used(tmp, dialog, n_id, pname)
local vars_used = ink_export.print_variables_used(tmp, dialog)
local sorted_d_list = yl_speak_up.get_dialog_list_for_export(dialog)
-- d_got_item may contain alternate texts - so it is of intrest here
-- (also links to other dialogs)
if(dialog.n_dialogs["d_got_item"]) then
table.insert(sorted_d_list, "d_got_item")
end
-- maybe not that useful to set up this one in inK; add it for completeness
if(dialog.n_dialogs["d_trade"]) then
table.insert(sorted_d_list, "d_trade")
end
-- make use of dialog names if wanted
local dialog_names = {}
for i, d_id in ipairs(sorted_d_list) do
if(use_d_name) then
local n = tostring(n_id).."_"..tostring(d_id)
local n = tostring(d_id)
local d = dialog.n_dialogs[d_id]
dialog_names[n] = (d.d_name or n)
end
@ -417,7 +441,7 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id)
local tmp2 = {}
local d = dialog.n_dialogs[d_id]
-- print the dialog knot, but without choices (those we add in the following loop)
local this_knot_name = ink_export.print_dialog_knot(tmp, n_id, d_id, d)
local this_knot_name = ink_export.print_dialog_knot(tmp, use_prefix, d_id, d, dialog_names)
-- iterate over all options
local sorted_o_list = yl_speak_up.get_sorted_options(dialog.n_dialogs[d_id].d_options or {}, "o_sort")
@ -434,7 +458,7 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id)
for k, r_id in ipairs(sorted_e_list) do
local r = o_data.o_results[r_id]
if(r and r.r_type and r.r_type == "dialog") then
target_dialog = tostring(n_id).."_"..tostring(r.r_value)
target_dialog = tostring(r.r_value)
alternate_text_on_success = r.alternate_text or ""
end
end
@ -455,7 +479,7 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id)
-- whatever dialog comes previously - the dialog, an action, or
-- another on_failure dialog - needs to lead to this dialog
target_dialog = ink_export.print_effect_knot(tmp2,
n_id, d_id, o_id, start_dialog,
use_prefix, d_id, o_id, start_dialog,
r, r_prev,
alternate_text_on_success, target_dialog,
dialog_names)
@ -470,37 +494,68 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id)
alternate_text_on_success,
sorted_e_list, o_data.o_results, 1)
-- if it is an action knot then the effects have to go to the action knot
local e_list = ink_export.translate_effect_list(dialog, o_data.o_results,
vars_used)
-- iterate backwards through the actions (though usually only one is supported)
for k = #sorted_a_list, 1, -1 do
local a_id = sorted_a_list[k]
local a = o_data.actions[a_id]
target_dialog = ink_export.print_action_knot(tmp2,
n_id, d_id, o_id, start_dialog,
use_prefix, d_id, o_id, start_dialog,
a,
alternate_text_on_success, target_dialog, dialog_names)
alternate_text_on_success, target_dialog, dialog_names,
e_list)
-- has been dealt with
alternate_text_on_success = ""
end
-- which preconditions can be translated to ink?
local p_list = ink_export.translate_precondition_list(dialog, o_data.o_prerequisites,
vars_used, n_id, dialog_names)
local e_list = ink_export.translate_effect_list(dialog, o_data.o_results,
vars_used, n_id)
vars_used, use_prefix, dialog_names)
-- what remains is to print the option/choice itself
local o_text = o_data.o_text_when_prerequisites_met
local o_prefix = ""
if(d.o_random) then
o_text = "[One of these options is randomly selected]"
o_prefix = "randomly_"
elseif(o_data.o_autoanswer) then
o_text = "[Automaticly selected if preconditions are met]"
o_prefix = "automaticly_"
end
-- if the target is an action knot: do not print the effect list as that belongs
-- to the action knot!
if(#sorted_a_list > 0) then
e_list = {}
end
ink_export.print_choice(tmp,
-- TODO: deal with when_prerequisites_not_met
o_data.o_text_when_prerequisites_met, n_id, start_dialog,
o_text, use_prefix, start_dialog,
alternate_text_on_success, target_dialog,
o_data.o_visit_only_once,
o_id, p_list, e_list, dialog_names)
o_data.o_visit_only_once, -- print + (often) or * (only once)
o_prefix..o_id, p_list, e_list, dialog_names)
-- deal with o_grey_when_prerequisites_not_met (grey out this answer)
if( o_data.o_text_when_prerequisites_not_met
and o_data.o_text_when_prerequisites_not_met ~= ""
and o_data.o_grey_when_prerequisites_not_met
and o_data.o_grey_when_prerequisites_not_met == "true") then
o_text = o_data.o_text_when_prerequisites_not_met
-- this option cannot be selected - so choose d_end as target dialog
ink_export.print_choice(tmp,
o_text, use_prefix, start_dialog,
alternate_text_on_success, "d_end",
o_data.o_visit_only_once, -- print + (often) or * (only once)
"grey_out_"..o_id, p_list, e_list, dialog_names)
end
-- Note: Showing an alternate text if the preconditions are not met is not
-- covered here. It makes little sense for the NPC as the option appears
-- but cannot be clicked. It exists for backward compatibility of old NPC
-- on the Your Land server.
end -- dealt with the option
table.insert(tmp, "\n")
-- add way to end talking to the NPC
ink_export.print_choice(tmp, "Farewell!", n_id, start_dialog,
nil, tostring(n_id).."_main", false, nil, dialog_names)
ink_export.print_choice(tmp, "Farewell!", use_prefix, start_dialog,
nil, "d_end", false, nil, dialog_names)
-- add the knots for actions and effects for this dialog and all its options:
for _, line in ipairs(tmp2) do

View File

@ -33,6 +33,10 @@ yl_speak_up.input_fs_action_npc_gives = function(player, formname, fields)
-- the npc_gives slot does not accept input - so we don't have to check for any misplaced items
-- but if the player aborts, give the item back to the NPC
if(fields.back_to_talk) then
-- actually take the item back into the NPC's inventory
local n_id = yl_speak_up.speak_to[pname].n_id
local npc_inv = minetest.get_inventory({type="detached", name="yl_speak_up_npc_"..tostring(n_id)})
yl_speak_up.action_take_back_failed_npc_gives(trade_inv, npc_inv)
-- strip the quest item info from the stack (so that it may stack again)
-- and give that (hopefully) stackable stack back to the NPC
yl_speak_up.action_quest_item_take_back(player)

View File

@ -1,23 +1,3 @@
-- helper function that is also used by export_to_ink.lua
-- returns a sorted dialog list without special or generic dialogs
yl_speak_up.get_dialog_list_for_export = function(dialog)
local liste = {}
if(not(dialog) or not(dialog.n_dialogs)) then
return liste
end
-- sort the list of dialogs by d_id
local liste_sorted = yl_speak_up.sort_keys(dialog.n_dialogs or {}, true)
for _, d_id in ipairs(liste_sorted) do
-- only normal dialogs - no d_trade, d_got_item, d_dynamic etc;
if(not(yl_speak_up.is_special_dialog(d_id))
-- also no generic dialogs (they do not come from this NPC)
and not(dialog.n_dialogs[d_id].is_generic)) then
table.insert(liste, d_id)
end
end
return liste
end
yl_speak_up.export_to_simple_dialogs_language = function(dialog, n_id)
@ -38,7 +18,7 @@ yl_speak_up.export_to_simple_dialogs_language = function(dialog, n_id)
table.insert(tmp, "\n")
for o_id, o_data in pairs(dialog.n_dialogs[d_id].d_options or {}) do
local target_dialog = nil
for r_id, r_data in pairs(o_data.o_results) do
for r_id, r_data in pairs(o_data.o_results or {}) do
if(r_data.r_type and r_data.r_type == "dialog") then
target_dialog = r_data.r_value
end
@ -68,8 +48,52 @@ yl_speak_up.input_export = function(player, formname, fields)
return yl_speak_up.show_fs(player, "export", "show_simple_dialogs")
elseif(fields and (fields.import or fields.back_from_error_msg)) then
return yl_speak_up.show_fs(player, "export", "import")
elseif(fields and fields.really_import and fields.new_dialog_input
and string.sub(fields.new_dialog_input, 1, 3) == "-> ") then
local pname = player:get_player_name()
if(not(pname) or not(yl_speak_up.speak_to[pname])) then
return
end
local n_id = yl_speak_up.speak_to[pname].n_id
-- can the player edit this npc?
if(not(yl_speak_up.may_edit_npc(player, n_id))) then
return yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:export",
formspec = yl_speak_up.build_fs_quest_edit_error(
"You do not own this NPC and are not allowed to edit it!",
"back_from_error_msg")})
end
-- import in ink format
local dialog = yl_speak_up.speak_to[pname].dialog
local log = {}
local log_level = 1
yl_speak_up.parse_ink.import_from_ink(dialog, fields.new_dialog_input, log_level, log)
-- save the changed dialog
yl_speak_up.save_dialog(n_id, dialog)
for i_, t_ in ipairs(log) do
minetest.chat_send_player(pname, t_)
end
-- log the change
return yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:export",
formspec = "size[10,3]"..
"label[0.5,1.0;Partially imported dialog data in ink format "..
" successfully.]"..
"button[3.5,2.0;2,0.9;back_from_error_msg;Back]"
})
elseif(fields and fields.really_import and fields.new_dialog_input) then
-- importing requires the "privs" priv
-- can that possibly be json format?
if(not(string.sub(fields.new_dialog_input, 1, 1) == "{")) then
return yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:export",
formspec = yl_speak_up.build_fs_quest_edit_error(
"This does not seem to be in .json format. Please make sure "..
"your import starts with a \"{\"!",
"back_from_error_msg")})
end
-- importing in .json format requires the "privs" priv
-- and it imports more information like npc name
if(not(minetest.check_player_privs(player, {privs=true}))) then
return yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:export",
@ -147,7 +171,7 @@ yl_speak_up.input_export = function(player, formname, fields)
-- save it
yl_speak_up.save_dialog(n_id, new_dialog)
-- log the change
yl_speak_up.log_change(pname, n_id, "Imported new dialog.")
yl_speak_up.log_change(pname, n_id, "Imported new dialog in .json format (complete).")
return yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:export",
formspec = "size[10,3]"..
@ -230,7 +254,7 @@ yl_speak_up.get_fs_export = function(player, param)
-- TODO
explanation = "This is the format used by the \"Ink\" scripting language. "..
"TODO: The export is not complete yet."
content = yl_speak_up.export_to_ink_language(dialog, n_id)
content = yl_speak_up.export_to_ink_language(dialog, tostring(n_id).."_")
elseif(param and param == "show_simple_dialogs") then
b3 = "label[9.8,17.6;Simple dialogs format]"
explanation = "This is the format used by the \"simple_dialogs\" mod. "..
@ -250,7 +274,7 @@ yl_speak_up.get_fs_export = function(player, param)
end
return table.concat({"size[20,20]label[4,0.5;Export of NPC ",
minetest.formspec_escape(n_id or "- ? -"),
"dialog data in .json format]",
" dialog data in .json format]",
"button[17.8,0.2;2.0,0.9;back;Back]",
"button[15.4,0.2;2.0,0.9;import;Import]",
"tooltip[import;WARNING: This is highly experimental and requires the \"privs\" priv.\n"..

View File

@ -213,10 +213,9 @@ yl_speak_up.get_fs_initial_config = function(player, n_id, d_id, is_initial_conf
"Export: Show the dialog in .json format which you can"..
"\n\tcopy and store on your computer.]",
-- name of the npc
"checkbox[2.2,0.9;show_nametag;;",
"checkbox[2.2,0.9;show_nametag;Show nametag;",
tostring(tmp_show_nametag),
"]",
"label[2.7,0.9;Show nametag]",
"label[0.2,1.65;Name:]",
"field[2.2,1.2;4,0.9;n_npc;;",
minetest.formspec_escape(tmp_name),

View File

@ -447,7 +447,8 @@ yl_speak_up.get_fs_talkdialog = function(player, n_id, d_id, alternate_text, rec
elseif(d_id and d_id ~= "d_generic_start_dialog" and yl_speak_up.speak_to[pname].d_id ~= nil) then
c_d_id = yl_speak_up.speak_to[pname].d_id
active_dialog = dialog.n_dialogs[c_d_id]
elseif dialog.n_dialogs ~= nil then
-- do this only if the dialog is already configured/created_at:
elseif dialog.n_dialogs ~= nil and dialog.created_at then
-- Find the dialog with d_sort = 0
c_d_id = yl_speak_up.get_start_dialog_id(dialog)
if(c_d_id) then

View File

@ -1,802 +0,0 @@
--###
-- 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_<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
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
-- returns true if someone is speaking to the NPC
yl_speak_up.npc_is_in_conversation = function(n_id)
for name, data in pairs(yl_speak_up.speak_to) do
if(data and data.n_id and data.n_id == n_id) then
return true
end
end
return false
end
-- returns a list of players that are in conversation with this NPC
yl_speak_up.npc_is_in_conversation_with = function(n_id)
local liste = {}
for name, data in pairs(yl_speak_up.speak_to) do
if(data and data.n_id and data.n_id == n_id) then
table.insert(liste, name)
end
end
return liste
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
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
-- this makes it a bit easier to access some values later on:
yl_speak_up.speak_to[pname]._self = self
-- 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
-- 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), false, dialog)
-- 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
-- checks if dialog exists
yl_speak_up.check_if_dialog_exists = function(dialog, d_id)
return (dialog and d_id
and dialog.n_dialogs
and dialog.n_dialogs[d_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
yl_speak_up.is_special_dialog = function(d_id)
if(not(d_id)) then
return false
end
return (d_id == "d_trade" or d_id == "d_got_item" or d_id == "d_dynamic" or d_id == "d_end")
end
yl_speak_up.d_name_to_d_id = function(dialog, d_name)
if(not(dialog) or not(dialog.n_dialogs) or not(d_name) or d_name == "") then
return nil
end
-- it is already the ID of an existing dialog
if(dialog.n_dialogs[d_name]) then
return d_name
end
-- search all dialogs for one with a fitting d_name
for k,v in pairs(dialog.n_dialogs) do
if(v and v.d_name and v.d_name == d_name) then
return k
end
end
end
-- get the name of a dialog (reverse of above)
yl_speak_up.d_id_to_d_name = function(dialog, d_id)
if(not(dialog) or not(dialog.n_dialogs) or not(d_id) or d_id == ""
or not(dialog.n_dialogs[d_id])
or not(dialog.n_dialogs[d_id].d_name)
or dialog.n_dialogs[d_id].d_name == "") then
return d_id
end
return dialog.n_dialogs[d_id].d_name
end
yl_speak_up.get_sorted_dialog_name_list = function(dialog)
local liste = {}
if(dialog and dialog.n_dialogs) then
for k, v in pairs(dialog.n_dialogs) do
-- this will be used for dropdown lists - so we use formspec_escape
table.insert(liste, minetest.formspec_escape(v.d_name or k or "?"))
end
-- sort alphabethicly
table.sort(liste)
end
return liste
end
-- how many own (not special, not generic) dialogs does the NPC have?
yl_speak_up.count_dialogs = function(dialog)
local count = 0
if(not(dialog) or not(dialog.n_dialogs)) then
return 0
end
for d_id, v in pairs(dialog.n_dialogs) do
if(d_id
and not(yl_speak_up.is_special_dialog(d_id))
and not(dialog.n_dialogs[d_id].is_generic)) then
count = count + 1
end
end
return count
end

858
functions_dialogs.lua Normal file
View File

@ -0,0 +1,858 @@
--
-- These functions here access and manipulate the "dialogs" data structure.
-- It is loaded for each player whenever the player talks to an NPC. Each
-- talking player gets *a copy* of that data structure.
--
-- As this mod is about this "dialogs" data structure and its editing, this
-- isn't the only place in this mod where the data structure is accessed
-- and/or manipulated. This here just contains some common functions.
--
--###
-- Helpers
--###
yl_speak_up.string_starts_with = function(str, starts_with)
return (string.sub(str, 1, string.len(starts_with)) == starts_with)
end
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
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
-- helper function for
-- yl_speak_up.get_fs_talkdialog and
-- yl_speak_up.check_and_add_as_generic_dialog
-- find the dialog with d_sort == 0 or lowest number
yl_speak_up.get_start_dialog_id = function(dialog)
if(not(dialog) or not(dialog.n_dialogs)) then
return nil
end
-- Find the dialog with d_sort = 0 or alternatively with the lowest number
local lowest_sort = nil
local d_id = nil
for k, v in pairs(dialog.n_dialogs) do
local nr = tonumber(v.d_sort)
if(not(lowest_sort) or (nr and nr >= 0 and nr < lowest_sort)) then
lowest_sort = nr
d_id = k
end
end
return d_id
end
-- helper function that is also used by export_to_ink.lua
-- returns a sorted dialog list without special or generic dialogs
yl_speak_up.get_dialog_list_for_export = function(dialog)
local liste = {}
if(not(dialog) or not(dialog.n_dialogs)) then
return liste
end
-- sort the list of dialogs by d_id
local liste_sorted = yl_speak_up.sort_keys(dialog.n_dialogs or {}, true)
for _, d_id in ipairs(liste_sorted) do
-- only normal dialogs - no d_trade, d_got_item, d_dynamic etc;
if(not(yl_speak_up.is_special_dialog(d_id))
-- also no generic dialogs (they do not come from this NPC)
and not(dialog.n_dialogs[d_id].is_generic)) then
table.insert(liste, d_id)
end
end
-- now that the list contains only normal dialogs, we can sort by d_sort
-- (thus allowing d_9 to be listed earlier than d_10 etc.)
table.sort(liste, function(a, b)
return dialog and dialog.n_dialogs and dialog.n_dialogs[a] and dialog.n_dialogs[b]
and ((tonumber(dialog.n_dialogs[a].d_sort or "") or 0)
< (tonumber(dialog.n_dialogs[b].d_sort or "") or 0)) end)
return liste
end
--###
--Formspecs
--###
-- 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)
-- Note: pname is only passed to yl_speak_up.add_new_option - which is only used if
-- dialog_text is empty (and only for logging)
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
-- update existing or create a new dialog named d_name with d_text
-- (useful for import from ink and likewise functionality)
-- this also prepares the dialog for options update
yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text)
if(dialog_name and yl_speak_up.is_special_dialog(dialog_name)) then
-- d_trade, d_got_item, d_dynamic and d_end are not imported because they need to be handled diffrently
table.insert(log, "Note: Not importing dialog text for \""..tostring(dialog_name).."\" because it is a special dialog.")
-- the options of thes special dialogs are still relevant
return dialog_name
end
-- does a dialog with name d_name already exist?
local d_id = yl_speak_up.d_name_to_d_id(dialog, dialog_name)
-- name the thing for logging purposes
local log_str = "Dialog "..tostring(d_id)
if(dialog_name and dialog_name ~= d_id) then
log_str = log_str.." ["..tostring(dialog_name).."]:"
else
log_str = log_str..": "
end
local is_new = false
if(not(d_id)) then
local next_id = nil
-- if dialog_name matches the d_<nr> pattern but d_<nr> does not exist,
-- then try to create *that* dialog
if(dialog_name and string.sub(dialog_name, 1, 2) == "d_") then
next_id = tonumber(string.sub(dialog_name, 3))
end
-- pname is nil - thus no logging and no adding of a back to start option
-- next_id is also usually nil - so just add a new dialog
d_id = yl_speak_up.add_new_dialog(dialog, nil, next_id, dialog_text)
if(not(d_id)) then
-- the creation may have failed (i.e. dialog not beeing a dialog,
-- or too many dialogs in dialog already)
table.insert(log, log_str.."FAILED to create new dialog.")
return nil
end
-- we got a new name for the log
log_str = "New dialog "..tostring(d_id).." ["..tostring(dialog_name).."]: "
is_new = true
table.insert(log, log_str.." Created successfully.")
elseif(dialog.n_dialogs[d_id].d_text ~= dialog_text) then
-- else update the text
table.insert(log, log_str.." Changed dialog text from \""..
tostring(dialog.n_dialogs[d_id].d_text).."\" to \""..tostring(dialog_text).."\".")
-- actually change the dialog text
dialog.n_dialogs[d_id].d_text = dialog_text
end
local d_data = dialog.n_dialogs[d_id]
-- set d_name if it differs from d_id
if(d_id ~= dialog_name
and (not(d_data.d_name)
or(d_data.d_name ~= dialog_name))) then
if(not(is_new)) then
-- log only if it's not a new dialog
table.insert(log, log_str.."Changed dialog name from \""..
tostring(d_data.d_name).."\" to \""..tostring(dialog_name).."\".")
end
-- actually change the dialog name
d_data.d_name = dialog_name
end
-- the random option is set for the dialog entire; we will have to process the individual
-- options in order to find out if this dialog is o_random; the first option that is sets
-- it for the dialog -> keep the old value
--d_data.o_random = nil
-- there may be existing options that won't get updated; deal with them:
-- remember which options the dialog has and which sort order they had
d_data.d_tmp_sorted_option_list = yl_speak_up.get_sorted_options(d_data.d_options or {}, "o_sort") or {}
-- this value is increased whenever an option gets updated - so that we can have options
-- that don't get an update sorted in after those options that did
d_data.d_tmp_sort_value = 1
-- mark all existing options as requirilng an update
for i, o_id in ipairs(d_data.d_tmp_sorted_option_list or {}) do
d_data.d_options[o_id].o_tmp_needs_update = true
end
-- mark this dialog as having received an update (meaning we won't have to update d_sort after
-- all dialogs have been updated)
d_data.d_tmp_has_been_updated = true
return d_id
end
-- helper function for update_dialog_options_completed;
-- adds a precondition of p_type "false" to the option so that the option is no longer displayed
-- if disable_option is false, then all preconditions of p_type "false" will be changed to p_type "true"
-- and thus the option will be shown to the player again
yl_speak_up.update_disable_dialog_option = function(o_data, disable_option)
-- is this otpion already deactivated?
local is_deactivated = false
for p_id, p in pairs(o_data.o_prerequisites or {}) do
if(p and p_id and p.p_type == "false") then
is_deactivated = true
-- if we want to re-enable the option, then this here is the place
if(not(disable_option)) then
-- change the type from false to true - this particular precondition
-- will now always be true
p.p_type = "true"
-- we continue work here because the player may have created multiple
-- options of this type
end
end
end
-- if not: add a precondition of type "false"
if(not(is_deactivated) and disable_option) then
-- we need to add a new precondition of type "false"
-- make sure we can add the prereq:
if(not(o_data.o_prerequisites)) then
o_data.o_prerequisites = {}
end
local future_p_id = "p_"..tostring(yl_speak_up.find_next_id(o_data.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
o_data.o_prerequisites[future_p_id] = { p_id = future_p_id, p_type = "false"}
end
end
-- call this *after* all dialog options have been updated for dialog_name
yl_speak_up.update_dialog_options_completed = function(log, dialog, d_id)
local d_data = dialog.n_dialogs[d_id]
if(not(d_data)) then
return
end
for i, o_id in ipairs(d_data.d_tmp_sorted_option_list or {}) do
local o_data = d_data.d_options[o_id]
if(o_data.o_tmp_needs_update) then
-- update the sort value so that this option will be listed *after* those
-- options that actually did get updated
o_data.o_sort = d_data.d_tmp_sort_value
d_data.d_tmp_sort_value = d_data.d_tmp_sort_value + 1
-- this option has now been processed
o_data.o_tmp_needs_update = nil
-- name the thing for logging purposes
local log_str = "Dialog "..tostring(d_id)
if(dialog_name and dialog_name ~= d_id) then
log_str = log_str.." ["..tostring(d_id).."]"
end
table.insert(log, log_str..", option <"..tostring(o_id)..">: "..
"Option exists in old dialog but not in import. Keeping option.")
-- add a precondition of p_type "false" to the option so that the option
-- is no longer displayed
yl_speak_up.update_disable_dialog_option(o_data, true)
end
end
-- clean up the dialog
d_data.d_tmp_sorted_option_list = nil
d_data.d_tmp_sort_value = nil
end
-- make sure only one dialog has d_sort set to 0 (and is thus the start dialog)
yl_speak_up.update_start_dialog = function(log, dialog, start_dialog_name, start_with_d_sort)
local start_d_id = yl_speak_up.d_name_to_d_id(dialog, start_dialog_name)
if(not(start_d_id)) then
return
end
for d_id, d in pairs(dialog.n_dialogs) do
if(d_id == start_d_id) then
if(not(d.d_sort) or d.d_sort ~= 0) then
table.insert(log, "Setting start dialog to "..tostring(start_dialog_name)..".")
end
d.d_sort = 0
-- the start dialog certainly is *a* start dialog (with the buttons)
d.is_a_start_dialog = true
elseif(not(d.d_tmp_has_been_updated)) then
-- sort this dialog behind the others
d.d_sort = start_with_d_sort
start_with_d_sort = start_with_d_sort + 1
end
d.d_tmp_has_been_updated = nil
end
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
-- Note: pname is only used for logging (and for changing o_sort)
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 ""),
}
local start_with_o_sort = nil
if(pname and pname ~= "") then
-- 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
start_with_o_sort = yl_speak_up.speak_to[pname].o_sort
end
-- necessary in order for it to work
local new_o_sort = yl_speak_up.sanitize_sort(dialog.n_dialogs[d_id].d_options, start_with_o_sort)
dialog.n_dialogs[d_id].d_options[future_o_id].o_sort = new_o_sort
-- 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
-- update existing or create a new option named option_name for dialog dialog_name
-- If option_name starts with..
-- new_ create a new option (discard the rest of option_name)
-- automaticly_ set o_autoanswer
-- randomly_ set o_random *for the dialog*
-- grey_out_ set o_text_when_prerequisites_not_met
-- ..and take what remains as option_name.
-- (useful for import from ink and likewise functionality)
--
-- TODO: these notes need to be taken care of in the calling function
-- Note: The calling function may need to adjust o_sort according to its needs.
-- Note: Preconditions, actions and effects are not handled here (apart from the "dialog"
-- effect/result for the redirection to the target dialog)
yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_name,
option_text, option_text_if_preconditions_false,
target_dialog, alternate_text, visit_only_once, sort_order)
-- does the dialog we want to add to exist?
local d_id = yl_speak_up.d_name_to_d_id(dialog, dialog_name)
if(not(d_id)) then
if(not(yl_speak_up.is_special_dialog(dialog_name))) then
-- the dialog does not exist - we cannot add an option to a nonexistant dialog
return nil
end
-- options for special dialogs have to start with "automaticly_"
local parts = string.split(option_name or "", "_")
if(not(parts) or not(parts[1]) or parts[1] ~= "automaticly") then
option_name = "automaticly_"..table.concat(parts[2], "_")
end
-- for d_trade and d_got_item effects and preconditions are created WITH DEFAULT VALUES TODO
d_id = dialog_name
-- make sure the relevant dialog and fields exist
dialog.n_dialogs[d_id] = dialog.n_dialogs[d_id] or {}
dialog.n_dialogs[d_id].d_options = dialog.n_dialogs[d_id].d_options or {}
end
-- name the thing for logging purposes
local log_str = "Dialog "..tostring(d_id)
if(dialog_name and dialog_name ~= d_id) then
log_str = log_str.." ["..tostring(dialog_name).."]"
end
log_str = log_str..", option <"..tostring(option_name)..">: "
local is_new = false
-- translate the name of the target_dialog if needed
if(target_dialog and not(yl_speak_up.is_special_dialog(target_dialog))) then
target_dialog = yl_speak_up.d_name_to_d_id(dialog, target_dialog)
end
-- TODO: dialogs d_got_item and d_trade are special
local o_id = option_name
local mode = 0
local text_when_prerequisites_not_met = ""
local parts = string.split(o_id, "_")
if(not(parts) or not(parts[1]) or not(parts[2])) then
table.insert(log, log_str.."FAILED to create unknown option \""..tostring(o_id).."\".")
return nil
elseif(o_id and parts[1] == "new") then
-- we are asked to create a *new* option
o_id = nil
elseif(o_id and parts[1] == "automaticly") then
-- this option will be automaticly selected if its preconditions are true
mode = 1
option_name = parts[2]
o_id = option_name
elseif(o_id and parts[1] == "randomly") then
-- this option will be randomly selected if its preconditions are true;
-- (that means all other options of this dialog will have to be randomly as well;
-- something which cannot be done here as there is no guarantee that all options
-- *exist* at this point)
mode = 2
option_name = parts[2]
o_id = option_name
elseif(o_id and parts[1] ~= "o") then
table.insert(log, log_str.."FAILED to create unknown option \""..tostring(o_id).."\".")
return nil
end
-- if the option does not exist: create it
if( not(dialog.n_dialogs[d_id].d_options)
or not(o_id) or o_id == ""
or not(dialog.n_dialogs[d_id].d_options[o_id])) then
local next_id = nil
-- get the id part (number) from o_id - because we may be creating a new option here -
-- but said option may have a diffrent *name* than what a new option would get by
-- default
if(o_id) then
next_id = string.sub(o_id, 3)
if(next_id == "" or not(tonumber(next_id))) then
next_id = nil
table.insert(log, log_str.."FAILED to create new option \""..tostring(o_id).."\".")
return
end
end
-- pname is nil - thus no logging here
o_id = yl_speak_up.add_new_option(dialog, nil, next_id, d_id, option_text, target_dialog)
if(not(o_id)) then
return nil
end
is_new = true
end
-- abbreviate that
local o_data = dialog.n_dialogs[d_id].d_options[o_id]
-- cchnage option_text if needed
if(o_data.o_text_when_prerequisites_met ~= option_text) then
table.insert(log, log_str.."Changed option text from \""..
tostring(o_data.o_text_when_prerequisites_met)..
"\" to \""..tostring(option_text).."\" for option \""..tostring(o_id).."\".")
end
-- actually update the text
o_data.o_text_when_prerequisites_met = option_text
-- chnage greyed out text if needed
if(o_data.o_text_when_prerequisites_not_met ~= option_text_if_preconditions_false
and option_text_if_preconditions_false) then
table.insert(log, log_str.."Changed greyed out text when prerequisites not met from \""..
tostring(o_data.o_text_when_prerequisites_not_met)..
"\" to \""..tostring(option_text_if_preconditions_false or "")..
"\" for option \""..tostring(o_id).."\".")
-- make sure the greyed out text gets shown (or not shown)
o_data.o_text_when_prerequisites_not_met = option_text_if_preconditions_false or ""
end
-- make grey_out_ text visible if necessary
if(o_data.o_text_when_prerequisites_not_met and o_data.o_text_when_prerequisites_not_met ~= ""
and option_text_if_preconditions_false and option_text_if_preconditions_false ~= "") then
-- make sure this text is really shown - and greyed out
-- (resetting this can only happen through editing the NPC directly; not through import)
o_data.o_hide_when_prerequisites_not_met = "false"
o_data.o_grey_when_prerequisites_not_met = "true"
else
-- if this were not set to true, then the player would see a clickable button for
-- the option - but that button would do nothing
o_data.o_hide_when_prerequisites_not_met = "true"
o_data.o_grey_when_prerequisites_not_met = "false"
end
local r_found = false
-- the target_dialog may have been changed
for r_id, r in pairs(o_data.o_results or {}) do
-- we found the right result/effect that holds the (current) target_dialog
if(r and r.r_type and r.r_type == "dialog") then
r_found = true
if(not(r.r_value) or r.r_value ~= target_dialog) then
if(is_new) then
table.insert(log, log_str.."Successfully created new option \""..
tostring(o_id).."\" with target dialog \""..
tostring(target_dialog).."\".")
else
table.insert(log, log_str.."Changed target dialog from \""..
tostring(r.r_value).."\" to \""..tostring(target_dialog)..
"\" for option \""..tostring(o_id).."\".")
end
-- actually change the target dialog
r.r_value = target_dialog
end
-- the alternate_text may have been changed
if(r.alternate_text ~= alternate_text) then
table.insert(log, log_str.."Changed alternate text from \""..
tostring(r.r_alternate_text).."\" to \""..tostring(alternate_text)..
"\" for option \""..tostring(o_id).."\".")
r.alternate_text = alternate_text
end
end
end
-- for some reason the effect pointing to the target dialog got lost!
if(r_found and is_new) then
table.insert(log, log_str.."Set target dialog to "..tostring(target_dialog)..
" for new option \""..tostring(o_id).."\".")
end
if(not(r_found)) then
-- create the result/effect that points to the target_dialog
local r_id = yl_speak_up.add_new_result(dialog, d_id, o_id)
if(r_id) then
o_data.o_results[r_id].r_type = "dialog"
o_data.o_results[r_id].r_value = target_dialog
o_data.o_results[r_id].alternate_text = alternate_text
table.insert(log, log_str.."Set target dialog to "..tostring(target_dialog)..
" for option \""..tostring(o_id).."\".")
end
end
-- "randomly selected" applies to the *dialog* - it is set there and not in the individual option
local d_data = dialog.n_dialogs[d_id]
-- is this option selected randomly?
if( mode == 2 and not(d_data.o_random)) then
table.insert(log, log_str.."Changed DIALOG \""..tostring(d_id).."\" to RANDOMLY SELECTED.")
d_data.o_random = 1
end
-- is this option selected automaticly if all preconditions are met?
if(mode == 1 and not(o_data.o_autoanswer)) then
o_data.o_autoanswer = 1
table.insert(log, log_str.."Changed option \""..tostring(o_id).."\" to AUTOMATICLY SELECTED.")
-- mode is 0 - that means everything is normal for this option
elseif(mode ~= 1 and o_data.o_autoanswer) then
o_data.o_autoanswer = nil
table.insert(log, log_str.."Removed AUTOMATICLY SELECTED from option \""..tostring(o_id).."\".")
end
-- the visit_only_once option is handled without logging as it might create too many
-- entries in the log without adding any helpful information
if(visit_only_once
and (not(o_data.o_visit_only_once)
or o_data.o_visit_only_once ~= 1)) then
o_data.o_visit_only_once = 1
elseif(not(visit_only_once)
and o_data.o_visit_only_once and o_data.o_visit_only_once == 1) then
o_data.o_visit_only_once = nil
end
-- set sort order of options (no logging because that might get too spammy)
if(sort_order) then
o_data.o_sort = sort_order
end
-- this option has been updated
o_data.o_tmp_needs_update = false
if(o_data.o_sort and d_data.d_tmp_sort_value and o_data.o_sort >= d_data.d_tmp_sort_value) then
-- make sure this stores the highest o_sort value we found
d_data.d_tmp_sort_value = o_data.o_sort + 1
end
return 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] = {}
dialog.n_dialogs[d_id].d_options[o_id].o_results[future_r_id].r_id = future_r_id
return future_r_id
end
-- TODO: we need yl_speak_up.update_dialog_option_result as well
-- 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
-- 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
-- checks if dialog exists
yl_speak_up.check_if_dialog_exists = function(dialog, d_id)
return (dialog and d_id
and dialog.n_dialogs
and dialog.n_dialogs[d_id])
end
yl_speak_up.is_special_dialog = function(d_id)
if(not(d_id)) then
return false
end
return (d_id == "d_trade" or d_id == "d_got_item" or d_id == "d_dynamic" or d_id == "d_end")
end
yl_speak_up.d_name_to_d_id = function(dialog, d_name)
if(not(dialog) or not(dialog.n_dialogs) or not(d_name) or d_name == "") then
return nil
end
-- it is already the ID of an existing dialog
if(dialog.n_dialogs[d_name]) then
return d_name
end
-- search all dialogs for one with a fitting d_name
for k,v in pairs(dialog.n_dialogs) do
if(v and v.d_name and v.d_name == d_name) then
return k
end
end
return nil
end
-- get the name of a dialog (reverse of above)
yl_speak_up.d_id_to_d_name = function(dialog, d_id)
if(not(dialog) or not(dialog.n_dialogs) or not(d_id) or d_id == ""
or not(dialog.n_dialogs[d_id])
or not(dialog.n_dialogs[d_id].d_name)
or dialog.n_dialogs[d_id].d_name == "") then
return d_id
end
return dialog.n_dialogs[d_id].d_name
end
-- how many own (not special, not generic) dialogs does the NPC have?
yl_speak_up.count_dialogs = function(dialog)
local count = 0
if(not(dialog) or not(dialog.n_dialogs)) then
return 0
end
for d_id, v in pairs(dialog.n_dialogs) do
if(d_id
and not(yl_speak_up.is_special_dialog(d_id))
and not(dialog.n_dialogs[d_id].is_generic)) then
count = count + 1
end
end
return count
end

View File

@ -0,0 +1,112 @@
--###
--Load and Save
--###
local function save_path(n_id)
return yl_speak_up.worldpath .. yl_speak_up.path .. DIR_DELIM .. n_id .. ".json"
end
-- 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
-- this deletes the dialog with id d_id from the npc n_id's dialogs;
-- it loads the dialogs from the npc's savefile, deletes dialog d_id,
-- and then saves the dialogs back to the npc's savefile in order to
-- keep things consistent
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
-- 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

328
functions_talk.lua Normal file
View File

@ -0,0 +1,328 @@
--###
-- 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 = {}
--###
-- Debug
--###
yl_speak_up.debug = true
---###
-- general formpsec
---###
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.get_sorted_dialog_name_list = function(dialog)
local liste = {}
if(dialog and dialog.n_dialogs) then
for k, v in pairs(dialog.n_dialogs) do
-- this will be used for dropdown lists - so we use formspec_escape
table.insert(liste, minetest.formspec_escape(v.d_name or k or "?"))
end
-- sort alphabethicly
table.sort(liste)
end
return liste
end
---###
-- player related
---###
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
---###
-- player and npc related
---###
-- 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
-- returns true if someone is speaking to the NPC
yl_speak_up.npc_is_in_conversation = function(n_id)
for name, data in pairs(yl_speak_up.speak_to) do
if(data and data.n_id and data.n_id == n_id) then
return true
end
end
return false
end
-- returns a list of players that are in conversation with this NPC
yl_speak_up.npc_is_in_conversation_with = function(n_id)
local liste = {}
for name, data in pairs(yl_speak_up.speak_to) do
if(data and data.n_id and data.n_id == n_id) then
table.insert(liste, name)
end
end
return liste
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
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
-- this makes it a bit easier to access some values later on:
yl_speak_up.speak_to[pname]._self = self
-- 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
-- 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), false, dialog)
-- 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
-- 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

1039
import_from_ink.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -212,7 +212,9 @@ yl_speak_up.reload = function(modpath, log_entry)
-- some generic dialogs
dofile(modpath .. "api/api_properties.lua")
-- the main functionality of the mod
dofile(modpath .. "functions.lua")
dofile(modpath .. "functions_dialogs.lua")
dofile(modpath .. "functions_save_restore_dialogs.lua")
dofile(modpath .. "functions_talk.lua")
-- implementation of the chat commands registered in register_once.lua:
dofile(modpath .. "chat_commands.lua")
@ -220,6 +222,10 @@ yl_speak_up.reload = function(modpath, log_entry)
dofile(modpath .. "api/api_npc_list.lua")
dofile(modpath .. "fs/fs_npc_list.lua")
-- this may load custom things like preconditions, actions, effects etc.
-- which may depend on the existance of other mods
dofile(modpath .. "addons/load_addons.lua")
-- some general functions that are useful for mobs_redo
-- (react to right-click, nametag color etc.)
-- only gets loaded if mobs_redo (mobs) exists as mod
@ -229,6 +235,8 @@ yl_speak_up.reload = function(modpath, log_entry)
dofile(modpath .. "export_to_ink.lua")
dofile(modpath .. "fs/fs_export.lua")
dofile(modpath .. "import_from_ink.lua")
-- edit_mode.lua has been moved to the mod npc_talk_edit:
-- dofile(modpath .. "editor/edit_mode.lua")

View File

@ -31,21 +31,57 @@ function yl_speak_up.do_mobs_on_rightclick(self, clicker)
--local item = clicker:get_wielded_item()
local name = clicker:get_player_name()
-- Take the mob only with net or lasso
if self.owner and self.owner == name then
local pos = self.object:get_pos()
if mobs:capture_mob(self, clicker, nil, 100, 100, true, nil) then
if(self.yl_speak_up) then
local n_id = "?"
if(self and self.yl_speak_up and self.yl_speak_up.id) then
n_id = "n_"..tostring(self.yl_speak_up.id)
-- if someone other than the owner placed the mob, then we need to
-- adjust the owner back from placer to real_owner
if(self.yl_speak_up.real_owner and self.yl_speak_up.real_owner ~= self.owner) then
self.owner = self.yl_speak_up.real_owner
end
end
-- Take the mob only with net or lasso
if self.owner and (self.owner == name or yl_speak_up.may_edit_npc(clicker, n_id)) then
local pos = self.object:get_pos()
self.yl_speak_up.last_pos = minetest.pos_to_string(pos, 0)
-- the mob can be picked up by someone who can just *edit* it but is not *the* owner
if(self.owner ~= name) then
self.yl_speak_up.real_owner = self.owner
end
-- try to capture the mob
local egg_stack = mobs:capture_mob(self, clicker, nil, 100, 100, true, nil)
if(egg_stack and self.yl_speak_up) then
minetest.log("action","[MOD] yl_speak_up "..
" NPC n_"..tostring(self.yl_speak_up.id)..
" named "..tostring(self.yl_speak_up.npc_name)..
" (owned by "..tostring(self.owner)..
") picked up by "..tostring(clicker:get_player_name())..
" at pos "..minetest.pos_to_string(pos, 0)..".")
-- players want to know *which* NPC will "hatch" from this egg;
-- sadly there is no point in modifying egg_data as that has already
-- been put into the inventory of the player and is just a copy now
local player_inv = clicker:get_inventory()
for i, v in ipairs(player_inv:get_list("main") or {}) do
local m = v:get_meta()
local d = minetest.deserialize(m:get_string("") or {})
-- adjust the description text of the NPC in the inventory
if(d and d.yl_speak_up and d.yl_speak_up.id) then
local d2 = d.yl_speak_up
local text = (d2.npc_name or "- nameless -").. ", "..
(d2.npc_description or "-").."\n"..
"(n_"..tostring(d2.id)..", owned by "..
tostring(d.owner).."),\n"..
"picked up at "..tostring(d2.last_pos or "?").."."
m:set_string("description", text)
player_inv:set_stack("main", i, v)
end
end
return
end
return
end
end
end
-- protect npc with mobs:protector
if mobs:protect(self, clicker) then

View File

@ -13,6 +13,17 @@ yl_speak_up.npc_priv_names = {
"effect_exec_lua", "effect_give_item", "effect_take_item", "effect_move_player",
}
-- make sure this table exists
if(not(yl_speak_up.npc_priv_needs_player_priv)) then
yl_speak_up.npc_priv_needs_player_priv = {}
end
-- and set it to privs if nothing is specified (because the *_lua are extremly dangerous npc_privs!)
for i, p in ipairs(yl_speak_up.npc_priv_names) do
if(not(yl_speak_up.npc_priv_needs_player_priv[p])) then
yl_speak_up.npc_priv_needs_player_priv[p] = "privs"
end
end
-- either the npc with n_id *or* if generic_npc_id is set the generic npc with the
-- id generic_npc_id needs to have been granted priv_name
yl_speak_up.npc_has_priv = function(n_id, priv_name, generic_npc_id)
@ -74,27 +85,43 @@ end
-- "If called with parameter [list], all granted privs for all NPC are shown.",
-- privs = {privs = true},
yl_speak_up.command_npc_talk_privs = function(pname, param)
-- can the player see the privs for all NPC? or just for those he can edit?
local list_all = false
local ptmp = {}
ptmp[yl_speak_up.npc_privs_priv] = true
if(minetest.check_player_privs(pname, ptmp)) then
list_all = true
end
if(not(param) or param == "") then
-- if the npc priv has a player priv as requirement, then list that
local tmp = {}
for i, p in ipairs(yl_speak_up.npc_priv_names) do
table.insert(tmp, tostring(p)..
" ["..tostring(yl_speak_up.npc_priv_needs_player_priv[p]).."]")
end
minetest.chat_send_player(pname,
"Usage: [grant|revoke|list] <n_id> <priv>\n"..
"The following privilege exist:\n\t"..
table.concat(yl_speak_up.npc_priv_names, ", ")..".")
"The following privilege exist [and require you to have this priv to set]:\n\t"..
table.concat(tmp, ", ")..".")
return
end
local player = minetest.get_player_by_name(pname)
local parts = string.split(param, " ")
if(parts[1] == "list") then
local text = "This list contains the privs of each NPC in the form of "..
"<npc_name>: <list of privs>"
local text = "This list contains the privs of each NPC you can edit "..
"in the form of <npc_name>: <list of privs>"
-- create list of all existing extra privs for npc
for n_id, v in pairs(yl_speak_up.npc_priv_table) do
text = text..".\n"..tostring(n_id)..":"
local found = false
for priv, w in pairs(v) do
text = text.." "..tostring(priv)
found = true
end
if(not(found)) then
text = text.." <none>"
if(list_all or yl_speak_up.may_edit_npc(player, n_id)) then
text = text..".\n"..tostring(n_id)..":"
local found = false
for priv, w in pairs(v) do
text = text.." "..tostring(priv)
found = true
end
if(not(found)) then
text = text.." <none>"
end
end
end
minetest.chat_send_player(pname, text..".")
@ -114,6 +141,31 @@ yl_speak_up.command_npc_talk_privs = function(pname, param)
table.concat(yl_speak_up.npc_priv_names, ", ")..".")
return
end
-- does the player have the necessary player priv to grant or revoke this npc priv?
local ptmp = {}
ptmp[yl_speak_up.npc_priv_needs_player_priv[priv]] = true
if(not(minetest.check_player_privs(pname, ptmp))) then
minetest.chat_send_player(pname, "You lack the \""..
tostring(yl_speak_up.npc_priv_needs_player_priv[priv])..
"\" priv required to grant or revoke this NPC priv!")
return
end
-- does the player have the right to edit/change this npc?
if(not(list_all) and not(yl_speak_up.may_edit_npc(player, n_id))) then
minetest.chat_send_player(pname, "You can only set privs for NPC which you can edit. \""..
tostring(n_id).." cannot be edited by you.")
return
end
-- revoking privs of nonexistant NPC is allowed - but not granting them privs
local id = tonumber(string.sub(n_id, 3)) or 0
if(command == "grant" and not(yl_speak_up.npc_list[id])) then
minetest.chat_send_player(pname,
"Unknown NPC \""..tostring(n_id).."\".\n")
return
end
if(command == "grant" and not(yl_speak_up.npc_priv_table[n_id])) then
yl_speak_up.npc_priv_table[n_id] = {}
end

View File

@ -198,9 +198,12 @@ yl_speak_up.get_quest_variable_value = function(player_name, variable_name)
-- the owner name is alrady encoded in the variable name
local k = tostring(variable_name)
if(not(variable_name) or not(player_name) or not(yl_speak_up.player_vars[ k ])) then
yl_speak_up.get_variable_metadata(k_long, "default_value", true)
return nil
end
-- return stored value OR the default value
return yl_speak_up.player_vars[ k ][ player_name ]
or yl_speak_up.player_vars[ k ][ "$META$"][ "default_value" ]
end
@ -368,6 +371,12 @@ yl_speak_up.set_variable_metadata = function(k, pname, meta_name, entry_name, ne
-- var_type (the type of the variable) is a single string
if(meta_name == "var_type") then
yl_speak_up.player_vars[ k ][ "$META$"][ meta_name ] = new_value
elseif(meta_name == "default_value") then
-- reset default value to nil with empty string:
if(new_value == "") then
new_value = nil
end
yl_speak_up.player_vars[ k ][ "$META$"][ meta_name ] = new_value
else
if( not(yl_speak_up.player_vars[ k ][ "$META$" ][ meta_name ])
or type(yl_speak_up.player_vars[ k ][ "$META$" ][ meta_name ]) ~= "table") then
@ -394,6 +403,22 @@ yl_speak_up.get_access_list_for_var = function(k, pname, access_what)
end
-- helper function that searces for variables that will be replaced with their
-- values in text when displayed; helper function for yl_speak_up.update_stored_npc_data
-- (for keeping track of which NPC uses which variables)
-- changes table vars_used
yl_speak_up.find_player_vars_in_text = function(vars_used, text)
if(not(text) or text == "") then
return vars_used
end
for v in string.gmatch(text, "%$VAR ([%w%s_%-%.]+)%$") do
-- add the $ prefix again
vars_used["$ "..tostring(v)] = true
end
return vars_used
end
-- the dialog data of an NPC is saved - use this to save some statistical data
-- plus store which variables are used by this NPC
-- TODO: show this data in a formspec to admins for maintenance
@ -414,26 +439,36 @@ yl_speak_up.update_stored_npc_data = function(n_id, dialog)
local anz_actions = 0
local anz_effects = 0
local anz_trades = 0
local variables_p = {}
local variables_e = {}
-- used in d.d_text dialog texts,
-- o.o_text_when_prerequisites_met, o.o_text_when_prerequisites_not_met,
-- preconditions and effects
local variables_used = {}
if(dialog and dialog.n_dialogs) then
for d_id, d in pairs(dialog.n_dialogs) do
anz_dialogs = anz_dialogs + 1
if(d) then
-- find all variables used in the text
variables_used = yl_speak_up.find_player_vars_in_text(variables_used, d.d_text)
end
if(d and d.d_options) then
for o_id, o in pairs(d.d_options) do
anz_options = anz_options + 1
variables_used = yl_speak_up.find_player_vars_in_text(variables_used, o.o_text_when_prerequisites_met)
variables_used = yl_speak_up.find_player_vars_in_text(variables_used, o.o_text_when_prerequisites_not_met)
if(o and o.o_prerequisites) then
for p_id, p in pairs(o.o_prerequisites) do
anz_preconditions = anz_preconditions + 1
if(p and p.p_type and p.p_type == "state"
and p.p_variable and p.p_variable ~= "") then
variables_p[ p.p_variable ] = true
variables_used[ p.p_variable ] = true
end
end
end
if(o and o.actions) then
for a_id, a_data in pairs(o.actions) do
anz_actions = anz_actions + 1
-- actions can have alternate_text
variables_used = yl_speak_up.find_player_vars_in_text(variables_used, a_data.alternate_text)
end
end
if(o and o.o_results) then
@ -441,8 +476,10 @@ yl_speak_up.update_stored_npc_data = function(n_id, dialog)
anz_effects = anz_effects + 1
if(r and r.r_type and r.r_type == "state"
and r.r_variable and r.r_variable ~= "") then
variables_e[ r.r_variable ] = true
variables_used[ r.r_variable ] = true
end
-- effects can have alternate_text
variables_used = yl_speak_up.find_player_vars_in_text(variables_used, r.alternate_text)
end
end
end
@ -478,17 +515,14 @@ yl_speak_up.update_stored_npc_data = function(n_id, dialog)
-- delete all old entries that are not longer needed
for k, v in pairs(yl_speak_up.player_vars) do
if(not(variables_p[ k ]) and not(variables_e[ k ])) then
if(not(variables_used[ k ])) then
yl_speak_up.set_variable_metadata(k, pname, "used_by_npc", n_id, false)
end
end
-- save in the variables' metadata which NPC uses it
-- (this is what we're mostly after - know which variable is used in which NPC)
for k, v in pairs(variables_p) do
yl_speak_up.set_variable_metadata(k, pname, "used_by_npc", n_id, true)
end
for k, v in pairs(variables_e) do
for k, v in pairs(variables_used) do
yl_speak_up.set_variable_metadata(k, pname, "used_by_npc", n_id, true)
end
-- force writing the data
@ -499,7 +533,7 @@ end
-- which NPC do use this variable?
yl_speak_up.get_variable_metadata = function(var_name, meta_name, get_as_is)
-- var_type (the type of the variable) is a single string
if(meta_name and var_name and meta_name == "var_type") then
if(meta_name and var_name and (meta_name == "var_type" or meta_name == "default_value")) then
if( not(yl_speak_up.player_vars[ var_name ])
or not(yl_speak_up.player_vars[ var_name ][ "$META$"])) then
return nil

View File

@ -254,6 +254,15 @@ The replacements will not be applied in edit mode.
Servers can define [additional custom replacements](#add-simple-variables).
It is also possible to insert the *value* of variables into the text. Only variables the owner of the NPC has read access to can be replaced. Example: The variable "Example Variable Nr. 3" (without blanks would be a better name, i.e. "example\_variable\_nr\_3"), created by "exampleplayer", could be inserted by inserting the following text into dialog texts and options:
$VAR exampleplayer Example Variable Nr. 3$
It will be replaced by the *value* that variable has for the player that is talking to the NPC.
Properties can be replaced in a similar way. The value of the property "job" of the NPC for example could thus be shown:
$PROP job$
Variables and properties can only be replaced if their *name* contains only alphanumeric signs (a-z, A-Z, 0-9), spaces, "\_", "-" and/or ".".
### 1.8 Alternate Text
<a name="alternate_text"></a>

View File

@ -35,6 +35,13 @@ end
-- show formspec with highest possible version information for the player
-- force_version: optional parameter
yl_speak_up.show_fs_ver = function(pname, formname, formspec, force_version)
-- catch errors
if(not(formspec)) then
force_version = "1"
formspec = "size[4,2]label[0,0;Error: No text found for form\n\""..
minetest.formspec_escape(formname).."\"]"..
"button_exit[1.5,1.5;1,0.5;exit;Exit]"
end
-- if the formspec already calls for a specific formspec version: use that one
if(string.sub(formspec, 1, 17) == "formspec_version[") then
minetest.show_formspec(pname, formname, formspec)