yl_speak_up/quest_api.lua

623 lines
22 KiB
Lua

-- 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:
-- $ <blank> <player name> <blank> <variable name> (makes it easier to grant read access)
-- the values are of the form:
-- <current player name as key> : <value of variable for that player as value>
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
-- leave this formspec
if(fields and (fields.quit or fields.exit)) then
local last_fs = yl_speak_up.speak_to[pname][ "working_at" ]
yl_speak_up.show_fs(player, last_fs)
-- add a new variable?
elseif(fields and fields.add_variable) then
local error_msg = ""
if(not(fields.add_variable_name) or fields.add_variable_name == ""
or fields.add_variable_name:trim() == "") then
error_msg = "Please enter the name of your variable!"
-- limit names to something more sensible
elseif(string.len(fields.add_variable_name) > 30) then
error_msg = "The name of your variable name is too long.\n"..
"Only up to 30 characters are allowed."
elseif(string.len(fields.add_variable_name:trim()) < 2) then
error_msg = "The name of your variable name is too short.\n"..
"It has to be at least 2 characters long."
else
fields.add_variable_name = fields.add_variable_name:trim()
local res = yl_speak_up.add_quest_variable(pname, fields.add_variable_name)
-- not really an error msg here - but fascilitates output
error_msg = "A new variable named\n \""..
minetest.formspec_escape(fields.add_variable_name)..
"\"\nhas been created."
if(not(res)) then
error_msg = "Failed to create variable named\n \""..
minetest.formspec_escape(fields.add_variable_name).."\"."
else
-- slect this new variable
local var_list = yl_speak_up.get_quest_variables(pname, true)
-- make names of own variables shorter
yl_speak_up.strip_pname_from_varlist(var_list, pname)
table.sort(var_list)
local index = table.indexof(var_list, fields.add_variable_name)
if(index and index > -1) then
yl_speak_up.speak_to[pname].tmp_index_variable = index + 1
end
end
end
yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:manage_variables",
formspec = "size[6,2]"..
"label[0.2,0.0;"..error_msg.."]"..
"button[1.5,1.5;2,0.9;back_from_msg;Back]"})
return
-- show where this variable is used
elseif(fields and fields.show_var_usage and fields.show_var_usage ~= "") then
yl_speak_up.show_fs(player, "msg", {
input_to = "yl_speak_up:manage_variables",
formspec = yl_speak_up.get_list_of_usage_of_variable(
fields.list_var_names, pname, true,
"back_from_msg",
"Back to manage variables",
-- not an internal variable
false)
})
return
-- TODO: delete variable
-- a var name was selected in the dropdown list
elseif(fields and fields.list_var_names and fields.list_var_names ~= "") then
local var_list = yl_speak_up.get_quest_variables(pname, true)
-- make names of own variables shorter
yl_speak_up.strip_pname_from_varlist(var_list, pname)
table.sort(var_list)
local index = table.indexof(var_list, fields.list_var_names)
if(fields.list_var_names == "Add variable:") then
index = 0
end
if(index and index > -1) then
yl_speak_up.speak_to[pname].tmp_index_variable = index + 1
end
-- show the same formspec again, with a diffrent variable selected
yl_speak_up.show_fs(player, "manage_variables")
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)
local pname = player:get_player_name()
-- variables owned by the player - including those with write access
local var_list = yl_speak_up.get_quest_variables(pname, true)
-- make names of own variables shorter
yl_speak_up.strip_pname_from_varlist(var_list, pname)
-- the yl_speak_up.create_dropdown_playerlist function needs a table - not a list
local table_of_vars = {}
for i, k in ipairs(var_list) do
table_of_vars[ k ] = true
end
-- "Add variable:" is currently selected
local additional_buttons = ""
if(not(yl_speak_up.speak_to[pname].tmp_index_variable)
or yl_speak_up.speak_to[pname].tmp_index_variable == 1) then
yl_speak_up.speak_to[pname].tmp_index_variable = 1
additional_buttons = "button[11.4,1.9;2.5,0.9;add_variable;Create variable]"..
"tooltip[add_variable;Create a new varialbe with the name\n"..
"you entered in the field to the left.]"
else
local add_read_button = ""
local add_write_button = ""
if(not(yl_speak_up.speak_to[pname].tmp_index_var_read_access)
or yl_speak_up.speak_to[pname].tmp_index_var_read_access == 1) then
yl_speak_up.speak_to[pname].tmp_index_var_read_access = 1
add_read_button = "button[12.9,2.9;1.0,0.9;add_read_access;Add]"..
"tooltip[add_read_access;Grant the player whose name you entered\n"..
"you entered in the field to the left read access\n"..
"to your variable.]"
end
if(not(yl_speak_up.speak_to[pname].tmp_index_var_write_access)
or yl_speak_up.speak_to[pname].tmp_index_var_write_access == 1) then
yl_speak_up.speak_to[pname].tmp_index_var_write_access = 1
add_write_button = "button[12.9,3.9;1.0,0.9;add_write_access;Add]"..
"tooltip[add_write_access;Grant the player whose name you entered\n"..
"you entered in the field to the left *write* access\n"..
"to your variable.]"
end
additional_buttons = "button[11.4,1.9;2.5,0.9;show_var_usage;Where is it used?]"..
"tooltip[show_var_usage;Show which NPC use this variable in which context.]"..
-- offer a dropdown list and a text input field for new varialbe names for adding
"label[0.2,3.05;Players with read access to this variable:]"..
yl_speak_up.create_dropdown_playerlist(player, pname,
{}, yl_speak_up.speak_to[pname].tmp_index_var_read_access, -- TODO
5.5, 2.9, 0.0, "list_var_read_access", "player", "Remove player from list",
"grant_player_var_read_access",
"Enter the name of the player that shall\n"..
"have read access to this variable.",
"revoke_player_var_read_access",
"If you click here, the selected player\n"..
"will no longer be able to add new\n"..
"pre(C)onditions which read your variable."
)..add_read_button..
"label[0.2,4.05;Players with *write* access to this variable:]"..
yl_speak_up.create_dropdown_playerlist(player, pname,
{}, yl_speak_up.speak_to[pname].tmp_index_var_write_access, -- TODO
5.5, 3.9, 0.0, "list_var_write_access", "player", "Remove player from list",
"grant_player_var_write_access",
"Enter the name of the player that shall\n"..
"have *write* access to this variable.",
"revoke_player_var_write_access",
"If you click here, the selected player\n"..
"will no longer be able to *write* new\n"..
"values into this variable."
)..add_write_button..
"label[0.2,5.05;Type of variable: "..
-- TODO: show actual type
minetest.colorize("#FFFF00","String/text or numerical value, depending "..
"on how you use it")..".]"
end
return "size[14,6.5]"..
"label[5.0,0.0;* Manage your variables *]"..
"label[0.2,0.8;Note: Each variable will store a diffrent value for each player who "..
"interacts with the NPC.\n"..
"You can grant read and write access to other players for your "..
"variables so that they can also use them as well.]"..
"label[0.2,2.05;Your variables:]"..
-- offer a dropdown list and a text input field for new varialbe names for adding
yl_speak_up.create_dropdown_playerlist(player, pname,
table_of_vars, yl_speak_up.speak_to[pname].tmp_index_variable,
2.2, 1.9, 1.0, "list_var_names", "variable", "Delete selected variable",
"add_variable_name",
"Enter the name of the new variable you\n"..
"want to create.",
"delete_variable",
"If you click here, the selected variable\n"..
"will be deleted."
)..
additional_buttons..
"button[0.0,0.2;1.0,0.6;back;Back]"..
"button[6.0,6.0;1.0,0.6;back;Back]"
end
-- variables are personalized; they are prefixed by "$ <PLAYER_NAME> <VAR_NAME>"
-- 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