aboutsummaryrefslogtreecommitdiff
path: root/ch_core/events.lua
diff options
context:
space:
mode:
Diffstat (limited to 'ch_core/events.lua')
-rw-r--r--ch_core/events.lua531
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")