From 769b4046b40b4d135fde67ef602a7f030216f049 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 5 Dec 2024 19:59:19 +0100 Subject: [PATCH 01/56] ask for initial_config at first talk --- fs/fs_talkdialog.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/fs_talkdialog.lua b/fs/fs_talkdialog.lua index e6115c6..7a30ee3 100644 --- a/fs/fs_talkdialog.lua +++ b/fs/fs_talkdialog.lua @@ -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 -- 2.47.3 From 5f6f48878817cb350a9284ca01819680672a0b60 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 5 Dec 2024 23:11:31 +0100 Subject: [PATCH 02/56] typo --- fs/fs_initial_config.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/fs_initial_config.lua b/fs/fs_initial_config.lua index 9e10b76..b176803 100644 --- a/fs/fs_initial_config.lua +++ b/fs/fs_initial_config.lua @@ -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), -- 2.47.3 From 6e604b05d0d56a225b8e629564d44f79de3b77c2 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 13 Dec 2024 17:27:29 +0100 Subject: [PATCH 03/56] fixed crash in simple dialogs export --- fs/fs_export.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/fs_export.lua b/fs/fs_export.lua index 650d712..4a429a7 100644 --- a/fs/fs_export.lua +++ b/fs/fs_export.lua @@ -38,7 +38,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 -- 2.47.3 From a54c395e91f55e22a209c02386bdc97de14409cf Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 12:47:45 +0100 Subject: [PATCH 04/56] show error message instead of crash if no formspec text supplied --- show_fs.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/show_fs.lua b/show_fs.lua index 3075712..b986b5a 100644 --- a/show_fs.lua +++ b/show_fs.lua @@ -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) -- 2.47.3 From bfeed377679d264551ad5e8a76fe3372110eee6a Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 13:50:58 +0100 Subject: [PATCH 05/56] allow d_got_item and d_trade to be used as target dialogs and as target for failed actions --- exec_actions.lua | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/exec_actions.lua b/exec_actions.lua index a35c79a..90130f4 100644 --- a/exec_actions.lua +++ b/exec_actions.lua @@ -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 -- 2.47.3 From 0316afdfa9bbe40eb08ae23910b7f0af62423a1f Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 14:36:19 +0100 Subject: [PATCH 06/56] take items back into npc inventory when npc_gives failed instead of forgetting them --- exec_actions.lua | 21 ++++++++++++++++++++- fs/fs_action_npc_gives.lua | 4 ++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/exec_actions.lua b/exec_actions.lua index 90130f4..250468a 100644 --- a/exec_actions.lua +++ b/exec_actions.lua @@ -312,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. @@ -331,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 @@ -360,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 diff --git a/fs/fs_action_npc_gives.lua b/fs/fs_action_npc_gives.lua index e775f15..53a255c 100644 --- a/fs/fs_action_npc_gives.lua +++ b/fs/fs_action_npc_gives.lua @@ -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) -- 2.47.3 From 9797ada402445fa0b3c606afe84c0bd22410c231 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 16:57:27 +0100 Subject: [PATCH 07/56] #5266 check if npc for npc_talk_privs exists --- npc_privs.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/npc_privs.lua b/npc_privs.lua index b0cdf93..d4b3fb5 100644 --- a/npc_privs.lua +++ b/npc_privs.lua @@ -114,6 +114,12 @@ yl_speak_up.command_npc_talk_privs = function(pname, param) table.concat(yl_speak_up.npc_priv_names, ", ")..".") return end + -- revoking privs of nonexistant NPC is allowed - but not granting them privs + if(command == "grant" and not(yl_speak_up.npc_list[n_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 -- 2.47.3 From 8af9a995a04e546c19950a6c85d2d49cf9166224 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 17:09:01 +0100 Subject: [PATCH 08/56] #5268 mention missing npc privs in debug mode --- exec_apply_effects.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/exec_apply_effects.lua b/exec_apply_effects.lua index 592b4d7..6245ea5 100644 --- a/exec_apply_effects.lua +++ b/exec_apply_effects.lua @@ -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 -- 2.47.3 From 349d140d095571d6e180315dc1782e40bc200da8 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 19:58:03 +0100 Subject: [PATCH 09/56] 2076 mobs_redo mobs can be picked up with lasso by those who can edit them and not just their owner --- interface_mobs_api.lua | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/interface_mobs_api.lua b/interface_mobs_api.lua index aeccc2c..3f15af0 100644 --- a/interface_mobs_api.lua +++ b/interface_mobs_api.lua @@ -31,21 +31,36 @@ 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() + -- 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)..".") + return end - return - end - end + end -- protect npc with mobs:protector if mobs:protect(self, clicker) then -- 2.47.3 From 95e721b8f97072a792c0ffda866fa831fa97f587 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 15 Dec 2024 21:04:10 +0100 Subject: [PATCH 10/56] #5809 NPC show name, descr, owner, last pos when picked up --- interface_mobs_api.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/interface_mobs_api.lua b/interface_mobs_api.lua index 3f15af0..36991c3 100644 --- a/interface_mobs_api.lua +++ b/interface_mobs_api.lua @@ -45,6 +45,7 @@ function yl_speak_up.do_mobs_on_rightclick(self, clicker) -- 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 @@ -58,6 +59,26 @@ function yl_speak_up.do_mobs_on_rightclick(self, clicker) " (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 end -- 2.47.3 From 1b45fc7792f4f2d7890beaa298fd5e3c08415176 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 19 Dec 2024 22:12:08 +0100 Subject: [PATCH 11/56] #5501 allow to configure setting of npc_privs --- chat_commands.lua | 13 ++------- config.lua | 29 +++++++++++++++---- npc_privs.lua | 72 ++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/chat_commands.lua b/chat_commands.lua index 78e00ac..2e8d57c 100644 --- a/chat_commands.lua +++ b/chat_commands.lua @@ -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.".. diff --git a/config.lua b/config.lua index 60da249..f3c38a2 100644 --- a/config.lua +++ b/config.lua @@ -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 ------------------------------------------------------------------------------ diff --git a/npc_privs.lua b/npc_privs.lua index d4b3fb5..e2f0f15 100644 --- a/npc_privs.lua +++ b/npc_privs.lua @@ -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".. - "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 ".. - ": " + local text = "This list contains the privs of each NPC you can edit ".. + "in the form of : " -- 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.." " + 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.." " + end end end minetest.chat_send_player(pname, text..".") @@ -114,8 +141,27 @@ 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 - if(command == "grant" and not(yl_speak_up.npc_list[n_id])) then + 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 -- 2.47.3 From 68e37e24b856bd30c1bc412e9b1949337dadeb6f Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 21 Dec 2024 22:53:09 +0100 Subject: [PATCH 12/56] allow to show value of variables and properties in dialog text and option text --- api/custom_functions_you_can_override.lua | 28 ++++++++++++++++++++++- readme.md | 9 ++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/api/custom_functions_you_can_override.lua b/api/custom_functions_you_can_override.lua index 1e69a32..e2764f0 100644 --- a/api/custom_functions_you_can_override.lua +++ b/api/custom_functions_you_can_override.lua @@ -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 diff --git a/readme.md b/readme.md index 478aa4f..56ffdca 100644 --- a/readme.md +++ b/readme.md @@ -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 -- 2.47.3 From e3094962c33f403b01fa464ff5d8bfec2a695ed0 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 25 Dec 2024 22:50:33 +0100 Subject: [PATCH 13/56] search texts for variable (display) usage when storing npc data --- quest_api.lua | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/quest_api.lua b/quest_api.lua index 2e5b058..8504994 100644 --- a/quest_api.lua +++ b/quest_api.lua @@ -394,6 +394,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 +430,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.alternate_text) end end if(o and o.o_results) then @@ -441,8 +467,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 +506,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 -- 2.47.3 From 5cd9a9aab6d5c7fd9688ddc7c47cf9fe9286b8cd Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 26 Dec 2024 16:01:39 +0100 Subject: [PATCH 14/56] fixed crash in quest_api when saving --- quest_api.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quest_api.lua b/quest_api.lua index 8504994..50948a4 100644 --- a/quest_api.lua +++ b/quest_api.lua @@ -459,7 +459,7 @@ yl_speak_up.update_stored_npc_data = function(n_id, dialog) 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.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 -- 2.47.3 From 28c3e7eed9252015bae662183310c4c1f7c2ae92 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 26 Dec 2024 16:07:41 +0100 Subject: [PATCH 15/56] removed surplus line in export to ink before farewell message --- export_to_ink.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index b00e027..7852d81 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -497,7 +497,6 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) o_data.o_visit_only_once, o_id, p_list, e_list, dialog_names) 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) -- 2.47.3 From 8456a10107156b8a17960a68deb348e863bfad10 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 26 Dec 2024 16:11:13 +0100 Subject: [PATCH 16/56] renamed main ink loop to postfix _d_end instead of main to make its point more obvious --- export_to_ink.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index 7852d81..f472f77 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -120,7 +120,7 @@ yl_speak_up.export_to_ink.print_choice = function(lines, choice_text, n_id, star divert_to = tostring(start_dialog) elseif(divert_to == "d_end" or divert_to == tostring(n_id).."_d_end") then -- go back to choosing between talking to NPC and end - divert_to = tostring(n_id).."_main" + divert_to = tostring(n_id).."_d_end" 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) @@ -391,9 +391,13 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) start_dialog = tostring(n_id).."_"..tostring(start_dialog) end - local main = tostring(n_id).."_main" - local tmp = {"-> ", main, - "\n=== ", main, " ===", + -- 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 = tostring(n_id).."_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+ End -> END"} @@ -499,7 +503,7 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) end -- dealt with the option -- 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) + nil, tostring(n_id).."_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 -- 2.47.3 From a7f5f3c8b056f2f8026cb37bec23321b29cf4ac9 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 26 Dec 2024 16:27:14 +0100 Subject: [PATCH 17/56] export d_got_item and d_trade to ink language as well --- export_to_ink.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/export_to_ink.lua b/export_to_ink.lua index f472f77..2705242 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -405,6 +405,15 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) local vars_used = ink_export.print_variables_used(tmp, dialog, n_id, pname) 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 = {} -- 2.47.3 From 293df54dac88c894aa35cddc3dce013224b30a20 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 26 Dec 2024 18:15:23 +0100 Subject: [PATCH 18/56] use specified prefix for all knots in ink export --- export_to_ink.lua | 91 +++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index 2705242..a519e1b 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -13,9 +13,9 @@ 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) table.insert(lines, "\n\n=== ") - table.insert(lines, tostring(knot_name or "ERROR")) + table.insert(lines, use_prefix..tostring(knot_name or "ERROR")) table.insert(lines, " ===") end @@ -55,7 +55,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 +111,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).."_d_end" - 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 +133,12 @@ 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) +yl_speak_up.export_to_ink.print_dialog_knot = function(lines, use_prefix, d_id, d) + local knot_name = 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) + ink_export.print_knot_name(lines, knot_name, use_prefix) -- many characters at the start of a line have a special meaning; -- hopefully they will not be obstrusive later on; @@ -159,26 +158,26 @@ 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, +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) - local knot_name = tostring(n_id).."_"..tostring(d_id).."_"..tostring(o_id).."_"..tostring(a.a_id) - ink_export.print_knot_name(lines, knot_name) + local knot_name = tostring(d_id).."_"..tostring(o_id).."_"..tostring(a.a_id) + ink_export.print_knot_name(lines, knot_name, use_prefix) table.insert(lines, "\n:action: ") table.insert(lines, a.a_id) table.insert(lines, " ") table.insert(lines, yl_speak_up.show_action(a)) - 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) - 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 end @@ -190,11 +189,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 knot_name = tostring(d_id).."_"..tostring(o_id).."_"..tostring(r.r_id) + ink_export.print_knot_name(lines, knot_name, use_prefix) table.insert(lines, "\n:effect: ") table.insert(lines, r.r_id) @@ -208,11 +207,11 @@ 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) @@ -221,7 +220,7 @@ 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 +305,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 +319,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 +359,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 @@ -388,21 +387,27 @@ 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 + -- 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).."_" + -- 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 = tostring(n_id).."_d_end" + 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), " -> ", 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 @@ -419,7 +424,7 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) 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 @@ -430,7 +435,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) -- iterate over all options local sorted_o_list = yl_speak_up.get_sorted_options(dialog.n_dialogs[d_id].d_options or {}, "o_sort") @@ -447,7 +452,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 @@ -468,7 +473,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) @@ -489,7 +494,7 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) 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) -- has been dealt with @@ -498,21 +503,21 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) -- 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) + vars_used, use_prefix, dialog_names) local e_list = ink_export.translate_effect_list(dialog, o_data.o_results, - vars_used, n_id) + vars_used) -- what remains is to print the option/choice itself ink_export.print_choice(tmp, -- TODO: deal with when_prerequisites_not_met - o_data.o_text_when_prerequisites_met, n_id, start_dialog, + o_data.o_text_when_prerequisites_met, use_prefix, start_dialog, alternate_text_on_success, target_dialog, o_data.o_visit_only_once, o_id, p_list, e_list, dialog_names) end -- dealt with the option -- add way to end talking to the NPC - ink_export.print_choice(tmp, "Farewell!", n_id, start_dialog, - nil, tostring(n_id).."_d_end", 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 -- 2.47.3 From e1f13f7bff914cfd82c959e218ad22eb8e97f4e7 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 26 Dec 2024 20:02:30 +0100 Subject: [PATCH 19/56] ink export: use d.d_name for actions and effects as well --- export_to_ink.lua | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index a519e1b..c2fdf7c 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -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, use_prefix) +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, use_prefix..tostring(knot_name or "ERROR")) + table.insert(lines, knot_name) table.insert(lines, " ===") + return knot_name end @@ -133,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, use_prefix, d_id, d) - local knot_name = 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, use_prefix) +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; @@ -160,8 +161,8 @@ end -- a action 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) - local knot_name = tostring(d_id).."_"..tostring(o_id).."_"..tostring(a.a_id) - ink_export.print_knot_name(lines, knot_name, use_prefix) + 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) @@ -179,7 +180,7 @@ yl_speak_up.export_to_ink.print_action_knot = function(lines, use_prefix, d_id, 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 @@ -192,8 +193,8 @@ end 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(d_id).."_"..tostring(o_id).."_"..tostring(r.r_id) - ink_export.print_knot_name(lines, knot_name, use_prefix) + 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) @@ -214,8 +215,7 @@ yl_speak_up.export_to_ink.print_effect_knot = function(lines, use_prefix, d_id, 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 @@ -435,7 +435,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, use_prefix, 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") -- 2.47.3 From 7e3ea186532db5ab63417ad281367b11afe8ba0a Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 27 Dec 2024 17:40:51 +0100 Subject: [PATCH 20/56] export to ink: support autoanswer, random and grey out answer --- export_to_ink.lua | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index c2fdf7c..342ab8c 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -508,12 +508,34 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) vars_used) -- what remains is to print the option/choice itself + local o_text = o_data.o_text_when_prerequisites_met + if( o_data.o_autoanswer) then + o_text = "[Automaticly selected if preconditions are met]" + elseif(o_data.o_random) then + o_text = "[One of these options is randomly selected]" + end ink_export.print_choice(tmp, - -- TODO: deal with when_prerequisites_not_met - o_data.o_text_when_prerequisites_met, use_prefix, start_dialog, + o_text, use_prefix, start_dialog, alternate_text_on_success, target_dialog, - o_data.o_visit_only_once, + o_data.o_visit_only_once, -- print + (often) or * (only once) 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 -- add way to end talking to the NPC ink_export.print_choice(tmp, "Farewell!", use_prefix, start_dialog, -- 2.47.3 From c4ebef21f08de50fc5a34fe072c51bb210dc23bc Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 1 Jan 2025 18:00:13 +0100 Subject: [PATCH 21/56] split up functions.lua into diffrent files --- functions.lua => functions_dialogs.lua | 418 +------------------------ functions_save_restore_dialogs.lua | 112 +++++++ functions_talk.lua | 314 +++++++++++++++++++ init.lua | 4 +- 4 files changed, 438 insertions(+), 410 deletions(-) rename functions.lua => functions_dialogs.lua (50%) create mode 100644 functions_save_restore_dialogs.lua create mode 100644 functions_talk.lua diff --git a/functions.lua b/functions_dialogs.lua similarity index 50% rename from functions.lua rename to functions_dialogs.lua index a8af983..68576a7 100644 --- a/functions.lua +++ b/functions_dialogs.lua @@ -1,33 +1,12 @@ ---### --- 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 - +-- +-- 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 --### @@ -39,20 +18,6 @@ yl_speak_up.get_number_from_id = function(any_id) 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 @@ -88,120 +53,12 @@ yl_speak_up.sanitize_sort = function(options, value) 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 @@ -459,247 +316,6 @@ yl_speak_up.sort_keys = function(t, simple) 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 @@ -717,22 +333,6 @@ yl_speak_up.check_if_dialog_exists = function(dialog, 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 diff --git a/functions_save_restore_dialogs.lua b/functions_save_restore_dialogs.lua new file mode 100644 index 0000000..363087e --- /dev/null +++ b/functions_save_restore_dialogs.lua @@ -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 + diff --git a/functions_talk.lua b/functions_talk.lua new file mode 100644 index 0000000..8fdb551 --- /dev/null +++ b/functions_talk.lua @@ -0,0 +1,314 @@ + +--### +-- 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 + +---### +-- 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 + diff --git a/init.lua b/init.lua index eeed388..3165626 100644 --- a/init.lua +++ b/init.lua @@ -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") -- 2.47.3 From 4cf06da1e491135cb16e5d32fa6e36e75ac2e544 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 1 Jan 2025 18:24:38 +0100 Subject: [PATCH 22/56] made functions in functions_dialog.lua only optionally dependant on pname --- functions_dialogs.lua | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 68576a7..c408aec 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -64,6 +64,8 @@ end -- 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) @@ -99,6 +101,7 @@ end -- 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 @@ -127,18 +130,25 @@ yl_speak_up.add_new_option = function(dialog, pname, next_id, d_id, option_text, 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..".") + + 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 -- 2.47.3 From 10e90e412ac688c00e593b79d3706de2c8928b90 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 1 Jan 2025 18:53:56 +0100 Subject: [PATCH 23/56] prefix options in ink export with automaticly_ or randomly_ if necessary --- export_to_ink.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index 342ab8c..6b7b6af 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -509,16 +509,19 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) -- what remains is to print the option/choice itself local o_text = o_data.o_text_when_prerequisites_met + local o_prefix = "" if( o_data.o_autoanswer) then o_text = "[Automaticly selected if preconditions are met]" + o_prefix = "automaticly_" elseif(o_data.o_random) then o_text = "[One of these options is randomly selected]" + o_prefix = "randomly_" end ink_export.print_choice(tmp, o_text, use_prefix, start_dialog, alternate_text_on_success, target_dialog, o_data.o_visit_only_once, -- print + (often) or * (only once) - o_id, p_list, e_list, dialog_names) + 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 ~= "" -- 2.47.3 From c0fcbecd63fa6d8648a2b583a48a5fbd8f30ac1b Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 1 Jan 2025 22:37:59 +0100 Subject: [PATCH 24/56] added functions for updating and importing dialogs and options for future ink import --- functions_dialogs.lua | 243 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index c408aec..ff3919f 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -11,6 +11,10 @@ -- 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" @@ -97,6 +101,50 @@ yl_speak_up.add_new_dialog = function(dialog, pname, next_id, dialog_text) 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) +yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) + -- 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 = " ["..tostring(dialog_name).."]: " + end + if(not(d_id)) then + -- pname is nil - thus no logging and no adding of a back to start option + -- next_id is also nil - so just add a new dialog + d_id = yl_speak_up.add_new_dialog(dialog, nil, nil, 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 + table.insert(log, log_str.."Successfully created dialog.") + + 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 + + -- set d_name if it differs from d_id + if(d_id ~= dialog_name + and (not(dialog.n_dialogs[d_id].d_name) + or(dialog.n_dialogs[d_id].d_name ~= dialog_name))) then + table.insert(log, log_str.."Changed dialog name from \"".. + tostring(dialog.n_dialogs[d_id].d_name).."\" to \""..tostring(dialog_name).."\".") + -- actually change the dialog name + dialog.n_dialogs[d_id].d_name = dialog_name + end + return 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 @@ -227,6 +275,198 @@ yl_speak_up.add_new_option = function(dialog, pname, next_id, d_id, option_text, 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 +-- 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: The calling function also needs to take care to set *all* dialog options to +-- *randomly selected* if at least *one* is set to this value. This cannot be done +-- here as the other dialog options may not even be definied here yet. +-- 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, target_dialog, + alternate_text, visit_only_once) + -- 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 + -- the dialog does not exist - we cannot add an option to a nonexistant dialog + return nil + 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 = " ["..tostring(dialog_name).."], option <"..tostring(option_name)..">: " + end + + -- 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 = "" + if( o_id and syl_speak_up.string_starts_with(o_id, "new_")) then + -- we are asked to create a *new* option + o_id = nil + elseif(o_id and yl_speak_up.string_starts_with(o_id, "automaticly_")) then + -- this option will be automaticly selected if its preconditions are true + mode = 1 + option_name = string.sub(o_id, string.find(o_id, "automaticly_") + 1) + o_id = option_name + elseif(o_id and yl_speak_up.string_starts_with(o_id, "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 = string.sub(o_id, string.find(o_id, "randomly_") + 1) + o_id = option_name + elseif(o_id and yl_speak_up.string_starts_with(o_id, "grey_out_")) then + -- this sets o_text_when_prerequisites_not_met + mode = 3 + option_name = string.sub(o_id, string.find(o_id, "grey_out_") + 1) + o_id = option_name + -- in this case we don't want to change the old text - just the greyed out one + -- (we keep option_text in case the option needs to be created) + text_when_prerequisites_not_met = option_text + 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.") + 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 + table.insert(log, log_str.."Successfully created new option \""..tostring(o_id).."\".") + + -- do not change the option_text when we want to change the text of the grey_out_ option text + elseif(dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met ~= option_text + and mode ~= 3) then + -- else update the text + table.insert(log, log_str.."Changed option text from \"".. + tostring(dialog.n_dialogs[d_id].d_text.d_options[o_id]).. + "\" to \""..tostring(option_text).."\" for option \""..tostring(o_id).."\".") + -- actually update the text + dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met = option_text + end + + -- abbreviate that + local o_data = dialog.n_dialogs[d_id].d_options[o_id] + + 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 + if(not(r.r_value) or r.r_value ~= target_dialog) then + table.insert(log, log_str.."Changed target dialog from \"".. + tostring(r.r_value).."\" to \""..tostring(target_dialog).. + "\" for option \""..tostring(o_id).."\".") + -- actually change the target dialog + r_found = true + 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(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 + + -- 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.") + -- actually update the text + -- is this option selected randomly? + elseif(mode == 2 and not(o_data.o_random)) then + o_data.o_random = 1 + table.insert(log, log_str.."Changed option \""..tostring(o_id).."\" to RANDOMLY SELECTED.") + -- grey out the given text and show that as answer when preconditions not met? + elseif(mode == 3) then + if((not(o_dat.o_text_when_prerequisites_not_met + or o_data.o_text_when_prerequisites_not_met ~= text_when_prerequisites_not_met))) then + + table.insert(log, log_str.."Changed option text WHEN PREREQUISITES NOT MET from \"".. + tostring(o_data.o_text_when_prerequisites_not_met).. + "\" to \""..tostring(text_when_prerequisites_not_met).. + "\" for option \""..tostring(o_id).."\".") + -- actually change it + o_data.o_text_when_prerequisites_not_met = text_when_prerequisites_not_met + end + -- 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 + -- mode is 0 - that means everything is normal for this option + if(o_data.o_autoanswer) then + o_data.o_autoanswer = nil + table.insert(log, log_str.."Removed AUTOMATICLY SELECTED from option \"".. + tostring(o_id).."\".") + end + if(o_data.o_random) then + o_data.o_random = nil + table.insert(log, log_str.."Removed RANDOMLY SELECTED from option \"".. + tostring(o_id).."\".") + end + -- set visibility back to default + o_data.o_hide_when_prerequisites_not_met = "false" + o_data.o_grey_when_prerequisites_not_met = "false" + 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.sb_v.o_visit_only_once) or o_data.o_visit_only_once ~= 1)) then + o_data.sb_v.o_visit_only_once = 1 + elseif(not(visit_only_once) + and o_data.sb_v.o_visit_only_once and o_data.o_visit_only_once == 1) then + o_data.sb_v.o_visit_only_once = nil + 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]) @@ -239,8 +479,10 @@ yl_speak_up.add_new_result = function(dialog, d_id, o_id) 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 @@ -366,6 +608,7 @@ yl_speak_up.d_name_to_d_id = function(dialog, d_name) return k end end + return nil end -- 2.47.3 From e5183813d733354b922ea71a1bfd0bcdeff3e947 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 1 Jan 2025 22:41:09 +0100 Subject: [PATCH 25/56] moved a function from _dialogs to _talk --- functions_dialogs.lua | 12 ------------ functions_talk.lua | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index ff3919f..6df93fe 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -624,18 +624,6 @@ yl_speak_up.d_id_to_d_name = function(dialog, d_id) 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? diff --git a/functions_talk.lua b/functions_talk.lua index 8fdb551..17d8210 100644 --- a/functions_talk.lua +++ b/functions_talk.lua @@ -36,6 +36,20 @@ yl_speak_up.get_error_message = function() 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 ---### -- 2.47.3 From 40314125a8ad3d8ee23b48d6009f52683887d7f3 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Wed, 1 Jan 2025 23:00:38 +0100 Subject: [PATCH 26/56] moved yl_speak_up.get_start_dialog_id into functions_dialog --- api/api_talk.lua | 20 -------------------- functions_dialogs.lua | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/api/api_talk.lua b/api/api_talk.lua index b1f8df3..449f7ae 100644 --- a/api/api_talk.lua +++ b/api/api_talk.lua @@ -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 diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 6df93fe..189f465 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -58,6 +58,27 @@ yl_speak_up.sanitize_sort = function(options, value) 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 + --### --Formspecs -- 2.47.3 From 8570fea8ba1f2369a9de00008f8e12907c77e475 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 2 Jan 2025 00:12:49 +0100 Subject: [PATCH 27/56] corrected obvious errors in a new function --- functions_dialogs.lua | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 189f465..19fd0ea 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -131,7 +131,7 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) -- name the thing for logging purposes local log_str = "Dialog "..tostring(d_id) if(dialog_name and dialog_name ~= d_id) then - log_str = " ["..tostring(dialog_name).."]: " + log_str = log_str.." ["..tostring(dialog_name).."]: " end if(not(d_id)) then -- pname is nil - thus no logging and no adding of a back to start option @@ -143,6 +143,8 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) 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).."]: " table.insert(log, log_str.."Successfully created dialog.") elseif(dialog.n_dialogs[d_id].d_text ~= dialog_text) then @@ -323,7 +325,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam -- name the thing for logging purposes local log_str = "Dialog "..tostring(d_id) if(dialog_name and dialog_name ~= d_id) then - log_str = " ["..tostring(dialog_name).."], option <"..tostring(option_name)..">: " + log_str = log_str.." ["..tostring(dialog_name).."], option <"..tostring(option_name)..">: " end -- translate the name of the target_dialog if needed @@ -335,7 +337,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam local o_id = option_name local mode = 0 local text_when_prerequisites_not_met = "" - if( o_id and syl_speak_up.string_starts_with(o_id, "new_")) then + if( o_id and yl_speak_up.string_starts_with(o_id, "new_")) then -- we are asked to create a *new* option o_id = nil elseif(o_id and yl_speak_up.string_starts_with(o_id, "automaticly_")) then @@ -389,7 +391,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam and mode ~= 3) then -- else update the text table.insert(log, log_str.."Changed option text from \"".. - tostring(dialog.n_dialogs[d_id].d_text.d_options[o_id]).. + tostring(dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met).. "\" to \""..tostring(option_text).."\" for option \""..tostring(o_id).."\".") -- actually update the text dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met = option_text @@ -444,7 +446,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam table.insert(log, log_str.."Changed option \""..tostring(o_id).."\" to RANDOMLY SELECTED.") -- grey out the given text and show that as answer when preconditions not met? elseif(mode == 3) then - if((not(o_dat.o_text_when_prerequisites_not_met + if((not(o_data.o_text_when_prerequisites_not_met or o_data.o_text_when_prerequisites_not_met ~= text_when_prerequisites_not_met))) then table.insert(log, log_str.."Changed option text WHEN PREREQUISITES NOT MET from \"".. @@ -478,11 +480,12 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam -- 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.sb_v.o_visit_only_once) or o_data.o_visit_only_once ~= 1)) then - o_data.sb_v.o_visit_only_once = 1 + 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.sb_v.o_visit_only_once and o_data.o_visit_only_once == 1) then - o_data.sb_v.o_visit_only_once = nil + and o_data.o_visit_only_once and o_data.o_visit_only_once == 1) then + o_data.o_visit_only_once = nil end return o_id end -- 2.47.3 From 06507880b64ae16b6ad1feda398d4e0088e202b4 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 2 Jan 2025 00:43:16 +0100 Subject: [PATCH 28/56] properly split option names for ink import --- functions_dialogs.lua | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 19fd0ea..eb73da5 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -337,26 +337,31 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam local o_id = option_name local mode = 0 local text_when_prerequisites_not_met = "" - if( o_id and yl_speak_up.string_starts_with(o_id, "new_")) then + 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 yl_speak_up.string_starts_with(o_id, "automaticly_")) then + elseif(o_id and parts[1] == "automaticly_") then -- this option will be automaticly selected if its preconditions are true mode = 1 - option_name = string.sub(o_id, string.find(o_id, "automaticly_") + 1) + option_name = parts[2] o_id = option_name - elseif(o_id and yl_speak_up.string_starts_with(o_id, "randomly_")) then + 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 = string.sub(o_id, string.find(o_id, "randomly_") + 1) + option_name = parts[2] o_id = option_name - elseif(o_id and yl_speak_up.string_starts_with(o_id, "grey_out_")) then + elseif(o_id and parts[1] == "grey_") then -- this sets o_text_when_prerequisites_not_met mode = 3 - option_name = string.sub(o_id, string.find(o_id, "grey_out_") + 1) + -- strip the out_ part as well + option_name = string.sub(parts[2], 5) o_id = option_name -- in this case we don't want to change the old text - just the greyed out one -- (we keep option_text in case the option needs to be created) @@ -376,7 +381,8 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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.") + table.insert(log, log_str.."FAILED to create new option \""..tostring(o_id).."\".") + return end end -- pname is nil - thus no logging here -- 2.47.3 From c0ed2cc148300bf41b5cca967176884fa5b03c7c Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 2 Jan 2025 17:30:00 +0100 Subject: [PATCH 29/56] moved function get_dialog_list_for_export --- fs/fs_export.lua | 20 -------------------- functions_dialogs.lua | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/fs/fs_export.lua b/fs/fs_export.lua index 4a429a7..ee7728d 100644 --- a/fs/fs_export.lua +++ b/fs/fs_export.lua @@ -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) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index eb73da5..a0edb39 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -80,6 +80,27 @@ yl_speak_up.get_start_dialog_id = function(dialog) 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 + return liste +end + + --### --Formspecs --### -- 2.47.3 From dc223d99f54634341aa46bfc4e5833b2046903df Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 2 Jan 2025 17:46:18 +0100 Subject: [PATCH 30/56] fixed logging of option changes in update option --- functions_dialogs.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index a0edb39..a83a169 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -346,8 +346,9 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam -- 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).."], option <"..tostring(option_name)..">: " + log_str = log_str.." ["..tostring(dialog_name).."]" end + log_str = log_str..", option <"..tostring(option_name)..">: " -- translate the name of the target_dialog if needed if(target_dialog and not(yl_speak_up.is_special_dialog(target_dialog))) then -- 2.47.3 From d9d10112cf03e4bd4d9e14f0b54cd5f5994d1c19 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 2 Jan 2025 18:14:22 +0100 Subject: [PATCH 31/56] allow to add sort_oder to update_dialog_option --- functions_dialogs.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index a83a169..2f74e4d 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -336,7 +336,7 @@ end -- 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, target_dialog, - alternate_text, visit_only_once) + 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 @@ -515,6 +515,10 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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_oder) then + o_data.o_sort = sort_order + end return o_id end -- 2.47.3 From 76f7e3756694725a25c48e2b37f038669b5c51a1 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 2 Jan 2025 20:16:40 +0100 Subject: [PATCH 32/56] sort dialogs in export according to d_sort instead of d_ alphabethicly --- functions_dialogs.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 2f74e4d..19a65fc 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -97,6 +97,9 @@ yl_speak_up.get_dialog_list_for_export = function(dialog) 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.n_dialogs[a].d_sort < dialog.n_dialogs[b].d_sort end) return liste end -- 2.47.3 From 829683f7500d3f493fe0e4243afc15d2ab50d73e Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 3 Jan 2025 21:03:55 +0100 Subject: [PATCH 33/56] pass o_text_when_prerequisites_not_met on to update_dialog_option --- functions_dialogs.lua | 79 +++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 19a65fc..58fc081 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -338,8 +338,9 @@ end -- here as the other dialog options may not even be definied here yet. -- 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, target_dialog, - alternate_text, visit_only_once, sort_order) +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 @@ -382,15 +383,9 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam mode = 2 option_name = parts[2] o_id = option_name - elseif(o_id and parts[1] == "grey_") then - -- this sets o_text_when_prerequisites_not_met - mode = 3 - -- strip the out_ part as well - option_name = string.sub(parts[2], 5) - o_id = option_name - -- in this case we don't want to change the old text - just the greyed out one - -- (we keep option_text in case the option needs to be created) - text_when_prerequisites_not_met = option_text + elseif(o_id and parts[1] ~= "o_") then + table.insert(log, log_str.."FAILED to create unknown option \""..tostring(o_id).."\".") + return nil end @@ -416,21 +411,44 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam return nil end table.insert(log, log_str.."Successfully created new option \""..tostring(o_id).."\".") - - -- do not change the option_text when we want to change the text of the grey_out_ option text - elseif(dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met ~= option_text - and mode ~= 3) then - -- else update the text - table.insert(log, log_str.."Changed option text from \"".. - tostring(dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met).. - "\" to \""..tostring(option_text).."\" for option \""..tostring(o_id).."\".") - -- actually update the text - dialog.n_dialogs[d_id].d_options[o_id].o_text_when_prerequisites_met = option_text 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 @@ -475,22 +493,6 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam elseif(mode == 2 and not(o_data.o_random)) then o_data.o_random = 1 table.insert(log, log_str.."Changed option \""..tostring(o_id).."\" to RANDOMLY SELECTED.") - -- grey out the given text and show that as answer when preconditions not met? - elseif(mode == 3) then - if((not(o_data.o_text_when_prerequisites_not_met - or o_data.o_text_when_prerequisites_not_met ~= text_when_prerequisites_not_met))) then - - table.insert(log, log_str.."Changed option text WHEN PREREQUISITES NOT MET from \"".. - tostring(o_data.o_text_when_prerequisites_not_met).. - "\" to \""..tostring(text_when_prerequisites_not_met).. - "\" for option \""..tostring(o_id).."\".") - -- actually change it - o_data.o_text_when_prerequisites_not_met = text_when_prerequisites_not_met - end - -- 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 -- mode is 0 - that means everything is normal for this option if(o_data.o_autoanswer) then @@ -503,9 +505,6 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam table.insert(log, log_str.."Removed RANDOMLY SELECTED from option \"".. tostring(o_id).."\".") end - -- set visibility back to default - o_data.o_hide_when_prerequisites_not_met = "false" - o_data.o_grey_when_prerequisites_not_met = "false" end -- the visit_only_once option is handled without logging as it might create too many -- 2.47.3 From 31434e69ceb530fd4ba035edb43e123663bc7b90 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 3 Jan 2025 21:51:36 +0100 Subject: [PATCH 34/56] less spammy logs for update_dialog* --- functions_dialogs.lua | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 58fc081..87aeb02 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -157,6 +157,7 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) if(dialog_name and dialog_name ~= d_id) then log_str = log_str.." ["..tostring(dialog_name).."]: " end + local is_new = false if(not(d_id)) then -- pname is nil - thus no logging and no adding of a back to start option -- next_id is also nil - so just add a new dialog @@ -169,7 +170,8 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) end -- we got a new name for the log log_str = "New dialog "..tostring(d_id).." ["..tostring(dialog_name).."]: " - table.insert(log, log_str.."Successfully created dialog.") + 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 @@ -183,8 +185,11 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) if(d_id ~= dialog_name and (not(dialog.n_dialogs[d_id].d_name) or(dialog.n_dialogs[d_id].d_name ~= dialog_name))) then - table.insert(log, log_str.."Changed dialog name from \"".. - tostring(dialog.n_dialogs[d_id].d_name).."\" to \""..tostring(dialog_name).."\".") + if(not(is_new)) then + -- log only if it's not a new dialog + table.insert(log, log_str.."Changed dialog name from \"".. + tostring(dialog.n_dialogs[d_id].d_name).."\" to \""..tostring(dialog_name).."\".") + end -- actually change the dialog name dialog.n_dialogs[d_id].d_name = dialog_name end @@ -353,6 +358,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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 @@ -410,7 +416,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam if(not(o_id)) then return nil end - table.insert(log, log_str.."Successfully created new option \""..tostring(o_id).."\".") + is_new = true end -- abbreviate that @@ -455,9 +461,15 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam -- we found the right result/effect that holds the (current) target_dialog if(r and r.r_type and r.r_type == "dialog") then if(not(r.r_value) or r.r_value ~= target_dialog) then - table.insert(log, log_str.."Changed target dialog from \"".. - tostring(r.r_value).."\" to \""..tostring(target_dialog).. - "\" for option \""..tostring(o_id).."\".") + 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_found = true r.r_value = target_dialog -- 2.47.3 From 16df2f9155ee981ac259467ce1eaa4e1d61fff6c Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 3 Jan 2025 22:03:14 +0100 Subject: [PATCH 35/56] fixed bug in get_dialog_list_for_export --- functions_dialogs.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 87aeb02..23850f7 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -99,7 +99,10 @@ yl_speak_up.get_dialog_list_for_export = function(dialog) 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.n_dialogs[a].d_sort < dialog.n_dialogs[b].d_sort end) + 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 -- 2.47.3 From 6370396feab2b004eac85d3288f866f3a1cc67f1 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 3 Jan 2025 22:25:53 +0100 Subject: [PATCH 36/56] o_random is part of a dialog - not of an option --- export_to_ink.lua | 8 ++++---- functions_dialogs.lua | 37 ++++++++++++++++--------------------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index 6b7b6af..efd5115 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -510,12 +510,12 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) -- what remains is to print the option/choice itself local o_text = o_data.o_text_when_prerequisites_met local o_prefix = "" - if( o_data.o_autoanswer) then - o_text = "[Automaticly selected if preconditions are met]" - o_prefix = "automaticly_" - elseif(o_data.o_random) then + 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 ink_export.print_choice(tmp, o_text, use_prefix, start_dialog, diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 23850f7..77cb17e 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -334,16 +334,13 @@ end -- If option_name starts with.. -- new_ create a new option (discard the rest of option_name) -- automaticly_ set o_autoanswer --- randomly_ set o_random +-- 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: The calling function also needs to take care to set *all* dialog options to --- *randomly selected* if at least *one* is set to this value. This cannot be done --- here as the other dialog options may not even be definied here yet. -- 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, @@ -499,27 +496,25 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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 + d_data.o_random = 1 + table.insert(log, log_str.."Changed DIALOG \""..tostring(o_id).."\" to RANDOMLY SELECTED.") + elseif(mode ~= 2 and d_data.o_random) then + d_data.o_random = nil + table.insert(log, log_str.."Changed DIALOG \""..tostring(o_id).."\" to RANDOMLY SELECTED.") + end + -- is this option selected automaticly if all preconditions are met? - if( mode == 1 and not(o_data.o_autoanswer)) then + 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.") - -- actually update the text - -- is this option selected randomly? - elseif(mode == 2 and not(o_data.o_random)) then - o_data.o_random = 1 - table.insert(log, log_str.."Changed option \""..tostring(o_id).."\" to RANDOMLY SELECTED.") - else -- mode is 0 - that means everything is normal for this option - if(o_data.o_autoanswer) then - o_data.o_autoanswer = nil - table.insert(log, log_str.."Removed AUTOMATICLY SELECTED from option \"".. - tostring(o_id).."\".") - end - if(o_data.o_random) then - o_data.o_random = nil - table.insert(log, log_str.."Removed RANDOMLY SELECTED from option \"".. - tostring(o_id).."\".") - end + elseif(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 -- 2.47.3 From 0d76f7492a8a121831cac35dda0d554bf7f1b084 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 4 Jan 2025 18:06:58 +0100 Subject: [PATCH 37/56] implemented update_dialog_options_completed for options that were not updated --- functions_dialogs.lua | 76 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 77cb17e..6572cf5 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -152,13 +152,16 @@ 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) -- 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).."]: " + log_str = log_str.." ["..tostring(dialog_name).."]:" + else + log_str = log_str..": " end local is_new = false if(not(d_id)) then @@ -174,32 +177,83 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) -- 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.") + 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 \"".. + 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(dialog.n_dialogs[d_id].d_name) - or(dialog.n_dialogs[d_id].d_name ~= dialog_name))) then + 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(dialog.n_dialogs[d_id].d_name).."\" to \""..tostring(dialog_name).."\".") + tostring(d_data.d_name).."\" to \""..tostring(dialog_name).."\".") end -- actually change the dialog name - dialog.n_dialogs[d_id].d_name = 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 + 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 return d_id 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.") + + -- TODO: this option may need a precondition that sets it to false (if that precondition doesn't already exist) + end + end + -- clean up the dialog + d_data.d_tmp_sorted_option_list = nil + d_data.d_tmp_sort_value = nil +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 @@ -528,9 +582,15 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam o_data.o_visit_only_once = nil end -- set sort order of options (no logging because that might get too spammy) - if(sort_oder) then + 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 -- 2.47.3 From 000978ef506a0ec10a37db647c5abe7c5ec8b89e Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 4 Jan 2025 18:14:48 +0100 Subject: [PATCH 38/56] try to perserve d_ dialog names in update_dialog --- functions_dialogs.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 6572cf5..7ccd3a7 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -165,9 +165,15 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) end local is_new = false if(not(d_id)) then + local next_id = nil + -- if dialog_name matches the d_ pattern but d_ 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 nil - so just add a new dialog - d_id = yl_speak_up.add_new_dialog(dialog, nil, nil, dialog_text) + -- 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) -- 2.47.3 From 51ab9e1ecb87db2894e5f65338a0f58b6bf672e3 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 4 Jan 2025 18:25:05 +0100 Subject: [PATCH 39/56] update_dialog now properly ignores special dialogs --- functions_dialogs.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 7ccd3a7..08df393 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -154,6 +154,11 @@ end -- (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 \""..tostring(dialog_name).."\" because it is a special dialog.") + return nil + 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 -- 2.47.3 From d2fdb6da6102d146c6f2d79b4aa9a86832dca1f6 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 4 Jan 2025 18:26:23 +0100 Subject: [PATCH 40/56] the dialog is randomly selected; not the option (typo) --- functions_dialogs.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 08df393..5bf2f0c 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -566,10 +566,10 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam -- is this option selected randomly? if( mode == 2 and not(d_data.o_random)) then d_data.o_random = 1 - table.insert(log, log_str.."Changed DIALOG \""..tostring(o_id).."\" to RANDOMLY SELECTED.") + table.insert(log, log_str.."Changed DIALOG \""..tostring(d_id).."\" to RANDOMLY SELECTED.") elseif(mode ~= 2 and d_data.o_random) then d_data.o_random = nil - table.insert(log, log_str.."Changed DIALOG \""..tostring(o_id).."\" to RANDOMLY SELECTED.") + table.insert(log, log_str.."Changed DIALOG \""..tostring(d_id).."\" to RANDOMLY SELECTED.") end -- is this option selected automaticly if all preconditions are met? -- 2.47.3 From 0cd28dcb5488dba1f8f8a5107f86e3a0c507aee3 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 4 Jan 2025 19:21:10 +0100 Subject: [PATCH 41/56] better loggin for update_dialog_option --- functions_dialogs.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 5bf2f0c..8eaeac1 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -525,6 +525,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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 \"".. @@ -536,7 +537,6 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam "\" for option \""..tostring(o_id).."\".") end -- actually change the target dialog - r_found = true r.r_value = target_dialog end -- the alternate_text may have been changed @@ -549,6 +549,10 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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) -- 2.47.3 From 7ce56ca733da570639cbbba248a65cc5b3ccd6c5 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sat, 4 Jan 2025 19:50:18 +0100 Subject: [PATCH 42/56] export to ink: add effect list to the action knot success option where it belongs --- export_to_ink.lua | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index efd5115..90261ba 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -160,7 +160,8 @@ end -- Parameter: -- a action 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) + 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) @@ -168,10 +169,11 @@ yl_speak_up.export_to_ink.print_action_knot = function(lines, use_prefix, d_id, 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", 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", use_prefix, start_dialog, a.alternate_text, a.a_on_failure, false, nil, @@ -488,6 +490,9 @@ 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] @@ -496,7 +501,8 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) target_dialog = ink_export.print_action_knot(tmp2, 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 @@ -504,8 +510,6 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) -- which preconditions can be translated to ink? local p_list = ink_export.translate_precondition_list(dialog, o_data.o_prerequisites, vars_used, use_prefix, dialog_names) - local e_list = ink_export.translate_effect_list(dialog, o_data.o_results, - vars_used) -- what remains is to print the option/choice itself local o_text = o_data.o_text_when_prerequisites_met @@ -517,6 +521,11 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) 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, o_text, use_prefix, start_dialog, alternate_text_on_success, target_dialog, -- 2.47.3 From 543c767dd56f75a4a68d7d3a2108f7b1625bb0f6 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 5 Jan 2025 11:31:30 +0100 Subject: [PATCH 43/56] added update_start_dialog for ink import --- fs/fs_export.lua | 2 +- functions_dialogs.lua | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/fs/fs_export.lua b/fs/fs_export.lua index ee7728d..8e4c675 100644 --- a/fs/fs_export.lua +++ b/fs/fs_export.lua @@ -230,7 +230,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".. diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 8eaeac1..f686506 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -264,6 +264,27 @@ yl_speak_up.update_dialog_options_completed = function(log, dialog, d_id) 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) + 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_sort) or d.d_sort == 0) then + -- all other dialogs are not *the* start dialog + d.d_sort = 1 + end + 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 -- 2.47.3 From 133a8ccf8d883f28333d390d7bbe3f268accb239 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 5 Jan 2025 11:46:59 +0100 Subject: [PATCH 44/56] allow to export to ink without _ prefix --- export_to_ink.lua | 8 ++++++-- fs/fs_export.lua | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index 90261ba..4ff70d9 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -378,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" @@ -396,7 +397,10 @@ yl_speak_up.export_to_ink_language = function(dialog, n_id) -- 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).."_" +-- 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 diff --git a/fs/fs_export.lua b/fs/fs_export.lua index 8e4c675..1a65c25 100644 --- a/fs/fs_export.lua +++ b/fs/fs_export.lua @@ -210,7 +210,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. ".. -- 2.47.3 From 7c801291f9816a634e7f21e930125cded8896e84 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 5 Jan 2025 20:29:47 +0100 Subject: [PATCH 45/56] update_dialog_option now can update d_trade and d_got_item dialogs (to a degree) --- functions_dialogs.lua | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index f686506..9d36bfa 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -157,7 +157,8 @@ 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 \""..tostring(dialog_name).."\" because it is a special dialog.") - return nil + -- 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) @@ -435,8 +436,20 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam -- 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 - -- the dialog does not exist - we cannot add an option to a nonexistant dialog - return nil + 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) -- 2.47.3 From d5b5832b029fc1cb2d5e5ae204e9d8226339c948 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Tue, 7 Jan 2025 20:02:23 +0100 Subject: [PATCH 46/56] fixed bug in update_dialog_option regarding automaticly selected and random options --- functions_dialogs.lua | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 9d36bfa..292627c 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -215,8 +215,8 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) -- 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 - d_data.o_random = nil + -- 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 @@ -603,11 +603,8 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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 - table.insert(log, log_str.."Changed DIALOG \""..tostring(d_id).."\" to RANDOMLY SELECTED.") - elseif(mode ~= 2 and d_data.o_random) then - d_data.o_random = nil - table.insert(log, log_str.."Changed DIALOG \""..tostring(d_id).."\" to RANDOMLY SELECTED.") end -- is this option selected automaticly if all preconditions are met? @@ -615,7 +612,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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(o_data.o_autoanswer) then + 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 -- 2.47.3 From 8e0f53fdb51e9716ff65c51f7fccd75bc368ec85 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Tue, 7 Jan 2025 21:04:37 +0100 Subject: [PATCH 47/56] added import_from_ink.lua - but no input for that yet --- export_to_ink.lua | 2 +- functions_dialogs.lua | 2 +- import_from_ink.lua | 1032 +++++++++++++++++++++++++++++++++++++++++ init.lua | 2 + 4 files changed, 1036 insertions(+), 2 deletions(-) create mode 100644 import_from_ink.lua diff --git a/export_to_ink.lua b/export_to_ink.lua index 4ff70d9..09caebf 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -410,7 +410,7 @@ yl_speak_up.export_to_ink_language = function(dialog, use_prefix) local tmp = {"-> ", main_loop, "\n=== ", main_loop, " ===", "\nWhat do you wish to do?", - "\n+ Talk to ", tostring(dialog.n_npc), " -> ", use_prefix..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) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 292627c..b42df08 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -156,7 +156,7 @@ end 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 \""..tostring(dialog_name).."\" because it is a special dialog.") + 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 diff --git a/import_from_ink.lua b/import_from_ink.lua new file mode 100644 index 0000000..c5ba066 --- /dev/null +++ b/import_from_ink.lua @@ -0,0 +1,1032 @@ +-- create the table for the functions: +yl_speak_up.parse_ink = {} +-- add a local abbreviation: +local parse_ink = yl_speak_up.parse_ink + +-- remove leading and tailing blanks, spaces, tabs, newlines and equal signs from names +-- of knots and stitches; +-- helper function for the parser +parse_ink.strip_name = function(s) + local CHAR_EQUAL = string.byte("=", 1) + local CHAR_BLANK = string.byte(" ", 1) + local CHAR_TAB = string.byte("\t", 1) + local CHAR_NEWLINE = string.byte('\n', 1) + local i = 1 + local k = string.len(s) + local b = string.byte(s, i) + while(i < k and (b==CHAR_EQUAL or b==CHAR_BLANK or b==CHAR_TAB or b==CHAR_NEWLINE)) do + i = i + 1 + b = string.byte(s, i) + end + b = string.byte(s, k) + while(k > i and (b==CHAR_EQUAL or b==CHAR_BLANK or b==CHAR_TAB or b==CHAR_NEWLINE)) do + k = k - 1 + b = string.byte(s, k) + end + return string.sub(s, i, k) +end + + +-- the actual parser for ink (more or less) +parse_ink.parse = function(text, print_debug) + -- just an abbreviation + local strip_name = parse_ink.strip_name + -- this is what this function shall find: + local knots = {} + + -- first: scan for things that have to start at the *beginning* of a line; + -- this limitation may or may not be part of ink (hard to tell), but it does + -- make parsing a lot easier while not imposing an undue limitation on writers + + -- optional whitespaces/tabs are allowed; + local CHAR_NEWLINE = string.byte('\n', 1) + -- whitespace characters + local CHAR_BLANK = string.byte(" ", 1) + local CHAR_TAB = string.byte("\t", 1) + -- allow to escape things at the start of a line + local CHAR_ESCAPE = string.byte("\\", 1) + + -- covers "* " choices and "*/" end of multiline comment + local CHAR_STAR = string.byte("*", 1) + local CHAR_SLASH = string.byte("/", 1) + local CHAR_PLUS = string.byte("+", 1) + local CHAR_MINUS = string.byte("-", 1) + -- for diverts + local CHAR_GREATER = string.byte(">", 1) + -- for text that is gluecd together + local CHAR_SMALLER = string.byte("<", 1) + -- == for knots; = for stitches + local CHAR_EQUAL = string.byte("=", 1) + -- for code: + local CHAR_TILDE = string.byte("~", 1) + -- for INCLUDE + local CHAR_I = string.byte("I", 1) + -- for VAR + local CHAR_V = string.byte("V", 1) + -- for CONST + local CHAR_C = string.byte("C", 1) + + -- detect labels at the start of choices and gathers + local CHAR_LABEL_START = string.byte("(", 1) + local CHAR_LABEL_END = string.byte(")", 1) + + -- this is used for a lot of things: + -- * printing values for variables + -- * checking conditions + -- * if/else and switch statements - which may even cover multiple lines + -- * alternatives, cycles, shuffles etc. + local CHAR_COND_START = string.byte("{", 1) + local CHAR_COND_END = string.byte("}", 1) + + local i = 1 + local i_max = string.len(text) + local at_line_start = false + -- TODO: turn that into constants? + local what = "" + local last_what = "" + local multiline_comment = false + local inline_comment = false + -- knots and stitches have names + local search_for_name = nil + local name_found = nil + local search_for_nesting = nil + -- choices and gathers can be nested + local nested = 0 + local last_nested = 0 + local search_for_label = nil + -- choices and gathers can have labels + local label_start = -1 + local label_found = nil + -- choices can have conditions + local search_for_condition = nil + local cond_start = -1 + local conditions = {} + -- the actual text/content of a choice or gather + local content = "" + -- we start with text (usually a divert to the main knot) + local content_start = 1 + -- the main program text...not a knot at the start + local content_type = "MAIN_PROGRAM" + local last_content_type = content_type + -- we need to know if we're inside a mutiline condition or alternative + local counted_curly_brackets = 0 + + local started_at = 1 -- used for printing out the line for debugging + while(i < i_max) do + local b = string.byte(text, i) + if( b == CHAR_NEWLINE) then + -- this is text (from node, stitch, gather, choice) that spans more than + -- one line + if(not(multiline_comment) + and not(inline_comment)) then + content = content..string.sub(text, content_start, i) + end + if(search_for_name) then + name_found = content + content = "" + search_for_name = false + end + -- the content of this line may become relevant + content_start = i + 1 + + at_line_start = true + + + if(print_debug) then + print("|LINE| "..string.sub(text, started_at, i - 1)) + end + if(what == "START_OF_MULTILINE_COMMENT") then + what = "INSIDE_MULTILINE_COMMENT" + else + what = "" + end + + started_at = i + 1 + -- inline comments end here + inline_comment = false + search_for_nesting = nil + search_for_label = nil + search_for_condition = nil + search_for_name = nil + elseif(multiline_comment) then + -- nothing can start inside a multilne comment apart from it ending + -- (we only allow multiline comments to start and end in their own lines, + -- not inside other texts or structures) + if(b == CHAR_STAR and string.byte(text, i + 1) == CHAR_SLASH) then + what = "END_OF_MULTILINE_COMMENT" + multiline_comment = false + -- just to be sure that the rest of the line is really skipped + inline_comment = true + at_line_start = false + end + -- TODO: handle inline comments + elseif(not(at_line_start)) then + -- choices and gathers can be nested + if(search_for_nesting) then + -- a divert -> is following a gather - + if((b == CHAR_MINUS and string.byte(text, i + 1) == CHAR_GREATER) + -- if escaped: no point searching for label or conditions + or b == CHAR_ESCAPE) then + search_for_nesting = nil + search_for_label = nil + search_for_condition = nil + elseif(b == search_for_nesting) then + nested = nested + 1 + elseif(b ~= CHAR_BLANK and b ~= CHAR_TAB) then + search_for_nesting = nil + -- choices (* and +) and gathers (-) may be followed by a (label): + if(b == CHAR_LABEL_START) then + label_start = i + 1 + search_for_label = CHAR_LABEL_END + -- if no label: did we find the start of a condition already? + elseif(b == CHAR_COND_START) + and (what == "NORMAL_CHOICE" or what == "STICKY_CHOICE") then + cond_start = i + 1 + search_for_condition = CHAR_COND_END + else + content_start = i + content = "" + end + end + elseif(search_for_label and search_for_label == CHAR_LABEL_END) then + if(b == CHAR_LABEL_END) then + label = string.sub(text, label_start, i-1) + search_for_label = nil + if(what == "NORMAL_CHOICE" or what == "STICKY_CHOICE") then + search_for_condition = CHAR_COND_START + end + content_start = i + 1 + content = "" + end + + -- choices can have multiple conditions (at least they are not multiline) + elseif(search_for_condition and search_for_condition == CHAR_COND_START) then + if(b == CHAR_COND_START) then + cond_start = i + 1 + search_for_condition = CHAR_COND_END + elseif(b ~= CHAR_BLANK and b ~= CHAR_TAB) then + -- no condition found + cond_start = -1 + search_for_condition = nil + -- next we get the actual text of the choice or gather + content_start = i + content = "" + end + elseif(search_for_condition and search_for_condition == CHAR_COND_END) then + if(b == CHAR_COND_END) then + table.insert(conditions, string.sub(text, cond_start, i-1)) + cond_start = -1 + -- there may be more conditions comming + search_for_condition = CHAR_COND_START + end + end + + -- not at line start: multiline alternatives or conditions + if(b == CHAR_COND_START and string.byte(text, i - 1) ~= CHAR_ESCAPE) then + -- one more open + counted_curly_brackets = counted_curly_brackets + 1 + elseif(b == CHAR_COND_END and string.byte(text, i - 1) ~= CHAR_ESCAPE) then + counted_curly_brackets = counted_curly_brackets - 1 + end + + -- nothing to do; we read until the end of the line + -- TODO: there are some inline things we need to check for as well + -- TODO: inline commends need to be processed here + elseif(b == CHAR_BLANK or b == CHAR_TAB) then + -- nothing to do; real start of the line not yet found + at_line_start = true + -- blanks at the beginning are usually just identation for better + -- readability of weaved content - best ignore these + if(not(multiline_comment) + and not(inline_comment) + and content_start > -1 + and content_start < i + 1) then + content_start = i + 1 + end + + elseif(at_line_start) then + last_what = what + last_nested = nested + nested = 1 + + -- "\" (escape the next character) + if( b == CHAR_ESCAPE) then + -- the line can no longer start with a special char; it is text + what = "TEXT" + -- at line start: multiline alternatives or conditions + elseif(b == CHAR_COND_START and string.byte(text, i - 1) ~= CHAR_ESCAPE) then + -- one more open + counted_curly_brackets = counted_curly_brackets + 1 + what = "TEXT" + elseif(b == CHAR_COND_END and string.byte(text, i - 1) ~= CHAR_ESCAPE) then + counted_curly_brackets = counted_curly_brackets - 1 + what = "TEXT" + -- "//" (start of inline comment) or "/* " (start of multiline comment) + elseif(b == CHAR_SLASH) then + local b2 = string.byte(text, i + 1) + if( b2 == CHAR_SLASH) then + what = "COMMENT" -- inline comment + -- remember that we are inside a comment + inline_comment = true + -- the text of the comment is not part of the content + content_start = -1 + elseif(b2 == CHAR_STAR) then + what = "START_OF_MULTILINE_COMMENT" + -- remember that we are inside a multiline comment + multiline_comment = true + -- the text of the multiline comment is not part of the content + content_start = -1 + else + what = "ERROR:_UNKNOWN_SYMBOL_FOLLOWING_\"/\"" + end + -- "*" (choice) - no matter what follows - it's a choice + elseif(b == CHAR_STAR) then + if(counted_curly_brackets > 0) then + -- we are inside a condition or alternate + -- normally choices would be allowed here in ink - but that'd be + -- too complicated + what = "TEXT" + else + what = "NORMAL_CHOICE" + nested = 1 + search_for_nesting = b + end + -- "+" (sticky choice) - no mater what follows - it's a sticky choice + elseif(b == CHAR_PLUS) then + if(counted_curly_brackets > 0) then + -- we are inside a condition or alternate + -- normally choices would be allowed here in ink - but that'd be + -- too complicated + what = "TEXT" + else + what = "STICKY_CHOICE" + nested = 1 + search_for_nesting = b + end + -- "-" (gather) or "->" divert + elseif(b == CHAR_MINUS) then + local b2 = string.byte(text, i + 1) + if(b2 == CHAR_GREATER) then + what = "TEXT" --DIVERT" + elseif(counted_curly_brackets > 0) then + -- we are inside a condition or alternate + what = "TEXT" + else + -- TODO: we might be inside a multiline if/else/switch branch + what = "GATHER" + nested = 1 + search_for_nesting = b + end + -- "==" (knot) or "= " (stitch) + elseif(b == CHAR_EQUAL) then + local b2 = string.byte(text, i + 1) + if( b2 == CHAR_EQUAL) then + what = "KNOT" + search_for_name = true + else + what = "STITCH" + search_for_name = true + end + -- "<>" (glue) apart from that it's a normal text line + elseif(b == CHAR_SMALLER) then + local b2 = string.byte(text, i + 1) + if( b2 == CHAR_GREATER) then + what = "TEXT_GLUE" + end + -- "~ " (code/calculation) + elseif(b == CHAR_TILDE) then + local b2 = string.byte(text, i + 1) + what = "CODE" + -- "INCLUDE " (code/calculation) + elseif(b == CHAR_I) then + local s = string.sub(text, i, i + 7) + if( s == "INCLUDE ") then + what = "INCLUDE" + else + -- else it is just text starting with a capital I + what = "TEXT" + end + -- "VAR " (variable definition) + elseif(b == CHAR_V) then + local s = string.sub(text, i, i + 3) + if( s == "VAR ") then + what = "VAR" + else + -- else it is just text starting with a capital V + what = "TEXT" + end + -- "CONST " (variable definition) + elseif(b == CHAR_C) then + local s = string.sub(text, i, i + 5) + if( s == "CONST ") then + what = "CONST" + else + -- else it is just text starting with a capital V + what = "TEXT" + end + else + what = "TEXT" + end + -- we have processed the start of the line and found a type + at_line_start = false + + -- observation: choices end at the end of a line + if(what == "TEXT" + and (content_type == "NORMAL_CHOICE" or content_type == "STICKY_CHOICE")) then + what = "WEAVE" + end + -- the collected content of the last knot, stitch, gather or choice ends + -- TODO: handle the situation of the last line + if( what == "NORMAL_CHOICE" + or what == "STICKY_CHOICE" + or what == "MAIN_PROGRAM" + or what == "GATHER" + or what == "KNOT" + or what == "STITCH" + or what == "WEAVE") then + -- there may have been content in the last line + if(content_start > -1) then + content = content..string.sub(text, content_start, i - 1) + end + -- remove tailing newlines and blanks + if(content) then + content = strip_name(content) + end + local divert_to = "" + if(content_type == "NORMAL_CHOICE" or content_type == "STICKY_CHOICE") then + local start_1, start = string.find(content, "->") + if(start) then + start = start + 1 + while(start < string.len(content) + and string.byte(content, start) == CHAR_BLANK) do + start = start + 1 + end + local ende = start + 1 + while(ende < string.len(content) + and string.byte(content, ende) ~= CHAR_BLANK + and string.byte(content, ende) ~= CHAR_NEWLINE) do + ende = ende + 1 + end + divert_to = strip_name(string.sub(content, start, ende)) + content = strip_name(string.sub(content, 1, start_1 - 1)) + end + end + + local knot = {} + knot.content_type = content_type + knot.name = strip_name(name_found or "") + knot.label = label + knot.nested = last_nested + knot.conditions = conditions + knot.text = content + knot.divert_to = divert_to + table.insert(knots, knot) + + if(not(print_debug)) then + name_found = nil + else + local s = " FOUND: "..content_type + if(name_found) then + -- name of a knot or stitch + -- TODO: strip leading and tailing blanks, tabs and = from found_name + s = s.." Name: "..tostring(knot.name) + name_found = nil + end + if(last_nested and last_nested > 0) then + s = s.."\n NESTED: "..tostring(last_nested) + end + if(label) then + s = s.."\n LABEL: ["..tostring(label).."]" + end + if(conditions and #conditions > 0) then + s = s.."\n COND: ["..table.concat(conditions, "] [").."]" + end + if(divert_to and divert_to ~= "") then + s = s.."\n TARGET: ["..tostring(divert_to).."]" + end + if(content and content ~= "") then + s = s.."\n STR: ["..tostring(content).."]:STR\n" + end + print(s) + end + + last_content_type = content_type + content_type = what + -- the old content ends + content_start = i + content = "" + + label = nil + conditions = {} + counted_curly_brackets = 0 + end + end + -- continue with the next charcter + i = i + 1 + end + return knots +end + + + +-- TODO: intended for future parsing of ink inline functionality +parse_ink.inspect_inline = function(text) + + local CHAR_INLINE_START = string.byte("{", 1) + local CHAR_INLINE_END = string.byte("}", 1) + local CHAR_ESCAPE = string.byte("\\", 1) + local CHAR_SEPERATOR = string.byte("|", 1) + local CHAR_ML_SEPERATOR = string.byte("-", 1) + local CHAR_DOPPELPUNKT = string.byte(":", 1) + local CHAR_NEWLINE = string.byte('\n', 1) + local CHAR_BLANK = string.byte(" ", 1) + local CHAR_TAB = string.byte("\t", 1) + local CHAR_CYCLE = string.byte("&", 1) + local CHAR_ONCE_ONLY = string.byte("!", 1) + local CHAR_SHUFFLE = string.byte("~", 1) + + local inline_starts = {} + local sep_starts = {} + local is_multiline = {} + local level = 0 + local i = 1 + local i_max = string.len(text) + while(i < i_max) do + local b = string.byte(text, i) + if(at_line_start and level > 0 and is_multiline[level] + and (b ~= CHAR_ML_SEPERATOR and b ~= CHAR_BLANK and b ~= CHAR_TAB)) then + at_line_start = false + end + if(string.byte(text, i-1) == CHAR_ESCAPE) then + -- do nothing; escape sign + elseif( b == CHAR_INLINE_START) then + table.insert(inline_starts, i) + table.insert(sep_starts, {i}) + table.insert(is_multiline, false) + level = level + 1 + elseif(level == 0) then + -- not inside an inline expression; do nothing + elseif(b == CHAR_NEWLINE) then + at_line_start = true + is_multiline[level] = true + elseif(b == CHAR_ML_SEPERATOR and is_multiline[level] and at_line_start) then +-- if(#sep_starts[level] == 1 and string.byte(text, sep_starts[level][1] == TODO) + table.insert(sep_starts[level], i) + elseif(b == CHAR_SEPERATOR and not(is_multiline[level])) then + table.insert(sep_starts[level], i) + elseif(b == CHAR_DOPPELPUNKT and #sep_starts[level] == 1) then + print("Condition: "..string.sub(text, sep_starts[level][1] + 1, i)) + -- up until now we had the condition + sep_starts[level][1] = i + elseif(b == CHAR_INLINE_END) then + local prefix = "" + for j = 1, level do + prefix = " "..prefix + end + local t = prefix..string.sub(text, inline_starts[level], i) + if(is_multiline[level]) then + print("MULTIL: "..tostring(t)) + else + print("INLINE: "..tostring(t)) + end + + table.insert(sep_starts[level], i) + local max = #sep_starts[level] + for c, pos in ipairs(sep_starts[level]) do + if(c < max) then + local t2 = prefix.." "..tostring(c)..") ".. string.sub(text, + pos + 1, sep_starts[level][c+1] - 1) + print(t2) + end + end + table.remove(sep_starts, level) + table.remove(is_multiline, level) + + local b2 = string.byte(text, inline_starts[level] + 1) +-- if(b2 == CHAR_CYCLE) then + +-- -- get the inline part +-- j = inline_starts[level] + 1 +-- -- remove leading blanks +-- while(j < i and string.byte(text, j) == CHAR_BLANK) do +-- j = j + 1 +-- end +-- -- remove tailing blanks +-- k = i +-- while(k > j and string.byte(text, j) == CHAR_BLANK) do +-- k = k - 1 +-- end +-- + table.remove(inline_starts, level) + level = level - 1 + end + i = i + 1 + end +end + + +-- analyze the intermediate knot data strutcture parse_ink.parse created: + +-- log to a table (debug_level > 0) and additionally to stdout (debug_level > 1) +parse_ink.print_debug = function(log_level, log, text) + if(not(log_level) or log_level < 1) then + return + end + if(log_level > 1) then + print(text) + end + if(not(log) or log_level <= 1) then + return + end + table.insert(log, text) +end + + +-- actions and effects are knots in ink but only part of the dialog in NPC dialogs; +-- find out which of their option knots serves which purpose +parse_ink.analyze_actions_or_effects = function(knot_list, what_to_analyze, cap_name, log_level, log) + parse_ink.print_debug(log_level, log, "\nAnalyzing "..what_to_analyze.."s:") + local success_str = "["..cap_name.." was successful]" + local failure_str = "["..cap_name.." failed]" + + for knot_name, knot in pairs(knot_list) do + parse_ink.print_debug(log_level, log, " Found "..tostring(what_to_analyze)..": "..tostring(knot_name)) + -- examine options (we are mostly intrested in their links) + for o_name, o_knot in pairs(knot.options) do + if(o_knot.text and o_knot.text == success_str) then + knot_list[knot_name].on_success = o_knot.divert_to + elseif(o_knot.text and o_knot.text == failure_str) then + knot_list[knot_name].on_failure = o_knot.divert_to + elseif(o_knot.text and o_knot.text == "[Back]") then + knot_list[knot_name].back = o_knot.divert_to + else + -- log that error regardless of log_level + parse_ink.print_debug(2, log, + "ERROR: Unsupported text \""..tostring(o_knot.text).. + "\" for option \""..tostring(o_name).. + "\" of "..what_to_analyze.." in knot name \"".. + tostring(knot_name).."\".") + end +-- print("o_knot "..tostring(o_name).." target: "..tostring(o_knot.divert_to or "- none -")) + end + end +end + + +-- this serarches for the *real* target dialog; +-- it populates option_knot[option_name].actions and option_knot.effects with the +-- actions and effects it encountered +-- (there had to be extra knots inserted for actions and effects - which are no dialogs in NPC +-- dialog sense but are required as ink knots so that divert_to can be used in a sensible way; +-- after all actions and effects can fail) +parse_ink.find_real_target_dialog = function(option_knot, target, prefix, start_dialog, dialogs, actions, effects, log_level, log) +-- print(" option_knot: "..tostring(option_knot).." Next link: "..tostring(target)) + -- go back to the start + if(not(target) or target == "" or target == start_dialog) then + return start_dialog + -- we found the end of the conversation - or another dialog + elseif(target == "END" or dialogs[target]) then + return target + -- we found a special dialog + elseif(yl_speak_up.is_special_dialog(parse_ink.strip_prefix(target, prefix))) then + return parse_ink.strip_prefix(target, prefix) + -- we found an action + elseif(actions[target]) then + -- avoid loops by restricting the number of actions visited + if(#option_knot.actions > 0) then + parse_ink.print_debug(2, log, + "WARNING: Aborting finding real target dialog. ".. + "Only one action allowed per option. Using start dialog instead.") + return start_dialog + end + -- remember that we visited this action + table.insert(option_knot.actions, target) + -- recursively find the option that corresponds to the success option + return parse_ink.find_real_target_dialog(option_knot, actions[target].on_success, prefix, start_dialog, dialogs, actions, effects, log_level, log) + -- we found an effect + elseif(effects[target]) then + -- avoid loops by restricting the number of effects visited + if(#option_knot.effects > 20) then + parse_ink.print_debug(2, log, + "WARNING: Aborting finding real target dialog. ".. + "Too many effects chained. Using start dialog instead.") + return start_dialog + end + -- remember that we visited this efffect + table.insert(option_knot.effects, target) + -- recursively find the option that corresponds to the success option + return parse_ink.find_real_target_dialog(option_knot, effects[target].on_success, prefix, start_dialog, dialogs, actions, effects, log_level, log) + -- the name was not found + else + parse_ink.print_debug(2, log, + "WARNING: Aborting finding real target dialog. ".. + "Could not find target dialog \""..tostring(target).."\". Using start dialog.") + return start_dialog + end +end + + +parse_ink.print_knots = function(knots) + -- debug output + for i, knot in ipairs(knots) do + -- TODO: knot.conditions + print("Knot: "..tostring(knot.content_type).." ["..tostring(knot.nested).."] ".. + tostring(knot.name).." ("..tostring(knot.label)..")".. + "\n Text: "..tostring(knot.text)) + end +end + + +-- analyzes all knots and populates the lists knot_data, dialogs, actions and effects +-- with the names of the knots +parse_ink.analyze_knots_by_type = function(knots, log_level, log) + -- start interpreting the meaning of the knots + local knot_data = {} + local dialogs = {} + local actions = {} + local effects = {} + local start_knot = nil + local last_knot_name = nil + local dialog_list = {} + parse_ink.print_debug(log_level, log, "\nIdentifying dialogs, actions, effects and options:") + for i, knot in ipairs(knots) do + if(knot.content_type == "KNOT") then + if(knot_data[knot_name]) then + parse_ink.print_debug(2, log, + "WARNING: Knot "..tostring(knot_name).." already defined.\n".. + "Using this new data and discarding old.") + end + knot_data[knot.name] = knot + knot_data[knot.name].options = {} + knot_data[knot.name].option_list = {} + last_knot_name = knot.name + -- actions and effects are just simulated as knots - they do not represent + -- any actual dialogs in the NPC sense + if(string.sub(knot.text, 1, 9) == ":action: ") then + actions[knot.name] = knot + parse_ink.print_debug(log_level, log, "ACTION: "..tostring(knot.name).."\n") + elseif(string.sub(knot.text, 1, 9) == ":effect: ") then + effects[knot.name] = knot + parse_ink.print_debug(log_level, log, "EFFECT: "..tostring(knot.name).."\n") + else + dialogs[knot.name] = knot + -- remember the sort order of the options + parse_ink.print_debug(log_level, log, "=== "..tostring(knot.name).." ===\n") + -- keep the order of appearance of the dialog names + table.insert(dialog_list, knot.name) + end + elseif(last_knot_name + and (knot.content_type == "NORMAL_CHOICE" or knot.content_type == "STICKY_CHOICE")) then + -- we need a label for the option so that it can be referenced correctly + -- between ink and NPC dialogs; if none is provided (new dialog), then we'll + -- create a temporary name (there may also be grey_out_ options); + -- [Farewell!] is also still included + if(not(knot.label) or knot.label == "") then + knot.label = "new_"..tostring(i) + end + knot_data[last_knot_name].options[knot.label] = knot + table.insert(knot_data[last_knot_name].option_list, knot.label) + parse_ink.print_debug(log_level, log, "- "..tostring(knot.label or "ERROR")..": "..knot.text) + elseif(knot.content_type == "WEAVE" + -- this is not really a WEAVE - it's the effects list and divert of an option + and i > 1 + and (knots[i-1].content_type == "NORMAL_CHOICE" + or knots[i-1].content_type == "STICKY_CHOICE") + and (knots[i-1].divert_to + or knots[i-1].divert_to == "")) then + local p = string.find(knot.text or "", "-> ") + if(p) then + knots[i-1].divert_to = string.sub(knot.text or "", p + 3) + if(p > 4) then + knots[i-1].effect_list = string.sub(knot.text, 1, p - 2) + else + knots[i-1].effect_list = "" + parse_ink.print_debug(2, log, + "ERROR: WEAVE with no text apart from divert: [".. + tostring(knot.text).."]") + end + elseif(knot.text) then + knots[i-1].text2 = knot.text + end + elseif(knot.content_type == "MAIN_PROGRAM") then + start_knot = knot + else + -- TODO "GATHER" + -- TODO "STITCH" + -- TODO "WEAVE" (real ones) + parse_ink.print_debug(2, log, + "ERROR: Cannot handle knots of type \""..tostring(knot.content_type).."\" yet.") + end + end + + return {knot_data = knot_data, dialogs = dialogs, actions = actions, effects = effects, start_knot = start_knot, dialog_list = dialog_list} +end + + + +parse_ink.find_dialog_name_prefix = function(start_knot, log_level, log) + -- the ink program has to start with "-> PREFIXd_end" (with PREFIX usually beeing something + -- like n_, so i.e. "-> n_105_d_end" might be a valid first line + -- Export to ink supports prefixes. + -- Prefixes are particulary useful if you want to combine the texts of multiple NPC in one + -- ink program for testing. + local prefix = "" + if(not(start_knot) or not(start_knot.text)) then + parse_ink.print_debug(2, log, + "ERROR: MAIN_PROGRAM/divert_to start knot not found. Please start your ink file ".. + "with a divert to the main dialog!") + else + prefix = start_knot.text + end + if(string.sub(prefix, string.len(prefix) - 4) ~= "d_end" + or string.sub(prefix, 1, 3) ~= "-> ") then + parse_ink.print_debug(2, log, + "ERROR: Your MAIN_PROGRAM/divert_to ought to contain a prefix (usually n__) ".. + ", followed by d_end. Example: \"-> n_105_d_end\". You used: \"".. + tostring(prefix).."\".") + else + prefix = string.sub(prefix, 4, string.len(prefix) - 5) + end + parse_ink.print_debug(log_level, log, "\nUsing prefix: "..tostring(prefix)) + return prefix +end + + +parse_ink.find_start_dialog_name = function(prefix, dialogs, log_level, log) + -- find out the real start dialog + local start_dialog = prefix.."d_end" + if(dialogs[start_dialog]) then + for o_name, o_knot in pairs(dialogs[start_dialog].options) do + -- the last option that diverts to anything else than END is what we're looking for + if(o_knot.divert_to and o_knot.divert_to ~= "END") then + start_dialog = o_knot.divert_to + end + end + end + parse_ink.print_debug(log_level, log, "\nUsing start dialog: "..tostring(start_dialog)) + return start_dialog +end + + +-- options link to *actions* and *effects* - because that is necessary to simulate +-- NPC behaviour in inc. But for importing that data, we need the real target link +-- that happens when all actions and effects were successful. +parse_ink.adjust_links = function(dialogs, actions, effects, start_dialog, prefix, log_level, log) + -- determine on_success, on_failure and back links for actions and effects + parse_ink.analyze_actions_or_effects(actions, "action", "Action", log_level, log) + parse_ink.analyze_actions_or_effects(effects, "effect", "Effect", log_level, log) + + parse_ink.print_debug(log_level, log, + "\nAssigning actions and effects to their options and adjusting those options' target dialog:") + -- now check if all options are linked properly; for this we check dialogs only + -- (actions and effects are referenced through their options) + for d_name, dialog_knot in pairs(dialogs) do + -- only the options have links + for o_name, o_knot in pairs(dialog_knot.options) do + -- actions that belong to this dialog + o_knot.actions = {} + -- effects that belong to this dialog + o_knot.effects = {} + local old_target = o_knot.divert_to + -- this function also fills the .actions and .effects table with any actions + -- and effects it finds on its way while following the successful option of + -- each action and effect encountered + local target = parse_ink.find_real_target_dialog(o_knot, o_knot.divert_to, prefix, start_dialog, dialogs, actions, effects, log_level, log) + if(target ~= o_knot.divert_to) then + parse_ink.print_debug(log_level, log, + "INFO: Changing target dialog for option \""..tostring(o_name).. + "\" of dialog "..tostring(d_name).. + "\n from "..tostring(o_knot.divert_to).. + "\n to: "..tostring(target).. + "\n Actions: ".. + table.concat(o_knot.actions, ", ").. + "\n Effects: ".. + table.concat(o_knot.effects, ", ")) + o_knot.divert_to = target + end + end + end +end + + +parse_ink.strip_prefix = function(text, prefix) + local i = string.len(prefix) + if(string.sub(text or "", 1, i) == prefix) then + return string.sub(text, i+1) + end + return text +end + + +-- strip "[" and "]" enclosing text +parse_ink.strip_brackets = function(text) + if(not(text)) then + return + end + local p1 = 1 + if(string.sub(text, 1, 1) == "[") then + p1 = 2 + end + local p2 = string.len(text) + -- remove tailing "]" (needed for ink) + if(string.sub(text, string.len(text)) == "]") then + p2 = p2 - 1 + end + return string.sub(text, p1, p2) +end + + + +-- actually doing the import +parse_ink.import_dialogs = function(dialog, dialogs, actions, effects, start_dialog, prefix, orig_dialog_list, log_level, log) + parse_ink.print_debug(log_level, log, "\n\nStarting dialog import:") + + local dialog_list = {} + -- remove the extra wrapper dialog (added for ink) from the list + for i, d_name in ipairs(orig_dialog_list) do + if(d_name ~= prefix.."d_end") then + table.insert(dialog_list, d_name) + end + end + -- we need to add the dialogs as such first so that target_dialog will work + for i, d_name in ipairs(dialog_list) do + local dialog_knot = dialogs[d_name] + local dialog_name = parse_ink.strip_prefix(d_name, prefix) + + local d_id = yl_speak_up.update_dialog(log, dialog, dialog_name, dialog_knot.text) + dialog_knot.d_id = d_id + end + + -- now we can add the options + for i, d_name in ipairs(dialog_list) do + local dialog_knot = dialogs[d_name] + local dialog_name = parse_ink.strip_prefix(d_name, prefix) + local d_id = dialog_knot.d_id + + -- identify and remove options that are grey_out_ texts + -- and store them in table greyed_out + local greyed_out = {} + local tmp_option_list = {} + for i, o_name in ipairs(dialog_knot.option_list) do + if(string.sub(o_name, 1, 9) == "grey_out_") then + local o_id = string.sub(o_name, 10) + greyed_out[o_id] = parse_ink.strip_brackets(dialog_knot.options[o_name].text) + else + table.insert(tmp_option_list, o_name) + end + end + + -- o_random belongs to the dialog; if one option is randomly, then the entire dialog is + local is_random = false + for i, o_name in ipairs(tmp_option_list) do + if(string.sub(o_name, 1, 9) == "randomly_") then + is_random = true + end + end + -- we now have all information to decude about the dialogs' o_random + if(is_random and not(dialog.n_dialogs[d_id].o_random)) then + parse_ink.print_debug(log_level, log, "Changed DIALOG \""..tostring(d_id).."\" to RANDOMLY SELECTED.") + dialog.n_dialogs[d_id].o_random = 1 + elseif(not(is_random) + and dialog.n_dialogs[d_id] + and dialog.n_dialogs[d_id].o_random) then + parse_ink.print_debug(log_level, log, "Changed DIALOG \""..tostring(d_id).."\" back to normal (was: RANDOMLY SELECTED).") + dialog.n_dialogs[d_id].o_random = nil + end + + + for i, o_name in ipairs(tmp_option_list) do + local o_knot = dialog_knot.options[o_name] + local option_text = o_knot.text + local visit_only_once = (o_knot.content_type == "NORMAL_CHOICE") + local alternate_text = nil + local target_dialog = o_knot.divert_to + target_dialog = parse_ink.strip_prefix(target_dialog, prefix) + + -- remove leading "[" (needed for ink but not for dialogs) + option_text = parse_ink.strip_brackets(option_text) + +-- if(o_knot.effect_list and o_knot.effect_list ~= "") then +-- print("EFFECT LIST: "..tostring(d_id).." "..tostring(o_name).." "..tostring(o_knot.effect_list)) +-- end + -- extract the alternate text + local p = string.find(o_knot.effect_list or "", "\n#") + if(p and p > 1 and string.sub(o_knot.effect_list, 1, 1) ~= "#") then + alternate_text = string.sub(o_knot.effect_list or "", 1, p) + else + -- TODO: the rest of this is the effect list as comment + p = 1 + end + -- ignore the automaticly added Farewell!-option + if(option_text ~= "Farewell!" or target_dialog == prefix.."d_end") then + local o_id = yl_speak_up.update_dialog_option(log, dialog, d_id, o_name, + option_text, greyed_out[o_name], target_dialog, + alternate_text, visit_only_once, + -- make sure o_sort gets set approprately: + i) + end + -- TODO: deal with preconditions + -- TODO: deal with actions + -- TODO: deal with effects + -- TODO: effects are not parsed yet + end + -- the dialog may already contain further options that are no longer needed; + -- those need o_sort set and a false precondition; update_dialog did prepare the + -- options already for this, and this function here does the necessary cleanup: + yl_speak_up.update_dialog_options_completed(log, dialog, dialog_knot.d_id) + end + + -- make sure the right start dialog is set + yl_speak_up.update_start_dialog(log, dialog, parse_ink.strip_prefix(start_dialog, prefix)) + + return dialog +end + + + + + + +-- helper function - yl_speak_up includes this, but we here not +-- TODO: use the same for export +yl_speak_up.show_effect = function(r) + local text = r.r_type or "ERROR: no r.r_type! " + for k, v in pairs(r or {}) do + if(k == "r_craft_grid") then + text = text.." "..tostring(k)..": "..table.concat(v or {}, ",") + elseif(k ~= "r_id" and k ~= "r_type") then + text = text.." "..tostring(k)..": "..tostring(v) + end + end + return text +end + + +-- parameters: +-- dialog a yl_speak_up NPC dialog data structure containing the dialogs of the NPC +-- text the ink program that is to be parsed +-- log_level how detailled a report shall be created in the table log +-- log table - log entries will be appended to it +parse_ink.import_from_ink = function(dialog, text, log_level, log) + -- parse the ink program; + -- the "false" stands for "do not print out the parsed lines to stdout" + local knots = parse_ink.parse(text, false) + + -- prepare the knots so that they can be turned into dialogs + local res = parse_ink.analyze_knots_by_type(knots, log_level, log) + -- the prefix helps to combine multiple NPC dialogs into one ink program; it is usally n__ + local prefix = parse_ink.find_dialog_name_prefix(res.start_knot, log_level, log) + -- which dialog is the start dialog? + local start_dialog = parse_ink.find_start_dialog_name(prefix, res.dialogs, log_level, log) + -- if there is an action or effect of the type "If the previous effect failed, ..", then the + -- export_to_ink will create extra knots for these actions and effects - and adjust the links; + -- this code here just turns it back to the target dialog that is shown when the action and all + -- effects were successful + parse_ink.adjust_links(res.dialogs, res.actions, res.effects, start_dialog, prefix, log_level, log) + + -- now we're ready to actually import this into the dialog datastructure; uses functions_dialog.lua + parse_ink.import_dialogs(dialog, res.dialogs, res.actions, res.effects, start_dialog, prefix, res.dialog_list, log_level, log) + + return dialog +end + diff --git a/init.lua b/init.lua index 3165626..8302207 100644 --- a/init.lua +++ b/init.lua @@ -231,6 +231,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") -- 2.47.3 From 40e7fc20870b1e15fb915f6328687af46ec61b64 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 9 Jan 2025 18:01:28 +0100 Subject: [PATCH 48/56] do not change d_sort of other dialogs when changing the start dialog --- functions_dialogs.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index b42df08..525d253 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -281,7 +281,7 @@ yl_speak_up.update_start_dialog = function(log, dialog, start_dialog_name) d.is_a_start_dialog = true elseif(not(d.d_sort) or d.d_sort == 0) then -- all other dialogs are not *the* start dialog - d.d_sort = 1 + d.d_sort = d.d_sort or tonumber(string.sub(d_id, 3)) or 1 end end end -- 2.47.3 From 7e5fc745c0e89001398f93318890be5d3f931f68 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 9 Jan 2025 23:19:09 +0100 Subject: [PATCH 49/56] fixed bug in ink import due to a wrong reimplementation of split command in test environment --- functions_dialogs.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 525d253..256005e 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -442,7 +442,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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 + 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 @@ -472,15 +472,15 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam 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 + 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 + 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 + 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 @@ -488,7 +488,7 @@ yl_speak_up.update_dialog_option = function(log, dialog, dialog_name, option_nam mode = 2 option_name = parts[2] o_id = option_name - elseif(o_id and parts[1] ~= "o_") then + elseif(o_id and parts[1] ~= "o") then table.insert(log, log_str.."FAILED to create unknown option \""..tostring(o_id).."\".") return nil end -- 2.47.3 From 799233c8a2509314bc05e4b6a0256e79edeb01c8 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 9 Jan 2025 23:20:49 +0100 Subject: [PATCH 50/56] partial ink import implemented --- fs/fs_export.lua | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/fs/fs_export.lua b/fs/fs_export.lua index 1a65c25..389c4f4 100644 --- a/fs/fs_export.lua +++ b/fs/fs_export.lua @@ -48,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", @@ -127,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]".. -- 2.47.3 From 9b2aae7fe14227d8e8a99bf465db58443f355071 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 12 Jan 2025 20:40:46 +0100 Subject: [PATCH 51/56] disable options that are not included in the current import by adding a 'false' precondition --- functions_dialogs.lua | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index 256005e..ee23a9a 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -232,6 +232,41 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) 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] @@ -255,8 +290,9 @@ yl_speak_up.update_dialog_options_completed = function(log, dialog, d_id) end table.insert(log, log_str..", option <"..tostring(o_id)..">: ".. "Option exists in old dialog but not in import. Keeping option.") - - -- TODO: this option may need a precondition that sets it to false (if that precondition doesn't already exist) + -- 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 -- 2.47.3 From a85f0629c50ed4b72002dd9f33f69b74a2cf0485 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 12 Jan 2025 22:28:19 +0100 Subject: [PATCH 52/56] ink import: sort dialogs according to order given in ink; sort existing but not imported dialogs in after that --- functions_dialogs.lua | 13 +++++++++---- import_from_ink.lua | 9 ++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/functions_dialogs.lua b/functions_dialogs.lua index ee23a9a..3b57d69 100644 --- a/functions_dialogs.lua +++ b/functions_dialogs.lua @@ -228,6 +228,9 @@ yl_speak_up.update_dialog = function(log, dialog, dialog_name, dialog_text) 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 @@ -302,7 +305,7 @@ 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) +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 @@ -315,10 +318,12 @@ yl_speak_up.update_start_dialog = function(log, dialog, start_dialog_name) 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_sort) or d.d_sort == 0) then - -- all other dialogs are not *the* start dialog - d.d_sort = d.d_sort or tonumber(string.sub(d_id, 3)) or 1 + 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 diff --git a/import_from_ink.lua b/import_from_ink.lua index c5ba066..2a5c8fb 100644 --- a/import_from_ink.lua +++ b/import_from_ink.lua @@ -889,12 +889,19 @@ parse_ink.import_dialogs = function(dialog, dialogs, actions, effects, start_dia end end -- we need to add the dialogs as such first so that target_dialog will work + local d_sort = 1 for i, d_name in ipairs(dialog_list) do local dialog_knot = dialogs[d_name] local dialog_name = parse_ink.strip_prefix(d_name, prefix) local d_id = yl_speak_up.update_dialog(log, dialog, dialog_name, dialog_knot.text) dialog_knot.d_id = d_id + + -- adjust d_sort so that it is the same order as in the import + if(d_id and dialog.n_dialogs[d_id] and d_name ~= start_dialog) then + dialog.n_dialogs[d_id].d_sort = d_sort + d_sort = d_sort + 1 + end end -- now we can add the options @@ -977,7 +984,7 @@ parse_ink.import_dialogs = function(dialog, dialogs, actions, effects, start_dia end -- make sure the right start dialog is set - yl_speak_up.update_start_dialog(log, dialog, parse_ink.strip_prefix(start_dialog, prefix)) + yl_speak_up.update_start_dialog(log, dialog, parse_ink.strip_prefix(start_dialog, prefix), d_sort) return dialog end -- 2.47.3 From 9af492c07ab8102b8a9bf5f7cfb7e02a2ca3c9cc Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 16 Jan 2025 00:52:19 +0100 Subject: [PATCH 53/56] added addons folder; added addon action send_mail --- addons/action_send_mail.lua | 188 ++++++++++++++++++++++++++++++++++++ addons/load_addons.lua | 16 +++ init.lua | 4 + 3 files changed, 208 insertions(+) create mode 100644 addons/action_send_mail.lua create mode 100644 addons/load_addons.lua diff --git a/addons/action_send_mail.lua b/addons/action_send_mail.lua new file mode 100644 index 0000000..bf8e7c5 --- /dev/null +++ b/addons/action_send_mail.lua @@ -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 diff --git a/addons/load_addons.lua b/addons/load_addons.lua new file mode 100644 index 0000000..ccff71c --- /dev/null +++ b/addons/load_addons.lua @@ -0,0 +1,16 @@ + +-- 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") +end diff --git a/init.lua b/init.lua index 8302207..d40a02c 100644 --- a/init.lua +++ b/init.lua @@ -222,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 -- 2.47.3 From 2e1732644fb8d26b9ed88219b9510b09bd8b66d5 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 16 Jan 2025 03:02:55 +0100 Subject: [PATCH 54/56] added addons effect send_mail --- addons/effect_send_mail.lua | 94 +++++++++++++++++++++++++++++++++++++ addons/load_addons.lua | 1 + 2 files changed, 95 insertions(+) create mode 100644 addons/effect_send_mail.lua diff --git a/addons/effect_send_mail.lua b/addons/effect_send_mail.lua new file mode 100644 index 0000000..51e3ca7 --- /dev/null +++ b/addons/effect_send_mail.lua @@ -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 diff --git a/addons/load_addons.lua b/addons/load_addons.lua index ccff71c..68879aa 100644 --- a/addons/load_addons.lua +++ b/addons/load_addons.lua @@ -13,4 +13,5 @@ if(minetest.global_exists("mail") and type(mail.send) == "function") then dofile(path_addons .. "action_send_mail.lua") + dofile(path_addons .. "effect_send_mail.lua") end -- 2.47.3 From 9077feb1a990a35e8e14c23d0c50becaf0c3d380 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 16 Jan 2025 21:21:32 +0100 Subject: [PATCH 55/56] addons: added effect send_coordinates (useful in combination with a waypoint_compass but can be used manually as well) --- addons/effect_send_coordinates.lua | 45 ++++++++++++++++++++++++++++++ addons/load_addons.lua | 3 ++ 2 files changed, 48 insertions(+) create mode 100644 addons/effect_send_coordinates.lua diff --git a/addons/effect_send_coordinates.lua b/addons/effect_send_coordinates.lua new file mode 100644 index 0000000..491b57b --- /dev/null +++ b/addons/effect_send_coordinates.lua @@ -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, +} diff --git a/addons/load_addons.lua b/addons/load_addons.lua index 68879aa..2fd9bc4 100644 --- a/addons/load_addons.lua +++ b/addons/load_addons.lua @@ -15,3 +15,6 @@ if(minetest.global_exists("mail") 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") -- 2.47.3 From 67fe90c984e49068372e8f587a408fa5c05031ff Mon Sep 17 00:00:00 2001 From: Sokomine Date: Sun, 9 Feb 2025 00:26:24 +0100 Subject: [PATCH 56/56] variables can now have a default value other than nil --- quest_api.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/quest_api.lua b/quest_api.lua index 50948a4..934d3a6 100644 --- a/quest_api.lua +++ b/quest_api.lua @@ -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 @@ -524,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 -- 2.47.3