-- 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 = yl_speak_up.restore_complete_var_name(variable_name, owner_name) if(not(owner_name) or not(variable_name)) then return false end -- actually delete the variable yl_speak_up.player_vars[ k ] = nil -- a variable was deleted - that deserves a forced save yl_speak_up.save_quest_variables(true) return true 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 -- (partly) the opposite of the function above - add the name of the player to a variable -- name again if needed yl_speak_up.restore_complete_var_name = function(var_name, pname) local vparts = string.split(var_name or "", " ") -- has the player name been stripped from the variable name for better readability? if(vparts and #vparts > 0 and vparts[1] ~= "$") then return "$ "..tostring(pname).." "..table.concat(vparts, " ") end return var_name 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 -- TODO: show this data in a formspec to admins for maintenance 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 ]), may_edit = dialog.n_may_edit or {}, 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, is_internal_var) -- TODO: check if the player really has read access to this variable if(not(is_internal_var)) then var_name = yl_speak_up.restore_complete_var_name(var_name, pname) end -- which NPC (might be several) is using this variable? -- TODO: ..or if the player at least is owner of these NPC or has extended privs 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..yl_speak_up.print_as_table_precon(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..yl_speak_up.print_as_table_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..yl_speak_up.print_as_table_action(a, pname) end end yl_speak_up.print_as_table_dialog(p_text, r_text, dialog, n_id, d_id, o_id, res, o, sort_value) end end end end end local formspec = yl_speak_up.print_as_table_prepare_formspec(res, "table_of_variable_uses", back_button_name, back_button_text) table.insert(formspec, "label[20.0,1.8;".. minetest.formspec_escape("Variable \"".. minetest.colorize("#FFFF00", tostring(var_name or "- ? -")).. "\" is used here:").."]") if(count_read > 0 or count_changed > 0) then table.insert(formspec, "label[16.0,31.0;The variable is accessed in ".. minetest.colorize("#FFFF00", tostring(count_read).." pre(C)onditions").. " and changed in ".. minetest.colorize("#55FF55", tostring(count_changed).." (Ef)fects").. ".]") elseif(not(is_internal_var)) then -- 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 "- ? -")).."\".]") else table.insert(formspec, "label[16.0,31.0;This is an internal variable and cannot be deleted.]") end return table.concat(formspec, "\n") end