diff options
Diffstat (limited to 'ch_core/events.lua')
-rw-r--r-- | ch_core/events.lua | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/ch_core/events.lua b/ch_core/events.lua new file mode 100644 index 0000000..d12ee6a --- /dev/null +++ b/ch_core/events.lua @@ -0,0 +1,531 @@ +ch_core.open_submod("events", {chat = true, data = true, lib = true, privs = true}) + +local safety_limit = 256 +local worldpath = minetest.get_worldpath() + +local event_types = {} +ch_core.event_types = event_types + +local sendable_event_types_cache = {--[[ + [hour_id] = { + [player_name] = {string, ...}, + } +]]} + +local function get_timestamp() + local cas = ch_time.aktualni_cas() + return cas:YYYY_MM_DD_HH_MM_SS(), cas +end + +local function timestamp_to_date(timestamp) + return string.sub(timestamp, 1, 10) +end + +local function timestamp_to_month_id(timestamp) + return string.sub(timestamp, 1, 7) +end + +local function timestamp_to_hour_id(timestamp) + return string.sub(timestamp, 1, 13) +end + +local function is_events_admin(pinfo) + return pinfo.role == "admin" or pinfo.privs.ch_events_admin +end + +local events_month_id -- ID aktuálního měsíce (aktualizuje se ve funkci add_event()) +local events = function() + local cas = ch_time.aktualni_cas() + local rok, mesic = cas.rok, cas.mesic + local result, result_month_id = {}, string.format("%04d-%02d", rok, mesic) + for i = 1, 3 do + local month_id = string.format("%04d-%02d", rok, mesic) + local filename = worldpath.."/events_"..month_id..".json" + local f = io.open(filename) + local data + if f then + data = f:read("*a") + f:close() + if data ~= nil then + data = assert(minetest.parse_json(data)) + end + if data == nil then + minetest.log("warning", "Loading events data from "..filename.." failed!") + end + end + result[i] = data or {} + minetest.log("info", "[ch_core/events] Loaded "..#result[i].." events for "..month_id) + if mesic > 1 then + mesic = mesic - 1 + else + mesic, rok = 12, rok - 1 + end + end + return result, result_month_id +end +events, events_month_id = events() +--[[ + -- index = 1 pro aktuální měsíc, 2 pro předchozí a 3 pro ten před ním + events[index] = { + -- pole je seřazené od nejstarší události po nejnovější + { + timestamp = STRING, -- časová známka události (úplná) + type = STRING, -- typ události (podle pole event_types) + text = STRING || nil, -- text události (značku {PLAYER} je potřeba před zobrazením nahradit jménem podle player_name) + -- je-li nil, použije se default_text podle typu události + player_name = STRING || nil, -- postava, které se událost týká (je-li) + }... + } +]] + +local function access_events(month_id) + if month_id ~= events_month_id then + minetest.log("warning", "Will open a new event month "..month_id.."!") + table.insert(events, 1, {}) + events_month_id = month_id + end + return events[1] +end + +local function save_events() + local filename = worldpath.."/events_"..events_month_id..".json" + local data, error_message = minetest.write_json(events[1]) + if data == nil then + error("save_events(): serialization error: "..tostring(error_message or "nil")) + end + minetest.safe_file_write(filename, data) +end + +local function get_event_text(event, type_def) + local event_text = event.text + if event_text == nil then + if type_def == nil then + return nil -- unknown event type + end + event_text = type_def.default_text or "[Bez textu]" + end + if event.player_name then + if type_def ~= nil and type_def.prepend_viewname then + event_text = ch_core.prihlasovaci_na_zobrazovaci(event.player_name)..": "..event_text + else + event_text = event_text:gsub("{PLAYER}", ch_core.prihlasovaci_na_zobrazovaci(event.player_name)) + end + end + return event_text +end + +--[[ + event_def = { + access = "public" or "admin" or "players" or "player_only" or "discard" or "player_and_admin", + description = string, -- popis typu události pro zobrazení ve formspecech a v četu + + color = "#RRGGBB" or nil, -- barva pro zobrazení události + default_text = string or nil, -- text události, který se použije, pokud nebude zadán žádný text ve volání ch_core.add_event() + chat_access = "public" or "admin" or "players" or "player_only" or or "player_and_admin" nil, -- komu se má událost vypsat v četu, bude-li online + send_access = "admin" or "players" or nil, -- kdo může událost ručně odeslat z dialogového okna + delete_access = "player_only" or nil, -- kdo kromě postav s právem ch_events_admin může událost smazat + + ignore = bool or nil, -- je-li true, budou události tohoto typu tiše ignorovány; v případě načtení ze souboru nebudou vypisovány + prepend_viewname = bool or nil, -- je-li true, značka "{PLAYER}" v textu události nebude rozpoznávána; namísto toho se + -- před text vloží zobrazovací jméno postavy oddělené ": " + } +]] +function ch_core.register_event_type(event_type, event_def) + if event_types[event_type] ~= nil then + error("Event type "..event_type.." is already registered.") + end + local access = event_def.access + if access ~= "public" and access ~= "admin" and access ~= "players" and access ~= "player_only" and access ~= "discard" and + access ~= "player_and_admin" + then + error("Invalid event access keyword: "..event_def.access) + end + local def = { + access = access, + color = event_def.color or "#ffffff", + description = assert(event_def.description), + } + if event_def.default_text ~= nil then + def.default_text = event_def.default_text + end + if event_def.ignore then + def.ignore = true + else + def.ignore = false + end + if event_def.prepend_viewname then + def.prepend_viewname = true + end + if event_def.chat_access ~= nil then + if event_def.chat_access == "admin" or event_def.chat_access == "players" or event_def.chat_access == "public" or + event_def.chat_access == "player_only" or event_def.chat_access == "player_and_admin" + then + def.chat_access = event_def.chat_access + else + minetest.log("warning", "Unsupported 'chat_access' value '"..event_def.chat_access.."' ignored!") + end + end + if event_def.send_access ~= nil then + if event_def.send_access == "admin" or event_def.send_access == "players" then + def.send_access = event_def.send_access + else + minetest.log("warning", "Unsupported 'send_access' value '"..event_def.send_access.."' ignored!") + end + end + if event_def.delete_access ~= nil then + if event_def.delete_access == "player_only" then + def.delete_access = event_def.delete_access + else + minetest.log("warning", "Unsupported 'delete_access' value '"..event_def.delete_access.."' ignored!") + end + end + event_types[event_type] = def +end + +function ch_core.add_event(event_type, text, player_name) + local type_def = event_types[event_type] + if type_def == nil then + minetest.log("warning", "Event added with unknown type: "..dump2({event_type = event_type, text = text, player_name = player_name})) + return + end + if type_def.ignore then + return -- ignore this event + end + if text ~= nil and #text > 1024 then + text = ch_core.utf8_truncate_right(text, 1024) + end + local timestamp = get_timestamp() + local month_id = timestamp_to_month_id(timestamp) + local event = { + timestamp = timestamp, + type = event_type, + text = text, + player_name = player_name, + } + if text == nil and type_def.default_text == nil then + minetest.log("warning", "Event of type "..event_type.." added with no text!") + end + if type_def.access ~= "discard" then + local current_events = assert(access_events(month_id)) + + -- Check safety limit: + if #current_events >= safety_limit then + local count = 0 + for i = #current_events, 1, -1 do + if current_events[i].type == event_type then + count = count + 1 + if count >= safety_limit then + minetest.log("warning", "Event NOT added, because the security limit "..safety_limit.." was achieved: ".. + timestamp.." <"..event_type.."> "..(get_event_text(event, type_def) or "nil")) + return + end + end + end + end + + table.insert(current_events, event) + save_events() + minetest.log("action", "Event #"..#current_events.." added: "..timestamp.." <"..event_type.."> "..(get_event_text(event, type_def) or "nil")) + + -- remove from sendable events cache: + if player_name ~= nil and is_events_admin(ch_core.normalize_player(player_name)) then + local cache = sendable_event_types_cache[timestamp_to_date(timestamp)] + if cache ~= nil then + cache = cache[player_name] + if cache ~= nil then + local i = table.indexof(cache, event_type) + if i ~= -1 then + table.remove(cache, i) + end + end + end + end + end + + -- send to chat + if type_def.chat_access ~= nil then + local players_to_send = {} + if type_def.chat_access == "player_only" then + if player_name ~= nil and ch_data.online_charinfo[player_name] ~= nil then + table.insert(players_to_send, player_name) + end + elseif type_def.chat_access == "players" then + for _, oplayer in ipairs(minetest.get_connected_players()) do + if ch_core.get_player_role(oplayer) ~= "new" then + table.insert(players_to_send, oplayer:get_player_name()) + end + end + elseif type_def.chat_access == "public" then + for _, oplayer in ipairs(minetest.get_connected_players()) do + table.insert(players_to_send, oplayer:get_player_name()) + end + elseif type_def.chat_access == "admin" then + for _, oplayer in ipairs(minetest.get_connected_players()) do + local opinfo = ch_core.normalize_player(oplayer) + if is_events_admin(opinfo) then + table.insert(players_to_send, opinfo.player_name) + end + end + elseif type_def.chat_access == "player_and_admin" then + for _, oplayer in ipairs(minetest.get_connected_players()) do + local opinfo = ch_core.normalize_player(oplayer) + if opinfo.player_name == player_name or is_events_admin(opinfo) then + table.insert(players_to_send, opinfo.player_name) + end + end + end + local message_to_send = {} + if type_def.color ~= nil then + table.insert(message_to_send, minetest.get_color_escape_sequence(type_def.color)) + end + table.insert(message_to_send, get_event_text(event, type_def).." "..ch_core.colors.light_gray.."<"..type_def.description..">") + message_to_send = table.concat(message_to_send) + for _, oplayer_name in ipairs(players_to_send) do + ch_core.systemovy_kanal(oplayer_name, message_to_send) + end + end +end + +--[[ + Vrací prostý seznam typů událostí, k nimž zadaná postava může mít přístup a pro které + dotaz do negative_set vyjde „pravdivý“. + player_name_or_player = PlayerRef or string, + negative_set = table or nil, + => {string, ...} +]] +function ch_core.get_event_types_for_player(player_name_or_player, negative_set) + if negative_set == nil then negative_set = {} end + local pinfo = ch_core.normalize_player(player_name_or_player) + local has_access_rights = { + public = true, + players = pinfo.role ~= "new", + admin = is_events_admin(pinfo), + player_only = true, + player_and_admin = true, + discard = false, + } + local result = {} + for event_type, type_def in pairs(event_types) do + if not negative_set[event_type] and has_access_rights[type_def.access] then + table.insert(result, event_type) + end + end + return result +end + +function ch_core.get_sendable_event_types_for_player(player_name_or_player) + local pinfo = ch_core.normalize_player(player_name_or_player) + local timestamp = get_timestamp() + local hour_id = timestamp_to_hour_id(timestamp) + local player_name = pinfo.player_name + local is_admin = is_events_admin(pinfo) + + local cache = sendable_event_types_cache[hour_id] + if cache == nil then + -- discard/renew cache + cache = {} + sendable_event_types_cache = {[hour_id] = cache} + end + local result = cache[player_name] + if result ~= nil then + return result -- cache hit + end + result = {} + if is_admin then + for event_type, type_def in pairs(ch_core.event_types) do + if type_def.send_access == "players" or type_def.send_access == "admin" then + table.insert(result, event_type) + end + end + elseif pinfo.role ~= "new" then -- nové postavy nemohou odesílat nic + local used_types_set = {} + for _, event in ipairs(events[events_month_id] or {}) do + if event.player_name == player_name and timestamp_to_hour_id(event.timestamp) == hour_id then + used_types_set[event.type] = true + end + end + for event_type, type_def in pairs(ch_core.event_types) do + if type_def.send_access == "players" and not used_types_set[event_type] then + table.insert(result, event_type) + end + end + end + -- seřadit: + if #result > 1 then + table.sort(result, function(et_a, et_b) + if et_a == et_b then return false end + if et_a == "custom" then return true end -- "custom" na začátek + if et_b == "custom" then return false end + return ch_core.utf8_mensi_nez(ch_core.event_types[et_a].description, ch_core.event_types[et_b].description, false) + end) + end + cache[player_name] = result + return result +end + +--[[ + Vrací pole prvků od nejnovější události po nejstarší. Každý prvek má strukturu: + { + id = STRING or nil, -- ID události, které umožňuje ji smazat (závisí na právech postavy) + time = STRING, -- časová známka v podobě, jak má být prezentována hráči/ce + description = STRING, -- označení typu zprávy + color = "#RRGGBB", -- barva pro zobrazení události, vždy ve formátu #RRGGBB + text = STRING, -- text, jak má být prezentován hráči/ce, nebo prázdný řetězec + } + + player_name_or_player -- jméno nebo PlayerRef + allowed_event_types -- seznam typů událostí, které vyfiltrovat, nebo nil pro všechny typy + limit -- maximální počet událostí, které vrátit; musí být uvedeno + date_limit -- datum ve formátu YYYY-MM-DD nebo nil; je-li vyplněno, vrátí jen událost z daného dne nebo novější +]] +function ch_core.get_events_for_player(player_name_or_player, allowed_event_types, limit, date_limit) + local pinfo = ch_core.normalize_player(player_name_or_player) + local result = {} + local has_access_rights = { + public = true, + players = pinfo.role ~= "new", + admin = is_events_admin(pinfo), + player_only = true, -- bude testováno samostatně + player_and_admin = true, -- bude testováno samostatně + discard = false, + } + local event_types_filtered = {} + if allowed_event_types ~= nil then + for _, event_type in ipairs(allowed_event_types) do + local type_def = event_types[event_type] + if type_def ~= nil and has_access_rights[type_def.access] then + event_types_filtered[event_type] = type_def + end + end + else + for event_type, type_def in pairs(event_types) do + if has_access_rights[type_def.access] then + event_types_filtered[event_type] = type_def + end + end + end + for events_month_iter, events_month in ipairs(events) do + for i = #events_month, 1, -1 do + local event = events_month[i] + local type_def = event_types_filtered[event.type] + if + type_def ~= nil and + not type_def.ignore and + (type_def.access ~= "player_only" or event.player_name == pinfo.player_name) and + (type_def.access ~= "player_and_admin" or event.player_name == pinfo.player_name or has_access_rights.admin) + then + local record = { + time = assert(event.timestamp), -- TODO... + description = assert(type_def.description), + text = get_event_text(event, type_def) or "", + color = assert(type_def.color), + } + if date_limit ~= nil and timestamp_to_date(event.timestamp) < date_limit then + return result -- event too old + end + if events_month_iter == 1 and + (has_access_rights.admin or + (type_def.delete_access == "player_only" and + event.player_name == pinfo.player_name and + timestamp_to_date(event.timestamp) == timestamp_to_date(get_timestamp()))) + then + record.id = tostring(i) + end + table.insert(result, record) + if #result >= limit then + return result + end + end + end + end + return result +end + +--[[ + id = STRING -- ID oznámení k smazání (podle záznamu z funkce ch_core.get_events_for_player() ) + descripton = STRING or nil -- je-li vyplněno, oznámení se smaže jen tehdy, pokud jeho popis přesně odpovídá zadanému + player_name = STRING or nil, -- je-li vyplněno, oznámení se smaže jen tehdy, pokud jeho player_name přesně odpovídá zadanému + vrací: true, pokud bylo oznámení úspěšně smazáno; false, pokud mazání selhalo +]] +function ch_core.remove_event(id, description, player_name) + minetest.log("warning", "ch_core.remove_event(): an event is going to be deleted: "..dump2({id = id, description = description})) + local index = tonumber(id) + local events_month = events[1] + if events_month == nil or index == nil or index < 1 or index > #events_month then + minetest.log("error", "ch_core.remove_event(): failed, because of invalid index "..tostring(id).." (#events_month = "..#events_month..")") + return false + end + local event = events_month[index] + if event == nil then + minetest.log("error", "ch_core.remove_event(): failed, because event not found") + return false + end + if description ~= nil then + local type_def = event_types[event.type] + if type_def == nil or type_def.description ~= description then + minetest.log("error", "ch_core.remove_event(): failed, because description does not match") + return false + end + end + if player_name ~= nil and event.player_name ~= player_name then + minetest.log("error", "ch_core.remove_event(): failed, because player_name does not match") + return false + end + table.remove(events_month, index) + save_events() + minetest.log("action", "Event removed: "..dump2(event)) + return true +end + +-- Hlášení chyb +ch_core.register_event_type("bug", { + access = "player_and_admin", + description = "hlášení chyby (/chyba)", + chat_access = "player_and_admin", + color = "#cccccc", +}) + +function ch_core.overridable.chyba_handler(player, text) + local pos = vector.round(player:get_pos()) + ch_core.add_event("bug", minetest.pos_to_string(pos).." "..text, player:get_player_name()) +end + +minetest.register_chatcommand("chyba", { + params = "<popis chyby>", + privs = {ch_registered_player = true}, + description = "Slouží k nahlášení chyby ve hře. Hlášení uvidíte jen vy a Administrace.", + func = function(player_name, param) + local player = minetest.get_player_by_name(player_name) + if player == nil then + return false, "Vnitřní chyba při nahlašování. Nahlašte prosím chybu jiným způsobem (např. herní poštou)." + end + ch_core.overridable.chyba_handler(player, param) + return true, "Chyba byla nahlášena. Děkujeme. Hlášením chyb pomáháte zlepšovat tento server." + end, +}) + +-- Událost "server_started" + +ch_core.register_event_type("server_started", { + description = "server spuštěn", + access = "admin", + color = "#aaaaaa", + default_text = "", +}) + +ch_core.register_event_type("server_shutdown", { + description = "server vypnut", + access = "admin", + color = "#aaaaaa", + default_text = "", +}) + +minetest.register_on_mods_loaded(function() + ch_core.add_event("server_started") +end) + +minetest.register_on_shutdown(function() + ch_core.add_event("server_shutdown") +end) + +ch_core.close_submod("events") |