-- just some handling of variables -- TODO: handle read (and write?) access for other players -- TODO: add a function to check if the player has read/write access -- TODO: mark some vars as "need to be saved" while others are less important (i.e. timestamps) -- the keys are of the form: -- $ (makes it easier to grant read access) -- the values are of the form: -- : yl_speak_up.player_vars = {} -- store when player_vars was last saved to disc yl_speak_up.player_vars_last_save_time = 0 -- save the data to disc; either if force_save is set or enough time has passed yl_speak_up.save_quest_variables = function(force_save) if(not(force_save) and (yl_speak_up.player_vars_last_save_time + yl_speak_up.player_vars_min_save_time > math.floor(minetest.get_us_time()/1000000))) then return end local json = minetest.write_json( yl_speak_up.player_vars ) -- actually store it on disk minetest.safe_file_write(yl_speak_up.worldpath..yl_speak_up.player_vars_save_file..".json", json) end -- load the data from disc yl_speak_up.load_quest_variables = function() -- load the data from the file local file, err = io.open(yl_speak_up.worldpath..yl_speak_up.player_vars_save_file..".json", "r") if err then return end io.input(file) local text = io.read() -- all values saved in the tables as such are strings local data = minetest.parse_json(text, -1) io.close(file) if(type(data) ~= "table") then return end for k,v in pairs(data) do if(v == -1) then data[ k ] = {} end end yl_speak_up.player_vars = data if(not(yl_speak_up.player_vars.meta)) then yl_speak_up.player_vars["meta"] = {} end end -- do so when this file is parsed yl_speak_up.load_quest_variables() -- new variables have to be added somehow yl_speak_up.add_quest_variable = function(owner_name, variable_name) local k = "$ "..tostring(owner_name).." "..tostring(variable_name) if(not(owner_name) or not(variable_name)) then return false end -- create a new empty table; -- keys will be the names of players for which values are set yl_speak_up.player_vars[ k ] = {} -- a new variable was created - that deserves a forced save yl_speak_up.save_quest_variables(true) return true end -- accidentally created or no longer needed variables need to be deleted somehow yl_speak_up.del_quest_variable = function(owner_name, variable_name) local k = "$ "..tostring(owner_name).." "..tostring(variable_name) if(not(owner_name) or not(variable_name)) then return false end -- a variable was deleted - that deserves a forced save yl_speak_up.save_quest_variables(true) yl_speak_up.player_vars[ k ] = nil end -- set the value of a variable used by a player in an NPC; -- returns false if the variable cannot be set (i.e. does not exist) yl_speak_up.set_quest_variable_value = function(player_name, variable_name, new_value) -- 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 return false end if(new_value ~= nil) then new_value = tostring(new_value) end yl_speak_up.player_vars[ k ][ player_name ] = new_value -- a quest variable was changed - save that to disc (but no need to force it) yl_speak_up.save_quest_variables(false) return true end -- get the value of a variable used by a player in an NPC; -- returns nil if the variable does not exist 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 return nil end return yl_speak_up.player_vars[ k ][ player_name ] end yl_speak_up.get_quest_variables = function(pname, has_write_access) if(not(pname)) then return {} end local liste = {} -- first: list the variables owned by the player for k, v in pairs(yl_speak_up.player_vars) do local parts = string.split(k, " ") if(parts and parts[1] and parts[1] == "$" and parts[2] and parts[2] == pname) then table.insert(liste, k) end end -- if the player has the right privs: allow to access all other variables as well if( minetest.check_player_privs(pname, {npc_master=true}) or minetest.check_player_privs(pname, {npc_talk_master=true})) then for k, v in pairs(yl_speak_up.player_vars) do local parts = string.split(k, " ") -- variables owned by *other* players if(parts and parts[1] and parts[1] == "$" and parts[2] and parts[2] ~= pname) then table.insert(liste, k) end end end -- TODO: insert those vars owned by other players where this one has read access to table.sort(liste) return liste end -- which variables can player pname read and use in preconditions? -- returns a sorted list yl_speak_up.get_quest_variables_with_read_access = function(pname) return yl_speak_up.get_quest_variables(pname, false) end -- which variables can player pname write and use in effects/results? yl_speak_up.get_quest_variables_with_write_access = function(pname) return yl_speak_up.get_quest_variables(pname, true) end yl_speak_up.input_fs_manage_variables = function(player, formname, fields) local pname = player:get_player_name() if(fields and fields.back_from_msg) then yl_speak_up.show_fs(player, "manage_variables") return end -- add a new variable? if(fields and fields.add_variable) then if(not(fields.add_variable_name) or fields.add_variable_name == "" or fields.add_variable_name:trim() == "") then yl_speak_up.show_fs(player, "msg", { input_to = "yl_speak_up:manage_variables", formspec = "size[6,2]".. "label[0.2,0.5;Please enter the name of your variable!]".. "button[1.5,1.5;2,0.9;back_from_msg;Back]"}) return end -- TODO: limit names to something more sensible? fields.add_variable_name = fields.add_variable_name:trim() local res = yl_speak_up.add_quest_variable(pname, fields.add_variable_name) local text = "A new variable named\n \""..tostring(fields.add_variable_name).. "\"\nhas been created." if(not(res)) then text = "Failed to create variable named\n \"".. tostring(fields.add_variable_name).."\"." end yl_speak_up.show_fs(player, "msg", { input_to = "yl_speak_up:manage_variables", formspec = "size[6,2]".. "label[0.2,0.0;"..minetest.formspec_escape(text).."]".. "button[1.5,1.5;2,0.9;back_from_msg;Back]"}) return end -- try to go back to the last formspec shown before this one if(not(yl_speak_up.speak_to[pname])) then return end local last_fs = yl_speak_up.speak_to[pname][ "working_at" ] yl_speak_up.show_fs(player, last_fs) end yl_speak_up.get_fs_manage_variables = function(player, param) return "size[12,4]".. "label[2.0,-0.2;* Manage your variables *]".. "label[0.2,1.0;Create this new variable:]".. "field[3.7,1.3;6.0,0.6;add_variable_name;;]".. "button[9.4,1.0;2.5,0.6;add_variable;Create variable]".. "tooltip[add_variable;Enter the name of your new variable.]".. -- TODO: delete variable "button[2.0,3.5;1.0,0.6;back;Back]" end -- variables are personalized; they are prefixed by "$ " -- helper function; -- strip "$ PNAME " from variable names (but only for those owned by player with name pname) yl_speak_up.strip_pname_from_var = function(var_name, pname) local parts = string.split(var_name, " ") if(parts and parts[1] and parts[1] == "$" and parts[2] and parts[2] == pname) then table.remove(parts, 1) -- remove "$" table.remove(parts, 1) -- remove pname return table.concat(parts, " ") end return var_name end -- does the opposite of the function above; adds "$ PNAME " if needed yl_speak_up.add_pname_to_var = function(var_name, pname) local parts = string.split(var_name, " ") if(parts and parts[1] and parts[1] ~= "$") then return "$ "..tostring(pname).." "..tostring(var_name) end return var_name end -- helper function for yl_speak_up.input_fs_edit_option_related -- and yl_speak_up.get_fs_edit_option_p_and_e_state yl_speak_up.strip_pname_from_varlist = function(var_list, pname) local var_list_text = "" -- strip pname from the variable names for i, v in ipairs(var_list) do var_list[i] = yl_speak_up.strip_pname_from_var(v, pname) -- useful for presenting a list var_list_text = var_list_text..","..minetest.formspec_escape(tostring(var_list[i])) end return var_list_text end -- helper function for saving NPC data; -- this only works if *someone* is currently talking to that NPC yl_speak_up.get_pname_for_n_id = function(n_id) for k, v in pairs(yl_speak_up.speak_to) do if(v and v.n_id and v.n_id == n_id) then return k end end 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 yl_speak_up.update_stored_npc_data = function(n_id, dialog) -- in order to determine the position of the NPC, we need its object local pname = yl_speak_up.get_pname_for_n_id(n_id) local npc_pos = "" if(pname) then local obj = yl_speak_up.speak_to[pname].obj if(obj and obj:get_pos()) then npc_pos = minetest.pos_to_string(obj:get_pos()) end end -- gather statistical data about the NPC and find out which variables it uses local anz_dialogs = 0 local anz_options = 0 local anz_preconditions = 0 local anz_actions = 0 local anz_effects = 0 local anz_trades = 0 local variables_p = {} local variables_e = {} if(dialog and dialog.n_dialogs) then for d_id, d in pairs(dialog.n_dialogs) do anz_dialogs = anz_dialogs + 1 if(d and d.d_options) then for o_id, o in pairs(d.d_options) do anz_options = anz_options + 1 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 end end end if(o and o.actions) then for a_id, a_data in pairs(o.actions) do anz_actions = anz_actions + 1 end end if(o and o.o_results) then for r_id, r in pairs(o.o_results) do 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 end end end end end end end if(dialog and dialog.trades) then for trade_id, t_data in pairs(dialog.trades) do -- not a trade that is the action of a dialog option; only trade list trades count if(not(t_data.d_id)) then anz_trades = anz_trades + 1 end end end -- add a special variable (if needed) for saving the NPC meta data if(not(yl_speak_up.player_vars[ "$NPC_META_DATA$" ])) then yl_speak_up.player_vars[ "$NPC_META_DATA$" ] = {} end yl_speak_up.player_vars[ "$NPC_META_DATA$" ][ n_id ] = { n_id = n_id, name = tostring(dialog.n_npc), owner = tostring(yl_speak_up.npc_owner[ n_id ]), pos = tostring(npc_pos), anz_dialogs = anz_dialogs, anz_options = anz_options, anz_preconditions = anz_preconditions, anz_actions = anz_actions, anz_effects = anz_effects, anz_trades = anz_trades, last_modified = os.date(), } -- 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 -- if the variable does not exist: create it if(not(yl_speak_up.player_vars[ k ])) then yl_speak_up.player_vars[ k ] = {} end if(not(yl_speak_up.player_vars[ k ][ "$META$" ])) then yl_speak_up.player_vars[ k ][ "$META$"] = { used_by_npc = {} } end yl_speak_up.player_vars[ k ][ "$META$"][ "used_by_npc" ][ n_id ] = true end for k, v in pairs(variables_e) do -- if the variable does not exist: create it if(not(yl_speak_up.player_vars[ k ])) then yl_speak_up.player_vars[ k ] = {} end if(not(yl_speak_up.player_vars[ k ][ "$META$" ])) then yl_speak_up.player_vars[ k ][ "$META$"] = { used_by_npc = {} } end yl_speak_up.player_vars[ k ][ "$META$"][ "used_by_npc" ][ n_id ] = true end -- force writing the data yl_speak_up.save_quest_variables(true) end -- which NPC do use this variable? yl_speak_up.get_npc_users_of_variable = function(var_name) -- no variable, or nothing stored? then it's not used by any NPC either if(not(var_name) or not(yl_speak_up.player_vars[ var_name ]) or not(yl_speak_up.player_vars[ var_name ][ "$META$"]) or not(yl_speak_up.player_vars[ var_name ][ "$META$"][ "used_by_npc" ])) then return {} end local npc_list = {} for n_id, v in pairs(yl_speak_up.player_vars[ var_name ][ "$META$"][ "used_by_npc" ]) do table.insert(npc_list, n_id) end table.sort(npc_list) return npc_list end -- find out where this variable is used in NPCs yl_speak_up.get_list_of_usage_of_variable = function(var_name, pname, check_preconditions, back_button_name, back_button_text) -- which NPC (might be several) is using this variable? local npc_list = yl_speak_up.get_npc_users_of_variable(var_name) -- list of all relevant preconditions, actions and effects local res = {} local count_read = 0 local count_changed = 0 for i, n_id in ipairs(npc_list) do -- the NPC may not even be loaded local dialog = yl_speak_up.load_dialog(n_id) if(dialog and dialog.n_dialogs) then for d_id, d in pairs(dialog.n_dialogs) do if(d and d.d_options) then for o_id, o in pairs(d.d_options) do local p_text = "" local r_text = "" local sort_value = 0 if(o and o.o_prerequisites and check_preconditions) then for p_id, p in pairs(o.o_prerequisites) do if(p and p.p_type and p.p_type == "state" and p.p_variable and p.p_variable == var_name) then p_text = p_text..",#FFFF00,".. minetest.formspec_escape(tostring(p_id)).. ",#FFFF00,pre(C)ondition,#FFFF00,".. minetest.formspec_escape(p.p_type)..",#FFFF00,".. minetest.formspec_escape( yl_speak_up.show_precondition(p, pname)) sort_value = (p.p_var_cmp_value or 0) count_read = count_read + 1 end end end if(o and o.o_results) then for r_id, r in pairs(o.o_results) do if(r and r.r_type and r.r_type == "state" and r.r_variable and r.r_variable == var_name) then r_text = r_text..",#55FF55,".. minetest.formspec_escape(tostring(r_id)).. ",#55FF55,(Ef)fect,#55FF55,".. minetest.formspec_escape(r.r_type)..",#55FF55,".. minetest.formspec_escape( yl_speak_up.show_effect(r, pname)) -- values set in the results are more important than -- those set in preconditions sort_value = (r.r_var_cmp_value or 0) count_changed = count_changed + 1 end end end -- if preconditions or effects apply: show the action as well if(o and o.actions and (p_text ~= "" or r_text ~= "")) then for a_id, a in pairs(o.actions) do -- no need to introduce an a_text; this will follow -- directly after p_text, and p_text is finished p_text = p_text..",FF9900,".. minetest.formspec_escape(tostring(a_id)).. ",#FF9900,(A)ction,#FF9900,".. minetest.formspec_escape(a.a_type)..",#FF9900,".. minetest.formspec_escape( yl_speak_up.show_action(a, pname)) end end if(p_text ~= "" or r_text ~= "") then -- show newlines as <\n> in order to save space local d_text = string.gsub( dialog.n_dialogs[ d_id ].d_text or "?", "\n", minetest.formspec_escape("
")) -- break the text up into lines of length x local parts = minetest.wrap_text(d_text, 80, true) -- only show the first two lines (we don't have infinite room) local prefix = ",#FFFFFF,,#FFFFFF,,#FFFFFF,,#BBBBFF," if(#parts < 2) then d_text = minetest.formspec_escape(parts[1]) elseif(#parts == 2) then d_text = minetest.formspec_escape(parts[1]).. prefix..minetest.formspec_escape(parts[2]) elseif(#parts == 3) then d_text = minetest.formspec_escape(parts[1]).. prefix..minetest.formspec_escape(parts[2]).. prefix..minetest.formspec_escape(parts[3]) else -- indicate that there is more d_text = minetest.formspec_escape(parts[1]).. prefix..minetest.formspec_escape(parts[2]).. prefix..minetest.formspec_escape(parts[3]).. minetest.formspec_escape(" [...]") end res[ tostring(n_id).." "..tostring(d_id).." "..tostring(o_id) ] = { text = "#6666FF,".. tostring(n_id)..",#6666FF,NPC,#6666FF,named:,#6666FF,".. minetest.formspec_escape(dialog.n_npc or "?")..",".. "#BBBBFF,".. tostring(d_id)..",#BBBBFF,Dialog,#BBBBFF,says:,#BBBBFF,".. d_text..",".. "#FFFFFF,".. tostring(o_id)..",#FFFFFF,Option,#FFFFFF,A:,#FFFFFF,".. minetest.formspec_escape(tostring( o.o_text_when_prerequisites_met or "?")).. p_text..r_text, sort_value = sort_value} end end end end end end local sorted_list = yl_speak_up.get_sorted_options(res, "sort_value") local sorted_res = {} for i, k in pairs(sorted_list) do table.insert(sorted_res, res[ k ].text) end local formspec = { "formspec_version[3]", "size[57,33]", -- back to the list with that one precondition or effect "button[0.2,0.2;56.6,1.2;"..back_button_name..";".. minetest.formspec_escape(back_button_text).."]", "button[0.2,31.6;56.6,1.2;"..back_button_name..";".. minetest.formspec_escape(back_button_text).."]", "label[20.0,1.8;".. minetest.formspec_escape("Variable \""..tostring(var_name or "- ? -").. "\" is used here:").."]".. -- type x_id color type text "tablecolumns[color,span=1;text;color,span=1;text;color,span=1;text;color,span=1;text]", "table[1.2,2.4;55.0,28.0;table_of_variable_uses;" } -- insert blank lines between lines belonging together table.insert(formspec, table.concat(sorted_res, ",#FFFFFF,,#FFFFFF,,#FFFFFF,,#FFFFFF,,").."]") if(#sorted_res > 0 and (count_read > 0 or count_changed > 0)) then table.insert(formspec, "label[16.0,31.0;The variable is accessed in "..tostring(count_read).. " pre(C)onditions and changed in "..tostring(count_changed).. " (Ef)ffects.]") else -- TODO: make delete_unused_variable work table.insert(formspec, "button[0.2,30.6;56.6,1.2;delete_unused_variable;".. minetest.formspec_escape("Delete this unused variable \"".. tostring(var_name or "- ? -")).."\".]") end return table.concat(formspec, "\n") end