minetest-quiz/init.lua

529 lines
17 KiB
Lua

-- quiz/init.lua
-- LUALOCALS < ---------------------------------------------------------
local minetest, pairs, type
= minetest, pairs, type
-- LUALOCALS > ---------------------------------------------------------
local MOD_NAME = minetest.get_current_modname()
if rawget(_G, MOD_NAME) then return end
quiz = {}
minetest.log("info", "Loading quiz Mod")
-- local defaultS = default.get_translator
local mkdir = minetest.mkdir
local store = minetest.get_mod_storage()
local MOD_PATH = minetest.get_modpath(MOD_NAME) .. "/"
local WORLD_PATH = minetest.get_worldpath() .. "/"
local STUDENTS_PATH = WORLD_PATH .. "/quiz/students/"
local mergeTable = dofile(MOD_PATH .. "merge_table.lua")
local array = dofile(MOD_PATH .. "array.lua")
-- Load support for MT game translation.
local S = minetest.get_translator(MOD_NAME)
-- escapes the characters "[", "]", "\", "," and ";", which can not be used in formspecs.
local esc = minetest.formspec_escape
local settings = yaml.readConfig(MOD_NAME, "config.yml", {"quiz"})
-- print(dump(mod_setting));
quiz.MOD_NAME = MOD_NAME
quiz.MOD_PATH = MOD_PATH
quiz.settings = settings
quiz.store = store
quiz.get_translator = S
local givemeItem = dofile(MOD_PATH.."giveme_item.lua")
local isOnline = dofile(MOD_PATH.."is_online.lua")
-- collects the player's sessions
local sessions = {}
quiz.sessions = sessions
local function getSession(playerName)
if type(playerName) ~= "string" then
playerName = playerName:get_player_name()
end
local result = sessions[playerName]
if result == nil then
result = {}
sessions[playerName] = result
result.dialogClosed = true
end
return result
end
quiz.getSession = getSession
local function clearSession(playerName)
local result = sessions[playerName]
if result ~= nil then
sessions[playerName] = nil
end
end
quiz.clearSession = clearSession
local function getFields(playerName, session)
if session == nil then session = getSession(playerName) end
local result = session.fields
if result == nil then
result = {}
session.fields = result
end
return result
end
local function getLastLeavedTime(playerName)
return store:get_int(playerName .. ":leavedTime")
end
quiz.getLastLeavedTime = getLastLeavedTime
-- record the last leaved time of a player
local function setLastLeavedTime(playerName, value)
return store:set_int(playerName ..":leavedTime", value)
end
quiz.setLastLeavedTime = setLastLeavedTime
-- TODO: use this minetest.get_player_information(name).connection_uptime
local function getUsedTime(playerName)
return store:get_int(playerName .. ":usedTime")
end
quiz.getUsedTime = getUsedTime
-- record the last used time of a player
local function setUsedTime(playerName, value)
return store:set_int(playerName ..":usedTime", value)
end
quiz.setUsedTime = setUsedTime
local function logQuiz(playerName, quiz, answer, ok)
local session = getSession(playerName)
local logDir = STUDENTS_PATH .. playerName
local logFile = logDir .. "/quiz-log.yml"
mkdir(logDir)
local content = {}
if quiz.type == "calc" then
content["title"] = quiz.calc
-- content["real_answer"] = quiz["real_answer"]
else
content["title"] = quiz.title
end
content["type"] = quiz.type or "text"
if quiz.options then content["options"] = quiz.options end
mergeTable(content, {
answer = answer, ok = ok,
start = os.date("%Y-%m-%dT%H:%M:%S", session.startQuizTime),
answerTime = session.answerTime
})
yaml.writeFile(logFile, {content}, "a")
end
quiz.logQuiz = logQuiz
local quizzes = dofile(MOD_PATH .. "quizzes.lua")
quiz.quizzes = quizzes
local function get_formspec(player_name, quiz, session)
local fields = getFields(player_name, session)
player_name = esc(S("Hi, @1", player_name) .. "," .. S("the challenge begins!"))
local title = esc(quizzes.getTitle(quiz) or "")
local desc = esc(quiz.desc or "")
local questionStr = esc(S("Question"))
local answerStr = esc(S("Answer"))
local formspec = {
"formspec_version[4]",
"size[10,9]",
"label[0,0.2;", player_name, "]",
"box[0.4,1.1;9.2,4.25;#999999]",
"textarea[0.4,1.1;9.2,4.25;;",questionStr, ";" , title, "]",
}
if desc and desc ~= "" then table.insert(formspec, "label[1.8,0.8;(".. desc ..")]") end
table.insert(formspec, "button_exit[3.2,8;3.5,0.8;ok;Ok]")
local options = fields._options
if options == nil then
options = mergeTable({}, quiz.options)
array.shuffle(options)
fields._options = options
end
if quiz.type == "select" and options and #options then
local ox = 0.5
local oy = 5.9
local x = ox
local y = oy
local columnCount = 3
for i=1,#options do
local fraq = (i-1) % columnCount
x = ox + fraq * 3.5
y = oy + math.modf((i-1) / columnCount) * 0.6
table.insert(formspec, "checkbox[".. x .. "," .. y .. ";opt".. i .. ";" .. options[i] .. "]")
if fraq == (columnCount - 1) then x = ox end
end
else
table.insert(formspec, "field[0.4,6;9.2,0.9;answer;" .. answerStr .. ";]")
end
-- table.concat is faster than string concatenation - `..`
return table.concat(formspec, "")
end
-- local motddesc = conf("get", "desc") or "terms"
local cmdname = settings.cmdName or S("answer")
-- local cmdparam = conf("get", "cmdparam") or ("to " .. motddesc)
local cmdDesc = settings.cmdDesc or S("type /@1 your answer in chat window", cmdname)
-- display two messages on HUD
local hudline1 = settings.hudline1 or S("You must answer the question to play.")
local hudline2 = settings.hudline2 or cmdDesc
local function resetWhenLeaving(playerName)
local session = getSession(playerName)
local enterTime = session.joinTime or 0
local currTime = os.time()
local usedTime = currTime - enterTime
if enterTime then
setLastLeavedTime(playerName, currTime)
if usedTime >= settings.totalPlayTime * 60 then
setUsedTime(playerName, 0)
elseif usedTime >= 30 then
setUsedTime(playerName, usedTime)
end
end
clearSession(playerName)
end
local function kickPlayer(playerName, reason)
-- resetWhenLeaving(playerName)
minetest.log("action", playerName .. " was kicked for " .. reason)
minetest.kick_player(playerName, reason)
end
-- create or update HUD
local function showHud(player, id, offset, text)
if not id then
return player:hud_add({
hud_elem_type = "text",
position = {x = 0.5, y = 0.5},
text = text,
number = 0xFFC000,
alignment = {x = 0, y = offset},
offset = {x = 0, y = offset}
})
end
player:hud_change(id, "text", text)
return id
end
local function hudcheck(pname)
if not pname then return end
pname = type(pname) == "string" and pname or pname:get_player_name()
minetest.after(0, function()
local player = minetest.get_player_by_name(pname)
if not player then return end
local session = getSession(pname)
local playerHud = session.huds
local hasPriv = minetest.check_player_privs(player, "interact")
if hasPriv then
player:hud_set_flags({crosshair = true})
if playerHud then
for _, id in pairs(playerHud) do
player:hud_remove(id)
end
end
session.huds = nil
return
end
player:hud_set_flags({crosshair = false})
if not playerHud then
playerHud = {}
session.huds = playerHud
end
playerHud[1] = showHud(player, playerHud[1], -1, hudline1)
if hudline2 and #hudline2 then
playerHud[2] = showHud(player, playerHud[2], 1, hudline2)
end
end)
end
local function revokePriv(playerName)
local grant = minetest.string_to_privs(settings.grant or "interact,shout")
local privs = minetest.get_player_privs(playerName)
-- print('TCL:: ~ file: init.lua ~ line 139 ~ revokePrivs', playerName, dump(privs));
if #privs then
for priv in pairs(grant) do
privs[priv] = nil
-- minetest.run_priv_callbacks(playerName, priv, playerName, "revoke")
end
minetest.set_player_privs(playerName, privs)
-- privs = minetest.get_player_privs(playerName)
-- print('TCL:: ~ file: init.lua ~ line 139 ~ revokePrivs result', playerName, dump(privs));
hudcheck(playerName)
end
end
local function grantPriv(playerName)
local grant = minetest.string_to_privs(settings.grant or "interact,shout")
local privs = minetest.get_player_privs(playerName)
local needUpdate = false
for priv in pairs(grant) do
if not privs[priv] then needUpdate = true end
privs[priv] = true
-- minetest.run_priv_callbacks(playerName, priv, playerName, "grant")
end
if needUpdate then
minetest.set_player_privs(playerName, privs)
-- print('TCL:: ~ file: init.lua ~ line 139 ~ grantPrivs', dump(privs));
hudcheck(playerName)
end
end
local function checkAnswer(playerName, fields, quiz)
local answer
local session = getSession(playerName)
if quiz and quiz.type == "select" and #quiz.options then
answer = {}
local options = fields._options
for i=1, #quiz.options do
if fields["opt" .. i] == "true" then
table.insert(answer, array.find(quiz.options, options[i]))
end
end
else
answer = fields and fields.answer
end
-- local playerName = aPlayer:get_player_name()
local result, errmsg = quizzes.check(playerName, answer, quiz)
-- print('TCL:: ~ file: checkAnswer.lua ~ line 235 ~ result', dump(result), dump(errmsg));
if errmsg then
if result then
-- all questions are answered!
if not session.allAnswered then
grantPriv(playerName)
minetest.chat_send_all(errmsg)
session.allAnswered = true
end
return true
else
revokePriv(playerName)
minetest.chat_send_player(playerName, errmsg)
end
else
-- local result, msg = quizzes.check(aPlayer, answer, quiz)
if result == true then
grantPriv(playerName)
minetest.chat_send_all(S("Congratuation @1, you got the answer!", playerName))
-- NodeItem CraftItem and ToolItem
local awards = settings.awards
if (#awards) then
local ix = math.random(#awards)
local v = givemeItem(playerName, awards[ix])
-- print("givemeItem to", playerName, dump(v))
end
return true
elseif answer and answer ~= "" then
minetest.chat_send_player(playerName, S("Hi, @1", playerName) .. "." ..
S("Sorry, the answer is not right, think it carefully")
)
elseif result == nil then
grantPriv(playerName)
return result
end
end
revokePriv(playerName)
return result, errmsg
end
local function openQuizView(playerName)
local player = minetest.get_player_by_name(playerName)
if player and player:get_hp() <= 0 then return end
-- if (not aPlayer) then return end
-- local playerName = aPlayer:get_player_name()
-- get the current quiz if no answer passed
local quiz, errmsg = checkAnswer(playerName)
local session = getSession(playerName)
session.startQuizTime = os.time()
if quiz and errmsg then return end
-- print('TCL:: ~ file: init.lua ~ line 112 ~ playerName', playerName);
local function on_close(state, player, fields)
local playerName = player:get_player_name()
local session = getSession(playerName)
state = getFields(playerName, session)
mergeTable(state, fields)
-- print('TCL:: ~ file: init.lua ~ line 364 ~ onclose player fields:', playerName, dump(fields));
-- print('TCL:: ~ file: init.lua ~ line 365 ~ onclose player state:', playerName, dump(state));
if fields.quit == minetest.FORMSPEC_SIGTIME then -- timeout reached
local vQuiz, vErrmsg = quizzes.getCurrent(playerName)
if vQuiz and not vErrmsg then
minetest.update_form(playerName, get_formspec(playerName, vQuiz, session))
return
end
-- local result = checkAnswer(playerName, fields.answer, vQuiz)
end
session.dialogClosed = true
if fields.quit == "true" then
local answerTime = os.time() - session.startQuizTime
session.answerTime = answerTime
if checkAnswer(playerName, state, quiz) then
session.extraDelay = (session.extraDelay or 0) + 60
end
session.fields = {}
end
-- minetest.update_form(playerName, get_formspec(playerName, quiz.title, quiz.desc))
-- elseif fields.answer and quiz.answer == fields.answer then
-- minetest.get_form_timer(playerName).stop()
-- minetest.chat_send_all("Cool, you are successful!")
-- else
-- minetest.get_form_timer(playerName).start(1)
-- end
end
if (type(quiz) == "table") then
session.dialogClosed = false
minetest.create_form(nil, playerName, get_formspec(playerName, quiz, session), on_close)
-- minetest.get_form_timer(playerName).start(1)
return true
end
end
quiz.openQuizView = openQuizView
local function checkGameTime(playerName)
local session = getSession(playerName)
local currTime = os.time()
local kickDelay = settings.kickDelay or 60
-- local lastJoinTime = joinTime[playerName] or 0
-- local checkInterval = settings.checkInterval
local lastLeavedTime = getLastLeavedTime(playerName) or currTime
local lastUsedTime = getUsedTime(playerName) or 0
local restTime = (settings.restTime or 0) * 60
local realRestTime = currTime - lastLeavedTime
-- print('TCL:: ~ file: init.lua ~ line 285 ~ register_on_joinplayer lastUsedTime', lastUsedTime);
session.totalPlayTime = settings.totalPlayTime
local leftPlayTime = settings.totalPlayTime * 60 - lastUsedTime
local leftRestTime = math.floor((restTime - realRestTime) / 60 + 0.5)
-- print("register_on_joinplayer:", playerName, settings.restTime, session.totalPlayTime, lastLeavedTime, restTime, leftRestTime)
if restTime > 0 and leftRestTime > 0 then
if leftPlayTime <= 0 then
minetest.chat_send_player(playerName, S("Hi, @1", playerName) .. ".\n" ..
S("The rest time is not over, please continue to rest your eyes.") .. "\n" ..
S("You have to rest for another @1 minutes.", leftRestTime) .. "\n" ..
S("You should quit game.") .. "\n" ..
S("It will automatically exit after @1 minute.", kickDelay / 60)
)
if session.kickJob then session.kickJob:cancel() end
session.kickJob = minetest.after(kickDelay, function()
kickPlayer(playerName, S("The rest time is not over, please continue to rest your eyes.") .. "\n" ..
S("You have to rest for another @1 minutes.", leftRestTime)
)
end)
return
end
else
leftPlayTime = settings.totalPlayTime * 60
end
-- print("checkGameTime", leftPlayTime, "seconds")
if leftPlayTime > 0 then
setUsedTime(playerName, 0)
if session.kickJob then session.kickJob:cancel() end
session.kickJob = minetest.after(leftPlayTime, function()
if isOnline(playerName) then
local restTimeMin = math.modf(restTime / 60)
local extraDelay = session.extraDelay or 0
minetest.chat_send_player(playerName, S("Hi, @1", playerName) .. ".\n" ..
S("Game time is over, please rest your eyes for at least @1 minutes.", restTimeMin) .. "\n" ..
S("You should quit game.") .. "\n" ..
S("It will automatically exit after @1 minute.", (kickDelay + extraDelay) / 60)
)
minetest.after(kickDelay + extraDelay, function()
kickPlayer(playerName, S("Game time is over, please rest your eyes for at least @1 minutes.", restTimeMin))
end)
end
end)
end
end
quiz.checkGameTime = checkGameTime
local function resetGameTime(playerName)
if playerName then
setLastLeavedTime(playerName, 0)
setUsedTime(playerName, 0)
local player = minetest.get_player_by_name(playerName)
if player then
local isAdmin = minetest.check_player_privs(player, "quiz")
if settings.forceAdminRest or not isAdmin then checkGameTime(playerName) end
end
end
end
quiz.resetGameTime = resetGameTime
minetest.register_on_joinplayer(function(player)
local playerName = player:get_player_name()
local isAdmin = minetest.check_player_privs(player, "quiz")
local session = getSession(playerName)
session.joinTime = os.time()
if settings.forceAdminRest or not isAdmin then checkGameTime(playerName) end
-- minetest.is_singleplayer()
if isAdmin then return end
local function doCheck()
local checkInterval = settings.checkInterval
-- print("doCheck interval:", checkInterval)
if (player) then hudcheck(playerName) end
local isNeedQuiz = type(playerName) == "string"
and not minetest.check_player_privs(playerName, "noquiz")
and (not minetest.check_player_privs(playerName, "quiz") or settings.forceAdminQuiz)
if (session.dialogClosed and isNeedQuiz) then openQuizView(playerName) end
if (checkInterval > 0) and isOnline(playerName) then
-- execute after checkInterval seconds
minetest.after(checkInterval, doCheck)
end
end
local delay = 0
if settings.immediateQuiz == false then
delay = (settings.idleInterval or 5) * 60 --> defaults to 5 min.
end
if (settings.checkInterval > 0) then
minetest.after(delay, doCheck)
else
minetest.after(delay, function()
hudcheck(playerName)
openQuizView(playerName)
end)
end
end)
minetest.register_on_leaveplayer(function(player)
local playerName = player:get_player_name()
resetWhenLeaving(playerName)
minetest.log("info", S("@1 has leaved", playerName))
minetest.chat_send_all(S("@1 has leaved", playerName))
end)
-- minetest.register_privilege(MOD_NAME, {
-- description = S("manage to quiz"),
-- give_to_singleplayer = false,
-- give_to_admin = false,
-- on_grant = hudcheck,
-- on_revoke = hudcheck
-- })
-- register chat commands
dofile(MOD_PATH.."chat_cmds.lua")