diff options
103 files changed, 15827 insertions, 0 deletions
diff --git a/ch_base/init.lua b/ch_base/init.lua new file mode 100644 index 0000000..78c6bb1 --- /dev/null +++ b/ch_base/init.lua @@ -0,0 +1,213 @@ +ch_base = {} + +local get_us_time = core.get_us_time +local n = 0 +local open_modname +local us_openned +local orig_node_count +local orig_memory +local enable_performance_logging = core.settings:get_bool("ch_enable_performance_logging", false) + +local function ifthenelse(a, b, c) + if a then return b else return c end +end + +--[[ +local function count_nodes() + local t = core.registered_nodes + local name = next(t) + local count = 0 + while name ~= nil do + count = count + 1 + name = next(t, name) + end + return count +end +]] + +local function count_nodes() + return 0 +end + +function ch_base.open_mod(modname) + if open_modname ~= nil then + error("ch_base.open_mod(): mod "..tostring(open_modname).." is already openned!") + end + open_modname = assert(modname) + us_openned = get_us_time() + orig_node_count = count_nodes() + orig_memory = collectgarbage("count") + n = n + 1 + print("["..modname.."] init started ("..n..")") +end + +function ch_base.close_mod(modname) + if open_modname ~= modname then + error("ch_base.close_mod(): mod "..tostring(modname).." is not openned!") + end + local diff = math.ceil((get_us_time() - us_openned) / 1000.0) + local memory_now = collectgarbage("count") + local memory_diff = memory_now - orig_memory + memory_diff = string.format(ifthenelse(memory_diff >= 0, "+%.3f kB", "%.3f kB"), memory_diff) + local msg = "["..modname.."] init finished ("..n..")" + if diff >= 10 then + msg = msg.." in "..diff.." ms" + end + local new_nodes = count_nodes() - orig_node_count + if new_nodes > 0 then + msg = msg.." +"..new_nodes.." nodes" + end + msg = msg.." "..memory_diff.."." + print(msg) + open_modname = nil + us_openned = nil +end + +function ch_base.mod_checkpoint(name) + if open_modname == nil then + error("ch_base.mod_checkpoint() outside of mod initialization!") + end + print("["..open_modname.."] checkpoint "..(name or "nil").." at "..math.ceil((get_us_time() - us_openned) / 1000.0).." ms") +end + +if enable_performance_logging then +-- Performance watching: +-- * globalstep +-- * ABM +-- * LBM +-- * entity on_step +-- * unmeasured time + +local counters = {} +local measured = {} -- key => {count = int, time = long} +local global_measure_start +local measure_start, measure_stop +local measure_active = false + +local function run_and_measure(label, f, ...) + if measure_active then + return f(...) + end + measure_start = get_us_time() + if global_measure_start == nil then + global_measure_start = measure_start + end + measure_active = true + local result = f(...) + measure_active = false + measure_stop = get_us_time() + local m = measured[label] + if m == nil then + measured[label] = {count = 1, time = measure_stop - measure_start, maxtime = measure_stop - measure_start} + else + m.count = m.count + 1 + m.time = m.time + (measure_stop - measure_start) + if m.time > m.maxtime then + m.maxtime = m.time + end + end + return result +end + +local function acquire_label(label_type) + local modname = open_modname or "nil" + local counter = counters[modname..":"..label_type] or 1 + counters[modname..":"..label_type] = counter + 1 + return modname..":"..label_type..":"..counter +end + +local orig_register_globalstep = assert(core.register_globalstep) +local orig_register_lbm = assert(core.register_lbm) +local orig_register_abm = assert(core.register_abm) +local orig_register_entity = assert(core.register_entity) + +function core.register_globalstep(func) + assert(func) + local label = acquire_label("globalstep") + return orig_register_globalstep(function(...) + return run_and_measure(label, func, ...) + end) +end + +function core.register_lbm(def, ...) + assert(type(def) == "table") + local orig_action = def.action + if type(orig_action) ~= "function" then + return orig_register_lbm(def, ...) + end + local label = acquire_label("lbm") + local new_def = {} + for k, v in pairs(def) do + new_def[k] = v + end + new_def.action = function(...) + return run_and_measure(label, orig_action, ...) + end + return orig_register_lbm(new_def, ...) +end + +function core.register_abm(def, ...) + assert(type(def) == "table") + local orig_action = def.action + assert(orig_action) + local label = acquire_label("abm") + local new_def = {} + for k, v in pairs(def) do + new_def[k] = v + end + new_def.action = function(...) + return run_and_measure(label, orig_action, ...) + end + return orig_register_abm(new_def, ...) +end + +function core.register_entity(name, def, ...) + assert(type(name) == "string") + assert(type(def) == "table") + local orig_on_step = def.on_step + if orig_on_step ~= nil then + local label = (open_modname or "nil")..":entity_on_step:["..name.."]" + local new_def = { + on_step = function(...) + return run_and_measure(label, orig_on_step, ...) + end, + } + setmetatable(new_def, {__index = def}) + def = new_def + end + return orig_register_entity(name, def, ...) +end + +local def = { + params = "", + description = "vypíše do logu naměřené údaje o výkonu", + privs = {server = true}, + func = function(player_name, param) + if not global_measure_start then + return false, "měření nezačalo" + end + local now = get_us_time() + local m = {} + local total_time = 0 + for label, data in pairs(measured) do + table.insert(m, {label, data}) + total_time = total_time + data.time + end + table.sort(m, function(a, b) return a[2].time > b[2].time end) + local result = {"Performance log ["..#m.." items]:"} + for _, package in ipairs(m) do + local label, data = package[1], package[2] + table.insert(result, string.format("- %s = %0.3f ms (c=%d) maxtime=%0.3f ms", + label, math.ceil(data.time / 1000.0), data.count, math.ceil(data.maxtime / 1000.0))) + end + table.insert(result, string.format("==============\ntotal measured: %0.3f ms\ntotal unmeasured: %0.3f", + math.ceil(total_time / 1000.0), math.ceil((now - global_measure_start - total_time) / 1000.0))) + result = table.concat(result, "\n") + core.log("action", result) + return true, "vypsáno" + end, +} + +core.register_chatcommand("vykon", def) +core.register_chatcommand("výkon", def) +end -- if enable_performance_logging diff --git a/ch_base/license.txt b/ch_base/license.txt new file mode 100644 index 0000000..0ee2bd3 --- /dev/null +++ b/ch_base/license.txt @@ -0,0 +1,24 @@ +License of source code +---------------------- + +The MIT License (MIT) +Copyright (C) 2024 Singularis + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +For more details: +https://opensource.org/licenses/MIT diff --git a/ch_base/mod.conf b/ch_base/mod.conf new file mode 100644 index 0000000..91a530d --- /dev/null +++ b/ch_base/mod.conf @@ -0,0 +1,2 @@ +name = ch_base +description = Some base functions for Český hvozd diff --git a/ch_core/.luacheckrc b/ch_core/.luacheckrc new file mode 100644 index 0000000..3087582 --- /dev/null +++ b/ch_core/.luacheckrc @@ -0,0 +1,160 @@ +allow_defined_top = true +max_line_length = 1024 +ignore = {"212"} + +globals = { + builtin_overrides = {fields = {"login_to_viewname"}}, + "ch_bank", + ch_core = {fields = { + "aktualni_cas", + "formspec_header", + "cancel_ch_timer", + "close_formspec", + "get_player_role", + "hotovost", + "ifthenelse", + "je_pryc", + "je_ve_vykonu_trestu", + "jmeno_na_prihlasovaci", + "precist_hotovost", + "prihlasovaci_na_zobrazovaci", + "set_temporary_titul", + "show_formspec", + "soukroma_zprava", + "start_ch_timer", + "systemovy_kanal", + "update_formspec", + }}, + ch_data = {fields = { + "correct_player_name_casing", + "delete_offline_charinfo", + "get_joining_online_charinfo", + "get_leaving_online_charinfo", + "get_offline_charinfo", + "get_or_add_offline_charinfo", + "save_offline_charinfo", + "should_show_help", + initial_offline_charinfo = { + read_only = false, + other_fields = true, + }, + is_acceptable_name = { + read_only = false, + }, + online_charinfo = { + read_only = false, + other_fields = true, + }, offline_charinfo = { + read_only = false, + other_fields = true, + }, + }}, + doors = {fields = { + "get", "login_to_viewname", "register_fencegate" + }}, + player_api = {fields = {"player_attached"}}, +} + + +read_globals = { + ch_base = {fields = { + "open_mod", "close_mod" + }}, + ch_time = {fields = { + "aktualni_cas", + "get_time_speed_during_day", + "get_time_speed_during_night", + "herni_cas_nastavit", + "set_time_speed_during_day", + "set_time_speed_during_night", + }}, + default = {fields = { + "can_interact_with_node", + "register_fence", + "register_fence_rail", + "register_mesepost", + "node_sound_stone_defaults", + }}, + screwdriver = {fields = {"ROTATE_FACE", "ROTATE_AXIS", handler = {read_only = false}}}, + math = {fields = {"ceil", "floor", "round"}}, + minetest = { + fields = { + "CONTENT_AIR", "CONTENT_IGNORE", "CONTENT_UNKNOWN", "EMERGE_CANCELLED", "EMERGE_ERRORED", "EMERGE_FROM_DISK", "EMERGE_FROM_MEMORY", "EMERGE_GENERATED", "LIGHT_MAX", "MAP_BLOCKSIZE", + "PLAYER_MAX_BREATH_DEFAULT", "PLAYER_MAX_HP_DEFAULT", "add_entity", "add_item", "add_node", "add_node_level", "add_particle", "add_particlespawner", "after", "async_event_handler", + "async_jobs", "auth_reload", "ban_player", "builtin_auth_handler", "bulk_set_node", "calculate_knockback", "callback_origins", "cancel_shutdown_requests", "chat_send_all", + "chat_send_player", "chatcommands", "check_for_falling", "check_password_entry", "check_player_privs", "check_single_for_falling", "clear_craft", "clear_objects", + "clear_registered_biomes", "clear_registered_decorations", "clear_registered_ores", "clear_registered_schematics", "close_formspec", "colorize", "colorspec_to_bytes", + "colorspec_to_colorstring", "compare_block_status", "compress", "cpdir", "craft_predict", "craftitemdef_default", "create_detached_inventory", "create_detached_inventory_raw", + "create_schematic", "debug", "decode_base64", "decompress", "delete_area", "delete_particlespawner", "deserialize", "detached_inventories", "dig_node", "dir_to_facedir", + "dir_to_fourdir", "dir_to_wallmounted", "dir_to_yaw", "disconnect_player", "do_async_callback", "do_item_eat", "dynamic_add_media", "dynamic_media_callbacks", + "emerge_area", "encode_base64", "encode_png", "env", "error_handler", "explode_scrollbar_event", "explode_table_event", "explode_textlist_event", "facedir_to_dir", + "features", "find_node_near", "find_nodes_in_area", "find_nodes_in_area_under_air", "find_nodes_with_meta", "find_path", "fix_light", "forceload_block", + "forceload_free_block", "format_chat_message", "formspec_escape", "fourdir_to_dir", "generate_decorations", "generate_ores", "get_all_craft_recipes", + "get_artificial_light", "get_auth_handler", "get_background_escape_sequence", "get_ban_description", "get_ban_list", "get_biome_data", "get_biome_id", + "get_biome_name", "get_builtin_path", "get_color_escape_sequence", "get_connected_players", "get_content_id", "get_craft_recipe", "get_craft_result", + "get_current_modname", "get_day_count", "get_decoration_id", "get_dig_params", "get_dir_list", "get_game_info", "get_gametime", "get_gen_notify", + "get_globals_to_transfer", "get_heat", "get_hit_params", "get_humidity", "get_inventory", "get_item_group", "get_last_run_mod", "get_mapgen_edges", + "get_mapgen_object", "get_mapgen_params", "get_mapgen_setting", "get_mapgen_setting_noiseparams", "get_meta", "get_mod_storage", "get_modnames", + "get_modpath", "get_name_from_content_id", "get_natural_light", "get_node", "get_node_drops", "get_node_group", "get_node_level", "get_node_light", + "get_node_max_level", "get_node_or_nil", "get_node_timer", "get_noiseparams", "get_objects_in_area", "get_objects_inside_radius", "get_password_hash", + "get_perlin", "get_perlin_map", "get_player_by_name", "get_player_information", "get_player_ip", "get_player_privs", "get_player_radius_area", + "get_player_window_information", "get_pointed_thing_position", "get_position_from_hash", "get_server_max_lag", "get_server_status", + "get_server_uptime", "get_spawn_level", "get_timeofday", "get_tool_wear_after_use", "get_translated_string", "get_translator", + "get_us_time", "get_user_path", "get_version", "get_voxel_manip", "get_worldpath", "global_exists", "handle_async", "handle_node_drops", + "has_feature", "hash_node_position", "hud_replace_builtin", "inventorycube", "is_area_protected", "is_colored_paramtype", "is_creative_enabled", + "is_nan", "is_player", "is_protected", "is_singleplayer", "is_yes", "item_eat", "item_pickup", "item_place", "item_place_node", + "item_place_object", "item_secondary_use", "itemstring_with_color", "itemstring_with_palette", "kick_player", "line_of_sight", "load_area", + "log", "luaentities", "mkdir", "mod_channel_join", "mvdir", "node_dig", "node_punch", "nodedef_default", "noneitemdef_default", + "notify_authentication_modified", "object_refs", "on_craft", "override_chatcommand", "override_item", "parse_coordinates", "parse_json", + "parse_relative_number", "place_node", "place_schematic", "place_schematic_on_vmanip", "player_exists", "pointed_thing_to_face_pos", + "pos_to_string", "privs_to_string", "punch_node", "raillike_group", "raycast", "read_schematic", "record_protection_violation", + "register_abm", "register_alias", "register_alias_force", "register_allow_player_inventory_action", "register_async_dofile", + "register_authentication_handler", "register_biome", "register_can_bypass_userlimit", "register_chatcommand", "register_craft", + "register_craft_predict", "register_craftitem", "register_decoration", "register_entity", "register_globalstep", "register_item", + "register_lbm", "register_node", "register_on_auth_fail", "register_on_authplayer", "register_on_chat_message", "register_on_chatcommand", + "register_on_cheat", "register_on_craft", "register_on_dieplayer", "register_on_dignode", "register_on_generated", "register_on_item_eat", + "register_on_item_pickup", "register_on_joinplayer", "register_on_leaveplayer", "register_on_liquid_transformed", "register_on_mapblocks_changed", + "register_on_mapgen_init", "register_on_modchannel_message", "register_on_mods_loaded", "register_on_newplayer", "register_on_placenode", + "register_on_player_hpchange", "register_on_player_inventory_action", "register_on_player_receive_fields", "register_on_prejoinplayer", + "register_on_priv_grant", "register_on_priv_revoke", "register_on_protection_violation", "register_on_punchnode", "register_on_punchplayer", + "register_on_respawnplayer", "register_on_rightclickplayer", "register_on_shutdown", "register_ore", "register_playerevent", "register_privilege", + "register_schematic", "register_tool", "registered_abms", "registered_aliases", "registered_allow_player_inventory_actions", "registered_biomes", + "registered_can_bypass_userlimit", "registered_chatcommands", "registered_craft_predicts", "registered_craftitems", "registered_decorations", + "registered_entities", "registered_globalsteps", "registered_items", "registered_lbms", "registered_nodes", "registered_on_authplayers", + "registered_on_chat_messages", "registered_on_chatcommands", "registered_on_cheats", "registered_on_crafts", "registered_on_dieplayers", + "registered_on_dignodes", "registered_on_generateds", "registered_on_item_eats", "registered_on_item_pickups", "registered_on_joinplayers", + "registered_on_leaveplayers", "registered_on_liquid_transformed", "registered_on_mapblocks_changed", "registered_on_modchannel_message", + "registered_on_mods_loaded", "registered_on_newplayers", "registered_on_placenodes", "registered_on_player_hpchange", + "registered_on_player_hpchanges", "registered_on_player_inventory_actions", "registered_on_player_receive_fields", + "registered_on_prejoinplayers", "registered_on_priv_grant", "registered_on_priv_revoke", "registered_on_protection_violation", + "registered_on_punchnodes", "registered_on_punchplayers", "registered_on_respawnplayers", "registered_on_rightclickplayers", + "registered_on_shutdown", "registered_ores", "registered_playerevents", "registered_privileges", "registered_tools", + "remove_detached_inventory", "remove_detached_inventory_raw", "remove_node", "remove_player", "remove_player_auth", + "request_http_api", "request_insecure_environment", "request_shutdown", "rgba", "rmdir", "rollback_get_last_node_actor", + "rollback_get_node_actions", "rollback_punch_callbacks", "rollback_revert_actions_by", "rotate_and_place", "rotate_node", + "run_callbacks", "run_priv_callbacks", "safe_file_write", "serialize", + "serialize_roundtrip", "serialize_schematic", "set_gen_notify", "set_last_run_mod", "set_mapgen_params", "set_mapgen_setting", + "set_mapgen_setting_noiseparams", "set_node", "set_node_level", "set_noiseparams", "set_player_password", "set_player_privs", + "set_timeofday", "setting_get", "setting_get_pos", "setting_getbool", "setting_save", "setting_set", "setting_setbool", + "settings", "sha1", "show_formspec", "show_general_help_formspec", "show_privs_help_formspec", "sound_fade", + "sound_play", "sound_stop", "spawn_falling_node", "spawn_item", "spawn_tree", "string_to_area", "string_to_pos", + "string_to_privs", "strip_background_colors", "strip_colors", "strip_foreground_colors", "strip_param2_color", + "swap_node", "tooldef_default", "transforming_liquid_add", "translate", "unban_player_or_ip", "unregister_biome", + "unregister_chatcommand", "unregister_item", "urlencode", "wallmounted_to_dir", "wrap_text", "write_json", "yaw_to_dir", + item_drop = {read_only = false}, + send_join_message = {read_only = false}, + send_leave_message = {read_only = false}, + }, + }, + string = {fields = {"split", "sub"}}, + table = {fields = {"copy", "indexof", "insert_all", "key_value_swap"}}, + vector = {fields = {"angle", "copy", "distance", "equals", "multiply", "new", "offset", "rotate", "round", "subtract", "to_string", "zero"}}, + + "AreaStore", "dump2", "emote", "hb", "ItemStack", "player_api", "wielded_light" +} +read_globals.core = read_globals.minetest + +files["ap.lua"].ignore = {"_max_xp"} +files["data.lua"].ignore = {"past_playtime"} +files["trade.lua"].ignore = {"_trade_state"} +files["teleportace.lua"].ignore = {"_old_pos"} diff --git a/ch_core/active_objects.lua b/ch_core/active_objects.lua new file mode 100644 index 0000000..45d80a7 --- /dev/null +++ b/ch_core/active_objects.lua @@ -0,0 +1,163 @@ +ch_core.open_submod("active_objects", {}) + +local objects = {} +-- [name] = {{/*current:*/ [key] = ObjRef, ...}, {/*prev*/ [key] = ObjRef, ...}} + +local function push_active_object(name, obj) + local key = tostring(obj) + local t = objects[name] + if t == nil then + objects[name] = {{[key] = obj}, {}} + else + t[1][key] = obj + end +end + +local function on_step_default(self) + push_active_object(assert(self.name), assert(self.object)) +end + +local function try_append(dst, src) + for k, v in pairs(src) do + if dst[k] == nil and v:get_pos() ~= nil then + dst[k] = v + end + end +end + +local function try_append_if_in_radius(dst, src, pos, radius) + assert(pos) + for k, v in pairs(src) do + if dst[k] == nil then + local v_pos = v:get_pos() + if v_pos ~= nil and vector.distance(pos, v_pos) <= radius then + dst[k] = v + end + end + end +end + +--[[ + Vrátí všechny aktivní objekty typu "name" v tabulce {[key] = ObjectRef}. + Platí pouze pro objekty používající ch_core.object_on_step() v poli on_step a hráčské postavy (name == "player"). +]] +function ch_core.get_active_objects(names) + local result = {} + local t = type(names) + if t == "nil" then + -- return all objects + for _, lists in pairs(objects) do + try_append(result, lists[1]) + try_append(result, lists[2]) + end + elseif t == "string" then + -- single type of objects + local lists = objects[names] + if lists ~= nil then + try_append(result, lists[1]) + try_append(result, lists[2]) + end + elseif t == "table" then + for _, name in ipairs(names) do + local lists = objects[name] + if lists ~= nil then + try_append(result, lists[1]) + try_append(result, lists[2]) + end + end + else + error("Invalid type of argument: "..t) + end + return result +end + +--[[ +local function append_if_inside_radius(dst, lists, pos, radius) + local new = {} + for i = 2, 1, -1 do + for key, obj in pairs(lists[i]) do + if new[key] == nil and vector.distance(obj:get_pos(), pos) <= radius then + new[key] = obj + dst[key] = obj + end + end + end +end +]] + +--[[ + Vrátí aktivní objekty typu "name", pokud se nacházejí v zadané oblasti, + v tabulce {[key] = ObjectRef}. + Platí pouze pro objekty používající ch_core.object_on_step() v poli on_step a hráčské postavy (name == "player"). +]] +function ch_core.get_active_objects_inside_radius(names, pos, radius) + local result = {} + local t = type(names) + if t == "nil" then + -- return all objects + for _, lists in pairs(objects) do + try_append_if_in_radius(result, lists[1], pos, radius) + try_append_if_in_radius(result, lists[2], pos, radius) + end + elseif t == "string" then + -- single type of objects + local lists = objects[names] + if lists ~= nil then + try_append_if_in_radius(result, lists[1], pos, radius) + try_append_if_in_radius(result, lists[2], pos, radius) + end + elseif t == "table" then + for _, name in ipairs(names) do + local lists = objects[name] + if lists ~= nil then + try_append_if_in_radius(result, lists[1], pos, radius) + try_append_if_in_radius(result, lists[2], pos, radius) + end + end + else + error("Invalid type of argument: "..t) + end + return result +end + +--[[ + Musí být zadána do pole on_step u objektů, které mají být hledány pomocí + funkcí z tohoto modulu. + orig_func - původní funkce on_step (bude volána), nebo nil +]] +function ch_core.object_on_step(orig_func) + if orig_func ~= nil then + return function(self, ...) + push_active_object(assert(self.name), assert(self.object)) + return orig_func(self, ...) + end + else + return on_step_default + end +end + +-- Globalstep +local skip = true +local function on_globalstep(dtime) + if skip then + skip = false + return + end + for _, name in pairs(objects) do + name[2] = name[1] + name[1] = {} + end + -- add players: + local players = {} + for _, player in ipairs(minetest.get_connected_players()) do + local key = tostring(player) + if players[key] ~= nil then + minetest.log("error", "Key duplicity in players table: "..player:get_player_name().." x "..players[key]:get_player_name().."!") + end + players[key] = player + end + objects.player = {players, {}} +end +minetest.register_globalstep(on_globalstep) + +ch_core.close_submod("active_objects") diff --git a/ch_core/ap.lua b/ch_core/ap.lua new file mode 100644 index 0000000..148eb77 --- /dev/null +++ b/ch_core/ap.lua @@ -0,0 +1,628 @@ +ch_core.open_submod("ap", {chat = true, data = true, events = true, hud = true, lib = true}) + +-- Systém „Activity Points“ + +local ap_increase = 2 +local ap_decrease = 1 +local ap_min = 0 +local ap_max_default = 26 +local ap_max_book = 10 +local min = math.min +local def + +ch_data.initial_offline_charinfo.ap_version = ch_core.verze_ap + +local levels = { + {base = 0, count = 500, next = 500}, +} + +local ifthenelse = assert(ch_core.ifthenelse) + +ch_core.register_event_type("level_up", { + description = "zvýšení úrovně", + access = "public", + chat_access = "public", + color = "#eeee00", +}) + +ch_core.register_event_type("level_down", { + description = "snížení úrovně", + access = "public", + chat_access = "public", +}) + +local function add_level(level, count) + local last_level_number = #levels + local last_level = levels[last_level_number] + levels[last_level_number + 1] = { + base = last_level.base + last_level.count, + count = count, + next = last_level.base + last_level.count + count + } + return last_level_number + 1 +end + +add_level(2, 1100) +add_level(3, 2400) +add_level(4, 4200) +add_level(5, 6500) +add_level(6, 9300) +add_level(7, 12600) +add_level(8, 16400) +add_level(9, 20700) +add_level(10, 25500) +add_level(11, 30800) +add_level(12, 36600) +add_level(13, 42900) +add_level(14, 49700) +add_level(15, 57000) +add_level(16, 64800) +add_level(17, 73100) +add_level(18, 81900) +add_level(19, 91200) +add_level(20, 101000) +add_level(21, 111300) +add_level(22, 122100) +add_level(23, 133400) +add_level(24, 145200) +add_level(25, 157500) +add_level(26, 170300) +add_level(27, 183600) +add_level(28, 197400) +add_level(29, 211700) +add_level(30, 226500) +add_level(31, 241800) +add_level(32, 257600) +add_level(33, 273900) +add_level(34, 290700) +add_level(35, 308000) +add_level(36, 325800) +add_level(37, 344100) +add_level(38, 362900) +add_level(39, 382200) +add_level(40, 402000) +add_level(41, 422300) +add_level(42, 443100) +add_level(43, 464400) +add_level(44, 486200) +add_level(45, 508500) +add_level(46, 531300) +add_level(47, 554600) +add_level(48, 578400) +add_level(49, 602700) +add_level(50, 627500) +add_level(51, 652800) +add_level(52, 678600) +add_level(53, 704900) +add_level(54, 731700) +add_level(55, 759000) +add_level(56, 786800) +add_level(57, 815100) +add_level(58, 843900) +add_level(59, 873200) +add_level(60, 903000) +add_level(61, 933300) +add_level(62, 964100) +add_level(63, 995400) +add_level(64, 1027200) +add_level(65, 1059500) +add_level(66, 1092300) +add_level(67, 1125600) +add_level(68, 1159400) +add_level(69, 1193700) +add_level(70, 1228500) +add_level(71, 1263800) +add_level(72, 1299600) +add_level(73, 1335900) +add_level(74, 1372700) +add_level(75, 1410000) +add_level(76, 1447800) +add_level(77, 1486100) +add_level(78, 1524900) +add_level(79, 1564200) +add_level(80, 1604000) +add_level(81, 1644300) +add_level(82, 1685100) +add_level(83, 1726400) +add_level(84, 1768200) +add_level(85, 1810500) +add_level(86, 1853300) +add_level(87, 1896600) +add_level(88, 1940400) +add_level(89, 1984700) +add_level(90, 2029500) +add_level(91, 2074800) +add_level(92, 2120600) +add_level(93, 2166900) +add_level(94, 2213700) +add_level(95, 2261000) +add_level(96, 2308800) +add_level(97, 2357100) +add_level(98, 2405900) +add_level(99, 2455200) +add_level(100, 2505000) +add_level(101, 2555300) +add_level(102, 2606100) +add_level(103, 2657400) +add_level(104, 2709200) +add_level(105, 2761500) +add_level(106, 2814300) +add_level(107, 2867600) +add_level(108, 2921400) +add_level(109, 2975700) +add_level(110, 3030500) +add_level(111, 3085800) +add_level(112, 3141600) +add_level(113, 3197900) +add_level(114, 3254700) +add_level(115, 3312000) +add_level(116, 3369800) +add_level(117, 3428100) +add_level(118, 3486900) +add_level(119, 3546200) +add_level(120, 3606000) +add_level(121, 3666300) +add_level(122, 3727100) +add_level(123, 3788400) +add_level(124, 3850200) +add_level(125, 3912500) +add_level(126, 3975300) +add_level(127, 4038600) +add_level(128, 4102400) + +local function adjust_coef(t, key, amount) + local ap_max = ap_max_default + if key == "book_coef" then + ap_max = ap_max_book + end + local value = t[key] + amount + if value > ap_max then + value = ap_max + elseif value < ap_min then + value = ap_min + end + t[key] = value + return value +end + +function ch_core.ap_get_level(l) + if l < 0 then + return nil + else + return levels[min(l, #levels)] + end +end + +function ch_core.ap_announce_craft(player_or_player_name, hash) + -- TODO: check for repeating hash + local player_name + if type(player_or_player_name) == "string" then + player_name = player_or_player_name + else + player_name = player_or_player_name and player_or_player_name:is_player() and player_or_player_name:get_player_name() + end + local online_charinfo = player_name and ch_data.online_charinfo[player_name] + if online_charinfo then + local ap = online_charinfo.ap + if ap then + ap.craft_gen = ap.craft_gen + 1 + end + end +end + +local update_xp_hud + +if minetest.get_modpath("hudbars") then + update_xp_hud = function(player, player_name, offline_charinfo) + local pinfo = ch_core.normalize_player(player) + local player_role = pinfo.role + local has_creative_priv = false + if pinfo.privs.creative then + local online_charinfo = ch_data.online_charinfo[pinfo.player_name] + if online_charinfo ~= nil then + -- a creative priv icon can only appear after a second after login + has_creative_priv = minetest.get_us_time() - online_charinfo.join_timestamp > 1000000 + end + end + local role_icon = ch_core.player_role_to_image(player_role, has_creative_priv, 16) + local skryt_body = player_role == "new" or offline_charinfo.skryt_body == 1 + + if skryt_body then + hb.hide_hudbar(player, "ch_xp") + else + local level, xp, max_xp = offline_charinfo.ap_level, offline_charinfo.ap_xp + if level and xp then + max_xp = ch_core.ap_get_level(level).count + else + level, xp, max_xp = 1, 0, ch_core.ap_get_level(1).count + end + hb.unhide_hudbar(player, "ch_xp") + hb.change_hudbar(player, "ch_xp", math.min(xp, max_xp), max_xp, role_icon, nil, nil, level, nil) + end + end +else + update_xp_hud = function(player, player_name, offline_charinfo) + return false + end +end + +function ch_core.ap_init(player, online_charinfo, offline_charinfo) + local vector_zero = vector.zero() + local default_coef = 0 + local player_name = online_charinfo.player_name + + if offline_charinfo.ap_level == nil or offline_charinfo.ap_version ~= ch_core.verze_ap then + offline_charinfo.ap_level = 1 + offline_charinfo.ap_xp = 0 + offline_charinfo.ap_version = ch_core.verze_ap + ch_data.save_offline_charinfo(player_name) + end + local ap = { + -- aktuální údaje + dig_gen = 0, + dig_pos = vector_zero, + look_h = 0, + look_h_gen = 0, + look_v = 0, + look_v_gen = 0, + place_gen = 0, + place_pos = vector_zero, + pos = player:get_pos(), + pos_x_gen = 0, + pos_y_gen = 0, + pos_z_gen = 0, + velocity = player:get_velocity(), + velocity_x_gen = 0, + velocity_y_gen = 0, + velocity_z_gen = 0, + control = player:get_player_control_bits(), + control_gen = 0, + chat_mistni_gen = 0, + chat_celoserverovy_gen = 0, + chat_sepot_gen = 0, + chat_soukromy_gen = 0, + craft_gen = 0, + book_gen = 0, + + -- koeficienty + book_coef = default_coef, + craft_coef = default_coef, + chat_coef = default_coef, + dig_coef = default_coef, + place_coef = default_coef, + walk_coef = default_coef, + } + online_charinfo.ap = table.copy(ap) + online_charinfo.ap1 = ap + online_charinfo.ap2 = ap + online_charinfo.ap3 = ap + online_charinfo.ap_modify_timestamp = minetest.get_us_time() + + return true +end + +function ch_core.ap_add(player_name, offline_charinfo, points_to_add, debug_coef_changes) + local player = minetest.get_player_by_name(player_name) + + if points_to_add == nil or points_to_add == 0 then + if player ~= nil then + update_xp_hud(player, player_name, offline_charinfo) + end + return 0 + end + + local player_role = ch_core.get_player_role(player_name) + local old_level, old_xp = offline_charinfo.ap_level, offline_charinfo.ap_xp + local new_level = old_level + local level_def = ch_core.ap_get_level(old_level) + local new_xp = old_xp + points_to_add + + if points_to_add > 0 then + -- vyhodnotit zvýšení úrovně + while new_xp >= level_def.count do + new_xp = new_xp - level_def.count + new_level = new_level + 1 + level_def = ch_core.ap_get_level(new_level) + end + else + -- vyhodnotit snížení úrovně + while new_xp < 0 do + if new_level > 1 then + new_level = new_level - 1 + level_def = ch_core.ap_get_level(new_level) + new_xp = new_xp + level_def.count + else + new_xp = 0 + break + end + end + end + + -- aktualizovat offline_charinfo + offline_charinfo.ap_xp = new_xp + if new_level ~= old_level then + offline_charinfo.ap_level = new_level + end + ch_data.save_offline_charinfo(player_name) + + -- je-li postava online, aktualizovat HUD + if player ~= nil then + update_xp_hud(player, player_name, offline_charinfo) + end + + if debug_coef_changes then + core.log("info", player_name..": level_op="..(new_level > old_level and ">" or new_level < old_level and "<" or "=")..", level="..old_level.."=>"..new_level..", xp="..old_xp.."=>"..new_xp..", coefs = "..table.concat(debug_coef_changes)) + end + + -- oznámit + if player_role ~= "new" then + if new_level > old_level then + -- ch_core.systemovy_kanal("", "Postava "..ch_core.prihlasovaci_na_zobrazovaci(player_name).." dosáhla úrovně "..new_level) + ch_core.add_event("level_up", "Postava {PLAYER} dosáhla úrovně "..new_level, player_name) + elseif new_level < old_level then + -- ch_core.systemovy_kanal("", "Postava "..ch_core.prihlasovaci_na_zobrazovaci(player_name).." klesla na úroveň "..new_level) + ch_core.add_event("level_down", "Postava {PLAYER} klesla na úroveň "..new_level, player_name) + end + end + + -- je-li postava online, aktualizovat online_charinfo.ap_modify_timestamp + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo ~= nil then + online_charinfo.ap_modify_timestamp = minetest.get_us_time() + end + + return new_xp, new_level +end + +function ch_core.ap_update(player, online_charinfo, offline_charinfo) + local result, ap, ap1, ap2, ap3 = 0, online_charinfo.ap, online_charinfo.ap1, online_charinfo.ap2, online_charinfo.ap3 + + -- posunout struktury + local ap_new = table.copy(ap) + online_charinfo.ap3 = ap2 + online_charinfo.ap2 = ap1 + online_charinfo.ap1 = ap + online_charinfo.ap = ap_new + + local control_diff_1 = ap.control_gen - ap1.control_gen + local control_diff_2 = ap1.control_gen - ap2.control_gen + local control_diff_3 = ap2.control_gen - ap3.control_gen + + -- aktivity + local act_book = ap.book_gen > ap1.book_gen + local act_chat = ap.chat_mistni_gen > ap1.chat_mistni_gen or ap.chat_celoserverovy_gen > ap1.chat_celoserverovy_gen or ap.chat_sepot_gen > ap1.chat_sepot_gen or ap.chat_soukromy_gen > ap1.chat_soukromy_gen + local act_craft = ap.craft_gen > ap1.craft_gen + local act_dig = ap.dig_gen > ap1.dig_gen and (not ap.dig_pos or not ap1.dig_pos or not ap2.dig_pos or vector.angle(vector.subtract(ap2.dig_pos, ap1.dig_pos), vector.subtract(ap1.dig_pos, ap.dig_pos)) ~= 0) + local act_place = ap.place_gen - ap1.place_gen > 1 + local act_walk = + (ap.look_h_gen - ap1.look_h_gen) + (ap.look_v_gen - ap1.look_v_gen) > 2 and + ap.velocity_x_gen - ap1.velocity_x_gen > 8 and + ap.velocity_z_gen - ap1.velocity_z_gen > 8 and + control_diff_1 ~= control_diff_2 and + control_diff_2 ~= control_diff_3 and + control_diff_3 ~= control_diff_1 + + local act_any = act_chat or act_craft or act_dig or act_place or act_walk + local debug_coef_changes = {} + + for coef_key, active in pairs({book_coef = act_book, chat_coef = act_chat, craft_coef = act_craft, dig_coef = act_dig, place_coef = act_place, walk_coef = act_walk}) do + local adjustment + if not active then + adjustment = -ap_decrease + elseif coef_key ~= "chat_coef" then + adjustment = ap_increase + else + adjustment = 2 * ap_increase -- čet => dvojnásobný vzrůst + end + local old_coef = ap_new[coef_key] + local new_coef = adjust_coef(ap_new, coef_key, adjustment) + result = result + new_coef + if new_coef ~= old_coef then + table.insert(debug_coef_changes, coef_key..":"..old_coef.."=>"..new_coef..";") + end + end + + if act_any then + offline_charinfo.past_ap_playtime = offline_charinfo.past_ap_playtime + ch_core.ap_interval + ch_data.save_offline_charinfo(online_charinfo.player_name) + end + + if result == 0 then + if debug_coef_changes[1] then + minetest.log("action", online_charinfo.player_name..": no xp change, but :"..table.concat(debug_coef_changes)) + end + return 0 + end + + ch_core.ap_add(online_charinfo.player_name, offline_charinfo, result, debug_coef_changes) + return result +end + +local function on_dignode(pos, oldnode, digger) + if digger == nil then + return + end + local player_name = digger:is_player() and digger:get_player_name() + local online_charinfo = player_name and ch_data.online_charinfo[player_name] + if online_charinfo then -- nil if digged by digtron + local ap = online_charinfo.ap + if ap then + local old_pos = ap.dig_pos + if not old_pos or pos.x ~= old_pos.x or pos.y ~= old_pos.y or pos.z ~= old_pos.z then + ap.dig_gen = ap.dig_gen + 1 + ap.dig_pos = pos + end + end + end +end + +local function on_placenode(pos, newnode, placer, oldnode, itemstack, pointed_thing) + if placer == nil then + return + end + local player_name = placer:is_player() and placer:get_player_name() + if player_name == nil or player_name == false or player_name == "" then + return + end + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo then + local ap = online_charinfo.ap + if ap then + local old_pos = ap.place_pos + pos = vector.round(pos) + if not old_pos or pos.x ~= old_pos.x or pos.y ~= old_pos.y or pos.z ~= old_pos.z then + ap.place_gen = ap.place_gen + 1 + ap.place_pos = pos + end + end + else + minetest.log("warning", "on_placenode called with an offline player "..player_name) + end +end + +local function on_craft(itemstack, player, old_craft_grid, craft_inv) + if craft_inv:get_location().type == "player" then + local ap = ch_core.safe_get_3(ch_data.online_charinfo, assert(player:get_player_name()), "ap") + if ap then + ap.craft_gen = ap.craft_gen + 1 + end + end +end + +local function on_player_inventory_action(player, action, inventory, inventory_info) + local ap = ch_core.safe_get_3(ch_data.online_charinfo, assert(player:get_player_name()), "ap") + if ap then + ap.craft_gen = ap.craft_gen + 1 + end +end + +local function on_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing) + local ap = ch_core.safe_get_3(ch_data.online_charinfo, assert(user:get_player_name()), "ap") + if ap then + ap.craft_gen = ap.craft_gen + 1 + end +end + +local function on_item_pickup(itemstack, picker, pointed_thing, time_from_last_punch, ...) + local ap = ch_core.safe_get_3(ch_data.online_charinfo, assert(picker:get_player_name()), "ap") + if ap then + ap.craft_gen = ap.craft_gen + 1 + end +end + +minetest.register_on_craft(on_craft) +minetest.register_on_dignode(on_dignode) +minetest.register_on_placenode(on_placenode) +minetest.register_on_player_inventory_action(on_player_inventory_action) +minetest.register_on_item_eat(on_item_eat) +minetest.register_on_item_pickup(on_item_pickup) + +local function body(admin_name, param) + local player_name, ap_to_add = param:match("^(%S+)%s(%S+)$") + if not player_name then + return false, "Chybné zadání!" + end + player_name = ch_core.jmeno_na_prihlasovaci(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if not offline_charinfo then + return false, "Vnitřní chyba: nenalezeno offline_charinfo pro '"..player_name.."'!" + end + ap_to_add = tonumber(ap_to_add) + local old_level, old_xp = offline_charinfo.ap_level, offline_charinfo.ap_xp + ch_core.ap_add(player_name, offline_charinfo, ap_to_add) + local new_level, new_xp = offline_charinfo.ap_level, offline_charinfo.ap_xp + return true, "Změna: úroveň "..old_level.." => "..new_level..", body: "..old_xp.." => "..new_xp +end + +def = { + params = "<Jméno_Postavy> <celé_číslo>", + privs = {server = true}, + description = "Přidá či odebere příslušné postavě body.", + func = body, +} +minetest.register_chatcommand("body", def) + +--[[ + Zobrazí či skryje ukazatel úrovní a bodů. Postava nemusí být zrovna + ve hře. +]] +function ch_core.showhide_ap_hud(player_name, show) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if not offline_charinfo then + return false + end + local new_value = ifthenelse(show, 0, 1) + if offline_charinfo.skryt_body == new_value then + return nil + end + offline_charinfo.skryt_body = new_value + ch_data.save_offline_charinfo(player_name) + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + update_xp_hud(player, player_name, offline_charinfo) + end + return true +end + +-- HUD +if minetest.get_modpath("hudbars") then + local hudbar_formatstring = "úr. @1: @2/@3" + local hudbar_formatstring_config = { + order = { "label", "value", "max_value" }, + textdomain = "hudbars", + } + local hudbar_defaults = { + icon = "ch_core_empty.png", bgicon = nil, bar = "hudbars_bar_xp.png" + } + hb.register_hudbar("ch_xp", 0xFFFFFF, "*", hudbar_defaults, 0, 100, true, hudbar_formatstring, hudbar_formatstring_config) + minetest.register_on_joinplayer(function(player, last_login) + local pinfo = ch_core.normalize_player(player) + local offline_charinfo = ch_data.get_offline_charinfo(pinfo.player_name) + --[[ + local level, xp, _max_xp = offline_charinfo.ap_level, offline_charinfo.ap_xp + if not (level and xp) then + max_xp = ch_core.ap_get_level(level).count + else + level, xp, _max_xp = 1, 0, ch_core.ap_get_level(1).count + end + ]] + local skryt_body = pinfo.role == "new" or offline_charinfo.skryt_body == 1 + hb.init_hudbar(player, "ch_xp", 0, 10, skryt_body) + update_xp_hud(player, pinfo.player_name, offline_charinfo) + end) + + def = { + description = "Skryje hráči/ce ukazatel úrovní a bodů, je-li zobrazen.", + func = function(player_name, param) + local result = ch_core.showhide_ap_hud(player_name, false) + if result == false then + return false, "Vnitřní chyba" + elseif result == nil then + return false, "Ukazatel úrovně a bodů je již skryt." + else + return true, "Ukazatel úrovně a bodů úspěšně skryt." + end + end, + } + + minetest.register_chatcommand("skrýtbody", def) + minetest.register_chatcommand("skrytbody", def) + + def = { + description = "Zobrazí hráči/ce ukazatel úrovní a bodů, je-li skryt.", + func = function(player_name, param) + local result = ch_core.showhide_ap_hud(player_name, true) + if result == false then + return false, "Vnitřní chyba" + elseif result == nil then + return false, "Ukazatel úrovně a bodů je již zobrazen." + else + return true, "Ukazatel úrovně a bodů úspěšně zobrazen." + end + end, + } + + minetest.register_chatcommand("zobrazitbody", def) +end + +ch_core.close_submod("ap") diff --git a/ch_core/areas.lua b/ch_core/areas.lua new file mode 100644 index 0000000..f032d49 --- /dev/null +++ b/ch_core/areas.lua @@ -0,0 +1,103 @@ +ch_core.open_submod("areas", {data = true, lib = true}) + +local world_path = minetest.get_worldpath() +local ch_areas_file_path = world_path.."/ch_areas_v2.json" + +local after_player_area_change = {} +local after_player_areas_change = {} + +local function load_ch_areas() + local data + local f, err = io.open(ch_areas_file_path, "r") + if not f then + minetest.log("warning", "CH areas file cannot be openned: "..(err or "nil")) + return {} + end + local s = f:read("*a") + if s then + data, err = minetest.parse_json(s) + end + io.close(f) + if not data then + minetest.log("warning", "CH areas file cannot be loaded: "..(err or "nil")) + return {} + end + local list = {} + for k, _ in pairs(data) do + table.insert(list, k) + end + table.sort(list) + minetest.log("warning", "CH areas loaded: "..table.concat(list, ", ")) -- [ ] DEBUG... + return data +end + +ch_core.areas = load_ch_areas() + +function ch_core.save_areas() + local data = minetest.write_json(ch_core.areas) + if not data then + minetest.log("error", "ch_core.save_areas(): failed to serialize data!") + return false + end + if not minetest.safe_file_write(ch_areas_file_path, data) then + minetest.log("error", "ch_core.save_areas(): failed to save data!") + return false + end + return true +end + +--[[ + POZNÁMKA: Tyto dva callbacky budou volány s parametry (player_name, old_areas, new_areas), + kde old_areas může být nil. +]] + +function ch_core.register_on_player_change_area(callback) + table.insert(after_player_area_change, callback) +end + +function ch_core.register_on_player_change_areas(callback) + table.insert(after_player_areas_change, callback) +end + +function ch_core.set_player_areas(player_name, player_areas) + assert(player_name) + assert(player_areas) + assert(type(player_areas) == "table") + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo == nil then + return + end + if #player_areas == 0 then + player_areas[1] = { + id = 0, + name = "Český hvozd", + type = 1, -- normal + -- def = nil + } + end + local old_player_areas = assert(online_charinfo.areas) + online_charinfo.areas = player_areas + local changes = 0 + if #old_player_areas == player_areas then + for i = 1, #old_player_areas do + if old_player_areas[i].id ~= player_areas[i].id then + changes = changes + 1 + end + end + if changes == 0 then + -- no changes + return + end + end + -- callbacks + if old_player_areas[1].id ~= player_areas[1].id then + for _, f in ipairs(after_player_area_change) do + f(player_name, old_player_areas, player_areas) + end + end + for _, f in ipairs(after_player_areas_change) do + f(player_name, old_player_areas, player_areas) + end +end + +ch_core.close_submod("areas") diff --git a/ch_core/barvy_linek.lua b/ch_core/barvy_linek.lua new file mode 100644 index 0000000..973b030 --- /dev/null +++ b/ch_core/barvy_linek.lua @@ -0,0 +1,25 @@ +ch_core.open_submod("barvy_linek", {}) + +ch_core.barvy_linek = { + [1] = {bgcolor = "#1e00ff"}, + [2] = {bgcolor = "#ff001e"}, + [3] = {bgcolor = "#7f007f"}, + [4] = {bgcolor = "#007f00"}, + [5] = {bgcolor = "#ffa400"}, + [6] = {bgcolor = "#ffff00", color = "#000000"}, + [7] = {bgcolor = "#797979"}, + [8] = {bgcolor = "#ff1ed0"}, + [9] = {bgcolor = "#00c0b1"}, + [10] = {bgcolor = "#000000"}, + [11] = {bgcolor = "#499ca5"}, + [12] = {bgcolor = "#8cfdee", color = "#000000"}, + [13] = {bgcolor = "#cf5f38"}, + [14] = {bgcolor = "#13c081"}, + [15] = {bgcolor = "#5622ca"}, + [16] = {bgcolor = "#998314"}, + [17] = {bgcolor = "#dce45d", color = "#000000"}, + [18] = {bgcolor = "#2046a6"}, + [19] = {bgcolor = "#63a7ef"}, + [20] = {bgcolor = "#a60939"}, +} +ch_core.close_submod("barvy_linek") diff --git a/ch_core/chat.lua b/ch_core/chat.lua new file mode 100644 index 0000000..d24ad68 --- /dev/null +++ b/ch_core/chat.lua @@ -0,0 +1,577 @@ +ch_core.open_submod("chat", {areas = true, privs = true, data = true, lib = true, nametag = true, udm = true}) + +local ifthenelse = ch_core.ifthenelse + +minetest.register_on_joinplayer(function(player, last_login) + local online_charinfo = ch_data.get_joining_online_charinfo(player) + if online_charinfo.doslech == nil then + online_charinfo.doslech = 65535 + end + online_charinfo.chat_ignore_list = {} -- player => true +end) + +-- BARVY +-- =========================================================================== + +local color_celoserverovy = minetest.get_color_escape_sequence("#ff8700") +local color_mistni = minetest.get_color_escape_sequence("#fff297") +local color_mistni_zblizka = minetest.get_color_escape_sequence("#64f231") -- 54cc29 +local color_soukromy = minetest.get_color_escape_sequence("#ff4cf3") +local color_sepot = minetest.get_color_escape_sequence("#fff297cc") +local color_systemovy = minetest.get_color_escape_sequence("#ffffff") -- #cccccc +local color_systemovy_tichy = minetest.get_color_escape_sequence("#cccccc") +local color_reset = minetest.get_color_escape_sequence("#ffffff") + +-- NASTAVENÍ +-- =========================================================================== +local vzdalenost_zblizka = 10 -- počet metrů, na které se ještě považuje hovor za hovor zblízka; nemá vliv na šepot +local vzdalenost_pro_ignorovani = 150 -- poloměr koule, ve které se nesmí nacházet ignorující postava, aby se zobrazila zpráva nad postavou + +-- HISTORIE +-- =========================================================================== +ch_core.last_mistni = {char = "", char_gen = 0, count = 0, zprava = ""} +ch_core.last_celoserverovy = {char = "", char_gen = 0, count = 0, zprava = ""} +ch_core.last_sepot = {char = "", char_gen = 0, count = 0, zprava = ""} +ch_core.last_soukromy = {char = "", char_gen = 0, count = 0, zprava = ""} + +function ch_core.systemovy_kanal(komu, zprava, volby) + if zprava == "" then + return true -- je-li zprava "", ignorovat + end + local is_alert = volby ~= nil and volby.alert + local color = ifthenelse(is_alert, color_soukromy, color_systemovy) + if not komu or komu == "" then -- je-li komu "", rozeslat všem + minetest.chat_send_all(color .. zprava .. color_reset) + if is_alert then + minetest.sound_play("chat3_bell", {gain = 1.0}, true) + end + else + minetest.chat_send_player(komu, color .. zprava .. color_reset) + if is_alert then + minetest.sound_play("chat3_bell", {to_player = komu, gain = 1.0}, true) + end + end +end + +function ch_core.chat(rezim, odkoho, zprava, pozice) + pozice = vector.new(pozice.x, pozice.y, pozice.z) + + if rezim ~= "celoserverovy" and rezim ~= "mistni" and rezim ~= "sepot" then + minetest.log("error", "ch_core.chat(): invalid chat mode '"..rezim.."'!!!") + return false + end + + local pocitadlo = 0 + local posl_adresat = "" + local odkoho_info = ch_data.online_charinfo[odkoho] or {} + local odkoho_doslech = odkoho_info.doslech or 65535 + local odkoho_s_diakritikou = ch_core.prihlasovaci_na_zobrazovaci(odkoho) + local barva_zpravy, barva_zpravy_zblizka, min_vzdal_ignorujici, min_vzdal_adresat + + if rezim == "celoserverovy" then + barva_zpravy = color_celoserverovy + barva_zpravy_zblizka = barva_zpravy + elseif rezim == "sepot" then + barva_zpravy = color_sepot + barva_zpravy_zblizka = barva_zpravy + else -- "mistni" + barva_zpravy = color_mistni + barva_zpravy_zblizka = color_mistni_zblizka + end + + local casti_zpravy = { + barva_zpravy, -- [1] (neměnit, reprezentuje kanál) + "", -- [2] + odkoho_s_diakritikou, -- [3] + ": ", -- [4] // mění se + barva_zpravy, -- [5] // mění se + zprava, -- [6] + "", -- [7] + color_systemovy_tichy, -- [8] + " (", -- [9] + 0, -- [10] // mění se + " m)", -- [11] + color_reset, -- [12] + } + + for _, komu_player in pairs(minetest.get_connected_players()) do + local komu = komu_player:get_player_name() + local komu_info = ch_data.online_charinfo[komu] + if komu_info and komu ~= odkoho then + local vzdalenost_odkoho_komu = math.ceil(vector.distance(pozice, komu_player:get_pos())) + local komu_doslech = komu_info.doslech or 65535 + if not komu_info.chat_ignore_list[odkoho] or minetest.check_player_privs(odkoho, "protection_bypass") then + local v_doslechu + if rezim == "celoserverovy" then + v_doslechu = true + else + v_doslechu = vzdalenost_odkoho_komu <= komu_doslech + if v_doslechu and rezim == "sepot" then + v_doslechu = vzdalenost_odkoho_komu <= 5 + end + end + if v_doslechu then + if vzdalenost_odkoho_komu <= vzdalenost_zblizka then + casti_zpravy[5] = barva_zpravy_zblizka + else + casti_zpravy[5] = barva_zpravy + end + if odkoho_info.chat_ignore_list[komu] then + casti_zpravy[4] = "(ign.): " + elseif vzdalenost_odkoho_komu > odkoho_doslech then + casti_zpravy[4] = "(m.d.): " + elseif rezim == "sepot" then + casti_zpravy[4] = "(šepot): " + else + casti_zpravy[4] = ": " + end + casti_zpravy[10] = vzdalenost_odkoho_komu + minetest.chat_send_player(komu, table.concat(casti_zpravy)) + pocitadlo = pocitadlo + 1 + if rezim == "sepot" then + minetest.sound_play("chat3_bell", {to_player = komu, gain = 1.0}, true) + end + + if not min_vzdal_adresat or vzdalenost_odkoho_komu < min_vzdal_adresat then + min_vzdal_adresat = vzdalenost_odkoho_komu + posl_adresat = komu + end + end + else + -- ignorující + if not min_vzdal_ignorujici or min_vzdal_ignorujici < vzdalenost_odkoho_komu then + min_vzdal_ignorujici = vzdalenost_odkoho_komu + end + end + end + end + + -- Zaslat postavě „odkoho“ + casti_zpravy[4] = ": " + casti_zpravy[5] = barva_zpravy_zblizka + casti_zpravy[9] = " [" + casti_zpravy[10] = pocitadlo + casti_zpravy[11] = " post.]" + minetest.chat_send_player(odkoho, table.concat(casti_zpravy)) + + -- Zobrazit nad postavou: + local zobrazeno_nad_postavou = "false" + if (rezim == "mistni" or rezim == "celoserverovy") + and min_vzdal_adresat and min_vzdal_adresat <= vzdalenost_zblizka + and (not min_vzdal_ignorujici or min_vzdal_ignorujici > vzdalenost_pro_ignorovani) + then + local player = minetest.get_player_by_name(odkoho) + if player then + local horka_zprava = ch_core.utf8_wrap(zprava, 60) + horka_zprava[1] = barva_zpravy_zblizka..horka_zprava[1] + horka_zprava.timeout = ch_core.cas + 15 + odkoho_info.horka_zprava = horka_zprava + player:set_nametag_attributes(ch_core.compute_player_nametag(odkoho_info, ch_data.get_offline_charinfo(odkoho))) + zobrazeno_nad_postavou = "true" + else + zobrazeno_nad_postavou = "fail" + end + end + + -- Zapsat do logu: + if rezim == "sepot" then + zprava = minetest.sha1(odkoho.."/"..zprava:gsub("%s", "")) + end + minetest.log("action", "CHAT:"..rezim..":"..odkoho..">"..pocitadlo.." characters(ex.:"..(posl_adresat or "nil")..", mv_adr="..(min_vzdal_adresat or "nil")..", znadpost="..zobrazeno_nad_postavou.."): "..zprava) + + -- Zaznamenat aktivitu: + if (rezim == "mistni" or rezim == "celoserverovy" or rezim == "sepot") then + local last = ch_core["last_"..rezim] + if zprava ~= last.zprava and pocitadlo > 0 then + last.zprava = zprava + if last.char ~= odkoho then + last.char = odkoho + last.char_gen = last.char_gen + 1 + last.count = 1 + else + last.count = last.count + 1 + end + end + end + + return true +end + +function ch_core.soukroma_zprava(odkoho, komu, zprava) + local odkoho_info = ch_data.online_charinfo[odkoho] + if not odkoho_info or odkoho == "" or zprava == "" then + minetest.log("warning", "private message not send: "..(odkoho_info and "true" or "false")..","..odkoho..","..#zprava) + return true + end + if komu == "" then + komu = odkoho_info.posl_soukr_adresat + if not komu then + ch_core.systemovy_kanal(odkoho, "Dosud jste nikomu nepsal/a, musíte uvést jméno adresáta/ky soukromé zprávy.") + return true + end + end + if not minetest.player_exists(komu) then + ch_core.systemovy_kanal(odkoho, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(komu) .. " neexistuje!") + return true + end + local komu_info = ch_data.online_charinfo[komu] + if komu_info then + if komu_info.chat_ignore_list[odkoho] and not minetest.check_player_privs(odkoho, "protection_bypass") then + ch_core.systemovy_kanal(odkoho, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(komu) .. " vás ignoruje!") + return true + end + minetest.chat_send_player(odkoho, color_soukromy .. "-> ".. ch_core.prihlasovaci_na_zobrazovaci(komu)..": "..zprava .. color_reset) + minetest.chat_send_player(komu, color_soukromy .. ch_core.prihlasovaci_na_zobrazovaci(odkoho)..": "..zprava .. color_reset) + odkoho_info.posl_soukr_adresat = komu + zprava = minetest.sha1(odkoho.."/"..zprava:gsub("%s", "")) + minetest.log("action", "CHAT:soukroma_zprava:"..odkoho..">"..komu..": "..zprava) + minetest.sound_play("chat3_bell", {to_player = komu, gain = 1.0}, true) + + -- Zaznamenat aktivitu: + local last = ch_core.last_soukromy + if zprava ~= last.zprava then + last.zprava = zprava + if last.char ~= odkoho then + last.char = odkoho + last.char_gen = last.char_gen + 1 + last.count = 1 + else + last.count = last.count + 1 + end + end + + return true + end + ch_core.systemovy_kanal(odkoho, komu .. " není ve hře!") + return true +end + +local function on_chat_message(jmeno, zprava) + local info = minetest.get_player_by_name(jmeno) + local info_pos = info:get_pos() + local online_charinfo = ch_data.online_charinfo[jmeno] + if not info then + minetest.log("warning", "No info found about player "..jmeno.."!") + return true + end + if not info_pos then + minetest.log("warning", "No position of player "..jmeno.."!") + return true + end + if not online_charinfo then + minetest.log("warning", "No online_charinfo of character "..jmeno.."!") + return true + end + + -- disrupt /pryč: + local pryc_func = online_charinfo.pryc + if pryc_func then + pryc_func(info, online_charinfo) + end + + local area = online_charinfo.areas + if area ~= nil then + area = area[1] + area = ch_core.areas[area.id] + if area ~= nil and area.udm_catch_chat and ch_core.udm_catch_chat(jmeno, zprava) then + return true + end + end + + local c = zprava:sub(1, 1) + if c == "!" then + -- celoserverový kanál + if not minetest.check_player_privs(jmeno, "shout") then + ch_core.systemovy_kanal(jmeno, "Nemůžete psát do celoserverového kanálu, protože vám bylo odebráno právo shout!") + return false + end + -- zprava = zprava:sub(zprava:sub(2,2) == " " and 3 or 2, #zprava) + return ch_core.chat("celoserverovy", jmeno, zprava:sub(2, -1), info_pos) + elseif c == "_" then + -- šepot + -- zprava = zprava:sub(zprava:sub(2,2) == " " and 3 or 2, #zprava) + return ch_core.chat("sepot", jmeno, zprava:sub(2, -1), info_pos) + elseif c == "\"" then + -- soukromá zpráva + local i = string.find(zprava, " ") + if not i then + return true + end + local komu + if i > 2 then + local kandidati = {} + local predpona = zprava:sub(2, i - 1) + for prihlasovaci, _ in pairs(ch_data.online_charinfo) do + local zobrazovaci = ch_core.prihlasovaci_na_zobrazovaci(prihlasovaci):gsub(" ", "_") + if (#prihlasovaci >= #predpona and prihlasovaci:sub(1, #predpona) == predpona) + or (#zobrazovaci >= #predpona and zobrazovaci:sub(1, #predpona) == predpona) then + table.insert(kandidati, prihlasovaci) + end + end + if #kandidati > 1 then + local s + if #kandidati < 5 then + s = "možnosti" + else + s = "možností" + end + ch_core.systemovy_kanal(jmeno, "CHYBA: Nejednoznačná předpona „"..predpona.."“ ("..#kandidati.." "..s..")") + return true + elseif #kandidati == 1 then + komu = kandidati[1] + else + komu = predpona + end + else + komu = "" + end + return ch_core.soukroma_zprava(jmeno, komu, zprava:sub(i + 1)) + else + -- místní zpráva + if c == "=" then + zprava = zprava:sub(2, -1) + end + return ch_core.chat("mistni", jmeno, zprava, info_pos) + end +end + +function ch_core.set_doslech(player_name, param) + if param == "" or string.match(param, "%D") or param + 0 < 0 or param + 0 > 65535 then + return false + end + param = math.max(0, math.min(65535, param + 0)) + local player_info = ch_data.online_charinfo[player_name] or {} + player_info.doslech = param + -- print("player_name=("..player_name.."), param=("..param..")") + ch_core.systemovy_kanal(player_name, "Doslech nastaven na " .. param) + return true +end + +function ch_core.set_ignorovat(player_name, name_to_ignore) + local player_info = ch_data.online_charinfo[player_name] + if not player_info then + minetest.log("error", "[ch_core] cannot found online_charinfo["..player_name.."]") + return false + end + local ignore_list = player_info.chat_ignore_list + if not ignore_list then + minetest.log("error", "[ch_core] online_charinfo["..player_name.."] does not have ignore_list!") + return false + end + name_to_ignore = ch_core.jmeno_na_prihlasovaci(name_to_ignore) + if not minetest.player_exists(name_to_ignore) then + ch_core.systemovy_kanal(player_name, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(name_to_ignore) .. " neexistuje!") + elseif name_to_ignore == player_name then + ch_core.systemovy_kanal(player_name, "Nemůžete ignorovat sám/a sebe!") + elseif minetest.check_player_privs(name_to_ignore, "protection_bypass") then + ch_core.systemovy_kanal(player_name, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(name_to_ignore) .. " má právo protection_bypass, takže ji nemůžete ignorovat!") + elseif ignore_list[name_to_ignore] then + ch_core.systemovy_kanal(player_name, "Postavu " .. ch_core.prihlasovaci_na_zobrazovaci(name_to_ignore) .. " již ignorujete!") + else + ignore_list[name_to_ignore] = 1 + ch_core.systemovy_kanal(player_name, "Nyní postavu " .. ch_core.prihlasovaci_na_zobrazovaci(name_to_ignore) .. " ignorujete. Toto platí, než se odhlásíte ze hry nebo než ignorování zrušíte příkazem /neignorovat.") + local target_info = ch_data.online_charinfo[name_to_ignore] + if target_info then -- pokud je hráč/ka online... + ch_core.systemovy_kanal(name_to_ignore, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(player_name) .. " vás nyní ignoruje.") + end + end + return true +end + +function ch_core.unset_ignorovat(player_name, name_to_unignore) + local player_info = ch_data.online_charinfo[player_name] + if not player_info then + minetest.log("error", "[ch_core] cannot found online_charinfo["..player_name.."]") + return false + end + local ignore_list = player_info.chat_ignore_list + if not ignore_list then + minetest.log("error", "[ch_core] online_charinfo["..player_name.."] does not have ignore_list!") + return false + end + + name_to_unignore = ch_core.jmeno_na_prihlasovaci(name_to_unignore) + if ignore_list[name_to_unignore] then + ignore_list[name_to_unignore] = nil + ch_core.systemovy_kanal(player_name, "Ignorování postavy " .. ch_core.prihlasovaci_na_zobrazovaci(name_to_unignore) .. " zrušeno.") + local target_info = ch_data.online_charinfo[name_to_unignore] + if target_info then -- pokud je hráč/ka online... + ch_core.systemovy_kanal(name_to_unignore, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(player_name) .. " vás přestala ignorovat. Nyní již můžete této postavě psát.") + end + else + ch_core.systemovy_kanal(player_name, "Postava " .. ch_core.prihlasovaci_na_zobrazovaci(name_to_unignore) .. " není vámi ignorována.") + end + return true +end + +minetest.register_on_chat_message(on_chat_message) + +minetest.override_chatcommand("me", { + params = "<text>", + description = "Pošle zprávu do základní kanálu četu. Funguje stejně jako přístupový znak „=“.", + func = function(player_name, message) + return on_chat_message(player_name, "="..message) + end, +}) + +minetest.override_chatcommand("msg", { + params = "<Jméno_Postavy> <text>", + description = "Zašle soukromou zprávu v četu. Funguje stejně jako přístupový znak „\"“, ale vyžaduje zadání jména postavy.", + func = function(player_name, text) + return on_chat_message(player_name, "\"" .. text) + end, +}) + +minetest.register_chatcommand("doslech", { + params = "[<metrů>]|uložit", + description = "Nastaví omezený doslech v chatu. Hodnota musí být celé číslo v rozsahu 0 až 65535. Bez parametru nastaví vypíše stávající hodnoty, s parametrem „uložit“ uloží aktuální doslech jako výchozí.", + privs = { shout = true }, + func = function(player_name, param) + local online_charinfo = ch_data.online_charinfo[player_name] + if not online_charinfo then + return false, "Vnitřní chyba!" + end + local aktualni_doslech = online_charinfo.doslech + local offline_charinfo = ch_data.offline_charinfo[player_name] + local vychozi_doslech = offline_charinfo and offline_charinfo.doslech + if param == "" then + ch_core.systemovy_kanal(player_name, "Aktuální doslech: "..aktualni_doslech.." m, výchozí doslech (po přihlášení): "..(vychozi_doslech and (vychozi_doslech.." m") or "neznámý")) + return true + elseif param == "uložit" or param == "ulozit" then + offline_charinfo = ch_data.get_offline_charinfo(player_name) + offline_charinfo.doslech = online_charinfo.doslech + ch_data.save_offline_charinfo(player_name) + ch_core.systemovy_kanal(player_name, "Doslech: "..aktualni_doslech.." m nastaven jako výchozí po přihlášení do hry.") + return true + else + return ch_core.set_doslech(player_name, param) + end + end, +}) +minetest.register_chatcommand("ignorovat", { + params = "<jménohráče/ky>", + description = "Do vašeho odhlášení nebudete dostávat žádné zprávy od daného hráče/ky, ledaže má právo protection_bypass.", + privs = {}, + func = function(player_name, name_to_ignore) + return ch_core.set_ignorovat(player_name, name_to_ignore) + end, +}) +minetest.register_chatcommand("neignorovat", { + params = "<jménohráče/ky>", + description = "Zruší omezení zadané příkazem /ignorovat.", + privs = {}, + func = function(player_name, name_to_unignore) + return ch_core.unset_ignorovat(player_name, name_to_unignore) + end, +}) + +minetest.register_chatcommand("kdojsem", { + description = "Vypíše zobrazované jméno postavy (včetně případných barev).", + privs = {}, + func = function(player_name, param) + local vysl = { + color_reset, + ch_core.prihlasovaci_na_zobrazovaci(player_name, true), + "\n", + color_systemovy, + "- přihlašovací jméno: ", + player_name, + } + local characters, main_name = ch_data.get_player_characters(player_name) + if #characters > 1 then + table.insert(vysl, "\n- hlavní postava: ") + table.insert(vysl, ch_core.prihlasovaci_na_zobrazovaci(main_name, true)) + table.insert(vysl, color_systemovy) + for i, name in ipairs(characters) do + characters[i] = ch_core.prihlasovaci_na_zobrazovaci(name, true) + end + table.insert(vysl, "\n- všechny postavy ["..#characters.."]: ") + table.insert(vysl, table.concat(characters, color_systemovy..", ")) + end + ch_core.systemovy_kanal(player_name, table.concat(vysl)) + return true + end, +}) + +local typy_postavy = { + admin = "správa serveru", + creative = "kouzelnická", + survival = "dělnická", + new = "nová", +} + +local function get_info_o(player_name, is_privileged) + player_name = ch_core.jmeno_na_prihlasovaci(player_name) + local player_role = ch_core.get_player_role(player_name) + if player_role == "none" then + return "*** Postava "..player_name.." neexistuje!" + end + local view_name = ch_core.prihlasovaci_na_zobrazovaci(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo == nil then + return "*** Nejsou uloženy žádné informace o "..view_name.."." + end + local result = { + "*** Informace o "..view_name..":".. + "\n* Druh postavy: "..(typy_postavy[player_role] or "neznámý typ postavy"), + } + local online_charinfo = ch_data.online_charinfo[player_name] -- may be nil! + local past_playtime = offline_charinfo.past_playtime or 0 + local past_ap_playtime = offline_charinfo.past_ap_playtime or 0 + local current_playtime + if online_charinfo ~= nil then + current_playtime = 1.0e-6 * (minetest.get_us_time() - online_charinfo.join_timestamp) + else + current_playtime = 0 + end + if not is_privileged then + past_playtime = math.floor(past_playtime) + past_ap_playtime = math.floor(past_ap_playtime) + current_playtime = math.floor(current_playtime) + end + if online_charinfo ~= nil then + table.insert(result, "\n* Odehraná doba [s]: "..current_playtime.." nyní + "..past_playtime.." dříve") + else + table.insert(result, "\n* Odehraná doba [s]: "..past_playtime.." (postava není ve hře)") + end + table.insert(result, "\n* Aktivní odehraná doba: "..past_ap_playtime.." s = "..(past_ap_playtime / 3600).." hod.") + local level_def = ch_core.ap_get_level(offline_charinfo.ap_level) + table.insert(result, "\n* Úroveň "..offline_charinfo.ap_level..". "..offline_charinfo.ap_xp.." bodů z "..level_def.count..", "..(level_def.base + offline_charinfo.ap_xp).." celkem") + table.insert(result, "\n* Doslech: aktuální "..(online_charinfo and online_charinfo.doslech and online_charinfo.doslech.." m" or "neznámý")..", výchozí "..(offline_charinfo.doslech and offline_charinfo.doslech.." m" or "neznámý")) + local all_characters, main_name = ch_data.get_player_characters(player_name) + if #all_characters >= 2 then + for i, name in ipairs(all_characters) do + all_characters[i] = ch_core.prihlasovaci_na_zobrazovaci(name) + end + table.insert(result, "\n* Všechny postavy hráče/ky: "..table.concat(all_characters, ", ").." (hlavní: "..ch_core.prihlasovaci_na_zobrazovaci(main_name)..")") + end + return table.concat(result) +end + +minetest.register_chatcommand("info_o", { + params = "<Jméno postavy>", + description = "Vypíše systémové informace o postavě", + privs = { server = true }, + func = function(admin_name, param) + minetest.chat_send_player(admin_name, get_info_o(param, true)) + return true + end, +}) + +minetest.register_chatcommand("info", { + description = "Vypíše systémové informace o vaší postavě", + func = function(player_name, param) + minetest.chat_send_player(player_name, get_info_o(player_name, false)) + return true + end, +}) + +-- log all chatcommands except /msg + +minetest.register_on_chatcommand(function(name, command, params) + if command ~= "msg" then + minetest.log("action", name.."$ /"..command.." "..params) + end +end) + +ch_core.close_submod("chat") diff --git a/ch_core/clean_players.lua b/ch_core/clean_players.lua new file mode 100644 index 0000000..30764e2 --- /dev/null +++ b/ch_core/clean_players.lua @@ -0,0 +1,64 @@ +ch_core.open_submod("clean_players", {data = true, lib = true, privs = true}) + +function ch_core.clean_players() + local all_players = ch_core.get_last_logins(false, {}) + local count = 0 + + for _, data in ipairs(all_players) do + if + -- podmínky pro smazání postavy: + -- 1) postava se nejmenuje Administrace + data.player_name ~= "Administrace" + -- 2) postava nemá právo server nebo ch_registered_player + and not minetest.check_player_privs(data.player_name, "server") and not minetest.check_player_privs(data.player_name, "ch_registered_player") + -- 3) postava nemá naplánovanou registraci jinou než "new" + and (data.pending_registration_type or "new") == "new" + -- 4) postava nemá odehráno 1.0 hodin nebo víc + and (data.played_hours_total < 1.0) + -- 5) postava není ve hře + and not data.is_in_game + -- 6) poslední přihlášení bylo alespoň před 60 dny + and data.last_login_before >= 60 + then + minetest.log("action", "Old player "..data.player_name.." is going to be DELETED because of inactivity!\n"..dump2({data})) + -- 1) Odstranit offline_charinfo + local r = ch_data.delete_offline_charinfo(data.player_name) + if not r then + minetest.log("error", "Removing offline charinfo of "..data.player_name.." failed!") + else + minetest.log("action", "- Offline charinfo of "..data.player_name.." successfully removed.") + end + -- 2) Odstranit hráčské informace + r = minetest.remove_player(data.player_name) + if r == 0 then + minetest.log("action", "- Player data of "..data.player_name.." successfully removed.") + else + minetest.log("error", "Removing of player data of "..data.player_name.." failed: "..(r or "nil")) + end + -- 3) Odstranit přihlašovací údaje + r = minetest.remove_player_auth(data.player_name) + if r then + minetest.log("action", "- Authentication data of "..data.player_name.." successfully removed.") + else + minetest.log("error", "Removing of authentication data of "..data.player_name.." failed: "..(r or "nil")) + end + -- 4) Zkontrolovat, že postava byla opravdu smazána. + r = minetest.player_exists(data.player_name) + if not r then + minetest.log("action", "Player "..data.player_name.." was removed.") + count = count + 1 + else + minetest.log("error", "Player "..data.player_name.." was not removed.") + end + end + end + + if count > 0 then + minetest.log("action", "[ch_core/clean_players] "..count.." players was removed.") + end + return count +end + +minetest.register_on_mods_loaded(function() minetest.after(10, ch_core.clean_players) end) + +ch_core.close_submod("clean_players") diff --git a/ch_core/creative_inventory.lua b/ch_core/creative_inventory.lua new file mode 100644 index 0000000..91b623d --- /dev/null +++ b/ch_core/creative_inventory.lua @@ -0,0 +1,1037 @@ +ch_core.open_submod("creative_inventory", {lib = true}) + +local none = {} +local partition_defs = { + { + name = "empty_buckets", + groups = none, + items = {"bucket:bucket_empty", "bucket_wooden:bucket_empty"}, + mods = none, + }, + { + name = "caste_nastroje", + groups = none, + items = {"moreblocks:circular_saw", "unifieddyes:airbrush", "bike:painter", + "advtrains:trackworker", "wrench:wrench", "replacer:replacer", + "ch_extras:total_station", "ch_extras:jumptool", "ch_extras:teleporter_unsellable", + "technic:mining_drill", "technic:mining_drill_mk2", "technic:mining_drill_mk3", + "bridger:scaffolding", "ch_core:chisel", + "orienteering:builder_compass_1", "ch_extras:lupa", "ch_extras:periskop", + "binoculars:binoculars"}, + mods = none, + }, + { + name = "kladivo_kovadlina", + groups = none, + items = none, + mods = {"anvil"}, + }, + { + name = "krumpace", + groups = {"pickaxe"}, + items = none, + mods = none, + }, + { + name = "lopaty", + groups = {"shovel"}, + items = none, + mods = none, + }, + { + name = "motyky", + groups = {"hoe"}, + items = none, + mods = none, + }, + { + name = "sekery", + groups = {"axe"}, + items = none, + mods = none, + }, + { + name = "srpy", + groups = {"sickle"}, + items = none, + mods = none, + }, + { + name = "mitrkosa", + groups = none, + items = {"farming:scythe_mithril"}, + mods = none, + }, + { + name = "trojnástroje", + groups = {"multitool"}, + items = none, + mods = none, + }, + { + name = "klice", + groups = none, + items = none, + mods = {"rotate"}, + }, + + { + name = "cestbudky", + groups = {"travelnet"}, + items = none, + mods = none, + }, + + { + name = "drevo", + groups = {"wood"}, + items = none, + mods = none, + exclude_items = { + "moreblocks:wood_tile", + "moreblocks:wood_tile_center", + "moreblocks:wood_tile_offset", + "moreblocks:wood_tile_full", + "pkarcs:acacia_wood_arc", + "pkarcs:acacia_wood_inner_arc", + "pkarcs:acacia_wood_outer_arc", + "pkarcs:acacia_wood_with_arc", + "pkarcs:aspen_wood_arc", + "pkarcs:aspen_wood_inner_arc", + "pkarcs:aspen_wood_outer_arc", + "pkarcs:aspen_wood_with_arc", + "pkarcs:junglewood_arc", + "pkarcs:junglewood_inner_arc", + "pkarcs:junglewood_outer_arc", + "pkarcs:junglewood_with_arc", + "pkarcs:pine_wood_arc", + "pkarcs:pine_wood_inner_arc", + "pkarcs:pine_wood_outer_arc", + "pkarcs:pine_wood_with_arc", + "pkarcs:wood_arc", + "pkarcs:wood_inner_arc", + "pkarcs:wood_outer_arc", + "pkarcs:wood_with_arc", + }, + -- not_in_mods = {"pkarcs"}, + -- not_in_mods = {"cottages", "moreblocks", "pkarcs"}, + }, + { + name = "drevokmeny", + groups = {"tree"}, + items = none, + mods = none, + exclude_items = {"cottages:water_gen", "bamboo:trunk"}, + }, + --[[ + { + name = "barvy", + groups = {"basic_dye"}, + items = none, + mods = none, + }, + { + name = "ploty", + groups = {"fence"}, + items = none, + mods = none, + exclude_items = {"technic:insulator_clip_fencepost"}, + }, ]] + { + name = "mesesloupky", + groups = {"mesepost_light"}, + items = none, + mods = none, + }, + { + name = "jil", + groups = {"bakedclay"}, + items = none, + mods = none, + }, + { + name = "kvetiny", + groups = {"flower"}, + items = none, + mods = none, + }, + { + name = "vlaky", + groups = {"at_wagon"}, + items = none, + mods = none, + }, + { + name = "elektrickestroje", + groups = {"technic_machine"}, + items = none, + mods = none, + exclude_items = {"digtron:power_connector"}, + }, + --[[ { + name = "obleceni", + groups = {"clothing"}, + items = none, + mods = none, + }, + { + name = "pláště", + groups = {"cape"}, + items = none, + mods = none, + }, + { + name = "technic_nn", + groups = {"technic_lv"}, + items = none, + mods = none, + }, + { + name = "technic_sn", + groups = {"technic_mv"}, + items = none, + mods = none, + }, + { + name = "technic_vn", + groups = {"technic_hv"}, + items = none, + mods = none, + }, ]] + { + name = "listy", + groups = {"leaves"}, + items = none, + mods = none, + }, + { + name = "ingoty", + groups = none, + items = {"basic_materials:brass_ingot", "default:bronze_ingot", "default:tin_ingot", "default:copper_ingot", "default:gold_ingot", "default:steel_ingot", + "moreores:mithril_ingot", "moreores:silver_ingot", "technic:chromium_ingot", "technic:cast_iron_ingot", "technic:stainless_steel_ingot", + "technic:lead_ingot", "technic:carbon_steel_ingot", "technic:mixed_metal_ingot", "technic:zinc_ingot", "technic:uranium0_ingot", + "technic:uranium_ingot", "technic:uranium35_ingot", + "aloz:aluminum_ingot"}, + mods = none, + }, + { + name = "loziska", + groups = none, + items = {"default:stone_with_tin", "default:stone_with_diamond", "default:stone_with_mese", "default:stone_with_copper", "default:stone_with_coal", + "default:stone_with_gold", "default:stone_with_iron", "denseores:large_tin_ore", "denseores:large_diamond_ore", "denseores:large_mese_ore", + "denseores:large_copper_ore", "denseores:large_mithril_ore", "denseores:large_silver_ore", "denseores:large_coal_ore", + "denseores:large_gold_ore", "denseores:large_iron_ore", "moreores:mineral_mithril", "moreores:mineral_silver", + "technic:mineral_chromium", "technic:mineral_lead", "technic:mineral_sulfur", "technic:mineral_uranium", "technic:mineral_zinc", + "aloz:stone_with_bauxite"}, + mods = none, + }, + { + name = "sklo", + groups = none, + items = {"building_blocks:woodglass", "building_blocks:smoothglass", "cucina_vegana:mushroomlight_glass", "darkage:glow_glass", + "darkage:glass", "default:obsidian_glass", "default:glass", "moreblocks:clean_glass", "moreblocks:clean_super_glow_glass", + "moreblocks:clean_glow_glass", "moreblocks:iron_glass", "moreblocks:super_glow_glass", "moreblocks:glow_glass", "moreblocks:coal_glass"}, + mods = none, + }, + { + name = "kombinace", + groups = none, + items = none, + mods = {"comboblock"}, + }, + { + name = "clothing_singlecolor", + groups = {"clothing_singlecolor", "cape_singlecolor"}, + items = none, + mods = none, + }, + { + name = "clothing_stripy", + groups = {"clothing_stripy", "cape_stripy"}, + items = none, + mods = none, + }, + { + name = "clothing_squared", + groups = {"clothing_squared", "cape_squared"}, + items = none, + mods = none, + }, + { + name = "clothing_fabric_singlecolor", + groups = {"clothing_fabric_singlecolor"}, + items = none, + mods = none, + }, + { + name = "clothing_fabric_stripy", + groups = {"clothing_fabric_stripy"}, + items = none, + mods = none, + }, + { + name = "clothing_fabric_squared", + groups = {"clothing_fabric_squared"}, + items = none, + mods = none, + }, + --[[ + { + name = "trojdesky", + groups = {"slab"}, + items = none, + mods = none, + }, + { + name = "trojsvahy", + groups = {"slope"}, + items = none, + mods = none, + }, ]] + { + name = "vypocetni_technika", + groups = none, + items = { + "homedecor:alarm_clock", "homedecor:digital_clock", "homedecor:dvd_vcr", + "homedecor:stereo", "homedecor:tv_stand", + }, + mods = {"computers", "digiterms", "home_workshop_machines"}, + }, + { + name = "platforms_1", + groups = {"platform=1"}, + items = none, + mods = none, + }, + { + name = "platforms_2", + groups = {"platform=2"}, + items = none, + mods = none, + }, + { + name = "platforms_3", + groups = {"platform=3"}, + items = none, + mods = none, + }, + { + name = "platforms_4", + groups = {"platform=4"}, + items = none, + mods = none, + }, +} + +function ch_core.overridable.is_clothing(item_name) + return minetest.get_item_group(item_name, "clothing") > 0 or + minetest.get_item_group(item_name, "cape") > 0 +end + +function ch_core.overridable.is_stairsplus_shape(item_name, item_def) + return false +end + +function ch_core.overridable.is_cnc_shape(item_name, item_def) + return false +end + +function ch_core.overridable.is_other_shape(item_name, groups) + return + (groups.fence and item_name ~= "technic:insulator_clip_fencepost") or + groups.gravestone or + groups.streets_manhole or + groups.streets_stormdrain or + item_name:sub(1, 7) == "pkarcs:" or + item_name:sub(1, 8) == "pillars:" or + item_name:sub(1, 10) == "si_frames:" +end + +local function is_experimental(name, groups) + return name:sub(1, 8) == "ch_test:" or (groups.experimental or 0) > 0 +end + +local function is_streets_signs(name, groups) + return name:sub(1, 8) == "streets:" and (groups.streets_tool or groups.sign) +end + +local categories = { + { + description = "výchozí paleta předmětů", + condition = function(itemstring, name, def, groups, palette_index) + local o = ch_core.overridable + return + not o.is_clothing(name) and + not o.is_stairsplus_shape(name, def) and + not o.is_cnc_shape(name, def) and + not o.is_other_shape(name, groups) and + not is_experimental(name, groups) and + not is_streets_signs(name, groups) and + (not groups.dye or groups.basic_dye) and + not groups.platform and + name:sub(1, 16) ~= "clothing:fabric_" and + name:sub(1, 15) ~= "letters:letter_" + end, + icon = { + {image = "ui_category_all.png^[resize:24x24", item = "unified_inventory:bag_large"}, + }, + }, + { + description = "tvary", + condition = function(itemstring, name, def, groups, palette_index) + local o = ch_core.overridable + return o.is_stairsplus_shape(name, def) or + o.is_cnc_shape(name, def) or + o.is_other_shape(name, groups) + end, + icon = { + {image = "[combine:120x120:-4,-4=technic_cnc_bannerstone.png^[resize:20x20", item = "technic:cnc"}, + }, + }, + { + description = "lakované předměty", + condition = function(itemstring, name, def, groups, palette_index) + return (groups.ud_param2_colorable or 0) > 0 and not is_experimental(name, groups) + end, + icon = { + {image = "lrfurn_armchair_inv.png^[resize:24x24", item = "lrfurn:armchair"}, + }, + }, + { + description = "barviva", + condition = function(itemstring, name, def, groups, palette_index) + return groups.dye ~= nil + end, + icon = { + {image = "dye_green.png^[resize:24x24", item = "dye:green"}, + }, + }, + { + description = "dopravní značení", + condition = function(itemstring, name, def, groups, palette_index) + return is_streets_signs(name, groups) or name == "streets:smallpole" or name == "streets:bigpole" or name == "streets:bigpole_short" + end, + icon = { + {image = "streets_sign_eu_50.png^[resize:24x24", item = "streets:sign_eu_50"}, + }, + }, + { + description = "jídlo a pití", + condition = function(itemstring, name, def, groups, palette_index) + return groups.ch_food ~= nil or groups.drink ~= nil or groups.ch_poison ~= nil + end, + icon = { + {image = "ch_extras_parekvrohliku.png^[resize:24x24", item = "ch_extras:parekvrohliku"}, + }, + }, + { + description = "látky (na šaty)", + condition = function(itemstring, name, def, groups, palette_index) + return name:sub(1, 16) == "clothing:fabric_" + end, + icon = { + { + image = "(clothing_fabric.png^[multiply:#f8f7f3)^(((clothing_fabric.png^clothing_inv_second_color.png)^[makealpha:0,0,0)^[multiply:#00b4e8)^[resize:24x24", + item = "clothing:fabric_white", + }, + }, + }, + { + description = "lokomotivy a vagony", + condition = function(itemstring, name, def, groups, palette_index) + return groups.at_wagon ~= nil + end, + icon = { + {image = "moretrains_engine_japan_inv.png^[resize:24x24", item = "advtrains:moretrains_engine_japan"}, + }, + }, + { + description = "nástroje", + condition = function(itemstring, name, def, groups, palette_index) + return minetest.registered_tools[name] ~= nil + end, + icon = { + {image = "moreores_tool_mithrilpick.png^[resize:24x24", item = "moreores:pick_mithril"}, + {image = "default_tool_mesepick.png", item = "default:pick_mese"}, + }, + }, + { + description = "nástupiště", + condition = function(itemstring, name, def, groups, palette_index) + return (groups.platform or 0) > 0 + end, + icon = { + {image = "basic_materials_cement_block.png^[resize:128x128^advtrains_platform.png^[resize:16x16", item = "advtrains:platform_low_cement_block"} + }, + }, + { + description = "oblečení a obuv", + condition = function(itemstring, name, def, groups, palette_index) + return ch_core.overridable.is_clothing(name) + end, + icon = { + {image = "(clothing_inv_shirt.png^[multiply:#98cd61)^[resize:24x24", item = "clothing:shirt_green"}, + }, + }, + { + description = "všechny předměty", + condition = function(itemstring, name, ndef, groups, palette_index) + return true + end, + icon = { + {image = "[combine:40x40:-11,-3=letters_pattern.png\\^letters_star_overlay.png\\^[makealpha\\:255,126,126^[resize:16x16", item = "letters:letter_star"}, + }, + }, + { + description = "experimentální předměty", + condition = function(itemstring, name, ndef, groups, palette_index) + return is_experimental(name, groups) + end, + icon = { + {image = "ch_test_4dir_1.png^[resize:16x16", item = "ch_test:test_color"}, + }, + }, + { + description = "test: svítící bloky", + condition = function(itemstring, name, def, groups, palette_index) + return minetest.registered_nodes[name] ~= nil and def.paramtype == "light" and (def.light_source or 0) > 0 + end, + icon = { + {image = "default_fence_pine_wood.png^default_mese_post_light_side.png^[makealpha:0,0,0^[resize:16x16", item = "default:mese_post_light_pine_wood"}, + }, + }, + { + description = "test: železnice kromě vozidel", + condition = function(itemstring, name, def, groups, palette_index) + return name:match("^advtrains") and not groups.at_wagon + end, + icon = { + {image = "advtrains_dtrack_placer.png^[resize:24x24", item = "advtrains:dtrack_placer"}, + }, + }, +} + +local function get_default_categories() + local descriptions, icons = {}, {} + for i, category_def in ipairs(categories) do + descriptions[i] = category_def.description.." [0]" + icons[i] = "unknown_item.png^[resize:16x16" + end + return { descriptions = descriptions, filter = {}, icons = icons } +end + +local function compute_categories(itemstrings) + local counts = {} + local descriptions = {} + local filter = {} + local icons = {} + local itemstrings_set = {} + local registered_items = minetest.registered_items + local string_cache = {} + + for i = 1, #categories do + counts[i] = 0 + end + + for _, itemstring in ipairs(itemstrings) do + if not itemstrings_set[itemstring] then + itemstrings_set[itemstring] = true + local stack = ItemStack(itemstring) + local name = stack:get_name() + local def = registered_items[name] + if def ~= nil then + local groups = def.groups or none + local palette_index + if name ~= itemstring then + palette_index = stack:get_meta():get_int("palette_index") + end + local mask = "" + for i, category_def in ipairs(categories) do + if category_def.condition(itemstring, name, def, groups, palette_index) then + mask = mask.."1" + counts[i] = counts[i] + 1 + else + mask = mask.."0" + end + end + if string_cache[mask] ~= nil then + mask = string_cache[mask] + else + string_cache[mask] = mask + end + filter[itemstring] = mask + end + end + end + for i, category_def in ipairs(categories) do + descriptions[i] = category_def.description.." ["..counts[i].."]" + icons[i] = "unknown_item.png^[resize:16x16" + for _, icon_def in ipairs(category_def.icon) do + if icon_def.item == nil or minetest.registered_items[icon_def.item] ~= nil then + icons[i] = assert(icon_def.image) + break + end + end + end + return { descriptions = descriptions, filter = filter, icons = icons } +end + +ch_core.creative_inventory = { + categories = get_default_categories(), + --[[ + = { + descriptions[index] = popisek, -- velikost #descriptions udává počet existujících kategorií + filter = { + [itemstring] = "00100[...]", -- maska kategorií pro každý itemstring + }, + icons[index] = textura, -- ikona kategorie (obrázek) + } + ]] + items_by_order = {}, + partitions_by_name = { + others = { + name = "others", + items_by_order = {}, + }, + }, + not_initialized = true, +} + +local function generate_palette_itemstrings(name, paramtype2, limit, list_to_add) + if list_to_add == nil then + list_to_add = {} + end + if limit == nil then + limit = 256 + end + local step + if paramtype2 == "color" then + step = 1 + elseif paramtype2 == "colorfacedir" or paramtype2 == "colordegrotate" then + step = 32 + elseif paramtype2 == "color4dir" then + step = 4 + elseif paramtype2 == "colorwallmounted" then + step = 8 + else + table.insert(list_to_add, name) + return list_to_add + end + local i = 0 + while i < limit and i * step < 256 do + local itemstring = minetest.itemstring_with_palette(name, i * step) + table.insert(list_to_add, itemstring) + i = i + 1 + end + return list_to_add +end + +local function sort_items(itemstrings, group_by_mod) + -- items is expected to be a sequence of item strings, e. g. {"default:cobble", "default:stick", ...} + -- the original list is not changed; the new list is returned + local counter_nil, counter_same, counter_diff = 0, 0, 0 + local itemstring_to_key = {} + + for _, itemstring in ipairs(itemstrings) do + local stack = ItemStack(itemstring) + local name = stack:get_name() + local desc = stack:get_description() or "" + local tdesc = minetest.get_translated_string("cs", desc) + if tdesc == nil then + counter_nil = counter_nil + 1 + tdesc = "" + else + if tdesc == desc then + counter_same = counter_same + 1 + else + counter_diff = counter_diff + 1 + end + end + if group_by_mod then + local index = name:find(":") + if index then + tdesc = name:sub(1, index)..tdesc + end + end + local palette_index = stack:get_meta():get_int("palette_index") + if palette_index > 0 then + tdesc = string.format("%s/%03d", tdesc, palette_index) + end + itemstring_to_key[itemstring] = ch_core.utf8_radici_klic(tdesc, false) + end + + local result = table.copy(itemstrings) + table.sort(result, function(a, b) return itemstring_to_key[a] < itemstring_to_key[b] end) + -- minetest.log("info", "sort_items() stats: "..counter_diff.." differents, "..counter_same.." same, "..counter_nil.." nil, "..(counter_diff + counter_same + counter_nil).." total, count = "..#result..".") + return result +end + +local creative_inventory_initialized = false + +-- should be called when any player logs in +function ch_core.update_creative_inventory(force_update) + if creative_inventory_initialized and not force_update then + minetest.log("verbose", "will not update_creative_inventory(): "..(creative_inventory_initialized and "true" or "false").." "..(force_update and "true" or "false")) + return false + end + minetest.log("verbose", "will update_creative_inventory()") + + -- 1. naplň překladové tabulky + local name_to_partition = {} + local group_to_partition = {} + local mod_to_partition = {} + local partition_to_exclude_items = {others = none} + for _ --[[order]], part_def in ipairs(partition_defs) do + if (part_def.exclude_items or none)[1] then + local set = {} + partition_to_exclude_items[part_def.name] = set + for _, name in ipairs(part_def.exclude_items) do + set[name] = true + end + else + partition_to_exclude_items[part_def.name] = none + end + for _, name in ipairs(part_def.items or none) do + if name_to_partition[name] then + minetest.log("warning", "ERROR in creative_inventory! Item "..name.." has been already set to partition "..name_to_partition[name].." while it is also set to partition "..part_def.name.."!") + else + name_to_partition[name] = part_def.name + end + end + for _, group in ipairs(part_def.groups or none) do + if group_to_partition[group] then + minetest.log("warning", "ERROR in creative_inventory! Group "..group.." has been already set to partition "..group_to_partition[group].." while it is also set to partition "..part_def.name.."!") + else + group_to_partition[group] = part_def.name + end + end + for _, mod in ipairs(part_def.mods or none) do + if mod_to_partition[mod] then + minetest.log("warning", "ERROR in creative_inventory! Mod "..mod.." has been already set to partition "..mod_to_partition[mod].." while it is also set to partition "..part_def.name.."!") + else + mod_to_partition[mod] = part_def.name + end + end + end + + -- 2. projdi registrované předměty a zařaď je do příslušných oddílů + local existing_partitions = {} + + for name, def in pairs(minetest.registered_items) do + local groups = def.groups or none + local nici = groups.not_in_creative_inventory + if (def.description or "") ~= "" and (nici == nil or nici <= 0) then -- not_in_creative_inventory => skip + local partition = "others" + if def._ch_partition then + partition = def._ch_partition + elseif name_to_partition[name] then + partition = name_to_partition[name] + else + local mod --[[, subname]] = name:match("^([^:]*):([^:]*)$") + local success = false + for group, rank in pairs(groups) do + if rank > 0 then + local groupplus = group.."="..rank + if group_to_partition[group] and not partition_to_exclude_items[group_to_partition[group]][name] then + partition = group_to_partition[group] + success = true + break + elseif group_to_partition[groupplus] and not partition_to_exclude_items[group_to_partition[groupplus]][name] then + partition = group_to_partition[groupplus] + success = true + break + end + end + end + if not success then + if mod and mod_to_partition[mod] and not partition_to_exclude_items[mod_to_partition[mod]][name] then + partition = mod_to_partition[mod] + end + end + end + + if partition_to_exclude_items[partition][name] then + partition = "others" + end + + local partition_list = existing_partitions[partition] + if not partition_list then + partition_list = {} + existing_partitions[partition] = partition_list + end + if groups.ui_generate_palette_items ~= nil and groups.ui_generate_palette_items > 0 then + generate_palette_itemstrings(name, def.paramtype2, groups.ui_generate_palette_items, partition_list) + else + table.insert(partition_list, name) + end + end + end + + -- 3. vyjmi oddíl "others" pro samostatné zpracování + local partition_others_list = existing_partitions.others + existing_partitions.others = nil + + -- 4. seřaď předměty v oddílech + for partition_name, item_list in pairs(table.copy(existing_partitions)) do + existing_partitions[partition_name] = sort_items(item_list, false) + end + if partition_others_list ~= nil then + partition_others_list = sort_items(partition_others_list, true) + end + + -- 5. seřaď oddíly + local partitions_in_order = {} + for _, part_def in ipairs(partition_defs) do + local partition_list = existing_partitions[part_def.name] + if partition_list then + table.insert(partitions_in_order, { name = part_def.name, items_by_order = partition_list }) + existing_partitions[part_def.name] = nil + end + end + local remaining_partitions = {} + for part_name, _ in pairs(existing_partitions) do + table.insert(remaining_partitions, part_name) + end + table.sort(remaining_partitions) + for _, part_name in ipairs(remaining_partitions) do + table.insert(partitions_in_order, { name = part_name, items_by_order = existing_partitions[part_name]}) + end + if partition_others_list then + table.insert(partitions_in_order, { name = "others", items_by_order = partition_others_list}) + end + + -- 6. sestav z nich partitions_by_name a items_by_order + local partitions_by_name = {} + local items_by_order = {} + local partitions_by_name_count = 0 + + for _, part_info in pairs(partitions_in_order) do + partitions_by_name[part_info.name] = part_info.items_by_order + table.insert_all(items_by_order, part_info.items_by_order) + partitions_by_name_count = partitions_by_name_count + 1 + end + + -- 7. přidej kategorie + local ci_categories = compute_categories(items_by_order) + + -- commit + local new_ci = { + categories = ci_categories, + items_by_order = items_by_order, + partitions_by_name = partitions_by_name, + } + ch_core.creative_inventory = new_ci + minetest.log("action", "ch_core creative_inventory updated: "..#items_by_order.." items in "..partitions_by_name_count.." partitions") + creative_inventory_initialized = true + return new_ci +end + +local ifthenelse = ch_core.ifthenelse +local function cmp_true_first(a, b) + if a then + return ifthenelse(b, 0, -1) + else + return ifthenelse(b, 1, 0) + end +end +local function cmp_str(a, b) + if a < b then + return -1 + elseif a > b then + return 1 + else + return 0 + end +end +local function cmp_inv(f) + return function(a, b, options) + return -f(a, b, options) + end +end + +local comparisons = { + { + name = "empty_last", + description_asc = "prázdná pole na konec", + description_desc = "prázdná pole první", + cmp = function(a, b) + local count_a, count_b = a:get_count(), b:get_count() + return cmp_true_first(count_a > 0, count_b > 0) + end, + }, + { + name = "by_description", + description_asc = "podle popisku (vzestupně)", + description_desc = "podle popisku (sestupně)", + cmp = function(a, b, options) + local s_a, s_b = a:get_description(), b:get_description() + local lang_code = options.lang_code + if s_a ~= nil then + if lang_code ~= nil then + s_a = minetest.get_translated_string(lang_code, s_a) + end + else + s_a = a:get_name() + end + if s_b ~= nil then + if lang_code ~= nil then + s_b = minetest.get_translated_string(lang_code, s_b) + end + else + s_b = b:get_name() + end + return cmp_str(ch_core.utf8_radici_klic(s_a, true), ch_core.utf8_radici_klic(s_b, true)) + end, + }, + { + name = "by_modname", + description_asc = "podle módu (vzestupně)", + description_desc = "podle módu (sestupně)", + cmp = function(a, b) + local sa = a:get_name():match("^([^:]*):.*") or "" + local sb = b:get_name():match("^([^:]*):.*") or "" + return cmp_str(sa, sb) + end, + }, + { + name = "by_itemname", + description_asc = "podle technického názvu (vzestupně)", + description_desc = "podle technického názvu (sestupně)", + cmp = function(a, b) + local sa = a:get_name():match("^[^:]*:(.*)$") or a:get_name() + local sb = b:get_name():match("^[^:]*:(.*)$") or b:get_name() + return cmp_str(sa, sb) + end, + }, + { + name = "by_count", + description_asc = "podle počtu (od nejmenšího)", + description_desc = "podle počtu (od největšího)", + cmp = function(a, b) + return a:get_count() - b:get_count() + end, + }, + { + name = "tools_first", + description_asc = "nástroje první", + description_desc = "nástroje na konec", + cmp = function(a, b) + return cmp_true_first(minetest.registered_tools[a:get_name()], minetest.registered_tools[b:get_name()]) + end, + }, + { + name = "tools_by_wear", + description_asc = "nástroje podle opotřebení (od nejmenšího)", + description_desc = "nástroje podle opotřebení (od největšího)", + cmp = function(a, b) + if minetest.registered_tools[a:get_name()] and minetest.registered_tools[b:get_name()]then + return a:get_wear() - b:get_wear() + else + return 0 + end + end, + }, + { + name = "books_by_ick", + description_asc = "knihy podle IČK (vzestupně)", + description_desc = "knihy podle IČK (sestupně)", + cmp = function(a, b) + if a:is_empty() or b:is_empty() then + return 0 + end + local a_name, b_name = a:get_name(), b:get_name() + if minetest.get_item_group(a_name, "book") == 0 or minetest.get_item_group(b_name, "book") == 0 then + return 0 + end + local a_meta, b_meta = a:get_meta(), b:get_meta() + local a_ick = ("0000000000000000"..a_meta:get_string("ick")):sub(-16, -1) + local b_ick = ("0000000000000000"..b_meta:get_string("ick")):sub(-16, -1) + return cmp_str(a_ick, b_ick) + end, + }, + { + name = "books_first", + description_asc = "knihy první", + description_desc = "knihy na konec", + cmp = function(a, b) + return cmp_true_first(not a:is_empty() and minetest.get_item_group(a:get_name(), "book") ~= 0, not b:is_empty() and minetest.get_item_group(b:get_name(), "book") ~= 0) + end, + }, + { + name = "books_by_author", + description_asc = "knihy podle autorství (vzestupně)", + description_desc = "knihy podle autorství (sestupně)", + cmp = function(a, b) + if a:is_empty() or minetest.get_item_group(a:get_name(), "book") == 0 or b:is_empty() or minetest.get_item_group(b:get_name(), "book") == 0 then + return 0 + end + return cmp_str(ch_core.utf8_radici_klic(a:get_meta():get_string("author"), false), ch_core.utf8_radici_klic(b:get_meta():get_string("author"), false)) + end, + }, + { + name = "books_by_title", + description_asc = "knihy podle názvu (vzestupně)", + description_desc = "knihy podle názvu (sestupně)", + cmp = function(a, b) + if a:is_empty() or minetest.get_item_group(a:get_name(), "book") == 0 or b:is_empty() or minetest.get_item_group(b:get_name(), "book") == 0 then + return 0 + end + return cmp_str(ch_core.utf8_radici_klic(a:get_meta():get_string("title"), false), ch_core.utf8_radici_klic(b:get_meta():get_string("title"), false)) + end, + }, +} + +local name_to_comparison_index = {} +local choices = {} +for i, cmp_def in ipairs(comparisons) do + name_to_comparison_index[cmp_def.name] = i + if cmp_def.description_asc ~= nil then + table.insert(choices, {name = cmp_def.name, desc = false, description = cmp_def.description_asc}) + end + if cmp_def.description_desc ~= nil then + table.insert(choices, {name = cmp_def.name, desc = true, description = cmp_def.description_desc}) + end +end + +function ch_core.sort_itemstacks(stacks, order_types, lang_code) + local cmps = {} + for _, otype in ipairs(order_types) do + local cmp_index = name_to_comparison_index[otype.name] + if cmp_index == nil then + error("Invalid order type '"..otype.name.."'!") + end + if otype.desc then + table.insert(cmps, cmp_inv(comparisons[cmp_index].cmp)) + else + table.insert(cmps, comparisons[cmp_index].cmp) + end + end + local options = {lang_code = lang_code} + table.sort(stacks, function(a, b) + local result + for i = 1, #cmps do + result = cmps[i](a, b, options) + if result < 0 then + return true + elseif result > 0 then + return false + end + end + return 0 + end) +end + +--[[ +Příklad: + local stacks = inv:get_list("main") + ch_core.sort_itemstacks(stacks, { + {name = "empty_last"}, + {name = "tools_first", desc = true}, + {name = "by_description"}, + {name = "by_modname"}, + {name = "by_itemname"}, + {name = "by_count", desc = true}, + }, ch_data.online_charinfo[player_name].lang_code) +]] + +ch_core.close_submod("creative_inventory") diff --git a/ch_core/data.lua b/ch_core/data.lua new file mode 100644 index 0000000..84a6bcc --- /dev/null +++ b/ch_core/data.lua @@ -0,0 +1,385 @@ +ch_core.open_submod("data") + +-- POZICE A OBLASTI +-- =========================================================================== + +local function setting_get_pos(name, setting_level) + local result = minetest.setting_get_pos(name) + if result ~= nil then + minetest.log("action", "Position setting "..name.." read: "..minetest.pos_to_string(result)) + elseif setting_level == "required" then + error("Required position setting "..name.." not set or have an invalid format!") + elseif setting_level == "optional" then + minetest.log("action", "Position setting "..name.." not set (optional).") + else + minetest.log("error", "Position setting "..name.." not set! Will be reset to zeroes.") + result = vector.zero() + end + return result +end + +local function setting_get_str(name, setting_level) + local result = minetest.settings:get(name) + if result ~= nil then + minetest.log("action", "String setting "..name.." read: "..result) + elseif setting_level == "required" then + error("Required string setting "..name.." not set!") + elseif setting_level == "optional" then + minetest.log("action", "String setting "..name.." not set (optional).") + else + minetest.log("error", "String setting "..name.." not set! Will be reset to an empty string") + result = "" + end + return result +end + +ch_core.config = { + povinne_vytisky_listname = setting_get_str("ch_povinne_vytisky_listname", "optional") or "main", +} + +ch_core.positions = { + zacatek_1 = setting_get_pos("static_spawnpoint") or vector.new(-70,9.5,40), + zacatek_2 = setting_get_pos("ch_zacatek_2"), + zacatek_3 = setting_get_pos("ch_zacatek_3"), + vezeni_min = setting_get_pos("ch_vezeni_min"), + vezeni_max = setting_get_pos("ch_vezeni_max"), + vezeni_cil = setting_get_pos("ch_vezeni_cil"), + vezeni_dvere = setting_get_pos("ch_vezeni_dvere", "optional"), + povinne_vytisky = setting_get_pos("ch_povinne_vytisky", "optional"), +} + +local zero_by_type = { + int = 0, + float = 0.0, + -- nil = nil, + string = "", + vector = vector.zero(), +} + +-- GLOBÁLNÍ DATA +-- =========================================================================== +local global_data_data_types = { + posun_casu = "int", + povinne_vytisky = "vector", + povinne_vytisky_listname = "string", + pristi_ick = "int", +} + +local initial_global_data = { + pristi_ick = 10000, +} + +for k, t in pairs(global_data_data_types) do + if initial_global_data[k] == nil then + initial_global_data[k] = zero_by_type[t] + end +end + +-- DATA O POSTAVÁCH +-- =========================================================================== +-- key => "int|float", unknown keys are deserialized as strings +local offline_charinfo_data_types = { + ap_level = "int", -- > 0 + ap_xp = "int", -- >= 0 + ap_version = "int", -- >= 0 + discard_drops = "int", -- 0 = nic, 1 = předměty házet do koše + domov = "string", + doslech = "int", -- >= 0 + extended_inventory = "int", -- 0 = normální velikost, 1 = rozšířený inventář + last_ann_shown_date = "string", -- datum, kdy byla hráči/ce naposledy vypsána oznámení po přihlášení do hry (YYYY-MM-DD) + last_login = "int", -- >= 0, in seconds since 1. 1. 2000 UTC; 0 is invalid value + neshybat = "int", -- 0 = shýbat se při stisku Shift, 1 = neshýbat se + no_ch_sky = "int", -- 0 = krásná obloha ano, 1 = ne + past_ap_playtime = "float", -- in seconds + past_playtime = "float", -- in seconds + pending_registration_privs = "string", + pending_registration_type = "string", + rezim_plateb = "int", -- >= 0, význam podle módu ch_bank + skryt_body = "int", -- 0 => zobrazit, 1 => skrýt + skryt_hlad = "int", -- 0 => zobrazovat (výchozí), 1 => skrýt + skryt_zbyv = "int", -- 0 => zobrazovat (výchozí), 1 => skrýt + stavba = "string", + ui_event_filter = "string", + zacatek_kam = "int", -- 1 => Začátek, 2 => Masarykovo náměstí, 3 => Hlavní nádraží + + trest = "int", +} + +local storage = ch_core.storage + +ch_core.global_data = table.copy(initial_global_data) + +local function is_acceptable_name(player_name) + local types = {} + for i = 1, #player_name do + local b = string.byte(player_name, i) + if b == 0x2d or b == 0x5f then + types[i] = '_' + elseif 0x30 <= b and b <= 0x39 then + types[i] = '0' + elseif b < 0x61 then + types[i] = 'A' + else + types[i] = 'a' + end + end + local digits, dashes = 0, 0 + for i = 1, #player_name do + if types[i] == '0' then + digits = digits + 1 + elseif digits > 0 then + -- číslice jsou dovoleny jen na konci jména + return false + elseif types[i] == '_' then + dashes = dashes + 1 + -- pomlčky a podtržítka jsou dovoleny jen mezi písmeny + if i == 1 or i == #player_name or string.lower(types[i - 1]) ~= 'a' or string.lower(types[i + 1]) ~= 'a' then + return false + end + end + end + return digits <= 4 and dashes <= 5 -- omezení počtu +end +ch_data.is_acceptable_name = is_acceptable_name + +local function is_invalid_player_name(player_name) + if type(player_name) == "number" then + player_name = tostring(player_name) + elseif type(player_name) ~= "string" then + return "Invalid player_name type "..type(player_name).."!" + end + if #player_name == 0 then + return "Empty player_name!" + end + if #player_name > 19 then + return "Player name "..player_name.." too long!" + end + if string.find(player_name, "[^_%w-]") then + return "Player name '"..player_name.."' contains an invalid character!" + end + return false +end + +--[[ +local function verify_valid_player_name(player_name) + local message = is_invalid_player_name(player_name) + if message then + error(message) + else + return tostring(player_name) + end +end +]] + +function ch_core.get_offline_charinfo(player_name) + core.log("warning", "Obsolete function ch_core.get_offline_charinfo() called!") + return ch_data.get_offline_charinfo(player_name) +end + +function ch_core.save_global_data(keys) + local ax = type(keys) + if ax == "table" then + ax = ch_core.save_global_data + for _, key in ipairs(keys) do + ax(key) + end + return + elseif ax ~= "string" and ax ~= "number" then + error("save_global_data() called with an argument of invalid type "..ax.."!") + end + + local data_type = global_data_data_types[keys] + if data_type == nil then + minetest.log("warning", "save_global_data() called for unknown key '"..keys.."', ignored.") + return false + end + local full_key = "/"..keys + local value = ch_core.global_data[keys] + if data_type == "int" then + storage:set_int(full_key, value or 0) + elseif data_type == "float" then + storage:set_float(full_key, value or 0.0) + elseif data_type == "vector" then + storage:set_string(full_key, minetest.pos_to_string(vector.round(value))) + else + storage:set_string(full_key, value or "") + end + return true +end + +-- datatype = "string"|"int"|"float"|"vector"|"nil" +function ch_core.save_offline_charinfo(player_name, keys) + core.log("warning", "Obsolete function ch_core.save_offline_charinfo() called!") + return ch_data.save_offline_charinfo(player_name) +end + +function ch_core.set_titul(player_name, titul) + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + offline_charinfo.titul = titul + ch_data.save_offline_charinfo(player_name) + local online_charinfo = ch_data.online_charinfo[player_name] + local player = core.get_player_by_name(player_name) + if player and online_charinfo and ch_core.compute_player_nametag then + player:set_nametag_attributes(ch_core.compute_player_nametag(online_charinfo, offline_charinfo)) + end + return true +end + +-- ch_core.set_temporary_titul() -- Nastaví či zruší dočasný titul postavy. +-- +function ch_core.set_temporary_titul(player_name, titul, titul_enabled) + if type(player_name) ~= "string" then + error("ch_core.set_temporary_titul(): Invalid player_name type: "..type(player_name).."!") + end + local online_charinfo = ch_data.online_charinfo[player_name] + if not online_charinfo or not titul or titul == "" then return false end + local dtituly = ch_core.get_or_add(online_charinfo, "docasne_tituly") + if titul_enabled then + dtituly[titul] = 1 + else + dtituly[titul] = nil + end + local player = core.get_player_by_name(player_name) + if player and ch_core.compute_player_nametag then + player:set_nametag_attributes(ch_core.compute_player_nametag(online_charinfo, ch_data.get_offline_charinfo(player_name))) + return true + else + return false + end +end + +local function restore_value_by_type(data_type, value, value_description) + if data_type == "int" then + return math.round(tonumber(value)) + elseif data_type == "float" then + return tonumber(value) + elseif data_type == "string" then + return value + elseif data_type == "vector" then + local result = minetest.string_to_pos(value) + if result == nil then + minetest.log("warning", "Invalid value ignored on restore! (description = "..(value_description or "nil")..")") + return zero_by_type["vector"] + end + return result + elseif data_type == "nil" then + return nil + else + error("restore_value_by_type() called with invalid data_type "..dump2(data_type).."!") + end +end + +-- restore offline data from the storage +local player_counter, player_field_counter, global_counter, delete_counter = 0, 0, 0, 0 +local player_set = {} +local storage_table = (storage:to_table() or {}).fields or {} +for full_key, value in pairs(storage_table) do + local player_name, key = full_key:match("^([^/]*)/(.+)$") + if player_name == "" and key ~= "" then + local data_type = global_data_data_types[key] + ch_core.global_data[key] = restore_value_by_type(data_type, value, "global property "..key) + global_counter = global_counter + 1 + --[[elseif player_name and not is_invalid_player_name(player_name) and (player_set[player_name] or ch_data.offline_charinfo[player_name] == nil) then + local ch_data_offline_charinfo = ch_data.get_or_add_offline_charinfo(player_name) + local data_type = offline_charinfo_data_types[key] + if data_type == "int" then + ch_data_offline_charinfo[key] = math.round(tonumber(value)) + core.log("warning", "Offline charinfo upgraded from storage: "..player_name.."/"..key.."=(int)"..tostring(ch_data_offline_charinfo[key])) + elseif data_type == "float" then + ch_data_offline_charinfo[key] = tonumber(value) + core.log("warning", "Offline charinfo upgraded from storage: "..player_name.."/"..key.."=(float)"..tostring(ch_data_offline_charinfo[key])) + else + ch_data_offline_charinfo[key] = value + core.log("warning", "Offline charinfo upgraded from storage: "..player_name.."/"..key.."=(string)\""..tostring(ch_data_offline_charinfo[key]).."\"") + end + player_field_counter = player_field_counter + 1 + if player_set[player_name] == nil then + player_set[player_name] = true + player_counter = player_counter + 1 + end]] + else + storage:set_string(full_key, "") + core.log("warning", "Invalid key '"..full_key.."' (value "..value..") removed from mod storage!") + delete_counter = delete_counter + 1 + end +end +print("[ch_core] Restored "..player_field_counter.." data pairs of "..player_counter.." players and "..global_counter.." global pairs from the mod storage. "..delete_counter.." was deleted.") + +for player_name, _ in pairs(player_set) do + ch_data.save_offline_charinfo(player_name) +end + +-- Check and update keys +for key, data_type in pairs(global_data_data_types) do + if ch_core.global_data[key] == nil and data_type ~= "nil" then + ch_core.global_data[key] = zero_by_type[data_type] + end +end + +def = { + description = "Zaznamená do příslušného souboru co nejvíc údajů o aktuálním stavu hráčské postavy. Pouze pro Administraci.", + params = "<Jmeno_postavy>", + privs = {server = true}, + func = function(admin_name, player_name) + local player = core.get_player_by_name(player_name) + if player == nil then + return false, "Postava neexistuje!" + end + player_name = player:get_player_name() + local inv = player:get_inventory() + local result = { + player_name = player_name, + pos = player:get_pos(), + velocity = player:get_velocity(), + hp = player:get_hp(), + inv_main = inv:get_list("main"), + inv_craft = inv:get_list("craft"), + wield_index = player:get_wield_index(), + armor_groups = player:get_armor_groups(), + animation = player:get_animation(), + attachment = {player:get_attach()}, + children = player:get_children(), + bone_overrides = player:get_bone_overrides(), + properties = player:get_properties(), + is_player = player:is_player(), + nametag_attributes = player:get_nametag_attributes(), + look_dir = player:get_look_dir(), + look_vertical = player:get_look_vertical(), + look_horizontal = player:get_look_horizontal(), + breath = player:get_breath(), + fov = {player:get_fov()}, + meta = player:get_meta():to_table(), + player_control = player:get_player_control(), + player_control_bits = player:get_player_control_bits(), + physics_override = player:get_physics_override(), + huds = player:hud_get_all(), + hud_flags = player:hud_get_flags(), + hotbar_size = player:hud_get_hotbar_itemcount(), + hotbar_image = player:hud_get_hotbar_image(), + hotbar_selected_image = player:hud_get_hotbar_selected_image(), + sky = player:get_sky(), + sun = player:get_sun(), + moon = player:get_moon(), + stars = player:get_stars(), + clouds = player:get_clouds(), + day_night_ratio_override = player:get_day_night_ratio(), + local_animation = {player:get_local_animation()}, + eye_offset = {player:get_eye_offset()}, + lighting = player:get_lighting(), + flags = player:get_flags(), + online_charinfo = ch_data.online_charinfo[player_name] or "nil", + offline_charinfo = ch_data.offline_charinfo[player_name] or "nil", + } + result = dump2(result) + local parts = string.split(result, "\n_[") + table.sort(parts) + result = table.concat(parts, "\n_[") + local path = core.get_worldpath().."/_dump_"..player_name..".txt" + core.safe_file_write(path, result) + return true, "Zaznamenáno do: "..path + end, +} + +core.register_chatcommand("dumpplayer", def) + +ch_core.close_submod("data") diff --git a/ch_core/dennoc.lua b/ch_core/dennoc.lua new file mode 100644 index 0000000..f7a307a --- /dev/null +++ b/ch_core/dennoc.lua @@ -0,0 +1,43 @@ +ch_core.open_submod("dennoc", {privs = true, chat = true}) + +--[[ + /dennoc + /dennoc den + /dennoc noc + /dennoc XX:XX +]] + +local def = { + privs = {}, + params = "[koeficient]", + description = "Nastaví osobní osvětlení světa.", + func = function(player_name, param) + local player = minetest.get_player_by_name(player_name) + if not player then + return false + end + + local c + if param == "" then + player:override_day_night_ratio(nil) + ch_core.systemovy_kanal(player_name, "/dennoc: osobní osvětlení světa zrušeno") + return true + elseif param == "den" then + c = 1 + elseif param == "noc" then + c = 0 + else + local s = param:gsub(",", ".") + c = tonumber(s) + end + if c ~= nil and 0.0 <= c and c <= 1.0 then + player:override_day_night_ratio(c) + ch_core.systemovy_kanal(player_name, "/dennoc: osobní osvětlení světa nastaveno na koeficient "..c) + return true + end + return false, "Neplatný formát parametru!" + end +} +minetest.register_chatcommand("dennoc", def) + +ch_core.close_submod("dennoc") diff --git a/ch_core/doc/online_charinfo.md b/ch_core/doc/online_charinfo.md new file mode 100644 index 0000000..c51a5e8 --- /dev/null +++ b/ch_core/doc/online_charinfo.md @@ -0,0 +1,33 @@ +# Dokumentace prvků struktury online\_charinfo + +## submodul \[data\] + +* public **player\_name** (string) -- obsahuje přihlašovací jméno postavy; vyplní se okamžitě při vzniku struktury; umožňuje zjistit přihlašovací jméno postavy ze samotného odkazu na online\_charinfo +* public **join\_timestamp** (float) -- při vstupu postavy do hry se vyplní na ch\_core.cas; umožňuje počítat odehranou dobu +* public **docasne\_tituly** (table: titul -> 1) -- tabulka dočasných titulů aktivních pro danou postavu + +## submodul \[chat\] + +* public **doslech** (int) -- aktuální doslech postavy, výchozí hodnota je 65535 +* public **chat\_ignore\_list** (table: player\_name -> true) -- množina hráčských jmen postav, které tato postava ignoruje; výchozí hodnotou je prázdná tabulka +* **posl_soukr_adresat** (string) -- přihlašovací jméno posledního adresáta/ky soukromé zprávy od této postavy; výchozí hodnota je nil +* public **horka\_zprava** (table: index \-> řádek + \["timeout"\] = čas vypršení) -- je-li nastavena, má se nad postavou zobrazit obsažená zpráva; první řádek zprávy vždy nastavuje barvu písma + +## submodul \[hud\] + +* public **player\_list\_huds** (table: index -> hud\_id) -- tabulka, která se vyplní jen v době, kdy je hráči/ce zobrazen seznam postav ve hře; obsahuje ID jednotlivých řádků seznamu, aby je bylo možno později zrušit; při skrytí seznamu se vždy nastaví na nil, podle její přítomnosti tedy lze testovat, zda je zobrazený seznam postav +* **hudbars** (table: index -> hudbar\_id) -- tabulka evidující obsazené „hudbars“; je pouze pro vnitřní použití funkcemi ch_core.try_alloc_hudbar() a ch_core.free_hudbar() + +## submodul \[vezeni\] + +* **last\_hudbar\_trest\_max** (int) -- poslední maximum ukazatele trestu +* **prison\_hudbar** (string) -- je-li zobrazen ukazatel trestu, tato položka obsahuje jeho ID; je určena jen pro vnitřní potřebu submodulu + +## submodul \[timers\] + +* public **ch\_timers** (table: timer\_id -> timer\_data) -- tabulka aktivních časovačů + +## submodul \[pryc\] + +* public **pryc** (funkce) -- je-li vyplněna, hráč/ka je pryč od počítače; zavoláním funkce se tento stav zruší +* **pryc\_hud\_id** (string) -- identifikátor HUD „pryč od počítače“ (pro vnitřní potřebu) diff --git a/ch_core/entity_register.lua b/ch_core/entity_register.lua new file mode 100644 index 0000000..1962673 --- /dev/null +++ b/ch_core/entity_register.lua @@ -0,0 +1,334 @@ +ch_core.open_submod("entity_register", {lib = true}) + +local ifthenelse = ch_core.ifthenelse + +local count = 0 -- aktuální počet zaregistrovaných entit +local next_gc_count = 16 -- počet, při jehož dosažení se spustí pročištění +local entities = {} -- mapa registrovaných entit: handle => entita +local name_index = { + ["__builtin:item"] = {}, +} -- mapa entity_name => {handle => object} (indexování __builtin:item je zapnuto předem) +local handle_generator = PcgRandom(os.time()) + +local function remove_registered_entity(handle) + local entity = entities[handle] + if entity == nil then + return false + end + local name = assert(entity.name) + local index = name_index[name] + if index ~= nil then + index[handle] = nil + end + entities[handle] = nil + entity._ch_entity_handle = nil + count = count - 1 + return true +end + +local function add_entity_to_register(handle, entity) + local object = entity.object + if not object:is_valid() or entities[handle] ~= nil then + return false + end + entities[handle] = entity + local index = name_index[entity.name] + if index ~= nil then + index[handle] = object + end + count = count + 1 + entity._ch_entity_handle = handle +end + +--[[ + Projde zaregistrované entity a odstraní ty, jejichž objekty již vypršely. + Vrací: + gc_count (int) [počet odstraněných entit], + result (table) [tabulka odstraněných entit (mapa handle => entita), velikost odpovídá gc_count], + remains_count (int) [počet zbylých (platných) registrovaných entit] +]] +function ch_core.collect_invalid_entities() + local result = {} + local gc_count = 0 + for handle, entity in pairs(entities) do + local o = entity.object + if o == nil or not o:is_valid() then + gc_count = gc_count + 1 + result[handle] = entity + end + end + if gc_count > 0 then + for handle, _ in pairs(result) do + remove_registered_entity(handle) + end + next_gc_count = 2 * count + end + return gc_count, result, count +end + +--[[ + Vypne indexování entit zadaného jména +]] +function ch_core.disable_entity_name_index(name) + name_index[name] = nil +end + +--[[ + Zapne indexování entit zadaného jména +]] +function ch_core.enable_entity_name_index(name) + if name_index[name] == nil then + local new_index = {} + for handle, entity in pairs(entities) do + if entity.name == name and entity.object:is_valid() then + new_index[handle] = entity.object + end + end + name_index[name] = new_index + end +end + +--[[ + Vrátí seznam handles pro entity daného typu v zadané oblasti. + Aby fungovala, musí být zapnutý entity_name_index pro daný typ entit. + Parametry: + * pos (vector), + * radius (float), + * name (string), + * return_type (enum: handle|entity|object or nil) [co vrátit v seznamu; nil znamená handle] + * handle_to_ignore (int32 or nil) [je-li nastaveno, entita se zadaným handle se do výstupního seznamu nezahrne] + Vrací: + a) seznam nalezených entit (typ prvků je určený parametrem return_type) + b) nil (pokud není zapnuto indexování entit daného typu) +]] +function ch_core.find_handles_in_radius(pos, radius, name, return_type, handle_to_ignore) + local result = {} + local index = name_index[name] + if index == nil then + return nil + end + if handle_to_ignore == nil then + handle_to_ignore = 0 + end + for handle, object in pairs(index) do + if handle ~= handle_to_ignore then + local opos = object:get_pos() -- get_pos() může vrátit nil pro vypršené objekty + if opos ~= nil and vector.distance(pos, opos) <= radius then + if return_type == nil or return_type == "handle" then + table.insert(result, handle) + elseif return_type == "entity" then + table.insert(result, entities[handle]) + else + table.insert(result, object) + end + end + end + end + return result +end + +--[[ + Vrátí počet zaregistrovaných entit bez ohledu na to, zda mají platné objekty. + Vrací: + count (int) +]] +function ch_core.get_count_of_registered_entities() + return count +end + +--[[ + Vrací handle pro danou (platnou) entitu. + Pokud je parametr nil, nebo jde o entitu, jejíž objekt již vypršel, vrací nil. + Jde-li o entitu s platným objektem, ale dosud neregistrovanou, zaregistruje ji a vrátí nové handle. + Vrací: + handle (int32 or nil) +]] +function ch_core.get_handle_of_entity(lua_entity) + if lua_entity == nil then + return nil + elseif type(lua_entity) == "table" then + local handle = lua_entity._ch_entity_handle + local is_valid = lua_entity.object:is_valid() + if type(handle) == "number" and entities[handle] ~= nil then + -- already has a handle + if is_valid then + return handle + else + -- object already expired! + remove_registered_entity(handle) + return nil + end + elseif not is_valid then + -- invalid object + return nil + end + local message + handle, message = ch_core.register_entity(lua_entity) + if message ~= nil then + core.log(ifthenelse(handle == nil, "error", "warning"), "ch_core.get_handle_of_entity(): "..message) + end + return handle + else + error("ch_core.get_handle_of_entity() called with an invalid argument: "..dump2({type = type(lua_entity), value = lua_entity})) + end +end + +--[[ + Existuje-li entita s platným objektem zaregistrovaná pod zadaným handle, vrátí ji. + Vyhledávání je možno omezit také na specifickou hodnotu 'name'. + Parametry: + * handle (int32 or nil) + * required_name (string or nil) + Vrací: + a) entity (table) [požadovaná entita], object (ObjRef) [její objekt] + b) nil, other_name (string) [odkazovaná entita existuje, ale liší se hodnotou 'name'] + c) nil, nil [parametr je nil nebo odkazovaná entita neexistuje nebo nemá platný objekt] +]] +function ch_core.get_entity_by_handle(handle, required_name) + if type(handle) == "number" then + local result = entities[handle] + if result ~= nil then + local o = result.object + if o == nil or not o:is_valid() then + remove_registered_entity(handle) + return nil + end + if required_name ~= nil and required_name ~= result.name then + return nil, result.name -- different name + end + return result, o + end + elseif handle ~= nil then + minetest.log("warning", "ch_core.get_entity_by_handle() called with an invalid argument: "..dump2({type = type(handle), value = handle})) + end + return nil, nil +end + +--[[ + Otestuje předaný argument, zda jde o použitelné handle pro registraci entit. + Vrací: success (bool or nil), reason (string) + Pro nil vrací nil, "nil". +]] +function ch_core.is_valid_entity_handle(h) + if h == nil then + return nil, "nil" + elseif type(h) ~= "number" then + return false, "not a number (invalid type)" + elseif math.floor(h) ~= h then + return false, "not an integer" + elseif h < -2147483648 or h > 2147483647 then + return false, "out of range" + elseif h == 0 then + return false, "zero is an invalid handle" + else + return true, "OK" + end +end + +local function normalize_lua_entity(lua_entity) + if type(lua_entity) == "table" and type(lua_entity.name) == "string" and lua_entity.object ~= nil and lua_entity.object:is_valid() then + return lua_entity + end +end + +--[[ + Zaregistruje dosud neregistrovanou entitu a vrátí handle. + Parametry: + * lua_entity (table or nil) + * handle_to_use (int32 or nil) + Vrací: + a) new_handle (int32), warning_message (string or nil) + b) nil, error_message (string) +]] +function ch_core.register_entity(lua_entity, handle_to_use) + if lua_entity == nil then + return nil, "nothing to register" + end + local success, reason = ch_core.is_valid_entity_handle(handle_to_use) + if success == false then + return nil, "handle_to_use is invalid: "..reason + end + lua_entity = normalize_lua_entity(lua_entity) + if lua_entity == nil then + return nil, "invalid or expired entity!" + end + local handle = lua_entity._ch_entity_handle + if type(handle) == "number" and entities[handle] ~= nil then + -- the entity already has a handle + return handle, ifthenelse(handle_to_use == nil or handle_to_use == handle, "already registered", "already registered under a different handle!") + end + handle = handle_to_use + local handle_type = "old" + local warning + if handle_to_use ~= nil then + local e = entities[handle_to_use] + if e ~= nil then + if e.object:is_valid() then + handle = nil + handle_type = "???" + warning = "requested handle already used for a different entity" + else + -- object with requested handle already expired + remove_registered_entity(handle_to_use) + end + end + end + + if handle == nil then + -- generate a new handle + repeat + repeat + handle = handle_generator:next() + until handle ~= 0 + assert(ch_core.is_valid_entity_handle(handle)) + until entities[handle] == nil + handle_type = "new" + end + add_entity_to_register(handle, lua_entity) + if count >= next_gc_count then + ch_core.collect_invalid_entities() + if entities[handle] == nil then + core.log("error", "Freshly added entity collected as garbage! This means internal error.") + return nil, "internal error" + end + end + return handle, nil +end + +--[[ + Je-li daná entita v registru, odstraní ji. Používá se zpravidla uvnitř obsluhy on_deactivate. + Lze ji přiřadit i přímo: + on_deactivate = ch_core.unregister_entity +]] +function ch_core.unregister_entity(lua_entity) + if lua_entity ~= nil then + local handle = lua_entity._ch_entity_handle + if type(handle) == "number" then + if entities[handle] ~= nil then + remove_registered_entity(handle) + return true + end + lua_entity._ch_entity_handle = nil -- for sure + end + end +end + +-- __builtin:item by se měla registrovat: +local builtin_item = core.registered_entities["__builtin:item"] +local function empty_function() end +local orig_on_activate = builtin_item.on_activate or empty_function +local orig_on_deactivate = builtin_item.on_deactivate or empty_function +local new_item = { + on_activate = function(self, ...) + orig_on_activate(self, ...) + ch_core.register_entity(self) + end, + on_deactivate = function(self, ...) + orig_on_deactivate(self, ...) + ch_core.unregister_entity(self) + end, +} +setmetatable(new_item, {__index = builtin_item}) +core.register_entity(":__builtin:item", new_item) + +ch_core.close_submod("entity_register") 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") diff --git a/ch_core/formspecs.lua b/ch_core/formspecs.lua new file mode 100644 index 0000000..5659e4c --- /dev/null +++ b/ch_core/formspecs.lua @@ -0,0 +1,217 @@ +ch_core.open_submod("formspecs", {data = true, lib = true}) + +-- API: +-- ch_core.show_formspec(player_or_player_name, formname, formspec, formspec_callback, custom_state, options) +-- local function formspec_callback(custom_state, player, formname, fields) + +--[[ + player_name => { + callback = function, + custom_state = ..., + formname = string, + object_id = int, + } +]] +local formspec_states = {} +local formspec_states_next_id = 1 + +local function def_to_string(label, defitem, separator) + if defitem == nil then + return "" + end + local t = type(defitem) + if t == "string" then + return label.."["..defitem.."]" + elseif t == "number" or t == "bool" then + return label.."["..tostring(defitem).."]" + elseif t == "table" then + if #defitem == 0 then + return label.."[]" + else + t = {} + for i = 1, #defitem do + t[i] = tostring(defitem[i]) + end + t[1] = label.."["..t[1] + t[#t] = t[#t].."]" + return table.concat(t, separator) + end + else + return "" + end +end + +local ifthenelse = ch_core.ifthenelse + +--[[ + Sestaví záhlaví formspecu. Dovolené klíče jsou: + -- formspec_version + -- size + -- position + -- anchor + -- padding + -- no_prepend (bool) + -- listcolors + -- bgcolor + -- background + -- background9 + -- set_focus + -- auto_background (speciální, vylučuje se s background a background9) +]] +function ch_core.formspec_header(def) + local result, size_element + + if def.size ~= nil then + if type(def.size) ~= "table" then + error("def.size must be a table or nil!") + end + local s = def.size + size_element = {"size["..tostring(s[1])} + for i = 2, #s - 1, 1 do + size_element[i] = tostring(s[i]) + end + size_element[#s] = tostring(s[#s]).."]" + size_element = table.concat(size_element, ",") + else + size_element = "" + end + + result = { + def_to_string("formspec_version", def.formspec_version, ""), -- 1 + size_element, -- 2 + def_to_string("position", def.position, ","), -- 3 + def_to_string("anchor", def.anchor, ","), -- 4 + def_to_string("padding", def.padding, ","), -- 5 + ifthenelse(def.no_prepend == true, "no_prepend[]", ""), -- 6 + def_to_string("listcolors", def.listcolors, ";"), -- 7 + def_to_string("bgcolor", def.bgcolor, ";"), -- 8 + def_to_string("background", def.background, ";"), -- 9 + def_to_string("background9", def.background9, ";"), -- 10 + def_to_string("set_focus", def.set_focus, ";"), -- 11 + } + if not def.background and not def.background9 and def.formspec_version ~= nil and def.formspec_version > 1 then + if def.auto_background == true then + if result[7] == "" then + -- colors according to Technic Chests: + result[7] = "listcolors[#7b7b7b;#909090;#000000;#6e823c;#ffffff]" + end + result[10] = "background9[0,0;1,1;ch_core_formspec_bg.png;true;16]" + -- result[9] = "background[0,0;"..fsw..","..fsh..";ch_core_formspec_bg.png]" + end + end + return table.concat(result) +end + +--[[ + Má-li daná postava zobrazen daný formspec, uzavře ho a vrátí true. + Jinak vrátí false. + Je-li call_callback true, nastavený callback se před uzavřením zavolá + s fields = {quit = "true"} a jeho návratová hodnota bude odignorována. +]] +function ch_core.close_formspec(player_name_or_player, formname, call_callback) + if formname == nil or formname == "" then + return false -- formname invalid + end + local p = ch_core.normalize_player(player_name_or_player) + if p.player == nil then + return false -- player invalid or not online + end + local formspec_state = formspec_states[p.player_name] + if formspec_state == nil or formspec_state.formname ~= formname then + return false -- formspec not open or the formname is different + end + if call_callback then + formspec_state.callback(formspec_state.custom_state, p.player, formname, {quit = "true"}) + end + minetest.close_formspec(p.player_name, formname) + if formspec_states[p.player_name] ~= nil and formspec_states[p.player_name].object_id == formspec_state.object_id then + formspec_states[p.player_name] = nil + end + return true +end + +--[[ + Zobrazí hráči/ce formulář a nastaví callback pro jeho obsluhu. + Callback nemusí být zavolán v nestandardních situacích jako + v případě odpojení klienta. +]] +function ch_core.show_formspec(player_name_or_player, formname, formspec, callback, custom_state, options) + local p = ch_core.normalize_player(player_name_or_player) + if p.player == nil then return false end -- player invalid or not online + + if formname == nil or formname == "" then + -- generate random formname + formname = "ch_core:"..minetest.sha1(tostring(bit.bxor(minetest.get_us_time(), math.random(1, 1099511627775))), false) + end + + local id = formspec_states_next_id + formspec_states_next_id = id + 1 + formspec_states[p.player_name] = { + callback = callback or function(...) return end, + custom_state = custom_state, + formname = formname, + object_id = id, + } + + minetest.show_formspec(p.player_name, formname, formspec) + return formname +end + +--[[ + Aktualizuje již zobrazený formspec. Vrátí true v případě úspěchu. + formspec_or_function může být buď řetězec, nebo funkce, která bude + pro získání řetězce zavolána s parametry: (player_name, formname, custom_state). + Pokud nevrátí řetězec, update_formspec skončí a vrátí false. +]] +function ch_core.update_formspec(player_name_or_player, formname, formspec_or_function) + if formname == nil or formname == "" then + return false -- formname invalid + end + local p = ch_core.normalize_player(player_name_or_player) + if p.player == nil then + return false -- player invalid or not online + end + local formspec_state = formspec_states[p.player_name] + if formspec_state == nil or formspec_state.formname ~= formname then + return false -- formspec not open or the formname is different + end + local t = type(formspec_or_function) + local formspec + if t == "string" then + formspec = formspec_or_function + elseif t == "function" then + formspec = formspec_or_function(p.player_name, formname, formspec_state.custom_state) + if type(formspec) ~= "string" then + return false + end + else + return false -- invalid formspec argument + end + minetest.show_formspec(p.player_name, formname, formspec) + return true +end + +local function on_player_receive_fields(player, formname, fields) + local player_name = assert(player:get_player_name()) + local formspec_state = formspec_states[player_name] + if formspec_state == nil then + return -- formspec not by ch_core + end + if formspec_state.formname ~= formname then + minetest.log("warning", player_name..": received fields of form "..(formname or "nil").." when "..(formspec_state.formname or "nil").." was expected") + formspec_states[player_name] = nil + return + end + local result = formspec_state.callback(formspec_state.custom_state, player, formname, fields, {}) -- custom_state, player, formname, fields + if type(result) == "string" then + -- string => show as formspec + formspec_states[player_name] = formspec_state + minetest.show_formspec(player_name, formname, result) + elseif fields ~= nil and fields.quit == "true" and formspec_states[player_name] ~= nil and formspec_states[player_name].object_id == formspec_state.object_id then + formspec_states[player_name] = nil + end + return true +end +minetest.register_on_player_receive_fields(on_player_receive_fields) + +ch_core.close_submod("formspecs") diff --git a/ch_core/hotbar.lua b/ch_core/hotbar.lua new file mode 100644 index 0000000..054210f --- /dev/null +++ b/ch_core/hotbar.lua @@ -0,0 +1,49 @@ +ch_core.open_submod("hotbar") + +function ch_core.predmety_na_liste(player, jako_pole) + local result = {} + if not player then + return result + end + local inv = player:get_inventory() + if not inv then + return result + end + local hotbar = inv:get_list("main") + if not hotbar then + return result + end + local hotbar_length = player:hud_get_hotbar_itemcount() + + if not hotbar_length then + hotbar_length = 8 + elseif hotbar_length > 32 then + hotbar_length = 32 + elseif hotbar_length < 1 then + hotbar_length = 1 + end + + if jako_pole then + for i = 1, hotbar_length, 1 do + local name = hotbar[i]:get_name() + if name and name ~= "" then + local t = result[name] + if t then + table.insert(t, i) + else + result[name] = {i} + end + end + end + else + for i = hotbar_length, 1, -1 do + local name = hotbar[i]:get_name() + if name and name ~= "" then + result[name] = i + end + end + end + return result +end + +ch_core.close_submod("hotbar") diff --git a/ch_core/hud.lua b/ch_core/hud.lua new file mode 100644 index 0000000..3883577 --- /dev/null +++ b/ch_core/hud.lua @@ -0,0 +1,392 @@ +ch_core.open_submod("hud", {chat = true, data = true, lib = true}) + +local ifthenelse = ch_core.ifthenelse + +-- PLAYER LIST +local base_y_offset = 90 +local y_scale = 20 + +local text_hud_defaults = { + type = "text", + position = { x = 0.5, y = 0 }, + -- offset + -- text + alignment = { x = 1, y = 1 }, + scale = { x = 100, y = 100 }, + number = 0xFFFFFF, +} + +function ch_core.show_player_list(player, online_charinfo) + if online_charinfo.player_list_huds then + return false + end + + -- gether the list of players + local items = {} + for c_player_name, c_online_charinfo in pairs(ch_data.online_charinfo) do + local c_offline_charinfo = ch_data.offline_charinfo[c_player_name] + local titul = c_offline_charinfo and c_offline_charinfo.titul + local dtituly = c_online_charinfo.docasne_tituly or {} + + if not titul then + if string.sub(c_player_name, -2) == "PP" then + titul = "pomocná postava" + elseif not minetest.check_player_privs(c_player_name, "ch_registered_player") then + titul = "nová postava" + end + end + + local zobrazovaci_jmeno = ch_core.prihlasovaci_na_zobrazovaci(c_player_name) + local text = zobrazovaci_jmeno + if titul then + text = text.." ["..titul.."]" + end + for dtitul, _ in pairs(dtituly) do + text = text.." ["..dtitul.."]" + end + table.insert(items, { name = zobrazovaci_jmeno, text = text }) + end + + -- sort the list + table.sort(items, function(a, b) return ch_core.utf8_mensi_nez(a.name, b.name, true) end) + + local huds, new_hud + local hud_defs = {} + + for _, item in ipairs(items) do + new_hud = table.copy(text_hud_defaults) + new_hud.offset = { x = 5, y = base_y_offset + 3 + #hud_defs * y_scale } + new_hud.text = item.text + table.insert(hud_defs, new_hud) + end + new_hud = { + type = "image", + alignment = { x = -1, y = 1 }, + position = { x = 1, y = 0 }, + offset = { x = 0, y = base_y_offset }, + text = "ch_core_white_pixel.png^[multiply:#333333^[opacity:128", + scale = { x = -50, y = #hud_defs * y_scale + 8 }, + number = text_hud_defaults.number, + } + huds = { + player:hud_add(new_hud) + } + online_charinfo.player_list_huds = huds + for i, hud_def in ipairs(hud_defs) do + huds[i + 1] = player:hud_add(hud_def) + end + return true +end + +function ch_core.hide_player_list(player, online_charinfo) + if not online_charinfo.player_list_huds then + return false + end + local huds = online_charinfo.player_list_huds + online_charinfo.player_list_huds = nil + for _, hud_id in ipairs(huds) do + player:hud_remove(hud_id) + end + return true +end + +local has_hudbars = minetest.get_modpath("hudbars") + +-- GAME TIME HUDBAR +if has_hudbars then + local icon_day = "ch_core_slunce.png^[resize:20x20" + local icon_night = "moon.png^[resize:20x20" + local bar_day = "hudbars_bar_day.png" + local bar_night = "hudbars_bar_night.png" + + hb.register_hudbar("ch_gametime", 0xCCCCCC, "", {icon = icon_day, bgicon = nil, bar = bar_day}, + 0, 100, true, "@1 min.", {order = {"value"}, textdomain = "hudbars"}) + + local function after_joinplayer(player_name) + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo ~= nil and offline_charinfo.skryt_zbyv ~= 1 then + hb.unhide_hudbar(player, "ch_gametime") + ch_core.update_gametime_hudbar({player}) + end + end + end + + local function on_joinplayer(player, last_login) + hb.init_hudbar(player, "ch_gametime") + minetest.after(1, after_joinplayer, player:get_player_name()) + end + minetest.register_on_joinplayer(on_joinplayer) + + function ch_core.show_gametime_hudbar(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo ~= nil and offline_charinfo.skryt_zbyv ~= 0 then + offline_charinfo.skryt_zbyv = 0 + ch_data.save_offline_charinfo(player_name) + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + hb.unhide_hudbar(player, "ch_gametime") + ch_core.update_gametime_hudbar({player}) + end + return true + end + return false + end + + function ch_core.hide_gametime_hudbar(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo ~= nil and offline_charinfo.skryt_zbyv ~= 1 then + offline_charinfo.skryt_zbyv = 1 + ch_data.save_offline_charinfo(player_name) + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + hb.hide_hudbar(player, "ch_gametime") + end + return true + end + return false + end + + function ch_core.is_gametime_hudbar_shown(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + return offline_charinfo ~= nil and offline_charinfo.skryt_zbyv ~= 1 + end + + local cache_time_speed = 0 + local cache_is_night + local cache_value = -1 + local dawn = 330 + local dusk = 1140 + + function ch_core.update_gametime_hudbar(players, timeofday) + local skip_cache = players ~= nil + if players == nil then + players = {} + for _, player in ipairs(minetest.get_connected_players()) do + local player_name = player:get_player_name() + local offline_charinfo = ch_data.offline_charinfo[player_name] or {} + if offline_charinfo.skryt_zbyv ~= 1 then + table.insert(players, player) + end + end + end + if #players == 0 then + return -- no players + end + + local tod = timeofday or minetest.get_timeofday() + if tod == nil then return end + tod = tod * 1440 -- převést na počet herních minut + local is_night = tod < dawn or tod >= dusk + local time_speed = tonumber(minetest.settings:get("time_speed")) + if time_speed == nil then + minetest.log("warning", "[ch_core/hud] Cannot determine time_speed!") + time_speed = 72 + end + + local new_value, new_max_value, new_icon, new_bar + if is_night then + new_value = math.ceil((ifthenelse(tod >= dusk, 1440 + dawn, dawn) - tod) / time_speed) + if skip_cache or time_speed ~= cache_time_speed or cache_is_night ~= is_night then + new_icon, new_bar = icon_night, bar_night + new_max_value = math.ceil((1440 - (dusk - dawn)) / time_speed) + cache_is_night, cache_time_speed = is_night, time_speed -- update cache + end + else + new_value = math.ceil((dusk - tod) / time_speed) + if skip_cache or time_speed ~= cache_time_speed or cache_is_night then + new_icon, new_bar = icon_day, bar_day + new_max_value = math.ceil((dusk - dawn) / time_speed) + cache_is_night, cache_time_speed = is_night, time_speed -- update cache + end + end + if skip_cache or new_value ~= cache_value then + cache_value = new_value + else + if new_max_value == nil and new_icon == nil then + return true -- nothing to update + end + new_value = nil -- don't update value if not necesarry + end + for _, player in ipairs(players) do + hb.change_hudbar(player, "ch_gametime", new_value, new_max_value, new_icon, nil, new_bar) + end + return true + end +else + function ch_core.show_gametime_hudbar(player_name) + return + end + + function ch_core.hide_gametime_hudbar(player_name) + return + end + + function ch_core.update_gametime_hudbar(players, tod) + return + end + + function ch_core.is_gametime_hudbar_shown(player_name) + return false + end +end + +-- CH_HUDBARS + +if not has_hudbars then + ch_core.count_of_ch_hudbars = 0 +else + ch_core.count_of_ch_hudbars = 2 + + local hudbar_formatstring = "@1: @2" + local hudbar_formatstring_config = { + order = { "label", "value" }, + textdomain = "hudbars", + } + local hudbar_defaults = { + icon = "default_snowball.png", bgicon = nil, bar = "hudbars_bar_timer.png" + } + for i = 1, ch_core.count_of_ch_hudbars, 1 do + hb.register_hudbar("ch_hudbar_"..i, 0xFFFFFF, "x", hudbar_defaults, 0, 100, true, hudbar_formatstring, hudbar_formatstring_config) + end + minetest.register_on_joinplayer(function(player, last_login) + for i = 1, ch_core.count_of_ch_hudbars, 1 do + hb.init_hudbar(player, "ch_hudbar_"..i, 0, 100, true) + end + end) +end + +function ch_core.try_alloc_hudbar(player) + local online_charinfo = ch_data.online_charinfo[player:get_player_name()] + if online_charinfo then + local hudbars = online_charinfo.hudbars + if not hudbars then + hudbars = {} + online_charinfo.hudbars = hudbars + end + for i = 1, ch_core.count_of_ch_hudbars, 1 do + if not hudbars[i] then + local result = "ch_hudbar_"..i + hudbars[i] = result + return result + end + end + end + return nil +end + +function ch_core.free_hudbar(player, hudbar_id) + local online_charinfo = ch_data.online_charinfo[player:get_player_name()] + if not online_charinfo then + minetest.log("warning", "Cannot get online_charinfo of player "..player:get_player_name().." to free a hudbar "..hudbar_id.."!") + return false + end + local hudbars = online_charinfo.hudbars + if not hudbars then + hudbars = {} + online_charinfo.hudbars = hudbars + end + + if hudbar_id:sub(1, 10) == "ch_hudbar_" then + local hudbar_index = tonumber(hudbar_id:sub(11, -1)) + if 1 <= hudbar_index and hudbar_index <= ch_core.count_of_ch_hudbars then + if hudbars[hudbar_index] then + hudbars[hudbar_index] = nil + hb.hide_hudbar(player, hudbar_id) + return true + else + return false -- not allocated + end + end + end + minetest.log("error", "Invalid hudbar_id to free: "..hudbar_id) + return false +end + +-- DATE AND TIME HUD +local datetime_hud_defaults = { + type = "text", + position = { x = 1, y = 1 }, + offset = { x = -5, y = -5 }, + text = "", + alignment = { x = -1, y = -1 }, + scale = { x = 100, y = 100 }, + number = 0x999999, + style = 2, + z_index = 50, +} + +local datetime_huds = { +} + +local function on_joinplayer(player, last_login) + local player_name = player:get_player_name() + datetime_huds[player_name] = { + counter = 0, + hud_id = player:hud_add(datetime_hud_defaults), + hud_text = "", + } +end + +local function on_leaveplayer(player, timed_out) + local player_name = player:get_player_name() + datetime_huds[player_name] = nil +end + +local acc_time = 0 + +local function on_step(dtime) + acc_time = acc_time + dtime + if acc_time > 0.5 then + local cas = ch_time.aktualni_cas() + local text = string.format("%s\n%d. %s %s\n%02d:%02d %s", + cas:den_v_tydnu_nazev(), cas.den, cas:nazev_mesice(2), cas.rok, cas.hodina, cas.minuta, cas:posun_text()) + for player_name, record in pairs(datetime_huds) do + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + local ltext = ch_core.prihlasovaci_na_zobrazovaci(player_name).."\n"..text + if record.hud_text ~= ltext then + record.hud_text = ltext + ltext = ltext.." ["..record.counter.."]" + player:hud_change(record.hud_id, "text", ltext) + record.counter = record.counter + 1 + end + end + end + end +end + +function ch_core.clear_datetime_hud(player) + local player_name = player:get_player_name() + local hud = datetime_huds[player_name] + if hud then + player:hud_remove(hud.hud_id) + datetime_huds[player_name] = nil + return true + else + return false + end +end + +local function clear_datetime_hud(player_name, param) + local player = minetest.get_player_by_name(player_name) + if player ~= nil and ch_core.clear_datetime_hud(player) then + return true + else + ch_core.systemovy_kanal(player_name, "CHYBA: okno s datem a časem nenalezeno, možná už je skyto.") + end +end + +local cc_def = { + description = "Do odhlášení skryje z obrazovky datum a čas.", + func = clear_datetime_hud, +} + +minetest.register_on_joinplayer(on_joinplayer) +minetest.register_on_leaveplayer(on_leaveplayer) +minetest.register_globalstep(on_step) +minetest.register_chatcommand("skrýtčas", cc_def) +minetest.register_chatcommand("skrytcas", cc_def) + +ch_core.close_submod("hud") diff --git a/ch_core/init.lua b/ch_core/init.lua new file mode 100644 index 0000000..50e94d6 --- /dev/null +++ b/ch_core/init.lua @@ -0,0 +1,518 @@ +ch_base.open_mod(core.get_current_modname()) + +local modpath = core.get_modpath("ch_core") +ch_core = { + storage = minetest.get_mod_storage(), + submods_loaded = {}, -- submod => true + + ap_interval = 15, -- interval pro podmód „ap“ + cas = 0, + gs_tasks = {}, + inventory_size = { + normal = 32, + extended = 64, + }, + supported_lang_codes = {cs = true, sk = true}, + verze_ap = 1, -- aktuální verze podmódu „ap“ + vezeni_data = { + min = vector.new(-1000, -1000, -1000), + max = vector.new(1000, 1000, 1000), + -- dvere = nil, + stred = vector.new(0, 0, 0), + }, + colors = { + black = minetest.get_color_escape_sequence("#000000"), + blue = minetest.get_color_escape_sequence("#0000AA"), + green = minetest.get_color_escape_sequence("#00AA00"), + cyan = minetest.get_color_escape_sequence("#00AAAA"), + red = minetest.get_color_escape_sequence("#AA0000"), + magenta = minetest.get_color_escape_sequence("#AA00AA"), + brown = minetest.get_color_escape_sequence("#AA5500"), + yellow = minetest.get_color_escape_sequence("#AAAA00"), + light_gray = minetest.get_color_escape_sequence("#CCCCCC"), + dark_gray = minetest.get_color_escape_sequence("#555555"), + light_blue = minetest.get_color_escape_sequence("#5555FF"), + light_green = minetest.get_color_escape_sequence("#55FF55"), + light_cyan = minetest.get_color_escape_sequence("#55FFFF"), + light_red = minetest.get_color_escape_sequence("#FF5555"), + light_magenta = minetest.get_color_escape_sequence("#FF55FF"), + light_yellow = minetest.get_color_escape_sequence("#FFFF55"), + white = minetest.get_color_escape_sequence("#FFFFFF"), + }, + overridable = { + -- funkce a proměnné, které mohou být přepsány z ostatních módů + reset_bank_account = function(player_name) return end, + trash_all_sound = "", -- zvuk k přehrání při mazání více předmětů + trash_one_sound = "", -- zvuk k přehrání při mazání jednoho předmětu + chyba_handler = function(player, text) return end, + }, +} + +ch_time.set_time_speed_during_day(26) -- was 24 +ch_time.set_time_speed_during_night(132) -- was 48 + +local current_submod + +function ch_core.open_submod(submod, required_submods) + if current_submod ~= nil then + error("[ch_core/"..current_submod.."] modul nebyl uzavřen!") + end + for s, c in pairs(required_submods or {}) do + if c and not ch_core.submods_loaded[s] then + error("ch_core submodule '"..s.."' is required to be loaded before '"..submod.."'!") + end + end + current_submod = submod + return true +end +function ch_core.close_submod(submod) + if current_submod == nil then + error("Vícenásobné volání ch_core.close_submod()!") + elseif current_submod ~= submod then + error("[ch_core/"..current_submod.."] modul chybně uzavřen jako "..submod.."!") + end + current_submod = nil + ch_core.submods_loaded[submod] = true + return true +end + +dofile(modpath .. "/active_objects.lua") +dofile(modpath .. "/markers.lua") +dofile(modpath .. "/barvy_linek.lua") +dofile(modpath .. "/nodes.lua") +dofile(modpath .. "/plaster.lua") +dofile(modpath .. "/hotbar.lua") +dofile(modpath .. "/vgroups.lua") +dofile(modpath .. "/data.lua") +dofile(modpath .. "/lib.lua") -- : data +dofile(modpath .. "/entity_register.lua") -- : lib +dofile(modpath .. "/interiors.lua") -- : lib +dofile(modpath .. "/shapes_db.lua") -- : lib +dofile(modpath .. "/penize.lua") -- : lib +dofile(modpath .. "/nodedir.lua") -- : lib +dofile(modpath .. "/formspecs.lua") -- : data, lib +dofile(modpath .. "/areas.lua") -- : data, lib +dofile(modpath .. "/nametag.lua") -- : data, lib +dofile(modpath .. "/privs.lua") +dofile(modpath .. "/clean_players.lua") -- : data, lib, privs +dofile(modpath .. "/localize_chatcommands.lua") -- : data, lib, privs +dofile(modpath .. "/udm.lua") -- : areas, data, lib +dofile(modpath .. "/chat.lua") -- : areas, data, lib, privs, nametag, udm +dofile(modpath .. "/shape_selector.lua") -- : chat, formspecs, lib +dofile(modpath .. "/events.lua") -- : chat, data, lib, privs +dofile(modpath .. "/stavby.lua") -- : chat, events, lib +-- dofile(modpath .. "/inv_inspector.lua") -- : data, formspecs, lib, chat +dofile(modpath .. "/podnebi.lua") -- : privs, chat +dofile(modpath .. "/dennoc.lua") -- : privs, chat +dofile(modpath .. "/hud.lua") -- : data, lib, chat +dofile(modpath .. "/ap.lua") -- : chat, data, events, hud, lib +dofile(modpath .. "/registrace.lua") -- : chat, data, events, lib, nametag +dofile(modpath .. "/pryc.lua") -- : data, lib, events, privs +dofile(modpath .. "/joinplayer.lua") -- : chat, data, formspecs, lib, nametag, pryc, events +dofile(modpath .. "/padlock.lua") -- : data, lib +dofile(modpath .. "/vezeni.lua") -- : privs, data, lib, chat, hud +dofile(modpath .. "/timers.lua") -- : data, chat, hud +dofile(modpath .. "/wielded_light.lua") -- : data, lib, nodes +dofile(modpath .. "/teleportace.lua") -- : data, lib, chat, privs, stavby, timers +dofile(modpath .. "/creative_inventory.lua") -- : lib +dofile(modpath .. "/kos.lua") -- : lib + + +local ifthenelse = assert(ch_core.ifthenelse) +local last_timeofday = 0 -- pravděpodobně se pokusí něco přehrát v prvním globalstepu, +-- ale to nevadí, protöže v tu chvíli stejně nemůže být ještě nikdo online. +local abs = math.abs +local get_timeofday = core.get_timeofday +local gain_1 = {gain = 1.0} +local head_bone_name = "Head" +local head_bone_override = { + position = {vec = vector.new(0, 6.35, 0), absolute = true}, + rotation = {vec = vector.zero(), absolute = true}, +} +local emoting = (core.get_modpath("emote") and emote.emoting) or {} +local globstep_dtime_accumulated = 0.0 +local hud_dtime_accumulated = 0.0 +local get_us_time = assert(core.get_us_time) +local has_wielded_light = core.get_modpath("wielded_light") +local custom_globalsteps = {} +local last_ap_timestamp = 0 +local use_forbidden_height = ifthenelse(core.settings:get_bool("ch_forbidden_height", false), true, false) +local gs_task +local gs_task_next_step +local gs_handler = { + cancel = "on_cancelled", + finished = "on_finished", + failed = "on_failed", +} + +local stepheight_low = {stepheight = 0.3} +local stepheight_high = {stepheight = 1.1} + +local function get_root(o) + local r = o:get_attach() + while r do + o = r + r = o:get_attach() + end + return o +end + +function ch_core.register_player_globalstep(func, index) + if not index then + index = #custom_globalsteps + 1 + end + if not func then + error("Invalid call to ch_core.register_player_globalstep()!") + end + custom_globalsteps[index] = func + return index +end + +local function globalstep(dtime) + if globstep_dtime_accumulated == 0 then + -- první globalstep: + ch_core.update_creative_inventory(true) + globstep_dtime_accumulated = globstep_dtime_accumulated + dtime + return + end + + globstep_dtime_accumulated = globstep_dtime_accumulated + dtime + ch_core.cas = globstep_dtime_accumulated + local ch_core_cas = globstep_dtime_accumulated + local us_time = get_us_time() + + -- DEN: 5:30 .. 19:00 + local tod = get_timeofday() + local byla_noc = last_timeofday < 0.2292 or last_timeofday > 0.791666 + local je_noc = tod < 0.2292 or tod > 0.791666 + if byla_noc and not je_noc then + -- Ráno + minetest.sound_play("birds", gain_1) + local new_speed = ch_time.get_time_speed_during_day() + if new_speed ~= nil then + core.settings:set("time_speed", tostring(new_speed)) + end + elseif not byla_noc and je_noc then + -- Noc + core.sound_play("owl", gain_1) + local new_speed = ch_time.get_time_speed_during_night() + if new_speed ~= nil then + core.settings:set("time_speed", tostring(new_speed)) + end + end + last_timeofday = tod + + local process_ap = us_time - last_ap_timestamp >= ch_core.ap_interval * 1000000 + if process_ap then + last_ap_timestamp = us_time + end + + hud_dtime_accumulated = hud_dtime_accumulated + dtime + if hud_dtime_accumulated > 1 then + hud_dtime_accumulated = 0 + ch_core.update_gametime_hudbar(nil, tod) + end + + -- PRO KAŽDÉHO HRÁČE/KU: + local connected_players = core.get_connected_players() + for _, player in pairs(connected_players) do + local player_name = player:get_player_name() + local player_pos = player:get_pos() + local online_charinfo = ch_data.online_charinfo[player_name] + local offline_charinfo = ch_data.get_or_add_offline_charinfo(player_name) + local disrupt_teleport_flag = false + local disrupt_pryc_flag = false + local player_wielded_item_name = player:get_wielded_item():get_name() or "" + local player_root = get_root(player) + + if online_charinfo then + local previous_wield_item_name = online_charinfo.wielded_item_name or "" + online_charinfo.wielded_item_name = player_wielded_item_name + + -- ÚHEL HLAVY: + local emote = emoting[player] + if not emote or emote ~= "lehni" then + local puvodni_uhel_hlavy = online_charinfo.uhel_hlavy or 0 + local novy_uhel_hlavy = player:get_look_vertical() - 0.01745329251994329577 * (online_charinfo.head_offset or 0) + local rozdil = novy_uhel_hlavy - puvodni_uhel_hlavy + if rozdil > 0.001 or rozdil < -0.001 then + if rozdil > 0.3 then + -- omezit pohyb hlavy + novy_uhel_hlavy = puvodni_uhel_hlavy + 0.3 + elseif rozdil < -0.3 then + novy_uhel_hlavy = puvodni_uhel_hlavy - 0.3 + end + head_bone_override.rotation.vec.x = -0.5 * (puvodni_uhel_hlavy + novy_uhel_hlavy) + player:set_bone_override(head_bone_name, head_bone_override) + online_charinfo.uhel_hlavy = novy_uhel_hlavy + end + else + head_bone_override.rotation.vec.x = 0 + player:set_bone_override(head_bone_name, head_bone_override) + end + + -- REAGOVAT NA KLÁVESY: + local old_control_bits = online_charinfo.klavesy_b or 0 + local new_control_bits = player:get_player_control_bits() + if new_control_bits ~= old_control_bits then + local new_controls = player:get_player_control() + local old_controls = online_charinfo.klavesy or new_controls + online_charinfo.klavesy = new_controls + online_charinfo.klavesy_b = new_control_bits + if new_controls.aux1 and not old_controls.aux1 then + ch_core.show_player_list(player, online_charinfo) + player:set_properties(stepheight_high) + elseif not new_controls.aux1 and old_controls.aux1 then + ch_core.hide_player_list(player, online_charinfo) + player:set_properties(stepheight_low) + end + + disrupt_pryc_flag = true + if not disrupt_teleport_flag and (new_controls.up or new_controls.down or new_controls.left or new_controls.right or new_controls.jump or new_controls.dig or new_controls.place) then + disrupt_teleport_flag = true + end + end + + -- VĚZENÍ, zakázaná výška: + if --[[ ch_core.submods_loaded["vezeni"] and ]] offline_charinfo.player.trest > 0 then + ch_core.vykon_trestu(player, player_pos, us_time, online_charinfo) + elseif use_forbidden_height and player_pos.y >= 1024 and player_pos.y <= 1256 then + -- zakázaná výška + minetest.log("warning", "Player "..player_name.." reached forbidden area at "..minetest.pos_to_string(player_pos).."!") + minetest.after(0.1, function() + ch_core.teleport_player({ + type = "admin", + player = player, + target_pos = ch_core.positions["zacatek_1"] or vector.zero(), + sound_after = "chat3_bell", + }) + ch_core.systemovy_kanal(player_name, "Dostali jste se do zakázaného výškového pásma! Pozice y >= 1024 jsou nepřístupné.") + end) + end + + -- pokud se změnil držený předmět, možná bude potřeba zobrazit jeho nápovědu + if player_wielded_item_name ~= previous_wield_item_name then + disrupt_pryc_flag = true + local help_def = ch_data.should_show_help(player, online_charinfo, player_wielded_item_name) + if help_def then + local zluta = minetest.get_color_escape_sequence("#ffff00") + local zelena = minetest.get_color_escape_sequence("#00ff00") + local s = zluta.."Nápověda k předmětu „"..zelena..(help_def.description or ""):gsub("\n", "\n "..zelena)..zluta.."“:\n "..zluta..(help_def._ch_help or ""):gsub("\n", "\n "..zluta) + ch_core.systemovy_kanal(player_name, s) + end + + -- periskop: + if online_charinfo.periskop ~= nil and player_wielded_item_name ~= "ch_extras:periskop" then + online_charinfo.periskop.cancel() + end + end + + -- ZRUŠIT /pryč: + if disrupt_pryc_flag and online_charinfo.pryc then + online_charinfo.pryc(player, online_charinfo) + end + + -- ČASOVAČE + local timers = online_charinfo.ch_timers + if timers then + for timer_id, timer_def in pairs(table.copy(timers)) do + local remains = timer_def.run_at - ch_core_cas + if remains <= 0.1 then + local func_to_run = timer_def.func + ch_core.cancel_ch_timer(online_charinfo, timer_id) + if func_to_run then + minetest.after(0.1, function() + return func_to_run() + end) + end + elseif timer_def.hudbar then + remains = math.ceil(remains) + if remains ~= timer_def.last_hud_value then + hb.change_hudbar(player, timer_def.hudbar, remains) + timer_def.last_hud_value = remains + end + end + end + end + + -- ZRUŠIT teleport + local teleport_def = ch_core.get_ch_timer_info(online_charinfo, "teleportace") + if teleport_def then + if not disrupt_teleport_flag then + local ts_pos = teleport_def.start_pos + if ts_pos then + disrupt_teleport_flag = abs(ts_pos.x - player_pos.x) > 0.5 or abs(ts_pos.z - player_pos.z) > 0.5 + end + end + if disrupt_teleport_flag then + ch_core.cancel_teleport(player_name, true) + end + end + + -- ZRUŠIT horkou zprávu + local horka_zprava = online_charinfo.horka_zprava + if horka_zprava and ch_core_cas >= horka_zprava.timeout then + online_charinfo.horka_zprava = nil + player:set_nametag_attributes(ch_core.compute_player_nametag(online_charinfo, offline_charinfo)) + end + + -- nesmrtelnost (stačilo by spouštět občas) + if minetest.is_creative_enabled(player_name) then + if not online_charinfo.is_creative then + online_charinfo.is_creative = true + ch_core.set_immortal(player, online_charinfo.is_creative) + end + else + if online_charinfo.is_creative then + online_charinfo.is_creative = false + ch_core.set_immortal(player, online_charinfo.is_creative) + end + end + + -- SPUSTIT registrované obslužné funkce + local i = 1 + while custom_globalsteps[i] do + custom_globalsteps[i](player, player_name, online_charinfo, offline_charinfo, us_time) + i = i + 1 + end + + -- SLEDOVÁNÍ AKTIVITY + local ap = online_charinfo.ap + if ap then + if player_pos.x ~= ap.pos.x then + ap.pos_x_gen = ap.pos_x_gen + 1 + end + if player_pos.y ~= ap.pos.y then + ap.pos_y_gen = ap.pos_y_gen + 1 + end + if player_pos.z ~= ap.pos.z then + ap.pos_z_gen = ap.pos_z_gen + 1 + end + ap.pos = player_pos + + local player_velocity = player_root:get_velocity() + if player_velocity.x ~= ap.velocity.x then + ap.velocity_x_gen = ap.velocity_x_gen + 1 + end + if player_velocity.y ~= ap.velocity.y then + ap.velocity_y_gen = ap.velocity_y_gen + 1 + end + if player_velocity.z ~= ap.velocity.z then + ap.velocity_z_gen = ap.velocity_z_gen + 1 + end + ap.velocity = player_velocity + + local player_control_bits = player:get_player_control_bits() + if player_control_bits ~= ap.control then + ap.control = player_control_bits + ap.control_gen = ap.control_gen + 1 + end + + local look_horizontal = player:get_look_horizontal() + if look_horizontal ~= ap.look_h then + ap.look_h = look_horizontal + ap.look_h_gen = ap.look_h_gen + 1 + end + local look_vertical = player:get_look_vertical() + if look_vertical ~= ap.look_v then + ap.look_v = look_vertical + ap.look_v_gen = ap.look_v_gen + 1 + end + + local last + last = ch_core.last_mistni + if last.char == player_name then + ap.chat_mistni_gen = last.char_gen + end + last = ch_core.last_celoserverovy + if last.char == player_name then + ap.chat_celoserverovy_gen = last.char_gen + end + last = ch_core.last_sepot + if last.char == player_name then + ap.chat_sepot_gen = last.char_gen + end + last = ch_core.last_soukromy + if last.char == player_name then + ap.chat_soukromy_gen = last.char_gen + end + + if process_ap then + ch_core.ap_update(player, online_charinfo, offline_charinfo) + end + elseif process_ap then + ch_core.ap_init(player, online_charinfo, offline_charinfo) + end + + -- NASTAVIT OSVĚTLENÍ [data, nodes, wielded_light] + -- - provést po proběhnutí registrovaných obslužných funkcí, + -- protože ty mohly osvětlení změnit + local light_slots = has_wielded_light and online_charinfo.wielded_lights + if light_slots then + local new_light_level = 0 + for _, light_level in pairs(light_slots) do + if light_level > new_light_level then + new_light_level = light_level + end + end + if new_light_level ~= online_charinfo.light_level then + minetest.log("info", "Light level of player "..player_name.." changed: "..online_charinfo.light_level.." to "..new_light_level) + if new_light_level > 0 then + wielded_light.track_user_entity(player, "ch_core", string.format("ch_core:light_%02d", new_light_level)) + else + wielded_light.track_user_entity(player, "ch_core", "default:cobble") + end + online_charinfo.light_level = new_light_level + online_charinfo.light_level_timestamp = us_time + elseif new_light_level > 0 and us_time - online_charinfo.light_level_timestamp > 5000000 then + -- refresh non-zero light level each 5 seconds + wielded_light.track_user_entity(player, "ch_core", string.format("ch_core:light_%02d", new_light_level)) + online_charinfo.light_level_timestamp = us_time + end + end + end + end + + -- globalstep tasks: + if gs_task ~= nil then + -- run the step + local context = gs_task.context + local step = gs_task.steps[gs_task_next_step] + local n_steps = #gs_task.steps + local result = gs_task.on_step(context, step, gs_task_next_step, n_steps) + local handler = gs_handler[result] -- if this crashes on nil, add 'or ""' + if handler == nil then + if gs_task_next_step < n_steps then + -- příště pokračovat dalším krokem stejného úkolu + gs_task_next_step = gs_task_next_step + 1 + else + -- úkol dokončen + handler = "on_finished" + end + end + if handler ~= nil then + -- zavolat obsluhu a skončit + handler = gs_task[handler] + if handler ~= nil then + handler(context, gs_task_next_step, n_steps) + end + gs_task = nil + end + + elseif ch_core.gs_tasks[1] ~= nil then + -- vyzvednout další úkol a zavolat jeho on_start() + gs_task = ch_core.gs_tasks[1] + gs_task_next_step = 1 + table.remove(ch_core.gs_tasks, 1) + if gs_task.on_start ~= nil then + local result = gs_task.on_start(gs_task.context) + local handler = gs_handler[result] -- if this crashes on nil, add 'or ""' + if handler ~= nil then + gs_task = nil + end + end + end +end +core.register_globalstep(globalstep) + +ch_base.close_mod(minetest.get_current_modname()) diff --git a/ch_core/interiors.lua b/ch_core/interiors.lua new file mode 100644 index 0000000..c7b15a4 --- /dev/null +++ b/ch_core/interiors.lua @@ -0,0 +1,125 @@ +ch_core.open_submod("interiors", {}) + +local normal = 0 +local passable = 1 +local evadable = 2 + +minetest.register_on_mods_loaded(function() + local passable_drawtypes = { + airlike = true, + firelike = true, + flowingliquid = true, + liquid = true, + plantlike = true, + plantlike_rooted = true, + signlike = true, + torchlike = true, + } + for _, ndef in pairs(minetest.registered_nodes) do + if passable_drawtypes[ndef.drawtype or "normal"] then -- or ndef.sunlight_propagates == true + ndef._ch_interior = passable + else + local groups = ndef.groups + if groups == nil then + ndef._ch_interior = normal + elseif groups.leaves then + ndef._ch_interior = passable + elseif groups.tree then + ndef._ch_interior = evadable + else + ndef._ch_interior = normal + end + end + end +end) + +local get_node = minetest.get_node +local registered_nodes = minetest.registered_nodes + +local function has_passable_neighbour(pos) + local ndef + pos.x = pos.x - 1 -- -x + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.z = pos.z - 1 -- -x -z + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.x = pos.x + 1 -- -z + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.x = pos.x + 1 -- +x -z + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.z = pos.z + 1 -- +x + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.z = pos.z + 1 -- +x +z + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.x = pos.x - 1 -- +z + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + pos.x = pos.x - 1 -- -x +z + ndef = registered_nodes[get_node(pos).name] + if ndef ~= nil and ndef._ch_interior == passable then + return true + end + return false +end + +--[[ + Prozkoumá, zda se zadaná pozice jeví být v interiéru. + Při použití na hráčskou postavu (player:get_pos()) je potřeba k souřadnici y přičíst 0.5. +]] +function ch_core.is_in_interior(pos) + local nlight = minetest.get_natural_light(pos, 0.5) or 0 + if nlight <= 0 then + return true + elseif nlight >= minetest.LIGHT_MAX then + return false + else + local ndef, ntype + local ray = minetest.raycast(pos, vector.offset(pos, 0, 20, 0), false, false) + for pointed_thing in ray do + if pointed_thing.type == "node" then + ndef = registered_nodes[get_node(pointed_thing.under)] + if ndef == nil then + return true -- under unknown node => interior + end + ntype = ndef._ch_interior + if ntype ~= passable then + if ntype == evadable then + local pos2 = vector.offset(pointed_thing.under, 0, 1, 0) + ndef = registered_nodes[get_node(pos2).name] + if ndef == nil or ndef._ch_interior == evadable then + return true -- under two evadable nodes => interior + end + pos2.y = pointed_thing.under.y + if not has_passable_neighbour(pointed_thing.under) then + return true -- under evadable node with no passable neighbour => interior + end + else + return true -- under full node => interior + end + end + end + end + return false + end +end + +ch_core.close_submod("interiors") diff --git a/ch_core/inv_inspector.lua b/ch_core/inv_inspector.lua new file mode 100644 index 0000000..59aa843 --- /dev/null +++ b/ch_core/inv_inspector.lua @@ -0,0 +1,104 @@ +ch_core.open_submod("inv_inspector", {data = true, formspec = true, lib = true, chat = true}) + +local required_privs = {protection_bypass = true} + +local admin_to_custom_state = {} + +local detached_inventory_callbacks = { + allow_move = function(inv, from_list, from_index, to_list, to_index, count, player) + return 0 + end, + allow_put = function(inv, listname, index, stack, player) + return 0 +--[[ + if ch_core.get_player_role(player) ~= "admin" then + return 0 + end + local admin_name = player:get_player_name() + local custom_state = admin_to_custom_state[admin_name] + if custom_state == nil then + return 0 + end + local tplayer = minetest.get_player_by_name(custom_state.tplayer_name) + if tplayer == nil then + return 0 + end + local remains = tplayer:get_inventory():add_item(custom_state.open_listname, stack) + ch_core.systemovy_kanal(player:get_player_name(), "Dávka úspěšně vložena do inventáře.") + return -1 +]] + end, + allow_take = function(inv, listname, index, stack, player) + return 0 +--[[ + if ch_core.get_player_role(player) ~= "admin" then + return 0 + end + local admin_name = player:get_player_name() + local custom_state = admin_to_custom_state[admin_name] + if custom_state == nil then + return 0 + end + local tplayer = minetest.get_player_by_name(custom_state.tplayer_name) + if tplayer == nil then + return 0 + end + local remains = tplayer:get_inventory():add_item(custom_state.open_listname, stack) + ch_core.systemovy_kanal(player:get_player_name(), "Dávka úspěšně vložena do inventáře.") + return -1 +]] + end, +} + +local inv = minetest.create_detached_inventory("ch_core_inv_manager", detached_inventory_callbacks) + +local function get_formspec(custom_state) + local formspec = { + "formspec_version[4]", + "size[20,16]", + "label[0.3,0.65;Inventáře:]", + "label[0.5,1.4;Postava:]", + "dropdown[2.25,1;5,0.75;postavy;A,B,C;1;false]", + "label[8,1.4;Inventář:]", + "dropdown[9.75,1;5,0.75;inventare;A,B,C;1;false]", + "button[15,0.75;4,1;nacist;načíst]", + + list[current_player;main;1,2.5;14,1;] +box[0.9,3.75;17.5,0.1;#000000] +list[current_player;main;1,4;1,9;] + + "list[current_player;main;1,2.5;14,4;]", + -- list[current_player;main;1,2.5;1,10;]" + } + + + return table.concat(formspec) +end + +local function formspec_callback(custom_state, player, formname, fields) + if fields.postavy then + local event = minetest.expode_dropdown_event(fields.postavy) + end + if not fields.nacist then + return + end + +end + +local function on_chat_command(admin_name, _) + local custom_state = { + admin_name = admin_name, + -- [ ] TODO + } + admin_to_custom_state[admin_name] = custom_state + ch_core.show_formspec(admin_name, "ch_core:inspekce_inv", get_formspec(custom_state), formspec_callback, custom_state, {}) +end + +minetest.register_chatcommand("inspekceinv", { + params = "", + description = "provede inspekci inventářů připojených klientů", + privs = required_privs, + func = on_chat_command, +}) + +ch_core.close_submod("inv_inspector") diff --git a/ch_core/joinplayer.lua b/ch_core/joinplayer.lua new file mode 100644 index 0000000..05ffdd7 --- /dev/null +++ b/ch_core/joinplayer.lua @@ -0,0 +1,505 @@ +ch_core.open_submod("joinplayer", {chat = true, data = true, events = true, formspecs = true, lib = true, nametag = true, pryc = true}) + +local F = minetest.formspec_escape +local ifthenelse = ch_core.ifthenelse + +ch_core.register_event_type("joinplayer", { + -- ignoruje postavy, které budou odpojeny, a turistické postavy + description = "vstup do hry", + access = "players", + default_text = "Připojila se postava: {PLAYER}", + chat_access = "public", +}) + +ch_core.register_event_type("joinplayer_new", { + -- jen turistické postavy, kromě těch, které budou odpojeny + description = "vstup do hry (tur.p.)", + access = "players", + default_text = "Připojila se turistická postava: {PLAYER}", + chat_access = "public", +}) + +ch_core.register_event_type("joinplayer_for_admin", { + -- všechny postavy + description = "vstup do hry*", + access = "admin", + chat_access = "admin", +}) + +ch_core.register_event_type("leaveplayer", { + -- ignoruje postavy, které budou odpojeny + description = "odchod ze hry", + access = "admin", + default_text = "Odpojila se postava: {PLAYER}", + chat_access = "public", +}) + +ch_core.register_event_type("leaveplayer_for_admin", { + -- všechny postavy + description = "odchod ze hry*", + access = "admin", + chat_access = "admin", + default_text = "{PLAYER} se odpojil/a ze hry", +}) + +local function get_invalid_locale_formspec(invalid_locale, protocol_version) + if invalid_locale == nil or invalid_locale == "" then + invalid_locale = "en" + end + + local krok1_cs, krok1_sk, krok1_en + local krok2_cs, krok2_sk, krok2_en + if protocol_version < 43 then + krok1_cs = F("1. Odpojte se ze hry a v hlavním menu na kartě Settings (Nastavení) klikněte na All Settings (Všechna nastavení).\n\n") + krok1_sk = F("1. Odpojte sa zo hry a v hlavnom menu na karte Settings (Nastavenia) kliknite na All Settings (Všetky nastavenia).\n\n") + krok1_en = F("1. Disconnect the client and in the main menu on the Settings tab click to \"All Settings\" button.\n\n") + + krok2_cs = F("2. Ve skupině „Client and Server“ (Klient a server) nastavite „Language“ („Jazyk“) na hodnotu „cs“ nebo „sk“.\n\n") + krok2_sk = F("2. V skupine „Client and Server“ (Klient a server) nastavte „Language“ („Jazyk“) na hodnotu „sk“ alebo „cs“.\n\n") + krok2_en = F("2. In the group \"Client and Server\" set \"Language\" to one of the values \"cs\" or \"sk\".\n\n") + else + -- Minetest >= 5.8.0 + krok1_cs = F("1. Odpojte se ze hry a klikněte na ozubené kolo v pravém horním rohu menu (Nastavení).\n\n") + krok1_sk = F("1. Odpojte sa zo hry a kliknite kliknite na ozubené koleso v pravom hornom rohu rozhrania (Nastavenia).\n\n") + krok1_en = F("1. Disconnect the client and click the gear in the top right corner of the interface (Settings).\n\n") + + krok2_cs = F("2. Ve skupině „User Interfaces“ (Užívateľské rozhranie) nebo „Accessibility“ nastavite „Language“ („Jazyk“) na hodnotu „Česky [cs]“ nebo „Slovenčina [sk]“.\n\n") + krok2_sk = F("2. V skupine „User Interfaces“ (Uživatelská rozhraní) lebo „Accessibility“ nastavte „Language“ („Jazyk“) na hodnotu „Slovenčina [sk]“ lebo „Česky [cs]“.\n\n") + krok2_en = F("2. In the group \"User Interfaces\" or \"Accessibility\" set \"Language\" to one of the values \"Česky [cs]\" or \"Slovenčina [sk]\".\n\n") + end + + local result = { + "formspec_version[4]", + "size[12,14]", + "label[0.375,0.5;česky:]", + "textarea[0.375,0.7;11,4;cz;;", + F("Připojili jste se na server Český hvozd. Server detekoval, že váš klient je nastaven na lokalizaci „"..invalid_locale.."“, která není na tomto serveru podporována. Abyste mohli pokračovat ve hře, musíte nastavit svého klienta na lokalizaci „cs“ nebo „sk“. Postup je následující:\n\n"), + krok1_cs, + krok2_cs, + F("3. Úplně restartujte klienta (vypněte ho a znovu zapněte).\n\n"), + F("4. Znovu se pokuste připojit na Český hvozd.\n\n"), + F("Pokud se vám tato zpráva zobrazuje, přestože máte uvedené nastavení správně, je to pravděpodobně chyba na straně serveru. Kontakt pro nahlášení takové chyby najdete na stránkách http://ceskyhvozd.svita.cz\n"), + "]", + "label[0.375,5.0;slovensky:]", + "textarea[0.375,5.2;11,4;sk;;", + F("Pripojili ste sa na server Český hvozd. Server detekoval, že váš klient je nastavený na lokalizáciu „"..invalid_locale.."“, ktorá nie je na tomto serveri podporovaná. Aby ste mohli pokračovať v hre, musíte nastaviť svojho klienta na lokalizáciu „sk“ alebo „cs“. Postup je nasledujúci:\n\n"), + krok1_sk, + krok2_sk, + F("3. Úplne reštartujte klienta (vypnite ho a znovu zapnite).\n\n"), + F("4. Znova sa pokúste pripojiť na Český hvozd.\n\n"), + F("Ak sa vám táto správa zobrazuje, hoci máte uvedené nastavenie správne, je to pravdepodobne chyba na strane servera. Kontakt pre nahlásenie takejto chyby nájdete na stránkach http://ceskyhvozd.svita.cz\n"), + "]", + "label[0.375,9.5;English:]", + "textarea[0.375,9.7;11,3;en;;", + F("You have connected to the Český Hvozd Server. The server detected that your client is set to localization \""..invalid_locale.."\" that is not supported on this server. To continue playing you must set up your client to one of the localizations \"cs\" or \"sk\". Please, bear in mind that playing on this server requires at least basic ability to read and write in Czech or Slovak language.\n\n"), + F("The way to set up the client localization is as follows:\n\n"), + krok1_en, + krok2_en, + F("3. Completely restart your client (close it and start it again).\n\n"), + F("4. Try to connect to Český Hvozd again.\n\n"), + F("If you have the Language setting set correctly, but this message still appears, it is probably a server-side bug. The contact information needed to report such bug is available in Czech on the website https://ceskyhvozd.svita.cz\n"), + "]", + "button_exit[1,13;10,0.75;zavrit;Odpojit / Odpojiť / Disconnect]", + } + return table.concat(result) +end + +local function invalid_locale_formspec_callback(custom_state, player, formname, fields) + if fields.quit then + minetest.disconnect_player(player:get_player_name(), "Klient je nastavený na jazyk nepodporovaný na straně serveru.") + end +end + +local new_player_texts = { + { + title = "Vítejte na serveru Český hvozd!", + formspec_text = F("Český hvozd je dělnicko-kouzelnický server **pro dospělé** s československou tématikou, plně lokalizovaný do češtiny. ".. + "V nabídce vlevo si zvol téma, které tě zajímá. Kliknutím na tlačítko „X“ toto okno zavřeš a vstoupíš do herního světa.".. + " Později ho můžeš znovu otevřít příkazem „/novinky“ nebo tím, že se odpojíš a znovu připojíš.\n\n".. + "Při objevování světa se ti mohou hodit přemísťovací příkazy „/začátek“, „/doma“ a „/domů“ a možnost běhat rychle ".. + "(pokud to neumíš, doporučuji nejdřív navštívit areál „Úvod do Luanti“). ".. + "Další informace o serveru, včetně mapy herního světa a instrukcí, jak získat práva potřebná ".. + "pro plnohodnotnou hru, najdeš na webu a wiki:\n\nhttps://ceskyhvozd.svita.cz\n\n".. + "Přeji příjemnou a zajímavou hru!\n-- Administrace\n"), + }, { + title = "Hraji na telefonu/tabletu...", + formspec_text = F("Důrazně doporučuji pro připojení k Českému hvozdu používat klienta na počítači (stolním nebo přenosném). ".. + "Zařízení s dotykovým ovládáním jako telefony či tablety jsou určena (a vhodná) pro hraní jednoduchých her ".. + "s jednoduchým ovládáním a nepropracovanou grafikou. Minetest Game na Českém hvozdu k takovým hrám nepatří.\n\n".. + "Hráči/ky hrající na chytrých telefonech a tabletech již na Českém hvozdu hlásili potíže při zadávání příkazů ".. + "v četu a při ovládání dialogových oken."), + }, { + title = "Ještě mi nebylo 18...", + formspec_text = F("Pokud je vám 15 až 17, můžete se po serveru porozhlédnout, a pokud vám připadne, že si již rozumíte ".. + "s dospělými a zaměření serveru je vám blízké, můžete usilovat o přijetí na server. V některých případech Administrace ".. + "učiní výjimku a přijme i hráče/ku z této věkové skupiny.\n\nJe-li vám 13 nebo 14, poraďte se se svými rodiči, protože ".. + "zde můžete narazit na obsah, který pro vás zatím není vhodný. Pokud vám to rodiče dovolí, můžete se na serveru porozhlédnout, ".. + "ale nic víc.\n\nJe-li vám méně než 13, tato hra pro vás není vhodná. V takovém případě se, prosím, odpojte, a hledejte jinde."), + }, { + title = "Co je Český hvozd za server?", + formspec_text = F("Český hvozd je dělnicko-kouzelnický server s československou tematikou, plně lokalizovaný do češtiny, ".. + "který nabízí pohodové a relativně civilizované prostředí pro dospělé české a slovenské hráče/ky ".. + "a spoustu mírových činností, které zde budete moci dělat.\n\nServer spravuje Singularis ".. + "prostřednictvím postavy jménem Administrace.\n\n".. + "Český hvozd byl otevřen pro veřejnost 3. prosince 2022 po zruba šesti měsících vývoje. ".. + "Nikdy na něm nebylo mnoho hráčů/ek, takže hra zde většinou připomíná spíš ".. + "hru v režimu jednoho hráče/ky, jen s občasným setkáním s druhým hráčem/kou.\n\n".. + "Kromě lokalizace (která ti snad umožní cítit se tu jako doma) je hlavní předností ".. + "spousta technických vylepšení a úprav implementovaných speciálně pro tento server, s nimiž se během hry setkáš ".. + "(např. ovládání herního četu)."), + }, { + title = "Jaká tu platí pravidla?", + formspec_text = F("Nejdůležitější pravidla lze shrnout takto:\n\n".. + "• Snaž se jít ostatním dobrým příkladem.\n\n".. + "• Snaž se, aby sis hru co nejlépe užil jak ty, tak všichni ostatní, kdo tu hrají.\n\n".. + "• Respektuj pokyny Administrace a zaměření serveru.\n\n".. + "Toto jsou nejdůležitější pravidla, která platí pro celý server. ".. + "Pro jednotlivá místa a činnosti platí další pravidla, která se ovšem dozvíš postupně, např. na cedulích ve hře."), + }, { + title = "Co mám teď dělat?", + formspec_text = F("Záleží na tom, jak dobře ovládáš Luanti jako takové.\n\nPokud jsi začátečník/ice, ".. + "nejlépe uděláš, když nejprve navštívíš výukový areál Úvod do Luanti (hned vedle Začátku), ".. + "tam se naučíš ovládání hry a vyzkoušíš si ho, což ti ušetří spoustu potíží později. Přinejmenším by ses měl/a naučit ".. + "ovládání herního četu\n\n".. + "Pokud ovládání hry zvládáš, začni průzkumem herního světa — můžeš tu cestovat (pěšky, pomocí cestovní budky, ".. + "vlakem, tramvají či na kole), ".. + "a pokud potkáš nějakého dalšího hráče/ku, můžeš s ní/m komunikovat. V okně inventáře si prohlédni paletu ".. + "předmětů a ostatní karty (zejména Nastavení).\n\n".. + "K návštěvě určitě doporučuji Výstaviště, kde jsou vystaveny různé bloky a tvary, které lze na tomto serveru používat ke stavění. ".. + "Velmi hezkým místem je také Rasterdam.\n\n".. + "Stavět, těžit, obchodovat nebo se usadit budeš moci, teprve až si zvolíš dělnický nebo kouzelnický styl hry a až ".. + "Administrace tvoji postavu schválí (podrobnější informace na wiki)."), + }, { + title = "Ovládání četu (chatu)", + formspec_text = F("Okno četu otevřeš klávesou T (pro napsání jedné zprávy) nebo klávesou F10 (zůstane otevřeno do dalšího stisku F10).\n\n".. + "Normální zprávy, které do četu zadáš, uvidí převážně jen postavy v okolí 50 metrů kolem vás (a Administrace). ".. + "Pokud má tvoje zpráva dojít všem hráčským postavám ve hře, musíš před ni napsat znak „!“, jedná se o takzvaný celoserverový kanál. ".. + "Normální zprávy (základní kanál) slouží primárně pro komunikaci na krátkou vzdálenost.\n\n".. + "Soukromou zprávu na jinou postavu ve hře zašleš tak, že před text zprávy vložíš uvozovku a jednoznačnou předponu jména postavy. ".. + "Např. zprávu Administraci (pokud je ve hře) můžeš poslat zadáním:\n\n\"Adm ahoj\n\n".. + "Pokud tebou zadaná předpona nebude jednoznačná, systém zprávu neodešle a zobrazí ti varování, takže budeš moci svoji chybu napravit.\n\n".. + "Pokud tebou zadaná zpráva začíná znakem „/“, pochopí ji server jako příkaz a tento příkaz vykoná (nebo ohlásí chybu, pokud takový příkaz nezná). ".. + "Příkazy v četu slouží k vyvolání různých akcí ve hře.\n\nChceš-li napsat zprávu na postavu, která není zrovna ve hře, ".. + "použij herní poštu (dostupnou příkazem „/pošta“ nebo tlačítkem v inventáři). ".. + "Herní pošta má ovládání podobné jednoduchému e-mailovému klientovi."), + }, { + title = "Nefungují mi háčky a čárky, co s tím?", + formspec_text = F("Máš-li problém s psaním diakritiky, piš bez ní. Všechny příkazy, jména postav a většinou i parametry příkazů ".. + "lze zadat bez diakritiky a systém si s tím poradí (tzn. např. místo /začátek stačí psát /zacatek)."), + }, { + title = "Jsou dostupné zdrojové kódy módů?", + formspec_text = F("Ano, aktuální a úplný zdrojový kód všech módů je dostupný v repozitáři:\n\nhttps://github.com/singularis-mzf/cesky-hvozd\n\n".. + "Veškerý zdrojový kód je svobodný, takže ho (v případě zájmu) můžeš v mezích svých technických dovedností využít na vlastních serverech.\n\n".. + "Server používá upravené verze módů podléhajících licenci AGPLv3 a jiným svobodným licencím."), + }, { + title = "Co slovenština?", + formspec_text = F("Slovenština je na Českém hvozdu vítána. Do slovenštiny jsou v současnosti lokalizovány pouze některé základní módy, ".. + "nemám totiž nikoho, kdo by měl zájem vytvářet slovenskou lokalizaci ostatních módů. Máš-li zájem to dělat, domluv se s Administrací, ".. + "je to ale spousta práce, protože na Českém hvozdu je nasazeno přes 200 módů."), + } +} + +local function dump_privs(privs) + local names = {} + for k, v in pairs(privs) do + if v then + table.insert(names, k) + end + end + table.sort(names, function(a, b) return a < b end) + return "["..#names.."]("..table.concat(names, ",")..")" +end + + +local function get_new_player_formspec(custom_state) + local formspec = { + ch_core.formspec_header({formspec_version = 4, size = {18, 10}, auto_background = true}), + "button_exit[16.8,0.25;0.8,0.8;zavrit;X]".. + "tooltip[zavrit;zavřít]".. + "style_type[table;font=italic]".. + "tablecolumns[text]".. + "table[0.5,0.5;8,9.1;volba;", + F(new_player_texts[1].title), + } + local volba = custom_state.volba + local heading_color = minetest.get_color_escape_sequence("#00FF00") + for i = 2, #new_player_texts do + table.insert(formspec, ","..F(new_player_texts[i].title)) + end + table.insert(formspec, ";"..volba.."]".. + "label[9,0.75;"..heading_color..F(new_player_texts[volba].title).."]".. + "box[8.9,1.15;8.7,8.45;#00000099]".. + "textarea[9,1.25;8.5,8.25;;;"..new_player_texts[volba].formspec_text.."\n]") + return table.concat(formspec) +end + +local function new_player_formspec_callback(custom_state, player, formname, fields) + if fields.quit then return end + if fields.volba then + local event = minetest.explode_table_event(fields.volba) + if event.type == "CHG" or event.type == "DCL" then + custom_state.volba = assert(tonumber(event.row)) + return get_new_player_formspec(custom_state) + end + end +end + +local function on_newplayer(player) + local player_name = player:get_player_name() + minetest.log("action", "[ch_core] New player '"..player_name.."'"); + ch_data.delete_offline_charinfo(player_name) + ch_data.get_joining_online_charinfo(player) + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + offline_charinfo.pending_registration_type = "new" +end + +local function after_joinplayer(player_name, join_timestamp) + local online_charinfo = ch_data.online_charinfo[player_name] + local player = minetest.get_player_by_name(player_name) + if player == nil or online_charinfo == nil or online_charinfo.join_timestamp ~= join_timestamp then + return + end + local controls = player:get_player_control() + if not controls.aux1 then + player:set_properties({stepheight = 0.3}) + end + player:set_clouds({density = 0}) -- disable clouds + --[[ + 5.5.x => formspec_version = 5, protocol_version = 40 + 5.6.x => formspec_version = 6, protocol_version = 41 + 5.7.x => formspec_version = 6, protocol_version = 42 + 5.8.0 => formspec_version = 7, protocol_version = 43 + 5.9.0 => formspec_version = ?, protocol_version = ? + 5.10.0 => formspec_version = 8, protocol_version = 46 + ]] + if online_charinfo.protocol_version < 42 and online_charinfo.protocol_version ~= 0 then + local client_version + if online_charinfo.protocol_version == 40 then + client_version = "5.5.x" + elseif online_charinfo.protocol_version == 41 then + client_version = "5.6.x" + else + client_version = "?.?.?" + end + ch_core.systemovy_kanal(player_name, minetest.get_color_escape_sequence("#cc5257").."VAROVÁNÍ: Váš klient je zastaralý! Zdá se, že používáte klienta Minetest "..client_version..", který nepodporuje některé moderní vlastnosti hry využívané na Českém hvozdu. Hra vám bude fungovat, ale některé bloky se nemusejí zobrazit správně. Pro správné zobrazení doporučujeme přejít na Minetest 5.7.0 nebo novější, máte-li tu možnost.") + end + + -- Vypsat posledních 5 přihlášených registrovaných postav: + -- (přeskočit vlastní postavu a předváděcí postavy) + local last_logins = ch_core.get_last_logins(true, {[player_name] = true, Jan_Rimbaba = true, Zofia_Slivka = true}) + if #last_logins > 0 then + local output = { + "INFORMACE: Registrované postavy objevivší se ve hře v poslední době: ", + } + -- local last_players = {} + for i, info in ipairs(last_logins) do + local viewname = ch_core.prihlasovaci_na_zobrazovaci(info.player_name, true) + local kdy = info.last_login_before + if kdy < 0 then + kdy = "???" + elseif kdy == 0 then + kdy = "dnes" + elseif kdy == 1 then + kdy = "včera" + else + kdy = "před "..kdy.." dny" + end + table.insert(output, ch_core.colors.light_green..viewname..ch_core.colors.white.." ("..kdy..")") + table.insert(output, ", ") + if i == 5 then break end + end + output[#output] = "" + ch_core.systemovy_kanal(player_name, table.concat(output)) + end + + minetest.log("action", "Player "..player_name.." after_joinplayer privs = "..dump_privs(minetest.get_player_privs(player_name))) +end + +local event_types = {"public_announcement", "announcement", "custom"} + +local function after_joinplayer_5min(player_name, join_timestamp) + local online_charinfo = ch_data.online_charinfo[player_name] + local offline_charinfo = ch_data.offline_charinfo[player_name] or {} + if online_charinfo == nil or online_charinfo.join_timestamp ~= join_timestamp or minetest.get_player_by_name(player_name) == nil then + return -- player probably already logged out + end + local cas = ch_time.aktualni_cas() + local dnes = cas:YYYY_MM_DD() + local last_ann_shown_date = offline_charinfo.last_ann_shown_date or "1970-01-01" + if last_ann_shown_date >= dnes then return end + local events = ch_core.get_events_for_player(player_name, event_types, 10, last_ann_shown_date) + if #events > 0 then + local output = {} + local counts_by_description = {} + for _, record in ipairs(events) do + local old_count = counts_by_description[record.description] or 0 + counts_by_description[record.description] = old_count + 1 + if old_count < 3 then + table.insert(output, 1, minetest.get_color_escape_sequence("#6666FF").."<"..record.description.."> ("..record.time:sub(1, 10)..") ".. + minetest.get_color_escape_sequence(record.color)..record.text) + end + end + ch_core.systemovy_kanal(player_name, table.concat(output, "\n")) + end + offline_charinfo.last_ann_shown_date = dnes + ch_data.save_offline_charinfo(player_name) +end + +local function on_joinplayer_pomodoro(player, player_name, online_charinfo) + local oc = ch_data.online_charinfo + local now = minetest.get_us_time() + local priv = {ch_registered_player = true} + local prev_leave_timestamp = online_charinfo.prev_leave_timestamp + if prev_leave_timestamp ~= nil and now - prev_leave_timestamp < 3600000000 then + minetest.log("warning", "on_joinplayer_pomodoro() not activated, because the player "..player_name.." has returned after "..math.floor((now - prev_leave_timestamp) / 1000000).." seconds") + return false -- relogin too early + end + if not minetest.check_player_privs(player_name, priv) then + minetest.log("warning", "on_joinplayer_pomodoro() not activated, because the player "..player_name.." is not registered") + return false -- the new player is not registered + end + for k, other_online_charinfo in pairs(oc) do + if k ~= player_name and minetest.check_player_privs(player_name, priv) then + local ap_modify_timestamp = other_online_charinfo.ap_modify_timestamp + if ap_modify_timestamp == nil or now - ap_modify_timestamp < 600000000 then + minetest.log("warning", "on_joinplayer_pomodoro() not activated, because of already online player "..k) + return false -- the new player is not alone + else + minetest.log("warning", "on_joinplayer_pomodoro() not broken, because the online player "..k.." has been inactive for "..math.floor((now - ap_modify_timestamp) / 1000000).."seconds.") + end + end + end + ch_time.herni_cas_nastavit(6, 0, 0) + return true +end + +function ch_core.show_new_player_formspec(player_name) + if minetest.get_player_by_name(player_name) == nil then + return false + end + local custom_state = {volba = 1} + ch_core.show_formspec(player_name, "ch_core:uvitani", get_new_player_formspec(custom_state), new_player_formspec_callback, custom_state, {}) +end + +local function on_joinplayer(player, last_login) + local player_name = player:get_player_name() + local online_charinfo = ch_data.get_joining_online_charinfo(player) + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + local news_role = assert(online_charinfo.news_role) + local lang_code = online_charinfo.lang_code + local protocol_version = online_charinfo.protocol_version + + ch_core.add_event("joinplayer_for_admin", "{PLAYER} se připojil/a do hry (NR="..news_role..", PV="..protocol_version..")", player_name) + + if news_role == "disconnect" then + minetest.disconnect_player(player_name, "Váš klient je příliš starý. ".. + "Pro připojení k tomuto serveru prosím použijte Minetest/Luanti 5.9.0 nebo novější. ".. + "Verze 5.7.x a 5.8.x budou fungovat, ale některé bloky nemusejí být zobrazeny správně.") + return true + elseif news_role == "invalid_name" then + minetest.disconnect_player(player_name, "Neplatné přihlašovací jméno '"..player_name.."'. Seznamte se, prosím, s pravidly serveru pro jména, nebo použijte jen písmena anglické abecedy.") + return true + end + if news_role == "invalid_locale" then + if minetest.check_player_privs(player_name, "server") then + minetest.after(0.2, function() + minetest.chat_send_player(player_name, "VAROVÁNÍ: U vašeho klienta byla detekována nepodporovaná lokalizace '"..lang_code.."'!") + end) + else + minetest.after(0.2, function() + ch_core.show_formspec(player_name, "ch_core:invalid_locale", get_invalid_locale_formspec(lang_code, protocol_version), invalid_locale_formspec_callback, {}, {}) + end) + return true + end + elseif news_role == "new_player" then + minetest.after(0.2, ch_core.show_new_player_formspec, player_name) + end + + player:set_nametag_attributes(ch_core.compute_player_nametag(online_charinfo, offline_charinfo)) + player:hud_set_flags({minimap = false, minimap_radar = false}) + + -- Reset the creative priv (set for the new characters) + local privs = minetest.get_player_privs(player_name) + minetest.log("action", "Player "..player_name.." joined with privs = "..dump_privs(privs)) + if privs.ch_registered_player then + if privs.creative then + privs.creative = nil + minetest.set_player_privs(player_name, privs) + minetest.log("action", "creative priv reset on join for "..player_name) + end + elseif not privs.creative then + privs.creative = true + minetest.set_player_privs(player_name, privs) + minetest.log("action", "creative priv set on join for "..player_name) + end + + -- Set the inventory size + ch_core.extend_player_inventory(player_name, offline_charinfo.extended_inventory == 1) + + -- Pomodoro functionality for single-players: + on_joinplayer_pomodoro(player, player_name, online_charinfo) + -- + + assert(online_charinfo.join_timestamp) + minetest.after(2, after_joinplayer, player_name, online_charinfo.join_timestamp) + minetest.after(5 * 60, after_joinplayer_5min, player_name, online_charinfo.join_timestamp) + return true +end + +core.send_join_message = function(player_name) + local player = core.get_player_by_name(player_name) + if player == nil then + return false + end + local online_charinfo = ch_data.get_joining_online_charinfo(player) + -- local lang_code = assert(online_charinfo.lang_code) + local news_role = assert(online_charinfo.news_role) + -- local protocol_version = assert(online_charinfo.protocol_version) + + if news_role ~= "disconnect" and news_role ~= "invalid_name" and news_role ~= "invalid_locale" then + ch_core.add_event(ifthenelse(news_role ~= "new_player", "joinplayer", "joinplayer_new"), nil, player_name) + end + return true +end + +local function on_leaveplayer(player) + local player_name = player:get_player_name() + local privs = minetest.get_player_privs(player_name) + + ch_core.add_event("leaveplayer_for_admin", nil, player_name) + + if privs.ch_registered_player and privs.creative then + privs.creative = nil + minetest.set_player_privs(player_name, privs) + minetest.log("action", "creative priv reset on leave for "..player_name) + + privs = minetest.get_player_privs(player_name) -- update variable + end + minetest.log("action", "Player "..player_name.." leaved with privs = "..dump_privs(privs)) +end + +core.send_leave_message = function(player_name, is_timedout) + local player = core.get_player_by_name(player_name) + if player == nil then + return false + end + local online_charinfo = ch_data.get_leaving_online_charinfo(player) + local news_role = assert(online_charinfo.news_role) + + if news_role ~= "disconnect" and news_role ~= "invalid_name" and news_role ~= "invalid_locale" then + ch_core.add_event("leaveplayer", nil, player_name) + end + return true +end + +-- AUTH logging: +local function on_authplayer(name, ip, is_success) + core.log("action", "AUTH(<"..tostring(name).."> @ <"..tostring(ip)..">) => "..ifthenelse(is_success, "true", "false")) +end + +core.register_on_newplayer(on_newplayer) +core.register_on_joinplayer(on_joinplayer) +core.register_on_leaveplayer(on_leaveplayer) +core.register_on_authplayer(on_authplayer) + +ch_core.close_submod("joinplayer") diff --git a/ch_core/kos.lua b/ch_core/kos.lua new file mode 100644 index 0000000..ac9bb83 --- /dev/null +++ b/ch_core/kos.lua @@ -0,0 +1,210 @@ +ch_core.open_submod("kos", {lib = true}) + +local player_name_to_trash_inv = {} +local trash_inv_width, trash_inv_height = 4, 2 + +local function return_zero() + return 0 +end + +local function on_take(inv, listname, index, stack, player) + local s = stack:to_string() + s = s:sub(1, 1024) + minetest.log("action", player:get_player_name().." retreived from the trash bin: "..s) +end + +local inv_callbacks = { + allow_move = return_zero, + allow_put = return_zero, + -- allow_take = yes, + on_take = on_take, +} + +local function on_joinplayer(player, _last_login) + local player_name = player:get_player_name() + local trash_inv = minetest.create_detached_inventory("ch_core_trash_"..player_name, inv_callbacks, player_name) + trash_inv:set_size("main", trash_inv_width * trash_inv_height) + player_name_to_trash_inv[player_name] = trash_inv +end + +local function on_leaveplayer(player, _timeouted) + local player_name = player:get_player_name() + minetest.remove_detached_inventory("ch_core_trash_"..player_name) + player_name_to_trash_inv[player_name] = nil +end + +minetest.register_on_joinplayer(on_joinplayer) +minetest.register_on_leaveplayer(on_leaveplayer) + +--[[ + Vrací nil, pokud pro danou postavu inventář koše neexistuje. + Jinak vrací: + { + inventory = InvRef, + location = string, + listname = string, + width = int, + height = int, + } +]] +function ch_core.get_trash_inventory(player_name) + local result = { + inventory = player_name_to_trash_inv[player_name], + location = "detached:ch_core_trash_"..player_name, + listname = "main", + width = trash_inv_width, + height = trash_inv_height, + } + if result.inventory ~= nil then + return result + end +end + +--[[ + Smaže obsah zadaného inventáře a zaznamená to jako příkaz daného + hráče/ky. + player_name -- přihlašovací jméno hráče/ky nebo nil + inv -- odkaz na inventář + listname -- listname ke smazání + description -- heslovitý popis kontextu mazání +]] +function ch_core.vyhodit_inventar(player_name, inv, listname, description) + if not player_name then + player_name = "???" + end + if not description then + description = "???" + end + local t = inv:get_list(listname) + if t == nil then + return false + end + local craftitems, tools, item_strings = {}, {}, {} + for _, stack in ipairs(t) do + if not stack:is_empty() then + if stack:get_stack_max() <= 1 then + table.insert(tools, stack) + else + table.insert(craftitems, stack) + end + table.insert(item_strings, stack:to_string():sub(1, 1024)) + end + end + + if #item_strings > 0 then + minetest.log("action", "Player "..player_name.." trashed "..#item_strings.." items ("..description.."): "..table.concat(item_strings, ", ")) + local empty_list = {} + inv:set_list(listname, empty_list) + if player_name ~= "???" then + + -- Trash inventory: + local trash_inv = player_name_to_trash_inv[player_name] + if trash_inv ~= nil then + local old_trash_list = trash_inv:get_list("main") + trash_inv:set_list("main", empty_list) + for i = #tools, 1, -1 do + trash_inv:add_item("main", tools[i]) + end + for i = #craftitems, 1, -1 do + trash_inv:add_item("main", craftitems[i]) + end + for i = #old_trash_list, 1, -1 do + local stack = old_trash_list[i] + if not stack:is_empty() then + trash_inv:add_item("main", stack) + end + end + if not trash_inv:is_empty("main") then + old_trash_list = trash_inv:get_list("main") + local n = 1 + while n < #old_trash_list and not old_trash_list[n + 1]:is_empty() do + n = n + 1 + end + -- reverse the list: + for i = 1, math.floor(n / 2) do + old_trash_list[i], old_trash_list[1 + n - i] = old_trash_list[1 + n - i], old_trash_list[i] + end + trash_inv:set_list("main", old_trash_list) + end + else + minetest.log("warning", "Player "..player_name.." has no trash inventory!") + end + + local trash_sound + if #item_strings == 1 then + trash_sound = ch_core.overridable.trash_one_sound + else + trash_sound = ch_core.overridable.trash_all_sound + end + if trash_sound ~= nil and trash_sound ~= "" then + minetest.sound_play(trash_sound, { to_player = player_name, gain = 1.0 }) + end + end + end + return true +end + +function ch_core.vyhodit_predmet(player_name, stack, description) + if not player_name then + player_name = "???" + end + if not description then + description = "???" + end + if stack == nil or stack:is_empty() then + return false + end + minetest.log("action", "Player "..player_name.." trashed an item ("..description.."): "..stack:to_string():sub(1, 1024)) + local trash_inv = player_name_to_trash_inv[player_name] + if trash_inv == nil then + minetest.log("warning", "Player "..player_name.." has no trash inventory!") + elseif trash_inv:room_for_item("main", stack) then + trash_inv:add_item("main", stack) + else + -- shift the inventory: + local list = trash_inv:get_list("main") + for i = 1, #list - 1 do + list[i] = list[i + 1] + end + list[#list] = stack + trash_inv:set_list("main", list) + end + local trash_sound = ch_core.overridable.trash_one_sound + if trash_sound ~= nil and trash_sound ~= "" then + minetest.sound_play(trash_sound, { to_player = player_name, gain = 1.0 }) + end + return true +end + +-- přepsat minetest.item_drop: +local old_minetest_item_drop = minetest.item_drop +function minetest.item_drop(itemstack, dropper, pos) + if minetest.is_player(dropper) then + local player_name = assert(dropper:get_player_name()) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if + offline_charinfo ~= nil and + offline_charinfo.discard_drops ~= nil and + offline_charinfo.discard_drops == 1 and + ch_core.get_trash_inventory(player_name) ~= nil + then + ch_core.vyhodit_predmet(player_name, itemstack, "item_drop") + return ItemStack() + end + end + local dropper_name = "???" + local dropper_pos = "(???)" + if dropper ~= nil and dropper.get_player_name ~= nil and dropper.get_pos ~= nil then + dropper_name = dropper:get_player_name() + dropper_pos = core.pos_to_string(vector.round(dropper:get_pos())) + elseif pos ~= nil then + dropper_pos = core.pos_to_string(vector.round(pos)) + end + local item_name = itemstack:get_name() + if core.registered_items[item_name] ~= nil then + core.log("action", "Item drop: "..dropper_name.." dropped "..itemstack:get_count().." of '"..item_name.."' at "..dropper_pos) + end + return old_minetest_item_drop(itemstack, dropper, pos) +end + +ch_core.close_submod("kos") diff --git a/ch_core/lib.lua b/ch_core/lib.lua new file mode 100644 index 0000000..299003f --- /dev/null +++ b/ch_core/lib.lua @@ -0,0 +1,2216 @@ +ch_core.open_submod("lib", {data = true}) + +-- DATA +-- =========================================================================== +local click_sound = { + name = "click", + gain = 0.05, +} +local diakritika = { + ["Á"] = "A", + ["Ä"] = "A", + ["Č"] = "C", + ["Ď"] = "D", + ["É"] = "E", + ["Ě"] = "E", + ["Í"] = "I", + ["Ĺ"] = "L", + ["Ľ"] = "L", + ["Ň"] = "N", + ["Ó"] = "O", + ["Ô"] = "O", + ["Ŕ"] = "R", + ["Ř"] = "R", + ["Š"] = "S", + ["Ť"] = "T", + ["Ú"] = "U", + ["Ů"] = "U", + ["Ý"] = "Y", + ["Ž"] = "Z", + ["á"] = "a", + ["ä"] = "a", + ["č"] = "c", + ["ď"] = "d", + ["é"] = "e", + ["ě"] = "e", + ["í"] = "i", + ["ĺ"] = "l", + ["ľ"] = "l", + ["ň"] = "n", + ["ó"] = "o", + ["ô"] = "o", + ["ŕ"] = "r", + ["ř"] = "r", + ["š"] = "s", + ["ť"] = "t", + ["ú"] = "u", + ["ů"] = "u", + ["ý"] = "y", + ["ž"] = "z", +} + +local diakritika_na_mala = { + ["Á"] = "á", + ["Ä"] = "ä", + ["Č"] = "č", + ["Ď"] = "ď", + ["É"] = "é", + ["Ě"] = "Ě", + ["Í"] = "Í", + ["Ĺ"] = "ĺ", + ["Ľ"] = "ľ", + ["Ň"] = "ň", + ["Ó"] = "ó", + ["Ô"] = "ô", + ["Ŕ"] = "ŕ", + ["Ř"] = "ř", + ["Š"] = "š", + ["Ť"] = "ť", + ["Ú"] = "ú", + ["Ů"] = "ů", + ["Ý"] = "ý", + ["Ž"] = "ž", +} + +local diakritika_na_velka = { + ["á"] = "Á", + ["ä"] = "Ä", + ["č"] = "Č", + ["ď"] = "Ď", + ["é"] = "É", + ["Ě"] = "Ě", + ["Í"] = "Í", + ["ĺ"] = "Ĺ", + ["ľ"] = "Ľ", + ["ň"] = "Ň", + ["ó"] = "Ó", + ["ô"] = "Ô", + ["ŕ"] = "Ŕ", + ["ř"] = "Ř", + ["š"] = "Š", + ["ť"] = "Ť", + ["ú"] = "Ú", + ["ů"] = "Ů", + ["ý"] = "Ý", + ["ž"] = "Ž", +} + +local facedir_to_rotation_data = { + [0] = vector.new(0, 0, 0), + [1] = vector.new(0, -0.5 * math.pi, 0), + [2] = vector.new(0, -math.pi, 0), + [3] = vector.new(0, 0.5 * math.pi, 0), + [4] = vector.new(-0.5 * math.pi, 0, 0), + [5] = vector.new(0, -0.5 * math.pi, -0.5 * math.pi), + [6] = vector.new(0.5 * math.pi, 0, math.pi), + [7] = vector.new(0, 0.5 * math.pi, 0.5 * math.pi), + [8] = vector.new(0.5 * math.pi, 0, 0), + [9] = vector.new(0, -0.5 * math.pi, 0.5 * math.pi), + [10] = vector.new(-0.5 * math.pi, 0, math.pi), + [11] = vector.new(0, 0.5 * math.pi, -0.5 * math.pi), + [12] = vector.new(0, 0, 0.5 * math.pi), + [13] = vector.new(-0.5 * math.pi, 0, 0.5 * math.pi), + [14] = vector.new(0, -math.pi, -0.5 * math.pi), + [15] = vector.new(0.5 * math.pi, 0, 0.5 * math.pi), + [16] = vector.new(0, 0, -0.5 * math.pi), + [17] = vector.new(0.5 * math.pi, 0, -0.5 * math.pi), + [18] = vector.new(0, -math.pi, 0.5 * math.pi), + [19] = vector.new(-0.5 * math.pi, 0, -0.5 * math.pi), + [20] = vector.new(0, 0, math.pi), + [21] = vector.new(0, 0.5 * math.pi, math.pi), + [22] = vector.new(0, -math.pi, math.pi), + [23] = vector.new(0, -0.5 * math.pi, math.pi), +} + +local hypertext_escape_replacements = { + ["\\"] = "\\\\\\\\", + ["<"] = "\\\\<", + [">"] = "\\\\>", + [";"] = "\\;", + [","] = "\\,", + ["["] = "\\[", + ["]"] = "\\]", +} + +local utf8_charlen = {} +for i = 1, 191, 1 do + -- 1 to 127 => jednobajtové znaky + -- 128 až 191 => nejsou dovoleny jako první bajt (=> vrátit 1 bajt) + utf8_charlen[i] = 1 +end +for i = 192, 223, 1 do + utf8_charlen[i] = 2 +end +for i = 224, 239, 1 do + utf8_charlen[i] = 3 +end +for i = 240, 247, 1 do + utf8_charlen[i] = 4 +end +for i = 248, 255, 1 do + utf8_charlen[i] = 1 -- neplatné UTF-8 +end + +local utf8_sort_data_1 = { + ["\x20"] = "\x20", -- < > + ["\x21"] = "\x21", -- <!> + ["\x22"] = "\x22", -- <"> + ["\x23"] = "\x23", -- <#> + ["\x25"] = "\x24", -- <%> + ["\x26"] = "\x25", -- <&> + ["\x27"] = "\x26", -- <'> + ["\x28"] = "\x27", -- <(> + ["\x29"] = "\x28", -- <)> + ["\x2a"] = "\x29", -- <*> + ["\x2b"] = "\x2a", -- <+> + ["\x2c"] = "\x2b", -- <,> + ["\x2d"] = "\x2c", -- <-> + ["\x2e"] = "\x2d", -- <.> + ["\x2f"] = "\x2e", -- </> + ["\x3a"] = "\x2f", -- <:> + ["\x3b"] = "\x30", -- <;> + ["\x3c"] = "\x31", -- <<> + ["\x3d"] = "\x32", -- <=> + ["\x3e"] = "\x33", -- <>> + ["\x3f"] = "\x34", -- <?> + ["\x40"] = "\x35", -- <@> + ["\x5b"] = "\x36", -- <[> + ["\x5c"] = "\x37", -- <\> + ["\x5d"] = "\x38", -- <]> + ["\x5e"] = "\x39", -- <^> + ["\x5f"] = "\x3a", -- <_> + ["\x60"] = "\x3b", -- <`> + ["\x7b"] = "\x3c", -- <{> + ["\x7c"] = "\x3d", -- <|> + ["\x7d"] = "\x3e", -- <}> + ["\x7e"] = "\x3f", -- <~> + ["\x24"] = "\x40", -- <$> + ["\x61"] = "\x41", -- <a> + ["\x41"] = "\x42", -- <A> + ["\x62"] = "\x47", -- <b> + ["\x42"] = "\x48", -- <B> + ["\x64"] = "\x4d", -- <d> + ["\x44"] = "\x4e", -- <D> + ["\x65"] = "\x51", -- <e> + ["\x45"] = "\x52", -- <E> + ["\x66"] = "\x57", -- <f> + ["\x46"] = "\x58", -- <F> + ["\x67"] = "\x59", -- <g> + ["\x47"] = "\x5a", -- <G> + ["\x68"] = "\x5b", -- <h> + ["\x48"] = "\x5c", -- <H> + ["\x69"] = "\x61", -- <i> + ["\x49"] = "\x62", -- <I> + ["\x6a"] = "\x65", -- <j> + ["\x4a"] = "\x66", -- <J> + ["\x6b"] = "\x67", -- <k> + ["\x4b"] = "\x68", -- <K> + ["\x6c"] = "\x69", -- <l> + ["\x4c"] = "\x6a", -- <L> + ["\x6d"] = "\x6f", -- <m> + ["\x4d"] = "\x70", -- <M> + ["\x6e"] = "\x71", -- <n> + ["\x4e"] = "\x72", -- <N> + ["\x6f"] = "\x75", -- <o> + ["\x4f"] = "\x76", -- <O> + ["\x70"] = "\x7b", -- <p> + ["\x50"] = "\x7c", -- <P> + ["\x71"] = "\x7d", -- <q> + ["\x51"] = "\x7e", -- <Q> + ["\x72"] = "\x7f", -- <r> + ["\x52"] = "\x80", -- <R> + ["\x73"] = "\x85", -- <s> + ["\x53"] = "\x86", -- <S> + ["\x74"] = "\x89", -- <t> + ["\x54"] = "\x8a", -- <T> + ["\x75"] = "\x8d", -- <u> + ["\x55"] = "\x8e", -- <U> + ["\x76"] = "\x93", -- <v> + ["\x56"] = "\x94", -- <V> + ["\x77"] = "\x95", -- <w> + ["\x57"] = "\x96", -- <W> + ["\x78"] = "\x97", -- <x> + ["\x58"] = "\x98", -- <X> + ["\x79"] = "\x99", -- <y> + ["\x59"] = "\x9a", -- <Y> + ["\x7a"] = "\x9d", -- <z> + ["\x5a"] = "\x9e", -- <Z> + ["\x30"] = "\xa1", -- <0> + ["\x31"] = "\xa2", -- <1> + ["\x32"] = "\xa3", -- <2> + ["\x33"] = "\xa4", -- <3> + ["\x34"] = "\xa5", -- <4> + ["\x35"] = "\xa6", -- <5> + ["\x36"] = "\xa7", -- <6> + ["\x37"] = "\xa8", -- <7> + ["\x38"] = "\xa9", -- <8> + ["\x39"] = "\xaa", -- <9> +} + +local utf8_sort_data_2 = { + ["\xc3\xa1"] = "\x43", -- <á> + ["\xc3\x81"] = "\x44", -- <Á> + ["\xc3\xa4"] = "\x45", -- <ä> + ["\xc3\x84"] = "\x46", -- <Ä> + ["\xc4\x8d"] = "\x4b", -- <č> + ["\xc4\x8c"] = "\x4c", -- <Č> + ["\xc4\x8f"] = "\x4f", -- <ď> + ["\xc4\x8e"] = "\x50", -- <Ď> + ["\xc3\xa9"] = "\x53", -- <é> + ["\xc3\x89"] = "\x54", -- <É> + ["\xc4\x9b"] = "\x55", -- <ě> + ["\xc4\x9a"] = "\x56", -- <Ě> + ["\x63\x68"] = "\x5d", -- <ch> + ["\x63\x48"] = "\x5e", -- <cH> + ["\x43\x68"] = "\x5f", -- <Ch> + ["\x43\x48"] = "\x60", -- <CH> + ["\xc3\xad"] = "\x63", -- <í> + ["\xc3\x8d"] = "\x64", -- <Í> + ["\xc4\xba"] = "\x6b", -- <ĺ> + ["\xc4\xb9"] = "\x6c", -- <Ĺ> + ["\xc4\xbe"] = "\x6d", -- <ľ> + ["\xc4\xbd"] = "\x6e", -- <Ľ> + ["\xc5\x88"] = "\x73", -- <ň> + ["\xc5\x87"] = "\x74", -- <Ň> + ["\xc3\xb3"] = "\x77", -- <ó> + ["\xc3\x93"] = "\x78", -- <Ó> + ["\xc3\xb4"] = "\x79", -- <ô> + ["\xc3\x94"] = "\x7a", -- <Ô> + ["\xc5\x95"] = "\x81", -- <ŕ> + ["\xc5\x94"] = "\x82", -- <Ŕ> + ["\xc5\x99"] = "\x83", -- <ř> + ["\xc5\x98"] = "\x84", -- <Ř> + ["\xc5\xa1"] = "\x87", -- <š> + ["\xc5\xa0"] = "\x88", -- <Š> + ["\xc5\xa5"] = "\x8b", -- <ť> + ["\xc5\xa4"] = "\x8c", -- <Ť> + ["\xc3\xba"] = "\x8f", -- <ú> + ["\xc3\x9a"] = "\x90", -- <Ú> + ["\xc5\xaf"] = "\x91", -- <ů> + ["\xc5\xae"] = "\x92", -- <Ů> + ["\xc3\xbd"] = "\x9b", -- <ý> + ["\xc3\x9d"] = "\x9c", -- <Ý> + ["\xc5\xbe"] = "\x9f", -- <ž> + ["\xc5\xbd"] = "\xa0", -- <Ž> +} + +local utf8_sort_data_3 = { + ["\x63"] = "\x49", -- <c> + ["\x43"] = "\x4a", -- <C> +} + +local entity_properties_list = { + "hp_max", + "breath_max", + "zoom_fov", + "eye_height", + "physical", + "collide_with_objects", + "collisionbox", + "selectionbox", + "pointable", + "visual", + "visual_size", + "mesh", + "textures", + "colors", + "use_texture_alpha", + "spritediv", + "initial_sprite_basepos", + "is_visible", + "makes_footstep_sound", + "automatic_rotate", + "stepheight", + "automatic_face_movement_dir", + "automatic_face_movement_max_rotation_per_sec", + "backface_culling", + "glow", + "nametag", + "nametag_color", + "nametag_bgcolor", + "infotext", + "static_save", + "damage_texture_modifier", + "shaded", + "show_on_minimap", +} + +local player_role_to_image = { + admin = "ch_core_koruna.png", + creative = "ch_core_kouzhul.png", + new = "ch_core_slunce.png", + none = "ch_core_empty.png", + survival = "ch_core_kladivo.png", +} + +local wm_to_4dir = { + -- směr "wallmounted" na "4dir" (ztrátová konverze) + [0] = 0, [1] = 0, [2] = 1, [3] = 3, [4] = 0, [5] = 2, [6] = 0, [7] = 0, +} +local wmc_to_4dirc = { + [0] = 0, + [8] = 8, + [16] = 12, + [24] = 20, + [32] = 28, + [40] = 84, + [48] = 76, + [56] = 64, + [64] = 128, + [72] = 132, + [80] = 136, + [88] = 140, + [96] = 144, + [104] = 148, + [112] = 152, + [120] = 156, + [128] = 192, + [136] = 196, + [144] = 200, + [152] = 204, + [160] = 208, + [168] = 212, + [176] = 216, + [184] = 220, + [192] = 224, + [200] = 228, + [208] = 232, + [216] = 236, + [224] = 240, + [232] = 244, + [240] = 248, + [248] = 252, +} + +-- KEŠ +-- =========================================================================== +local utf8_sort_cache = { +} + +-- LOKÁLNÍ FUNKCE +-- =========================================================================== +local function cmp_oci(a, b) + return (ch_data.offline_charinfo[a].last_login or -1) < (ch_data.offline_charinfo[b].last_login or -1) +end + +local function get_player_role_by_privs(privs) + if privs.protection_bypass then + return "admin" + elseif not privs.ch_registered_player then + return "new" + elseif privs.give then + return "creative" + else + return "survival" + end +end + +-- VEŘEJNÉ FUNKCE +-- =========================================================================== + +--[[ +Přidá nástroji opotřebení, pokud „player“ nemá právo usnadnění hry nebo není nil. +]] +function ch_core.add_wear(player, itemstack, wear_to_add) + local player_name = player and player.get_player_name and player:get_player_name() + if not player_name or not minetest.is_creative_enabled(player_name) then + local new_wear = itemstack:get_wear() + wear_to_add + if new_wear > 65535 then + itemstack:clear() + elseif new_wear >= 0 then + itemstack:set_wear(new_wear) + else + itemstack:set_wear(0) + end + end + return itemstack +end + +--[[ + Sestaví nové pole "groups" následujícím způsobem: + Vezme dvojice z "base"; přepíše je dvojicemi z "inherit" (případně pouze dvojicemi vybranými pomocí "inherit_list") + a nakonec výsledek přepíše dvojicemi z "override". Přitom dodržuje, že pokud nějaké dvojici vyjde hodnota 0, + tato dvojice se na výstupu vůbec neobjeví. Kterýkoliv parametr může být nil; u parametrů base, inherit a override + se to interpretuje jako prázdná tabulka. Je-li inherit_list == nil, pak se z "inherit" vezmou všechny dvojice. + + base = table or nil, -- základ, ze kterého se vychází (dvojice s nejnižší prioritou) + override = table or nil, -- dvojice s nejvyšší prioritou (přepíšou vše ostatní) + inherit = table or nil, -- přidat/přepsat tyto hodnoty do base + inherit_list = {string, ...} or nil -- je-li nastaveno, pak se z inherit budou brát pouze klíče z tohoto seznamu a v uvedeném pořadí, jinak všechny klíče +]] +function ch_core.assembly_groups(default, override, inherit, inherit_list) + local result = {} + if default ~= nil then + for k, v in pairs(default) do + if v ~= 0 then + result[k] = v + -- result je prázdný, takže zde není třeba nastavovat nil + end + end + end + if inherit ~= nil then + if inherit_list ~= nil then + for _, group in ipairs(inherit_list) do + local value = inherit[group] + if value ~= nil then + if value ~= 0 then + result[group] = value + else + result[group] = nil + end + end + end + else + for group, value in pairs(inherit) do + if value ~= 0 then + result[group] = value + else + result[group] = nil + end + end + end + end + if override ~= nil then + for group, value in pairs(override) do + if value ~= 0 then + result[group] = value + else + result[group] = nil + end + end + end + return result +end + +--[[ +Ověří, že předaný argument je funkce a zavolá ji s ostatními zadanými argumenty. +Vrátí její výsledek. Není-li předaný argument funkce, nevrátí nic. +]] +function ch_core.call(f, ...) + if type(f) == "function" then + return f(...) + end +end + +--[[ +Určí typ postavy (admin|creative|new|survival) a zkontroluje, +zda je to jeden z akceptovaných. +player_or_player_name může být PlayerRef nebo přihlašovací jméno postavy. +accepted_roles může být buď řetězec nebo seznam řetězců. +]] +function ch_core.check_player_role(player_or_player_name, accepted_roles) + local role = ch_core.get_player_role(player_or_player_name) + if role == nil then + return nil + end + if type(accepted_roles) == "string" then + return role == accepted_roles + end + for _, r in ipairs(accepted_roles) do + if role == r then + return true + end + end + return false +end + +--[[ +Smaže recepty jako minetest.clear_craft(), ale s lepším logováním. +]] +function ch_core.clear_crafts(log_prefix, crafts) + if log_prefix == nil then + log_prefix = "" + else + log_prefix = log_prefix.."/" + end + local get_us_time = minetest.get_us_time + local count = 0 + for k, v in pairs(crafts) do + -- minetest.log("action", "Will clear craft "..log_prefix..k) + -- print("CLEAR_CRAFTS("..log_prefix.."): "..dump2(crafts)) + if v.output ~= nil or v.type == "fuel" then + if minetest.clear_craft(v) then + count = count + 1 + else + minetest.log("warning", "Craft "..log_prefix..k.." not cleared! Dump = "..dump2(v)) + end + else + local start = get_us_time() + if minetest.clear_craft(v) then + count = count + 1 + local stop = get_us_time() + minetest.log("action", "Craft "..log_prefix..k.." cleared in "..((stop - start) / 1000.0).." ms. Dump = "..dump2(v)) + else + minetest.log("warning", "Craft "..log_prefix..k.." not cleared! Dump = "..dump2(v)) + end + end + end + return count +end + +--[[ +Převede param2 z colorwallmounted na color4dir paletu. +]] +function ch_core.colorwallmounted_to_color4dir(param2) + local dir = wm_to_4dir[param2 % 8] + param2 = param2 - param2 % 8 + local color = wmc_to_4dirc[param2 - param2 % 8] + return dir + color +end + +--[[ +Jako vstup přijímá pole dávek (např. z funkce InvRef:get_list()) +a vrátí počet prázdných dávek v něm (může být 0). +]] +function ch_core.count_empty_stacks(stacks) + local count = 0 + for _, stack in ipairs(stacks) do + if stack:is_empty() then + count = count + 1 + end + end + return count +end + +--[[ +Serializuje pole dávek do řetězce. +Vrací tabulku: +{ + success = bool, -- true v případě úspěchu, false v případě selhání + + -- v případě úspěchu: + result = string, -- výsledný řetězec (pro prázdný inventář je to prázdný řetězec) + lengths = {int, ...}, -- délky itemstringů jednotlivých stacků + orig_result_length = int, -- délka výsledného řetězce před kompresí + + -- v případě selhání: + reason = "single_stack_limit" or "disallow_nested", -- typ selhání + overlimit_index = int or nil, -- v případě selhání "single_stack_limit" index dávky, která překročila limit + overlimit_length = int or nil, -- v případě selhání "single_stack_limit" délku řetězce vráceného to_string() + nested_index = int or nil, -- v případě selhání "disallow_nested" index (první) dávky, která obsahuje vnořený inventář +} +]] +function ch_core.serialize_stacklist(stacks, single_stack_limit, disallow_nested) + if single_stack_limit == nil then + single_stack_limit = 65535 + end + local data = {} + local lengths = {} + for i, stack in ipairs(stacks) do + if stack:is_empty(stack) then + data[i] = "" + lengths[i] = 0 + else + if disallow_nested then + -- does not contain a nested inventory? + local itemdef = minetest.registered_items[stack:get_name()] + if itemdef ~= nil and itemdef._ch_nested_inventory_meta ~= nil and stack:get_meta():get_string(itemdef._ch_nested_inventory_meta) ~= "" then + minetest.log("action", "Stacklist not serialized because of a nested inventory in "..stack:get_name()..".") + return { + success = false, + reason = "disallow_nested", + nested_index = i, + } + end + end + local s = stack:to_string() + if #s > single_stack_limit then + minetest.log("action", "Stacklist not serialized because of a single stack limit: "..#s.." > "..single_stack_limit..".") + return { + success = false, + reason = "single_stack_limit", + overlimit_index = i, + overlimit_length = #s, + } + end + data[i] = s + lengths[i] = #s + end + end + local orig_str = minetest.serialize(data) + local str = minetest.encode_base64(minetest.compress(orig_str, "deflate")) + minetest.log("action", "Stacklist serialized, resulting length = "..#str..".") + return { + success = true, + result = str, + lengths = lengths, + orig_result_length = #orig_str, + } +end + +--[[ +Deserializuje řetězec serializovaný pomocí funkce ch_core.serialize_stacklist(). +V případě neúspěchu vrátí nil. +]] +function ch_core.deserialize_stacklist(str) + local result = minetest.deserialize(minetest.decompress(minetest.decode_base64(str), "deflate")) + if result ~= nil then + for i = 1, #result do + result[i] = ItemStack(result[i]) + end + end + return result +end + +--[[ +Spočítá a / b a vrátí celočíselný výsledek a zbytek. +]] +function ch_core.divrem(a, b) + local div = math.floor(a / b) + local rem = a % b + return div, rem +end + +--[[ + Vrátí funkci, která přijme jako první parametr název souboru (či cestu) a pokusí + se ho načíst a spustit jako funkci Lua a vrátit výsledky. + args = table || nil, -- pole parametrů, které mají být předávány volanému souboru, + pokud nejsou žádné parametry specifikovány v rámci volání; žádný z parametrů nesmí být nil! + options = nil || { + path = string || bool || nil, + -- je-li nil nebo true, funkce se pokusí první parametr doplnit o cestu módu, který byl načítán v momentě její konstrukce + -- je-li false, funkce se pokusí použít cestu tak, jak je + -- je-li string, daný řetězec se připojí před zadaný parametr a oddělí "/" + nofail = bool || nil, -- je-li true, funkce bude tiše ignorovat selhání při načítání souboru + } +]] +function ch_core.compile_dofile(args, options) + if args == nil then args = {} end + if options == nil then options = {} end + local modname = minetest.get_current_modname() + local modpath = modname and minetest.get_modpath(modname) + local result = function(name, ...) + local filepath + assert(name) + if options.path == nil or options.path == true then + if modpath == nil then + error("no mod is loading now!") + end + filepath = modpath.."/"..name + elseif options.path == false then + filepath = name + else + filepath = options.path.."/"..name + end + local largs = {...} + if #largs == 0 then + largs = args + end + local f, errmsg = loadfile(filepath) + if f ~= nil then + return f(unpack(largs)) + elseif options.nofail == true then + return + else + error("dofile("..filepath..") failed!: "..(errmsg or "nil")) + end + end + return result +end + +function ch_core.extend_player_inventory(player_name, extend) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo == nil then + minetest.log("error", "ch_core.extend_player_inventory() called on player "..player_name.." that has no offline_charinfo!") + return false + end + local player = minetest.get_player_by_name(player_name) + local target_size + if extend then + -- extend the inventory + target_size = ch_core.inventory_size.extended + if player ~= nil then + local inv = player:get_inventory() + local inv_size = inv:get_size("main") + if inv_size < target_size then + inv:set_size("main", target_size) + minetest.log("action", "Player inventory "..player_name.."/main was extended from "..inv_size.." slots to "..target_size..".") + end + end + if offline_charinfo.extended_inventory ~= 1 then + offline_charinfo.extended_inventory = 1 + ch_data.save_offline_charinfo(player_name) + end + + elseif not extend and offline_charinfo.extended_inventory == 1 then + -- shrink the inventory + target_size = ch_core.inventory_size.normal + if player ~= nil then + local inv = player:get_inventory() + local inv_size = inv:get_size("main") + if inv_size < target_size then + inv:set_size("main", target_size) + minetest.log("action", "Player inventory "..player_name.."/main was extended from "..inv_size.." slots to "..target_size..".") + elseif inv_size > target_size then + local current_items = inv:get_list("main") + local overflown_stacks = {} + inv:set_size("main", target_size) + for i = target_size + 1, inv_size do + local stack = current_items[i] + if not stack:is_empty() then + stack = inv:add_item("main", stack) + if not stack:is_empty() then + table.insert(overflown_stacks, stack) + end + end + end + minetest.log("action", "Player inventory "..player_name.."/main was shrinked from "..inv_size.." slots to "..target_size..". "..#overflown_stacks.." overflown stacks.") + if #overflown_stacks > 0 then + local player_pos = assert(player:get_pos()) + for _, stack in ipairs(overflown_stacks) do + if core.add_item(player_pos, stack) == nil then + minetest.log("error", "Spawning overflown item "..stack:to_string().." failed! The item is lost.") + end + end + end + end + end + if offline_charinfo.extended_inventory == 1 then + offline_charinfo.extended_inventory = 0 + ch_data.save_offline_charinfo(player_name) + end + else + -- no change + return + end +end + +--[[ +Pro zadanou hodnotu facedir vrátí rotační vektor symbolizující rotaci +z výchozího otočení (facedir = 0) do otočení cílového. +]] +function ch_core.facedir_to_rotation(facedir) + return vector.copy(facedir_to_rotation_data[facedir % 24]) +end + +--[[ +V zadaném textu odzvláštní všechny znaky, které mají speciální význam +uvnitř prvku hypetext[] ve formspecu. Tato funkce již v sobě zahrnuje +funkci minetest.formspec_escape, takže její obsah by už měl být doslovně +vložen do formspecu bez dalšího zpracování. +]] +function ch_core.formspec_hypertext_escape(text) + local result = string.gsub(text, "[][><\\,;]", hypertext_escape_replacements) + return result +end + +--[[ +Vrátí seznam všech známých hráčských postav (včetně těch, které nejsou ve hře). +Ke každé postavě vrátí strukturu {prihlasovaci, zobrazovací}. +Seznam je seřazený podle zobrazovacího jména postavy. +-- as_map - je-li true, vrátí seznam (neseřazený) jako mapu z přihlašovacího jména postavy +-- include_privs - je-li true, každý záznam bude navíc obsahovat položky 'role' a 'privs'; + tuto variantu nelze volat z inicializačního kódu +]] +function ch_core.get_all_players(as_map, include_privs) + if include_privs and not minetest.get_gametime() then + error("ch_core.get_all_players(): include_privs == true is allowed only when the game is running!") + end + local list, map = {}, {} + for prihlasovaci, _ in pairs(ch_data.offline_charinfo) do + local exists = (not include_privs) or minetest.player_exists(prihlasovaci) + if exists then + local record = { + prihlasovaci = prihlasovaci, + zobrazovaci = ch_core.prihlasovaci_na_zobrazovaci(prihlasovaci), + } + if include_privs then + record.privs = minetest.get_player_privs(prihlasovaci) + record.role = get_player_role_by_privs(record.privs) + end + table.insert(list, record) + map[prihlasovaci] = record + end + end + if as_map then + return map + end + table.sort(list, function(a, b) + return ch_core.utf8_mensi_nez(a.zobrazovaci, b.zobrazovaci, true) + end) + return list +end + +--[[ +Vrátí seznam všech/jen registrovaných postav, seřazený podle času posledního přihlášení, +od nejčerstvějšího po nejstarší. + +registered_only - je-li true, počítá pouze registrované postavy +name_to_skip - je-li nastaveno, postava s daným přihl. jménem se nepočítá + +Výsledkem je seznam struktur ve formátu: + { + player_name = STRING, -- přihl. jméno postavy + last_login_before = INT, -- před kolika (kalendářními) dny se postava přihlásila; -1, není-li k dispozici + played_hours_total = FLOAT, -- hodiny ve hře + played_hours_actively = FLOAT, -- aktivně odehrané hodiny ve hře + is_in_game = BOOL, -- je postava aktuálně ve hře? + pending_registration_type = STRING or nil, + is_registered = BOOL, + } +]] +function ch_core.get_last_logins(registered_only, names_to_skip) + local new_players = {} -- new players + local reg_players = {} -- registered players + local shifted_eod = os.time() - 946684800 -- EOD = end of day + shifted_eod = shifted_eod + 86400 - (shifted_eod % 86400) + + if names_to_skip == nil then + names_to_skip = {} + elseif type(names_to_skip) == "string" then + names_to_skip = {[names_to_skip] = true} + elseif type(names_to_skip) ~= "table" then + error("names_to_skip: invalid type of argument!") + end + + for other_player_name, _ in pairs(ch_data.offline_charinfo) do + if not names_to_skip[other_player_name] then + if minetest.check_player_privs(other_player_name, "ch_registered_player") then + table.insert(reg_players, other_player_name) + elseif not registered_only then + table.insert(new_players, other_player_name) + end + end + end + if registered_only then + new_players = reg_players + table.sort(new_players, cmp_oci) + else + table.sort(new_players, cmp_oci) + table.sort(reg_players, cmp_oci) + table.insert_all(new_players, reg_players) + end + local result = {} + for i = #new_players, 1, -1 do + local other_player_name = new_players[i] + local offline_charinfo = ch_data.offline_charinfo[other_player_name] + local info = { + player_name = other_player_name + } + local last_login = offline_charinfo.last_login + if last_login == 0 then + info.last_login_before = -1 + else + info.last_login_before = math.floor((shifted_eod - last_login) / 86400) + end + info.played_hours_total = math.round(offline_charinfo.past_playtime / 36) / 100 + info.played_hours_actively = math.round(offline_charinfo.past_ap_playtime / 36) / 100 + info.is_in_game = ch_data.online_charinfo[other_player_name] ~= nil + if (offline_charinfo.pending_registration_type or "") ~= "" then + info.pending_registration_type = offline_charinfo.pending_registration_type or "" + end + if minetest.check_player_privs(other_player_name, "ch_registered_player") then + info.is_registered = true + else + info.is_registered = false + end + table.insert(result, info) + end + return result +end + +--[[ +Najde hráčskou postavu nejbližší k dané pozici. Parametr player_name_to_ignore +je volitelný; je-li vyplněn, má obsahovat přihlašovací jméno postavy +k ignorování. + +Vrací „player“ a „player:get_pos()“; v případě neúspěchu vrací nil. +]] +local get_connected_players = minetest.get_connected_players +function ch_core.get_nearest_player(pos, player_name_to_ignore) + local result_player, result_pos, result_distance_2 = 1e+20 + for player_name, player in pairs(get_connected_players()) do + if player_name ~= player_name_to_ignore then + local player_pos = player:get_pos() + local x, y, z = player_pos.x - pos.x, player_pos.y - pos.y, player_pos.z - pos.z + local distance_2 = x * x + y * y + z * z + if distance_2 < result_distance_2 then + result_distance_2 = distance_2 + result_player = player + result_pos = player_pos + end + end + end + return result_player, result_pos +end + +--[[ +Vrátí t[k]. Pokud je to nil, přiřadí tam prázdnou tabulku {} a vrátí tu. +]] +function ch_core.get_or_add(t, k) + local result = t[k] + if result == nil then + result = {} + t[k] = result + end + return result +end + +--[[ +Načte z metadat pozici uloženou pomocí ch_core.set_pos_to_meta(). +Není-li tam taková uložena, vrátí vector.zero(). +]] +function ch_core.get_pos_from_meta(meta, key) + return vector.new(meta:get_float(key.."_x"), meta:get_float(key.."_y"), meta:get_float(key.."_z")) +end + +--[[ +Určí podle práv typ postavy (admin|creative|new|survival). +Pokud player_or_player_name není PlayerRef nebo jméno postavy, vrátí nil. +]] +function ch_core.get_player_role(player_or_player_name) + local result = ch_core.normalize_player(player_or_player_name).role + if result ~= "none" then + return result + else + return nil + end +end + +--[[ + Vrátí seznam hráčských postav (ObjectRef) ve vymezené oblasti. + Jde o bezprostřední náhradu za core.get_objects_inside_radius(). +]] +function ch_core.get_players_inside_radius(center, radius) + local result = {} + for _, player in ipairs(core.get_connected_players()) do + if vector.distance(center, player:get_pos()) <= radius then + table.insert(result, player) + end + end + return result +end + +--[[ +Vygeneruje šablonu pro stránku formspecu pro unified_inventory. +id -- string, required -- rozlišující textové ID pro připojení za prvky ch_scrollbar[12]_ +player_viewname -- string, optional -- jméno postavy pro zobrazení v záhlaví (může být barevné) +title -- string, optional -- nadpis pro zobrazení v záhlaví +scrollbars -- table, required -- definuje maxima pro posuvníky oblastí a současně také rozložení oblastí; + tato verze podporuje jen rozložení {left = ..., right = ...} a {top = ..., bottom = ...} +perplayer_formspec -- odpovídající parametr z rozhraní unified_inventory; definuje rozložení formuláře +online_charinfo -- table, optional -- je-li zadáno, stavy posuvníků se nastaví podle stejně pojmenovaných polí v dané tabulce, budou-li přítomna + +Výstup má formát: + { + fs_begin, fs_middle, fs_end -- string; řetězce pro použití jako formspec; vlastní obsah vložte kolem fs_middle + form1, form2 = {x, y, w, h, key, scrollbar_max} -- udává pozice a velikost podformulářů v okně unified_inventory; + v praxi jsou podstatné především 'w' a 'h' (šířka a výška podoblasti) + } +]] +function ch_core.get_ui_form_template(id, player_viewname, title, scrollbars, perplayer_formspec, online_charinfo) + local fs = assert(perplayer_formspec) + local fs_begin, fs_middle, fs_end = {fs.standard_inv_bg}, {}, {} + local form1, form2 = {}, {} + local sbar_width = 0.5 + local style + + if scrollbars.left ~= nil and scrollbars.right ~= nil then + style = "left_right" + elseif scrollbars.top ~= nil and scrollbars.bottom ~= nil then + style = "top_bottom" + else + error("Unsupported UI formspec style!") + end + + if style == "left_right" then + form1.x = fs.std_inv_x + form1.y = fs.form_header_y + 0.5 + form1.w = 10.0 + form1.h = fs.std_inv_y - fs.form_header_y - 1.25 + form1.key = "left" + form1.scrollbar_max = scrollbars.left + + form2.x = fs.page_x - 0.25 + form2.y = 0.5 + form2.w = fs.pagecols - 1 + form2.h = fs.pagerows - 1 + fs.page_y + form2.key = "right" + form2.scrollbar_max = scrollbars.right + elseif style == "top_bottom" then + form1.x = fs.std_inv_x + form1.y = fs.form_header_y + 0.5 + form1.w = 17.25 + form1.h = fs.std_inv_y - fs.form_header_y - 1.25 + form1.key = "top" + form1.scrollbar_max = scrollbars.top + + form2.x = fs.page_x - 0.25 + form2.y = fs.std_inv_y - 0.5 + form2.w = fs.pagecols - 1 + form2.h = 5.5 + 0.5 + form2.scrollbar_max = scrollbars.bottom + else + error("not implemented yet") + end + + if title ~= nil then + table.insert(fs_begin, "label["..(form1.x + 0.05)..","..(form1.y - 0.3)..";") + if player_viewname ~= nil then + table.insert(fs_begin, minetest.formspec_escape(ch_core.colors.light_green..player_viewname..ch_core.colors.white.." — ")) + end + table.insert(fs_begin, minetest.formspec_escape(title)) + table.insert(fs_begin, "]") + end + + if (form1.scrollbar_max or 0) > 0 then + table.insert(fs_begin, "scroll_container["..form1.x..","..form1.y..";"..form1.w..","..form1.h..";ch_scrollbar1_"..id..";vertical]") + -- CONTENT will be inserted here + table.insert(fs_middle, "scroll_container_end[]") + -- insert a scrollbar + if (form1.scrollbar_max or 0) > 0 then + local scrollbar_state = online_charinfo ~= nil and online_charinfo["ch_scrollbar1_"..id] + if scrollbar_state ~= nil then + scrollbar_state = tostring(scrollbar_state) + else + scrollbar_state = "" + end + table.insert(fs_middle, + "scrollbaroptions[max="..form1.scrollbar_max..";arrows=show]".. + "scrollbar["..(form1.x + form1.w - sbar_width)..","..form1.y..";"..sbar_width..","..form1.h..";vertical;ch_scrollbar1_"..id..";".. + scrollbar_state.."]") + end + else + table.insert(fs_begin, "container["..form1.x..","..form1.y.."]") + -- CONTENT will be inserted here + table.insert(fs_middle, "container_end[]") + end + + if (form2.scrollbar_max or 0) > 0 then + table.insert(fs_middle, "scroll_container["..form2.x..","..form2.y..";"..form2.w..","..form2.h..";ch_scrollbar2_"..id..";vertical]") + -- CONTENT will be inserted here + table.insert(fs_end, "scroll_container_end[]") + -- insert a scrollbar + if (form2.scrollbar_max or 0) > 0 then + local scrollbar_state = online_charinfo ~= nil and online_charinfo["ch_scrollbar2_"..id] + if scrollbar_state ~= nil then + scrollbar_state = tostring(scrollbar_state) + else + scrollbar_state = "" + end + table.insert(fs_end, + "scrollbaroptions[max="..form2.scrollbar_max..";arrows=show]".. + "scrollbar["..(form2.x + form2.w - sbar_width)..","..form2.y..";"..sbar_width..","..form2.h..";vertical;ch_scrollbar2_"..id..";".. + scrollbar_state.."]") + end + else + table.insert(fs_middle, "container["..form2.x..","..form2.y.."]") + -- CONTENT will be inserted here + table.insert(fs_end, "container_end[]") + end + + return { + fs_begin = table.concat(fs_begin), + fs_middle = table.concat(fs_middle), + fs_end = table.concat(fs_end), + form1 = form1, + form2 = form2, + } +end + +--[[ + Vrátí t[k1]. Pokud je to nil, vyplní t[k1] = {} a vrátí přiřazenou tabulku. +]] +function ch_core.goa1(t, k) + local r = t[k] + if r ~= nil then return r end + r = {} + t[k] = r + return r +end + +--[[ + Vrátí t[k1][k2]. Pokud některá z položek chybí, vyplní ji novou prázdnou tabulkou. +]] +function ch_core.goa2(t, k1, k2) + local r1 = t[k1] + if r1 == nil then + r1 = {} + t[k1] = {[k2] = r1} + return r1 + end + local r2 = r1[k2] + if r2 == nil then + r2 = {} + r1[k2] = r2 + return r2 + end + return r2 +end +local goa2 = ch_core.goa2 + +--[[ + Vrátí t[k1][k2][k3]. Pokud některá z položek chybí, vyplní ji novou prázdnou tabulkou. +]] +function ch_core.goa3(t, k1, k2, k3) + local r = t[k1] + if r ~= nil then + return goa2(r, k2, k3) + else + r = {} + t[k1] = {[k2] = {[k3] = r}} + return r + end +end + +--[[ + Vrátí t[k1][k2][k3][k4]. Pokud některá z položek chybí, vyplní ji novou prázdnou tabulkou. +]] +function ch_core.goa4(t, k1, k2, k3, k4) + return goa2(goa2(t, k1, k2), k3, k4) +end + +--[[ +Jednoduchá funkce, která vyhodnotí condition jako podmínku +a podle výsledku vrátí buď true_result, nebo false_result. +]] +function ch_core.ifthenelse(condition, true_result, false_result) + if condition then + return true_result + else + return false_result + end +end + +--[[ + Vrací funkci function(itemstack, user, pointed_thing), + která zavolá do_item_eat() podle hodnoty skupiny ch_food, + drink nebo ch_poison u daného předmětu. Není-li předmět v těchto + skupinách, vrátí nil. +]] +local item_eat_cache = {} +function ch_core.item_eat(replace_with_item) + if replace_with_item == nil then + replace_with_item = "" + elseif type(replace_with_item) ~= "string" then + error("replace_with_item must be string or nil") + end + local result = item_eat_cache[replace_with_item] + if result == nil then + result = function(itemstack, user, pointed_thing) + local name = itemstack:get_name() + if name == nil or name == "" or minetest.registered_items[name] == nil then return end + local food = minetest.get_item_group(name, "ch_food") + local drink = minetest.get_item_group(name, "drink") + local poison = minetest.get_item_group(name, "ch_poison") + local health + if poison ~= 0 then + local normal = math.max(food, drink) + if normal ~= 0 and math.random(5) ~= 3 then + health = normal + else + health = -poison + end + else + health = math.max(food, drink) + if health <= 0 then + return + end + end + return minetest.do_item_eat(health, replace_with_item, itemstack, user, pointed_thing) + end + item_eat_cache[replace_with_item] = result + end + return result +end + +--[[ + Vytvoří pomocnou strukturu pro položku dropdown[] ve formspecu. + Pomocná struktura obsahuje položky: + - function get_index_from_value(value, default_index) + - function get_value_from_index(index, default_index) + - table index_to_value // původní předaná tabulka (musí být sekvence) + - string formspec_list // již odzvláštněný seznam k použití ve formspecu + - table value_to_index // mapuje text položky na první odpovídající index +]] +function ch_core.make_dropdown(index_to_value) + local F = minetest.formspec_escape + local escaped_values = {} + local value_to_index = {} + for i, value in ipairs(index_to_value) do + escaped_values[i] = F(value) + end + for i = #index_to_value, 1, -1 do + value_to_index[index_to_value[i]] = i + end + return { + get_index_from_value = function(value, default_index) + if value ~= nil then + return value_to_index[value] or tonumber(default_index) + else + return tonumber(default_index) + end + end, + get_value_from_index = function(index, default_index) + index = tonumber(index) + if index ~= nil and index_to_value[index] ~= nil then + return index_to_value[index] + else + return index_to_value[default_index] + end + end, + index_to_value = index_to_value, + formspec_list = table.concat(escaped_values, ","), + value_to_index = value_to_index, + } +end + +--[[ +Přijme parametr, kterým může být: + a) přihlašovací jméno postavy (bez ohledu na diakritiku) + b) zobrazovací jméno postavy + c) objekt postavy + d) tabulka s volatelnou funkcí get_player_name() +Ve všech případech vrátí tabulku s prvky: +{ + role, -- role postavy nebo "none", pokud neexistuje + player_name, -- skutečné přihlašovací jméno existující postavy, nebo "", pokud postava neexistuje; nikdy nebude nil + viewname, -- zobrazovací jméno postavy (bez barev), nebo "", pokud postava neexistuje; nikdy nebude nil + player, -- je-li postava ve hře, PlayerRef, jinak nil + privs, -- tabulka práv postavy; pokud neexistuje, {}; nikdy nebude nil +} +]] +function ch_core.normalize_player(player_name_or_player) + local arg_type = type(player_name_or_player) + local player_name, player + if arg_type == "string" then + player_name = player_name_or_player + elseif arg_type == "number" then + player_name = tostring(player_name_or_player) + elseif (arg_type == "table" or arg_type == "userdata") and type(player_name_or_player.get_player_name) == "function" then + player_name = player_name_or_player:get_player_name() + if type(player_name) ~= "string" then + player_name = "" + else + if minetest.is_player(player_name_or_player) then + player = player_name_or_player + end + end + else + player_name = "" + end + player_name = ch_core.jmeno_na_prihlasovaci(player_name) + local correct_name = ch_data.correct_player_name_casing(player_name) + if correct_name ~= nil then + player_name = correct_name + end + if player_name ~= "" and not minetest.player_exists(player_name) then + player_name = ch_core.jmeno_na_prihlasovaci(player_name) + if not minetest.player_exists(player_name) then + player_name = "" + end + end + if player_name == "" then + return {role = "none", player_name = "", viewname = "", privs = {}} + end + local privs = minetest.get_player_privs(player_name) + return { + role = get_player_role_by_privs(privs), + player_name = player_name, + viewname = ch_core.prihlasovaci_na_zobrazovaci(player_name), + privs = privs, + player = player or minetest.get_player_by_name(player_name), + } +end + +--[[ +Vytvoří kopii vstupu (input) a zapíše do ní nové hodnoty skupin podle +parametru override. Skupiny s hodnotou 0 v override z tabulky odstraní. +Je-li některý z parametrů nil, je interpretován jako prázdná tabulka. + +ZASTARALÁ: použijte raději ch_core.assembly_groups(). +]] +function ch_core.override_groups(input, override) + return ch_core.assembly_groups(input, override) +end + +--[[ +Převede zobrazovací nebo přihlašovací jméno na přihlašovací jméno, +bez ohledu na to, zda takové jméno existuje. +]] +function ch_core.jmeno_na_prihlasovaci(jmeno) + return ch_core.odstranit_diakritiku(jmeno):gsub(" ", "_") +end + +--[[ + Vrátí existující přihlašovací jméno postavy odpovídající uvedenému + jménu až na velikost písmen a diakritiku (+ konverzi ' ' na '_'), nebo nil, pokud + taková postava neexistuje. +]] +function ch_core.jmeno_na_existujici_prihlasovaci(jmeno) + if jmeno == nil then return nil end + local result = ch_core.odstranit_diakritiku(jmeno):gsub(" ", "_") + result = ch_data.correct_player_name_casing(result) + if result and ch_data.offline_charinfo[result] then + return result + else + return nil + end +end + +--[[ +Převede všechna písmena v řetězci na malá, funguje i na písmena s diakritikou. +]] +function ch_core.na_mala_pismena(s) + local l = #s + local i = 1 + local res = "" + local c + while i <= l do + c = diakritika_na_mala[s:sub(i, i + 1)] + if c then + res = res .. c + i = i + 2 + else + res = res .. s:sub(i, i) + i = i + 1 + end + end + return string.lower(res) +end + +--[[ +Převede všechna písmena v řetězci na velká, funguje i na písmena s diakritikou. +]] +function ch_core.na_velka_pismena(s) + local l = #s + local i = 1 + local res = "" + local c + while i <= l do + c = diakritika_na_velka[s:sub(i, i + 1)] + if c then + res = res .. c + i = i + 2 + else + res = res .. s:sub(i, i) + i = i + 1 + end + end + return string.upper(res) +end + +--[[ +Vrátí počet bloků uvnitř oblasti vymezené dvěma krajními body (na pořadí nezáleží). +Výsledkem je vždy kladné celé číslo. +]] +function ch_core.objem_oblasti(pos1, pos2) + return math.ceil(math.abs(pos1.x - pos2.x) + 1) * math.ceil(math.abs(pos1.y - pos2.y) + 1) * math.ceil(math.abs(pos1.z - pos2.z) + 1) +end + +--[[ +Všechna písmena s diakritikou převede na odpovídající písmena bez diakritiky. +Ostatní znaky ponechá. +]] +function ch_core.odstranit_diakritiku(s) + local l = #s + local i = 1 + local res = "" + local c + while i <= l do + c = diakritika[s:sub(i, i + 1)] + if c then + res = res .. c + i = i + 2 + else + res = res .. s:sub(i, i) + i = i + 1 + end + end + return res +end + +--[[ +Pokud je zadaný klient ve hře (musí jít o přihlašovací jméno), přehraje mu zvuk kliknutí. +Parametr může být nil; v takovém případě neudělá nic. +]] +function ch_core.play_click_sound_to(player_name) + if player_name ~= nil then + minetest.sound_play(click_sound, {to_player = player_name}, true) + end +end + +--[[ +K zadané roli postavy vrátí odpovídající ikonu. +]] +function ch_core.player_role_to_image(player_role, has_creative_priv, image_height) + if image_height ~= nil and image_height ~= 32 and image_height ~= 16 then + error("ch_core.player_role_to_image(): image height "..image_height.." is unsupported!") + end + local result = assert(player_role_to_image[player_role] or player_role_to_image["none"]) + if player_role ~= "new" and has_creative_priv then + result = "[combine:48x32:0,0="..result..":16,0="..player_role_to_image.creative + if image_height == 16 then + result = result.."^[resize:24x16" + end + elseif image_height == 16 then + result = result.."^[resize:16x16" + end + return result +end + +--[[ +Otestuje, zda pozice „pos“ leží uvnitř oblasti vymezené v_min a v_max. +]] +function ch_core.pos_in_area(pos, v_min, v_max) + return v_min.x <= pos.x and pos.x <= v_max.x and + v_min.y <= pos.y and pos.y <= v_max.y and + v_min.z <= pos.z and pos.z <= v_max.z +end + +--[[ +vrátí dva vektory: první s minimálními souřadnicemi a druhý s maximálními, +obojí zaokrouhlené na celočíselné souřadnice +]] +function ch_core.positions_to_area(v1, v2) + local x1, x2, y1, y2, z1, z2 + + if v1.x <= v2.x then + x1 = v1.x + x2 = v2.x + else + x1 = v2.x + x2 = v1.x + end + + if v1.y <= v2.y then + y1 = v1.y + y2 = v2.y + else + y1 = v2.y + y2 = v1.y + end + + if v1.z <= v2.z then + z1 = v1.z + z2 = v2.z + else + z1 = v2.z + z2 = v1.z + end + + return vector.round(vector.new(x1, y1, z1)), vector.round(vector.new(x2, y2, z2)) +end + +--[[ +Pokud dané přihlašovací jméno existuje, převede ho na jméno bez barev (výchozí) +nebo s barvami. Pro neexistující jména vrací zadaný řetězec. +]] +function ch_core.prihlasovaci_na_zobrazovaci(prihlasovaci, s_barvami) + local offline_info, jmeno + if not prihlasovaci then + error("ch_core.prihlasovaci_na_zobrazovaci() called with bad arguments!") + end + if minetest.player_exists(prihlasovaci) then + offline_info = ch_data.offline_charinfo[prihlasovaci] or {} + if s_barvami then + jmeno = offline_info.barevne_jmeno + if jmeno then return jmeno end + end + jmeno = offline_info.jmeno + if jmeno then return jmeno end + end + return prihlasovaci +end + +--[[ +Zaregistruje bloky, které mají něco společného. +]] +function ch_core.register_nodes(common_def, nodes, crafts) + if type(common_def) ~= "table" then + error("common_def must be a table!") + end + if type(nodes) ~= "table" then + error("nodes must be a table!") + end + if crafts ~= nil and type(crafts) ~= "table" then + error("crafts must be a table or nil!") + end + + for node_name, node_def in pairs(nodes) do + local def = table.copy(common_def) + for k, v in pairs(node_def) do + def[k] = v + end + minetest.register_node(node_name, def) + end + + if crafts ~= nil then + for _, def in ipairs(crafts) do + minetest.register_craft(def) + end + end +end + +--[[ +Smaže data týkající se dané postavy. +]] +function ch_core.remove_player(player_name, options) + if options == nil then + options = {} + end + player_name = ch_core.jmeno_na_prihlasovaci(player_name) + if not minetest.player_exists(player_name) then + return false, player_name.." neexistuje!" + end + local results = {player_name.." odstraněn/a:"} + -- remove player data + if options.player_data ~= false then + if minetest.remove_player(player_name) == 0 then + table.insert(results, "player_data") + end + end + -- remove offline charinfo + local f = ch_core.delete_offline_charinfo + if options.offline_charinfo ~= false and f ~= nil then + if f(player_name) then + table.insert(results, "offline_charinfo") + end + end + -- remove auth data + if options.player_auth ~= false then + if minetest.remove_player_auth(player_name) then + table.insert(results, "player_auth") + end + end + return true, table.concat(results, " ") +end + +--[[ +Otočí axis-aligned bounding box o zadané otočení. Nejde-li o pravoúhlé +otočení, výsledný kvádr bude větší. <zatím nezkoušeno> +]] +function ch_core.rotate_aabb(aabb, r) + local points = {} + for _, x in ipairs({r[1], r[4]}) do + for _, y in ipairs({r[2], r[5]}) do + for _, z in ipairs({r[3], r[6]}) do + table.insert(points, vector.rotate(vector.new(x, y, z), r)) + end + end + end + local p = points[1] + local result = {p.x, p.y, p.z, p.x, p.y, p.z} + for i = 2, 8 do + p = points[i] + if p.x < result[1] then + result[1] = p.x + elseif p.x > result[4] then + result[4] = p.x + end + if p.y < result[2] then + result[2] = p.y + elseif p.y > result[5] then + result[5] = p.y + end + if p.z < result[3] then + result[3] = p.z + elseif p.z > result[6] then + result[6] = p.z + end + end + return result +end + +--[[ + Otočí axis-aligned bounding box takovým způsobem, jako zadaná facedir-hodnota 0 až 23 + otočí blok s paramtype2 == "facedir". Vrátí nový aabb. V případě selhání vrátí nil. +]] +function ch_core.rotate_aabb_by_facedir(aabb, facedir) + if 0 <= facedir and facedir < 24 and facedir_to_rotation_data[facedir] then + local a = vector.new(aabb[1], aabb[2], aabb[3]) + local b = vector.new(aabb[4], aabb[5], aabb[6]) + local r = facedir_to_rotation_data[facedir] + a = vector.rotate(a, r) + b = vector.rotate(b, r) + return { + math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z), + math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z), + } + else + return nil + end +end + +--[[ +Provede operaci t[k1][k2]... s tím, že pokud je kterýkoliv z parametrů nil +nebo na nil po cestě narazí, vrátí nil. Číslo v názvu udává celkový počet +parametrů (včetně t) a musí být v rozsahu 2 až 7 včetně. +]] +function ch_core.safe_get_2(t, k1) + if t and k1 ~= nil then return t[k1] end + return nil +end +function ch_core.safe_get_3(t, k1, k2) + local result + if t and k1 ~= nil and k2 ~= nil then + result = t[k1] + if result ~= nil then + return result[k2] + end + end + return nil +end +function ch_core.safe_get_4(t, k1, k2, k3) + local result + if t and k1 ~= nil and k2 ~= nil and k3 ~= nil then + result = t[k1] + if result ~= nil then + result = result[k2] + if result ~= nil then + return result[k3] + end + end + end + return nil +end +function ch_core.safe_get_5(t, k1, k2, k3, k4) + local result + if t and k1 ~= nil and k2 ~= nil and k3 ~= nil and k4 ~= nil then + result = t[k1] + if result ~= nil then + result = result[k2] + if result ~= nil then + result = result[k3] + if result ~= nil then + return result[k4] + end + end + end + end + return nil +end +function ch_core.safe_get_6(t, k1, k2, k3, k4, k5) + local result + if t and k1 ~= nil and k2 ~= nil and k3 ~= nil and k4 ~= nil and k5 ~= nil then + result = t[k1] + if result ~= nil then + result = result[k2] + if result ~= nil then + result = result[k3] + if result ~= nil then + result = result[k4] + if result ~= nil then + return result[k5] + end + end + end + end + end + return nil +end +function ch_core.safe_get_7(t, k1, k2, k3, k4, k5, k6) + local result + if t and k1 ~= nil and k2 ~= nil and k3 ~= nil and k4 ~= nil and k5 ~= nil and k6 ~= nil then + result = t[k1] + if result ~= nil then + result = result[k2] + if result ~= nil then + result = result[k3] + if result ~= nil then + result = result[k4] + if result ~= nil then + result = result[k5] + if result ~= nil then + return result[k6] + end + end + end + end + end + end + return nil +end + +--[[ +Provede operaci t[k1][k2]... s tím, že pokud je kterýkoliv z parametrů nil +nebo na nil po cestě narazí, vrátí nil. Číslo v názvu udává celkový počet +parametrů (včetně t) a musí být v rozsahu 2 až 7 včetně. +V této verzi knihovny neprovádí kontrolu, zda je t indexovatelné. +]] +function ch_core.safe_get_2(t, k1) + if t and k1 ~= nil then return t[k1] end + return nil +end +function ch_core.safe_get_3(t, k1, k2) + local result + if t and k1 ~= nil and k2 ~= nil then + result = t[k1] + if result ~= nil then + return result[k2] + end + end + return nil +end +function ch_core.safe_get_4(t, k1, k2, k3) + local result + if t and k1 ~= nil and k2 ~= nil and k3 ~= nil then + result = t[k1] + if result ~= nil then + result = result[k2] + if result ~= nil then + return result[k3] + end + end + end + return nil +end +function ch_core.safe_get_5(t, k1, k2, k3, k4) + if k4 ~= nil then + local result = ch_core.safe_get_4(t, k1, k2, k3) + if result ~= nil then + return result[k4] + end + end + return nil +end +function ch_core.safe_get_6(t, k1, k2, k3, k4, k5) + if k4 ~= nil and k5 ~= nil then + local result = ch_core.safe_get_4(t, k1, k2, k3) + if result ~= nil then + result = result[k4] + if result ~= nil then + return result[k5] + end + end + end + return nil +end +function ch_core.safe_get_7(t, k1, k2, k3, k4, k5, k6) + if k4 ~= nil and k5 ~= nil and k6 ~= nil then + local result = ch_core.safe_get_4(t, k1, k2, k3) + if result ~= nil then + result = result[k4] + if result ~= nil then + result = result[k5] + if result ~= nil then + return result[k6] + end + end + end + end + return nil +end + +--[[ +Provede operaci t[k1][k2]... = v s tím, že pokud je kterýkoliv z parametrů +kromě „v“ nil nebo na nil po cestě narazí, vrátí false. +Pokud přiřazení uspěje, vrátí true. +Číslo v názvu udává celkový počet parametrů (včetně t a v) +a musí být v rozsahu 3 až 8 včetně. +V této verzi knihovny neprovádí kontrolu, zda je t indexovatelné. +]] +function ch_core.safe_set_3(t, k1, v) + if t and k1 ~= nil then + t[k1] = v + return true + end + return false +end +function ch_core.safe_set_4(t, k1, k2, v) + if k2 ~= nil then + t = ch_core.safe_get_2(t, k1) + if t then + t[k2] = v + return true + end + end + return false +end +function ch_core.safe_set_5(t, k1, k2, k3, v) + if k3 ~= nil then + t = ch_core.safe_get_3(t, k1, k2) + if t then + t[k3] = v + return true + end + end + return false +end +function ch_core.safe_set_6(t, k1, k2, k3, k4, v) + if k4 ~= nil then + t = ch_core.safe_get_4(t, k1, k2, k3) + if t then + t[k4] = v + return true + end + end + return false +end +function ch_core.safe_set_7(t, k1, k2, k3, k4, k5, v) + if k5 ~= nil then + t = ch_core.safe_get_5(t, k1, k2, k3, k4) + if t then + t[k5] = v + return true + end + end + return false +end +function ch_core.safe_set_8(t, k1, k2, k3, k4, k5, k6, v) + if k6 ~= nil then + t = ch_core.safe_get_6(t, k1, k2, k3, k4, k5) + if t then + t[k6] = v + return true + end + end + return false +end + +--[[ +Nastaví dané postavě status „immortal“. Používá se pro postavy s právem +usnadnění hry. +]] +function ch_core.set_immortal(player, true_or_false) + if true_or_false then + local properties = player:get_properties() + player:set_armor_groups({immortal = 1}) + player:set_hp(properties.hp_max) + player:set_breath(properties.breath_max) + else + player:set_armor_groups({immortal = 0}) + end + return true +end + +--[[ +Uloží do metadat souřadnice x, y, z včetně desetinné části. +]] +function ch_core.set_pos_to_meta(meta, key, pos) + local x_key, y_key, z_key = key.."_x", key.."_y", key.."_z" + meta:set_float(x_key, pos.x) + meta:set_float(y_key, pos.y) + meta:set_float(z_key, pos.z) + local stored_pos = vector.new(meta:get_float(x_key), meta:get_float(y_key), meta:get_float(z_key)) + if not vector.equals(pos, stored_pos) then + minetest.log("warning", "Position truncated when stored to metadata: "..vector.to_string(pos).." truncated to: "..vector.to_string(stored_pos)) + end + return pos +end + +--[[ +Přesune klíče definující vlastnosti entity do podtabulky initial_properties. +Provádí úpravy přímo v předané tabulce a vrací ji (tzn. nevytváří kopii). +]] +function ch_core.upgrade_entity_properties(entity_def, options) + if options == nil then + options = {} + end + local base_properties = options.base_properties -- základ pro doplnění zcela chybějících vlastností + local in_place = options.in_place ~= false -- provádět změny v původní tabulce initial_properties, je-li dostupná; výchozí: true + local keep_fields = options.keep_fields == true -- ponechat původní pole v původní tabulce; výchozí: false + + local initial_properties + if entity_def.initial_properties == nil then + initial_properties = {} + elseif in_place then + initial_properties = entity_def.initial_properties + else + initial_properties = table.copy(entity_def.initial_properties) + end + for _, k in ipairs(entity_properties_list) do + if entity_def[k] ~= nil then + if initial_properties[k] == nil then + initial_properties[k] = entity_def[k] + end + if not keep_fields then + entity_def[k] = nil + end + end + end + if base_properties ~= nil then + for k, v in pairs(base_properties) do + if initial_properties[k] == nil then + initial_properties[k] = v + end + end + end + entity_def.initial_properties = initial_properties + return entity_def +end + +--[[ +Vrátí počet UTF-8 znaků řetězce. +]] +function ch_core.utf8_length(s) + if s == "" then + return 0 + end + local i, byte, bytes, chars + i = 1 + chars = 0 + bytes = string.len(s) + while i <= bytes do + byte = string.byte(s, i) + if byte < 192 then + i = i + 1 + else + i = i + utf8_charlen[byte] + end + chars = chars + 1 + end + return chars +end + +--[[ +Začne v řetězci `s` na fyzickém indexu `i` a bude se posouvat o `seek` +UTF-8 znaků doprava (pro záporný počet doleva); vrátí výsledný index +(na první bajt znaku), nebo nil, pokud posun přesáhl začátek, +resp. konec řetězce. +]] +function ch_core.utf8_seek(s, i, seek) + local bytes = string.len(s) + if i < 1 or i > bytes then + return nil + end + local b + if seek > 0 then + while true do + b = string.byte(s, i) + if b < 192 then + i = i + 1 + else + i = i + utf8_charlen[b] + end + if i > bytes then + return nil + end + seek = seek - 1 + if seek == 0 then + return i + end + end + elseif seek < 0 then + while true do + i = i - 1 + if i < 1 then + return nil + end + b = string.byte(s, i) + if b < 128 or b >= 192 then + -- máme další znak + seek = seek + 1 + if seek == 0 then + return i + end + end + end + else + return i + end +end + +--[[ + Je-li řetězec s delší než max_chars znaků, vrátí jeho prvních max_chars znaků + + "...", jinak vrátí původní řetězec. +]] +function ch_core.utf8_truncate_right(s, max_chars, dots_string) + local i = ch_core.utf8_seek(s, 1, max_chars) + if i then + return s:sub(1, i - 1) .. (dots_string or "...") + else + return s + end +end + +--[[ +Rozdělí řetězec na pole neprázdných podřetězců o stanovené maximální délce +v UTF-8 znacích; v každé části vynechává mezery na začátku a na konci části; +přednostně dělí v místech mezer. Pro prázdný řetězec +(nebo řetězec tvořený jen mezerami) vrací prázdné pole. +]] +function ch_core.utf8_wrap(s, max_chars, options) + local i = 1 -- index do vstupního řetězce s + local s_bytes = string.len(s) + local result = {} -- výstupní pole + local r_text = "" -- výstupní řetězec + local r_chars = 0 -- počet UTF-8 znaků v řetězci r + local r_sp_begin -- index první mezery v poslední sekvenci mezer v r_text + local r_sp_end -- index poslední mezery v poslední sekvenci mezer v r_text + local b -- kód prvního bajtu aktuálního znaku + local c_bytes -- počet bajtů aktuálního znaku + + -- options + local allow_empty_lines, max_result_lines, line_separator + if options then + allow_empty_lines = options.allow_empty_lines -- true or false + max_result_lines = options.max_result_lines -- nil or number + line_separator = options.line_separator -- nil or string + end + + while i <= s_bytes do + b = string.byte(s, i) + -- print("byte["..i.."] = "..b.." ("..s:sub(i, i)..") r_sp = ("..(r_sp_begin or "nil")..".."..(r_sp_end or "nil")..")") + if r_chars > 0 or (b ~= 32 and (b ~= 10 or allow_empty_lines)) then -- na začátku řádky ignorovat mezery + if b < 192 then + c_bytes = 1 + else + c_bytes = utf8_charlen[b] + end + -- vložit do r další znak (není-li to konec řádky) + if b ~= 10 then + r_text = r_text..s:sub(i, i + c_bytes - 1) + r_chars = r_chars + 1 + + if b == 32 then + -- znak je mezera + if r_sp_begin then + if r_sp_end then + -- začátek nové skupiny mezer (už nějaká byla) + r_sp_begin = string.len(r_text) + r_sp_end = nil + end + elseif not r_sp_end then + -- začátek první skupiny mezer (ještě žádná nebyla) + r_sp_begin = string.len(r_text) + end + else + -- znak není mezera ani konec řádky + if r_sp_begin and not r_sp_end then + r_sp_end = string.len(r_text) - c_bytes -- uzavřít skupinu mezer + end + end + end + + if r_chars >= max_chars or b == 10 then + -- dosažen maximální počet znaků nebo znak \n => uzavřít řádku + if line_separator and #result > 0 then + result[#result] = result[#result]..line_separator + end + if r_chars < max_chars or not r_sp_begin then + -- žádné mezery => tvrdé dělení + table.insert(result, r_text) + r_text = "" + r_chars = 0 + r_sp_begin, r_sp_end = nil, nil + elseif not r_sp_end then + -- průběžná skupina mezer => rozdělit zde + table.insert(result, r_text:sub(1, r_sp_begin - 1)) + r_text = "" + r_chars = 0 + r_sp_begin, r_sp_end = nil, nil + else + -- byla skupina mezer => rozdělit tam + table.insert(result, r_text:sub(1, r_sp_begin - 1)) + r_text = r_text:sub(r_sp_end + 1, -1) + r_chars = ch_core.utf8_length(r_text) + r_sp_begin, r_sp_end = nil, nil + if r_chars > 0 and b == 10 then + i = i - c_bytes -- read this \n-byte again + end + end + if max_result_lines and #result >= max_result_lines then + return result -- skip reading other lines + end + end + i = i + c_bytes + else + i = i + 1 + end + end + if r_chars > 0 then + if line_separator and #result > 0 then + result[#result] = result[#result]..line_separator + end + if r_sp_begin and not r_sp_end then + -- průběžná skupina mezer + table.insert(result, r_text:sub(1, r_sp_begin - 1)) + else + table.insert(result, r_text) + end + end + return result +end +function ch_core.utf8_radici_klic(s, store_to_cache) + local result = utf8_sort_cache[s] + if not result then + local i = 1 + local l = s:len() + local c, k + result = {} + while i <= l do + c = s:sub(i, i) + k = utf8_sort_data_1[c] + if k then + table.insert(result, k) + i = i + 1 + else + k = utf8_sort_data_2[s:sub(i, i + 1)] + if k then + table.insert(result, k) + i = i + 2 + else + k = utf8_sort_data_3[c] + table.insert(result, k or c) + i = i + 1 + end + end + end + result = table.concat(result) + if store_to_cache then + utf8_sort_cache[s] = result + end + end + return result +end + +function ch_core.utf8_mensi_nez(a, b, store_to_cache) + a = ch_core.utf8_radici_klic(a, store_to_cache) + b = ch_core.utf8_radici_klic(b, store_to_cache) + return a < b +end + +--[[ +-- KÓD INICIALIZACE +-- =========================================================================== +local dbg_table = ch_core.storage:to_table() +if not dbg_table then + print("STORAGE: nil") +else + for key, value in pairs(dbg_table.fields) do + print("STORAGE: <"..key..">=<"..value..">") + end +end +]] + +doors.login_to_viewname = ch_core.prihlasovaci_na_zobrazovaci +doors.viewname_to_login = ch_core.jmeno_na_prihlasovaci + +-- PŘÍKAZY +-- =========================================================================== +def = { + description = "Vypíše seznam neregistrovaných postav seřazený podle času posledního přihlášení.", + privs = {server = true}, + func = function(player_name, param) + local last_logins = ch_core.get_last_logins(false) + local result = {} + + for i = #last_logins, 1, -1 do + local info = last_logins[i] + local viewname = ch_core.prihlasovaci_na_zobrazovaci(info.player_name) + local s = "- "..viewname.." (posl. přihl. před "..info.last_login_before.." dny, odehráno "..info.played_hours_total.. + " hodin, z toho "..info.played_hours_actively.." aktivně)" + if info.is_in_game then + s = s.." <je ve hře>" + end + if info.pending_registration_type ~= nil then + s = s.." <plánovaná registrace: "..info.pending_registration_type..">" + end + if info.is_registered then + s = s.." <registrovaná postava>" + end + table.insert(result, s) + end + + result = table.concat(result, "\n") + minetest.log("warning", result) + minetest.chat_send_player(player_name, result) + return true + end, +} + +minetest.register_chatcommand("postavynauklid", def) +minetest.register_chatcommand("postavynaúklid", def) + +ch_core.close_submod("lib") diff --git a/ch_core/license.txt b/ch_core/license.txt new file mode 100644 index 0000000..7772c6a --- /dev/null +++ b/ch_core/license.txt @@ -0,0 +1,129 @@ +License of source code +---------------------- + +GNU Lesser General Public License, version 2.1 +Copyright (C) 2022-2023 Singularis <singularis@volny.cz> + +This program is free software; you can redistribute it and/or modify it under the terms +of the GNU Lesser General Public License as published by the Free Software Foundation; +either version 2.1 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details: +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + + +Licenses of media (sounds, textures) +------------------------------------ +ch_core_0.png +ch_core_1.png +ch_core_2.png +ch_core_3.png +ch_core_4.png +ch_core_5.png +ch_core_6.png +ch_core_7.png +ch_core_8.png +ch_core_9.png +ch_core_10.png +ch_core_11.png +ch_core_12.png +ch_core_13.png +ch_core_14.png +ch_core_15.png +ch_core_dialog_bg.png +ch_core_dot.png +ch_core_pryc.png +ch_core_empty.png +ch_core_white_pixel.png +ch_core_white_frame32.png +ch_core_normal.obj +ch_core_normal_45.obj +ch_core_kladivo.png +ch_core_kouzhul.png +ch_core_slunce.png +ch_core_koruna.png +ch_core_chessboard.png +ch_core_wood_noise_32.png +ch_core_wood_noise_64.png +ch_core_wood_noise_128.png +ch_core_planks_128.png +ch_core_diagfiller_22a.obj +ch_core_diagfiller_22b.obj +ch_core_diagfiller_45.obj + Author: Singularis + License: CC-BY-SA 4.0 + +ch_core_kcs_1h.png +ch_core_kcs_1zcs.png + Author: Singularis + License: CC-BY-SA 4.0 + Used source: https://commons.wikimedia.org/wiki/File:1_haler_CSK_(1962-1986).png (CC-BY-SA 4.0; author: Arvedui89) + +ch_core_kcs_1kcs.png + Author: Singularis + License: CC-BY-SA 4.0 + Used source: https://commons.wikimedia.org/wiki/File:1_koruna_CSK_(1991-1992).png (CC-BY-SA 4.0; author: Arvedui89) + +chat3_bell.ogg + Author: Brandon75689 + License: CC-BY-SA 3.0 + Source: https://github.com/octacian/chat3 (unmodified) + Original source: https://opengameart.org/content/point-bell + +flag_cz.png +flag_sk.png + License: public domain due to Czech and Slovak copyright law + Source: https://raw.githubusercontent.com/snowyu/minetest-country_flags + Derived from: https://github.com/lipis/flag-icons + +ch_core_formspec_bg.png + Author: Singularis + License: CC BY-SA 4.0 + Used source: + technic_chest_form_bg.png by RealBadAngel, used under licence WTFPL + (https://github.com/minetest-mods/technic) + +ch_core_chisel.png + Author: Singularis + License: CC BY-SA 3.0 + Used source: + https://commons.wikimedia.org/wiki/File:Chisel_wood_24mm.jpg (CC BY-SA 3.0; author: Za) + +ch_core_clay.png + Author: Singularis + License: CC BY-SA 3.0 + Derived from: „default_clay.png“ from Minetest Game (CC BY-SA 3.0) + Copyright (C) 2010-2018: + celeron55, Perttu Ahola <celeron55@gmail.com> + Cisoun + G4JC + VanessaE + RealBadAngel + Calinou + MirceaKitsune + Jordach + PilzAdam + jojoa1997 + InfinityProject + Splizard + Zeg9 + paramat + BlockMen + sofar + Neuromancer + Gambit + asl97 + KevDoy + Mito551 + GreenXenith + kaeza + kilbith + tobyplowy + CloudyProton + TumeniNodes + Mossmanikin + random-geek + Extex101 + An0n3m0us diff --git a/ch_core/localize_chatcommands.lua b/ch_core/localize_chatcommands.lua new file mode 100644 index 0000000..1962dea --- /dev/null +++ b/ch_core/localize_chatcommands.lua @@ -0,0 +1,86 @@ +ch_core.open_submod("localize_chatcommands", {data = true, lib = true, privs = true}) + +local defs = { + admin = { + description = "Vypíše jméno postavy správce/kyně serveru.", + }, + ban = { + description = "Vyhostí hráče/ky na IP adrese daného klienta nebo zobrazí seznam vyhoštění.", + }, + clearinv = { + description = "Vyprázdní váš inventář, případně inventář jiné postavy.", + }, + clearobjects = { + description = "Smaže ze světa všechny objekty.", + }, + clearpassword = { + description = "Nastaví postavě prázdné heslo.", + }, + days = { + description = "Vypíše počet dní od založení světa.", + }, + deleteblocks = { + description = "Smaže z databáze mapové bloky v oblasti pos1 až pos2 (<pos1> a <pos2> musejí být v závorkách).", + }, + help = { + czech_command = "pomoc", + description = "Vypíše nápovědu pro příkazy nebo seznam práv (-t: výstup do četu)", + }, +} + +local function extract_fields(t, ...) + local result = {} + for _, k in ipairs({...}) do + if t[k] ~= nil then + result[k] = t[k] + end + end + return result +end + +local p_d_p = {"params", "description", "privs"} + +for command, def in pairs(defs) do + if minetest.registered_chatcommands[command] then + local override = {} + local valid = false + for _, k in ipairs(p_d_p) do + if def[k] ~= nil then + override[k] = def[k] + valid = true + end + end + if def.func == nil and def.override_func ~= nil and minetest.registered_chatcommands[command].func ~= nil then + override.func = def.func(minetest.registered_chatcommands[command].func) + valid = true + end + + if valid then + minetest.override_chatcommand(command, override) + end + if def.czech_command then + minetest.register_chatcommand(def.czech_command, + extract_fields(minetest.registered_chatcommands[command], "params", "description", "privs", "func")) + end + else + minetest.log("warning", "Chat command /"..command.." not exists to be localized!") + end +end + +if minetest.get_modpath("builtin_overrides") then + builtin_overrides.login_to_viewname = ch_core.prihlasovaci_na_zobrazovaci +end + +local def = { + params = "[jméno postavy]", + description = "Vypíše vaše práva nebo práva jiné postavy.", + func = function(player_name, param) + local privs_def = minetest.registered_chatcommands.privs + return privs_def.func(player_name, ch_core.jmeno_na_prihlasovaci(param)) + end, +} + +minetest.register_chatcommand("prava", def) +minetest.register_chatcommand("práva", def) + +ch_core.close_submod("localize_chatcommands") diff --git a/ch_core/luacheck.sh b/ch_core/luacheck.sh new file mode 100644 index 0000000..5d68b19 --- /dev/null +++ b/ch_core/luacheck.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec luacheck init.lua active_objects.lua markers.lua barvy_linek.lua nodes.lua hotbar.lua vgroups.lua data.lua lib.lua interiors.lua shapes_db.lua shape_selector.lua penize.lua formspecs.lua areas.lua nametag.lua privs.lua clean_players.lua localize_chatcommands.lua udm.lua chat.lua events.lua stavby.lua podnebi.lua dennoc.lua hud.lua ap.lua registrace.lua pryc.lua joinplayer.lua padlock.lua vezeni.lua timers.lua wielded_light.lua teleportace.lua creative_inventory.lua kos.lua nodedir.lua diff --git a/ch_core/markers.lua b/ch_core/markers.lua new file mode 100644 index 0000000..4f8d34e --- /dev/null +++ b/ch_core/markers.lua @@ -0,0 +1,49 @@ +ch_core.open_submod("markers", {}) + +local texture = "ch_core_chessboard.png^[makealpha:255,255,255^[opacity:240" + +local function remove_object(self) + self.object:remove() +end +local function remove_object_after(self, delay) + if delay > 0 then + minetest.after(delay, remove_object, self) + else + remove_object(self) + end +end +local function remove_object_after_timeout(self, shift) + remove_object_after(self, (self.timeout or shift) - shift) +end +local function on_activate(self, staticdata, dtime_s) + minetest.after(1, remove_object_after_timeout, self, 1) +end + +local def = { + initial_properties = { + physical = false, + pointable = false, + visual = "cube", + textures = {texture, texture, texture, texture, texture, texture}, + use_texture_alpha = true, + backface_culling = false, + static_save = false, + shaded = false, + }, + on_activate = on_activate, +} +minetest.register_entity("ch_core:markers", def) + +function ch_core.show_aabb(coords, timeout) + local size = vector.new(coords[4] - coords[1], coords[5] - coords[2], coords[6] - coords[3]) + local center = vector.new( + 0.5 * (coords[4] + coords[1]) - 0.5, + 0.5 * (coords[5] + coords[2]) - 0.5, + 0.5 * (coords[6] + coords[3]) - 0.5) + local obj = minetest.add_entity(center, "ch_core:markers") + obj:set_properties{visual_size = size} + obj:get_luaentity().timeout = timeout or 16 + return obj +end + +ch_core.close_submod("markers") diff --git a/ch_core/mod.conf b/ch_core/mod.conf new file mode 100644 index 0000000..8bf47ee --- /dev/null +++ b/ch_core/mod.conf @@ -0,0 +1,4 @@ +name = ch_core +description = Vlastní úpravy serveru Český hvozd +depends = ch_base, ch_data, ch_time, basic_materials, default, doors, dye, player_api, screwdriver, sethome, wool, xpanes +optional_depends = emote, hudbars, wielded_light diff --git a/ch_core/models/ch_core_diagfiller_22a.obj b/ch_core/models/ch_core_diagfiller_22a.obj new file mode 100644 index 0000000..c3373fd --- /dev/null +++ b/ch_core/models/ch_core_diagfiller_22a.obj @@ -0,0 +1,23 @@ +g all +v -0.500000 -0.5 1.500000 +v -0.500000 -0.5 -0.500000 +v 0.500000 -0.5 -0.500000 +v 0.500000 -0.5 1.500000 +v -0.500000 -0.49 1.500000 +v -0.500000 -0.49 -0.500000 +v 0.500000 -0.49 -0.500000 +v 0.500000 -0.49 1.500000 +v -0.500000 -0.4899 1.500000 +v -0.500000 -0.4899 -0.500000 +v 0.500000 -0.4899 -0.500000 +v 0.500000 -0.4899 1.500000 +vt 0 0 +vt 0 2 +vt 1 1 +vt 1 0 +vn 0.0000 1.0000 0.0000 +s off +f 8/2/1 7/1/1 6/4/1 +f 4/2/1 3/3/1 11/3/1 12/2/1 +f 2/2/1 4/3/1 12/3/1 10/2/1 +f 10/3/1 11/2/1 3/2/1 2/3/1 diff --git a/ch_core/models/ch_core_diagfiller_22b.obj b/ch_core/models/ch_core_diagfiller_22b.obj new file mode 100644 index 0000000..299b022 --- /dev/null +++ b/ch_core/models/ch_core_diagfiller_22b.obj @@ -0,0 +1,23 @@ +g all +v -0.500000 -0.5 1.500000 +v -0.500000 -0.5 -0.500000 +v 0.500000 -0.5 -0.500000 +v 0.500000 -0.5 1.500000 +v -0.500000 -0.49 1.500000 +v -0.500000 -0.49 -0.500000 +v 0.500000 -0.49 -0.500000 +v 0.500000 -0.49 1.500000 +v -0.500000 -0.4899 1.500000 +v -0.500000 -0.4899 -0.500000 +v 0.500000 -0.4899 -0.500000 +v 0.500000 -0.4899 1.500000 +vt 0 0 +vt 0 1 +vt 1 2 +vt 1 0 +vn 0.0000 1.0000 0.0000 +s off +f 7/1/1 6/4/1 5/3/1 +f 1/2/1 3/3/1 11/3/1 9/2/1 +f 2/2/1 1/3/1 9/3/1 10/2/1 +f 10/3/1 11/2/1 3/2/1 2/3/1 diff --git a/ch_core/models/ch_core_diagfiller_45.obj b/ch_core/models/ch_core_diagfiller_45.obj new file mode 100644 index 0000000..fdc9785 --- /dev/null +++ b/ch_core/models/ch_core_diagfiller_45.obj @@ -0,0 +1,31 @@ +g all +v -0.500000 -0.5 0.500000 +v -0.500000 -0.5 -0.500000 +v 0.500000 -0.5 -0.500000 +v 0.500000 -0.5 0.500000 +v -0.500000 -0.49 0.500000 +v -0.500000 -0.49 -0.500000 +v 0.500000 -0.49 -0.500000 +v 0.500000 -0.49 0.500000 +v 0.500000 -0.5 1.500000 +v 0.500000 -0.5 0.500000 +v 1.500000 -0.5 0.500000 +v 1.500000 -0.5 1.500000 +v 0.500000 -0.49 1.500000 +v 0.500000 -0.49 0.500000 +v 1.500000 -0.49 0.500000 +v 1.500000 -0.49 1.500000 +vt 0 0 +vt 0 1 +vt 1 1 +vt 1 0 +vn 0.0000 1.0000 0.0000 +s off +f 8/2/1 7/1/1 6/4/1 +f 4/2/1 3/3/1 7/3/1 8/2/1 +f 2/2/1 4/3/1 8/3/1 6/2/1 +f 6/3/1 7/2/1 3/2/1 2/3/1 +f 16/2/1 15/1/1 14/4/1 +f 12/2/1 11/3/1 15/3/1 16/2/1 +f 10/2/1 12/3/1 16/3/1 14/2/1 +f 14/3/1 15/2/1 11/2/1 10/3/1 diff --git a/ch_core/models/ch_core_normal.obj b/ch_core/models/ch_core_normal.obj new file mode 100644 index 0000000..dbe0cdf --- /dev/null +++ b/ch_core/models/ch_core_normal.obj @@ -0,0 +1,36 @@ +g top +v -0.500000 -0.5 0.500000 +v -0.500000 -0.5 -0.500000 +v 0.500000 -0.5 -0.500000 +v 0.500000 -0.5 0.500000 +v -0.500000 0.5 0.500000 +v -0.500000 0.5 -0.500000 +v 0.500000 0.5 -0.500000 +v 0.500000 0.5 0.500000 +vt 0 0 +vt 0 1 +vt 1 1 +vt 1 0 +vn 0.0000 1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn -1.0000 0.0000 0.0000 +vn 0.0000 0.0000 -1.0000 +vn 1.0000 0.0000 0.0000 +vn 0.0000 0.0000 1.0000 +s off +f 8/2/1 7/1/1 6/4/1 5/3/1 +g bottom +s off +f 1/4/2 2/3/2 3/2/2 4/1/2 +g right +s off +f 5/3/3 6/2/3 2/1/3 1/4/3 +g left +s off +f 4/1/5 3/4/5 7/3/5 8/2/5 +g back +s off +f 1/1/6 4/4/6 8/3/6 5/2/6 +g front +s off +f 6/3/4 7/2/4 3/1/4 2/4/4 diff --git a/ch_core/models/ch_core_normal_45.obj b/ch_core/models/ch_core_normal_45.obj new file mode 100644 index 0000000..902fcf7 --- /dev/null +++ b/ch_core/models/ch_core_normal_45.obj @@ -0,0 +1,36 @@ +g top +v -0.707107 -0.5 0.000000 +v 0.000000 -0.5 -0.707107 +v 0.707107 -0.5 0.000000 +v 0.000000 -0.5 0.707107 +v -0.707107 0.5 0.000000 +v 0.000000 0.5 -0.707107 +v 0.707107 0.5 0.000000 +v 0.000000 0.5 0.707107 +vt 0 0 +vt 0 1 +vt 1 1 +vt 1 0 +vn 0.0000 1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn -0.7071 0.0000 -0.7071 +vn 0.7071 0.0000 -0.7071 +vn 0.7071 0.0000 0.7071 +vn -0.7071 0.0000 0.7071 +s off +f 8/2/1 7/1/1 6/4/1 5/3/1 +g bottom +s off +f 1/4/2 2/3/2 3/2/2 4/1/2 +g right +s off +f 5/3/3 6/2/3 2/1/3 1/4/3 +g left +s off +f 4/1/5 3/4/5 7/3/5 8/2/5 +g back +s off +f 1/1/6 4/4/6 8/3/6 5/2/6 +g front +s off +f 6/3/4 7/2/4 3/1/4 2/4/4 diff --git a/ch_core/nametag.lua b/ch_core/nametag.lua new file mode 100644 index 0000000..e3ca802 --- /dev/null +++ b/ch_core/nametag.lua @@ -0,0 +1,181 @@ +ch_core.open_submod("nametag", {data = true, lib = true}) + +-- local nametag_color_red = minetest.get_color_escape_sequence("#cc5257"); +--local nametag_color_blue = minetest.get_color_escape_sequence("#6693ff"); +local nametag_color_green = minetest.get_color_escape_sequence("#48cc3d"); +--local nametag_color_yellow = minetest.get_color_escape_sequence("#fff966"); +-- local nametag_color_aqua = minetest.get_color_escape_sequence("#66f8ff"); +local nametag_color_grey = minetest.get_color_escape_sequence("#cccccc"); +local color_reset = minetest.get_color_escape_sequence("#ffffff") + +local nametag_nochat_bgcolor_table = {r = 0, g = 0, b = 0, a = 0} +local nametag_chat_bgcolor_table = {r = 0, g = 0, b = 0, a = 255} + + +-- local nametag_color_bgcolor_table = {r = 0, g = 0, b = 0, a = 0} +local nametag_color_normal_table = {r = 255, g = 255, b = 255, a = 255} +local nametag_color_unregistered_table = {r = 204, g = 204, b = 204, a = 255} -- 153? +local nametag_color_unregistered = nametag_color_grey + +local nametag_padding_left = " " +local nametag_padding_right = nametag_padding_left + +function ch_core.compute_player_nametag(online_charinfo, offline_charinfo) + local color, bgcolor + local titul, barevne_jmeno, local_color_reset, staly_titul + local player_name = online_charinfo.player_name + local casti = {} + + table.insert(casti, nametag_padding_left) + + if string.sub(player_name, -2) == "PP" then + -- pomocná postava + local_color_reset = nametag_color_grey + color = nametag_color_unregistered + staly_titul = "pomocná postava" + elseif minetest.check_player_privs(player_name, "ch_registered_player") then + -- registrovaná postava + local_color_reset = color_reset + color = nametag_color_normal_table + staly_titul = offline_charinfo.titul + if staly_titul == "" then + staly_titul = nil + end + else + -- neregistrovaná postava + local_color_reset = nametag_color_grey + color = nametag_color_unregistered_table + staly_titul = "nová postava" + end + + for dtitul, _ in pairs(online_charinfo.docasne_tituly or {}) do + titul = (titul or "").."*"..dtitul.."*\n" + end + if titul then + -- dočasný titul + table.insert(casti, nametag_color_green) + table.insert(casti, titul) + table.insert(casti, local_color_reset) + elseif staly_titul then + -- trvalý titul + table.insert(casti, "*"..staly_titul.."*\n") + end + + barevne_jmeno = offline_charinfo.barevne_jmeno + if not barevne_jmeno then + barevne_jmeno = local_color_reset..(offline_charinfo.jmeno or player_name) + end + table.insert(casti, barevne_jmeno) + + local horka_zprava = online_charinfo.horka_zprava + if horka_zprava then + table.insert(casti, ":\n") + table.insert(casti, horka_zprava[1]) + if horka_zprava[2] then + table.insert(casti, "\n") + table.insert(casti, horka_zprava[2]) + if horka_zprava[3] then + table.insert(casti, "\n") + table.insert(casti, horka_zprava[3]) + end + end + bgcolor = nametag_chat_bgcolor_table + else + bgcolor = nametag_nochat_bgcolor_table + end + + table.insert(casti, nametag_padding_right) + + local text = table.concat(casti):gsub("\n", nametag_padding_right.."\n"..nametag_padding_left) + return {color = color, bgcolor = bgcolor, text = text} +end + +minetest.register_chatcommand("nastavit_barvu_jmena", { + params = "<prihlasovaci_jmeno_postavy> [#RRGGBB]", + description = "Nastaví nebo zruší postavě barevné jméno", + privs = { server = true }, + func = function(player_name, param) + local i = string.find(param, " ") + local login, color + if not i then + login = param + color = "" + else + login = param:sub(1, i - 1) + color = param:sub(i + 1, -1) + end + if not minetest.player_exists(login) then + return false, "Postava s přihlašovacím jménem "..login.." neexistuje!" + end + local offline_charinfo = ch_data.get_offline_charinfo(login) + local jmeno = offline_charinfo.jmeno or login + if color == "" then + offline_charinfo.barevne_jmeno = nil + else + if jmeno == "Administrace" then + offline_charinfo.barevne_jmeno = minetest.get_color_escape_sequence("#cc5257").."Admin"..minetest.get_color_escape_sequence("#6693ff").."istrace"..color_reset + else + offline_charinfo.barevne_jmeno = minetest.get_color_escape_sequence(string.lower(color))..jmeno..color_reset + end + end + ch_data.save_offline_charinfo(login) + local online_charinfo = ch_data.online_charinfo[player_name] + local player = online_charinfo and minetest.get_player_by_name(login) + if online_charinfo and player then + player:set_nametag_attributes(ch_core.compute_player_nametag(online_charinfo, offline_charinfo)) + end + return true + end, +}) + +minetest.register_chatcommand("nastavit_jmeno", { + params = "<nové jméno postavy>", + description = "Nastaví zobrazované jméno postavy", + privs = { server = true }, + func = function(player_name, param) + local login = ch_core.jmeno_na_prihlasovaci(param) + if not minetest.player_exists(login) then + return false, "Postava '"..login.."' neexistuje!" + end + local offline_charinfo = ch_data.get_offline_charinfo(login) + local puvodni_jmeno = offline_charinfo.jmeno or login + local message + if puvodni_jmeno ~= param then + offline_charinfo.jmeno = param + local barevne_jmeno = offline_charinfo.barevne_jmeno + if barevne_jmeno then + offline_charinfo.barevne_jmeno = barevne_jmeno:gsub(puvodni_jmeno, param) + end + ch_data.save_offline_charinfo(login) + message = "Jméno nastaveno: "..login.." > "..param + else + message = "Titulek obnoven: "..login.." > "..param + end + local online_charinfo = ch_data.online_charinfo[login] + local player = online_charinfo and minetest.get_player_by_name(login) + if online_charinfo and player then + player:set_nametag_attributes(ch_core.compute_player_nametag(online_charinfo, offline_charinfo)) + end + return true, message + end, +}) + +minetest.register_chatcommand("nastavit_titul", { + params = "<prihlasovaci_jmeno_postavy> [text titulu]", + description = "Nastaví nebo zruší postavě titul nad jménem", + privs = { server = true }, + func = function(player_name, param) + local login, new_titul + local i = string.find(param, " ") + if i then + login = ch_core.jmeno_na_prihlasovaci(param:sub(1, i - 1)) + new_titul = param:sub(i + 1) + else + login = ch_core.jmeno_na_prihlasovaci(param) + new_titul = "" + end + return ch_core.set_titul(login, new_titul) + end, +}) + +ch_core.close_submod("nametag") diff --git a/ch_core/nodedir.lua b/ch_core/nodedir.lua new file mode 100644 index 0000000..a4e24ae --- /dev/null +++ b/ch_core/nodedir.lua @@ -0,0 +1,137 @@ +ch_core.open_submod("nodedir", {lib = true}) + +ch_core.registered_nodedir_groups = {} + +local node_to_info = {} + +function ch_core.register_nodedir_group(def) + local to_add = {} + local new_group = {} + for i = 0, 23 do + local nodename = def[i] + if type(nodename) == "string" then + if node_to_info[nodename] ~= nil then + error(nodename.." is already registered in a different nodedir group!") + elseif to_add[nodename] == nil then + to_add[nodename] = {facedir = i, group = new_group} + end + new_group[i] = nodename + elseif nodename ~= nil then + error("Invalid nodename type: "..type(nodename)) + elseif i == 0 then + error("Nodedir name [0] is required!") + end + end + assert(type(new_group[0]) == "string") + for k, v in pairs(to_add) do + node_to_info[k] = v + end + table.insert(ch_core.registered_nodedir_groups, new_group) + return true +end + +local facedir_table = {} +for i = 0, 23 do + facedir_table[i] = true +end + +local function assert_is_facedir(facedir) + if type(facedir) == "number" and facedir_table[facedir] then + return facedir + else + error("Invalid facedir value: "..dump2({type = type(facedir), value = facedir})) + end +end + +--[[ + Pokud je zadaný blok registrován, vrátí jeho otočení typu facedir, jinak vrací nil. +]] +function ch_core.get_nodedir(nodename) + local info = node_to_info[assert(nodename)] + if info ~= nil then + return info.facedir + else + return nil + end +end + +--[[ + Pokud je zadaný blok registrován a podporuje cílové otočení, vrátí název odpovídajícího cílového bloku. + Jinak vrací nil. +]] +function ch_core.get_nodedir_nodename(current_nodename, new_facedir) + local info = node_to_info[assert(current_nodename)] + if info ~= nil then + return info ~= nil and info.group[assert_is_facedir(new_facedir)] + else + return nil + end +end + +--[[ + Pokud je zadaný blok registrován, nastaví v množině 'set' klíče odpovídající + zadanému bloku a jeho ostatním otočením na true. + 'set' může být nil, v takovém případě jen vrátí návratovou hodnotu, zda je blok registrován. + Vrací true, pokud je zadaný blok registrován, jinak false. +]] +function ch_core.fill_nodedir_equals_set(set, nodename) + local info = node_to_info[assert(nodename)] + if info == nil then return false end + if set ~= nil then + local group, n = info.group + for i = 0, 23 do + n = group[i] + if n ~= nil then + set[n] = true + end + end + end + return true +end + +-- override screwdriver handler: +local old_screwdriver_handler = assert(screwdriver.handler) +function screwdriver.handler(itemstack, user, pointed_thing, mode, uses) + if pointed_thing.type ~= "node" then return end + local node = minetest.get_node(pointed_thing.under) + local nodedir_facedir = ch_core.get_nodedir(node.name) + if nodedir_facedir == nil then + -- call a normal handler + return old_screwdriver_handler(itemstack, user, pointed_thing, mode, uses) + end + local pos = pointed_thing.under + local player_name = (user and user:get_player_name()) or "" + if minetest.is_protected(pos, player_name) then + minetest.record_protection_violation(pos, player_name) + return + end + local new_facedir + if mode == screwdriver.ROTATE_FACE then + new_facedir = bit.band(nodedir_facedir, 0x1C) + bit.band(nodedir_facedir + 1, 0x03) + elseif mode == screwdriver.ROTATE_AXIS then + new_facedir = (nodedir_facedir + 4) % 24 + else + return -- unknown mode + end + local new_node_name = ch_core.get_nodedir_nodename(node.name, new_facedir) + if new_node_name == nil then + return -- orientation not supported by the node + end + + if new_node_name ~= node.name then + local old_node = table.copy(node) + node.name = new_node_name + minetest.swap_node(pos, node) + minetest.check_for_falling(pos) + local old_node_def = core.registered_nodes[old_node.name] + if old_node_def ~= nil and old_node_def.after_nodedir_rotate ~= nil then + old_node_def.after_nodedir_rotate(pos, old_node, node, itemstack, user) + end + end + if not minetest.is_creative_enabled(player_name) then + itemstack:add_wear_by_uses(uses or 200) + end + return itemstack +end + +ch_core.close_submod("nodedir") diff --git a/ch_core/nodes.lua b/ch_core/nodes.lua new file mode 100644 index 0000000..da11bce --- /dev/null +++ b/ch_core/nodes.lua @@ -0,0 +1,93 @@ +ch_core.open_submod("nodes") + +local def + +-- ch_core.light_{0..15} +--------------------------------------------------------------- +local box = { + type = "fixed", + fixed = {{-1/16, -8/16, -1/16, 1/16, -7/16, 1/16}} +} +def = { + description = "0", + drawtype = "nodebox", + node_box = box, + selection_box = box, + -- top, bottom, right, left, back, front + tiles = {"ch_core_white_pixel.png^[opacity:0"}, + use_texture_alpha = "clip", + inventory_image = "default_invisible_node_overlay.png", + wield_image = "default_invisible_node_overlay.png", + paramtype = "light", + paramtype2 = "none", + light_source = 0, + + sunlight_propagates = true, + walkable = false, + pointable = true, + buildable_to = true, + floodable = true, + drop = "" +} + +for i = 0, minetest.LIGHT_MAX do + local name = "light_" + if i < 10 then + name = name.."0" + end + name = name..i + def.description = string.format("administrátorské světlo %02d", i) + if i > 0 then + def.groups = {ch_core_light = i} + def.light_source = i + end + local image = "ch_core_white_pixel.png^[resize:16x16^[multiply:#eeeeaa^[colorize:#000000:"..math.floor((minetest.LIGHT_MAX - i) / minetest.LIGHT_MAX * 255).."^default_invisible_node_overlay.png^ch_core_"..math.min(i, 15)..".png" + def.inventory_image = image + def.wield_image = image + + minetest.register_node("ch_core:"..name, table.copy(def)) + if i > 0 and minetest.get_modpath("wielded_light") then + wielded_light.register_item_light("ch_core:"..name, i, false) + end +end +minetest.register_alias("ch_core:light_max", "ch_core:light_"..minetest.LIGHT_MAX) + +-- Glass overrides: + +local tex_edge = "ch_core_white_pixel.png^[multiply:#bbbbbb" +local tex_glass = "ch_core_white_pixel.png^[opacity:40" +local tex_framedglass = tex_glass.."^[resize:32x32^(ch_core_white_frame32.png^[multiply:#bbbbbb)" +local tile_edge = {name = tex_edge, backface_culling = true} +local tile_framedglass = {name = tex_framedglass, backface_culling = true} + + +-- DEBUG test: +tile_framedglass.name = tex_glass + + + +core.override_item("default:glass", { + tiles = {tex_framedglass, tex_glass}, + use_texture_alpha = "blend", +}) + + +local override = {use_texture_alpha = "blend"} +for _, c in ipairs({"a", "b", "c", "d", "cd_a", "cd_b", "cd_c", "cd_d"}) do + local n = "doors:door_glass_"..c + if core.registered_nodes[n] ~= nil then + core.override_item(n, override) + end +end + +for _, n in ipairs({"xpanes:pane_flat", "xpanes:pane"}) do + local t = assert(core.registered_nodes[n].tiles) + for i, tile in ipairs(t) do + if type(tile) == "string" and tile == "default_glass.png" then + t[i] = "ch_core_white_pixel.png^[opacity:20^[resize:32x32^(ch_core_white_frame32.png^[multiply:#bbbbbb)" + end + end + core.override_item(n, {tiles = t, use_texture_alpha = "blend"}) +end + +ch_core.close_submod("nodes") diff --git a/ch_core/padlock.lua b/ch_core/padlock.lua new file mode 100644 index 0000000..9c8fb19 --- /dev/null +++ b/ch_core/padlock.lua @@ -0,0 +1,101 @@ +ch_core.open_submod("padlock", {data = true, lib = true}) + +--[[ + +How to use padlock: + +1. Add "on_padlock_place = function(player, pos, owner)" to the node definition. + The function should check, if a padlock is present. + If the padlock is already present, it should return false. + Otherwise it should add the padlock and return true. +2. Add "on_padlock_remove = function(player, pos, owner)" to the node definition. + The function should check, if a padlock is present. + If the padlock is missing, it should return false. + Otherwise it should remove the padlock and return true. +3. Add node to the "padlockable = 1" group (optional). +4. Add "ch_core.dig_padlock(pos, player)" to the can_dig before returning true. + (This function will call on_padlock_remove to test and remove a padlock.) +]] + +local function padlock_remove(itemstack, player, pointed_thing) + if pointed_thing.type == "object" then + local o, e, f + o = pointed_thing.ref + e = o:get_luaentity() + f = e and e.on_punch + if f then + return f(e, player) + else + return + end + end + local pos = minetest.get_pointed_thing_position(pointed_thing, false) + if not player or not player:is_player() or not pos then + return nil + end + local nodedef = minetest.registered_nodes[minetest.get_node(pos).name] + if not nodedef or not nodedef.on_padlock_remove then + return nil + end + local player_name = player:get_player_name() + local meta = minetest.get_meta(pos) + local owner = meta:get_string("owner") + if owner == player_name or minetest.check_player_privs(player, "protection_bypass") then + if nodedef.on_padlock_remove(player, pos, owner) then + -- padlock removed => give it to the player + player:get_inventory():add_item("main", itemstack:peek_item(1)) + end + else + minetest.chat_send_player(player_name, "*** Nemáte právo odstranit zámek z tohoto objektu!") + end + return nil +end + +local function padlock_place(itemstack, player, pointed_thing) + if pointed_thing.type == "object" then + local o, e, f + o = pointed_thing.ref + e = o:get_luaentity() + f = e and e.on_rightclick + if f then + return f(e, player) + else + return + end + end + + local pos = minetest.get_pointed_thing_position(pointed_thing, false) + if not player or not player:is_player() or not pos then + return nil + end + local nodedef = minetest.registered_nodes[minetest.get_node(pos).name] + if not nodedef or not nodedef.on_padlock_place then + return nil + end + local player_name = player:get_player_name() + local meta = minetest.get_meta(pos) + local owner = meta:get_string("owner") + if owner == player_name or minetest.check_player_privs(player, "protection_bypass") then + if nodedef.on_padlock_place(player, pos, owner) then + -- padlock placed + itemstack:take_item(1) + return itemstack + end + else + minetest.chat_send_player(player_name, "*** Nemáte právo umístit zámek na tento objekt!") + end + return nil +end + +minetest.override_item("basic_materials:padlock", { + on_use = padlock_remove, + on_place = padlock_place, +}) + +-- call ch_core.dig_padlock(pos, player) in can_dig before returning true +function ch_core.dig_padlock(pos, player) + padlock_remove(ItemStack("basic_materials:padlock"), player, {type = "node", under = pos, above = vector.new(pos.x, pos.y + 1, pos.z)}) + return true +end + +ch_core.close_submod("padlock") diff --git a/ch_core/penize.lua b/ch_core/penize.lua new file mode 100644 index 0000000..55ed190 --- /dev/null +++ b/ch_core/penize.lua @@ -0,0 +1,552 @@ +ch_core.open_submod("penize", {lib = true}) + +-- ch_core:kcs_{h,kcs,zcs} +minetest.register_craftitem("ch_core:kcs_h", { + description = "haléř československý", + inventory_image = "ch_core_kcs_1h.png", + stack_max = 10000, + groups = {money = 1}, +}) +minetest.register_craftitem("ch_core:kcs_kcs", { + description = "koruna československá (Kčs)", + inventory_image = "ch_core_kcs_1kcs.png", + stack_max = 10000, + groups = {money = 2}, +}) +minetest.register_craftitem("ch_core:kcs_zcs", { + description = "zlatka československá (Zčs)", + inventory_image = "ch_core_kcs_1zcs.png", + stack_max = 10000, + groups = {money = 3}, +}) + +local penize = { + ["ch_core:kcs_h"] = 1, + ["ch_core:kcs_kcs"] = 100, + ["ch_core:kcs_zcs"] = 10000, +} + +local payment_methods = {} + +--[[ + Zformátuje částku do textové podoby, např. "-1 235 123,45". + Částka může být záporná. Druhá vrácená hodnota je doporučený + hexadecimální colorstring pro hodnotu. + -- n : int + => text : string, colorstring : string +]] +function ch_core.formatovat_castku(n) + -- minus, halere, string, division, remainder + local m, h, s, d, r, color + if n < 0 then + m = "-" + n = -n + else + m = "" + end + n = math.ceil(n) + if m ~= "" then + color = "#bb0000" + elseif n < 100 then + color = "#ffffff" + else + color = "#00ff00" + end + d = math.floor(n / 100.0) + r = n - 100.0 * d + if r > 0 then + h = string.format("%02d", r) + else + h = "-" + end + s = string.format("%d", d) + if #s > 3 then + local t + r = #s % 3 + t = {s:sub(1, r)} + s = s:sub(r + 1, -1) + while #s >= 3 do + table.insert(t, s:sub(1, 3)) + s = s:sub(4, -1) + end + s = table.concat(t, " ") + end + return m..s..","..h, color +end + +--[[ + Vrátí tabulku ItemStacků s penězi v dané výši. Částka musí být nezáporná. + Případné desetinné číslo se zaokrouhlí dolů. Pro nulu vrací prázdnou tabulku. +]] +function ch_core.hotovost(castka) + local debug = {"puvodni castka: "..castka} + local stacks = {} + castka = math.floor(castka) + if castka < 0 then + return stacks + end + while castka > 10000 * 10000 do -- 10 000 zlatek + table.insert(stacks, ItemStack("ch_core:kcs_zcs 10000")) + castka = castka - 10000 * 10000 + table.insert(debug, "ch_core:kcs_zcs 10000 => "..castka) + end + while castka > 10000 * 100 do -- 10 000 korun + local n = math.floor(castka / 10000) + assert(n >= 1 and n <= 10000) + table.insert(stacks, ItemStack("ch_core:kcs_zcs "..n)) + castka = castka - n * 10000 + table.insert(debug, "ch_core:kcs_zcs "..n.." => "..castka) + end + local n = math.floor(castka / 100) + assert(n >= 0 and n <= 10000) + if n > 0 then + table.insert(stacks, ItemStack("ch_core:kcs_kcs "..n)) + table.insert(debug, "ch_core:kcs_kcs "..n.." => "..castka) + end + castka = castka - n * 100 + if castka > 0 then + assert(castka >= 1 and castka <= 100) + table.insert(stacks, ItemStack("ch_core:kcs_h "..castka)) + table.insert(debug, "ch_core:kcs_h "..castka.." => 0") + end + return stacks +end + +-- 0 = upřednostňovat platby z/na účet +-- 1 = přijímat v hotovosti, platit z účtu +-- 2 = přijímat na účet, platit hotově +-- 3 = upřednostňovat hotovost +-- 4 = zakázat platby z účtu + +--[[ +function ch_core.nastaveni_prichozich_plateb(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo == nil then + return {} + end + local rezim = offline_charinfo.rezim_plateb + return {cash = true, bank = true, prefer_cash = rezim ~= 0 and rezim ~= 2} +end + +function ch_core.nastaveni_odchozich_plateb(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo == nil then + return {} + end + local rezim = offline_charinfo.rezim_plateb + return {cash = true, bank = rezim ~= 4, prefer_cash = rezim >= 2} +end +]] +--[[ + Parametr musí být ItemStack, seznam ItemStacků nebo nil. + Je-li to seznam, vrátí součet hodnoty všech nalezených peněz (nepeněžní dávky ignoruje). + Je-li to dávka peněz, vrátí jejich hodnotu (nezáporné celé číslo). + Jinak vrací nil. +]] +function ch_core.precist_hotovost(stacks) + if stacks == nil then + return nil + elseif type(stacks) == "table" then + local result = 0 + for _, stack in ipairs(stacks) do + local v = penize[stack:get_name()] + if v ~= nil then + result = result + v * stack:get_count() + end + end + return result + else + local stack = stacks + local v = penize[stack:get_name()] + if v ~= nil then + return v * stack:get_count() + end + end +end + +-- current_count, count_to_remove +-- vrací: count_to_remove_now, hundreds_to_remove +-- hodnota count_to_remove_now může být i záporné číslo v rozsahu -100 až -1, +-- v takovém případě značí absolutní hodnota počet mincí, které je nutno přidat +local function remove100(current_count, count_to_remove) + if count_to_remove <= current_count then + return count_to_remove, 0 + end + local count_to_remove_ones = count_to_remove % 100 + local count_to_remove_hundreds = (count_to_remove - count_to_remove_ones) / 100 + local new_count = current_count - count_to_remove_ones + local new_count_hundreds = math.floor(new_count / 100) + return count_to_remove_ones + 100 * new_count_hundreds, count_to_remove_hundreds - new_count_hundreds +end + +--[[ + Pokusí se z uvedených počtů mincí odebrat mince tak, + aby byla odebrána přesně zadaná hodnota. Vrátí nil, + pokud je hodnota větší než součet hodnoty všech dostupných mincí. + - items: table {["ch_core:kcs_h"] = (int >= 0) or nil, ...} + - amount: int >= 0 + - vrací: {ch_core_kcs_1h = int, ...} or nil + vrácený údaj značí, kolik mincí je potřeba odebrat z inventáře; + může být záporný, v takovém případě uvádí, kolik mincí je + potřeba do inventáře přidat +]] +function ch_core.rozmenit(items, amount) + local current_h = items["ch_core:kcs_h"] or 0 + local current_kcs = items["ch_core:kcs_kcs"] or 0 + local current_zcs = items["ch_core:kcs_zcs"] or 0 + if current_h < 0 or current_kcs < 0 or current_zcs < 0 then + error("Chybné zadání rozměňování! "..dump2({items = items, amount = amount})) + end + local h_to_remove, kcs_to_remove, zcs_to_remove + h_to_remove, kcs_to_remove = remove100(current_h, amount) + kcs_to_remove, zcs_to_remove = remove100(current_kcs, kcs_to_remove) + if zcs_to_remove <= current_zcs then + -- verify the result: + if (h_to_remove + 100 * kcs_to_remove + 10000 * zcs_to_remove) ~= amount or h_to_remove > current_h or kcs_to_remove > current_kcs then + error("Internal error in ch_core.rozmenit(): "..dump2({current_h = current_h, current_kcs = current_kcs, current_zcs = current_zcs, h_to_remove = h_to_remove, kcs_to_remove = kcs_to_remove, zcs_to_remove = zcs_to_remove, amount = amount, items = items, value_to_remove = h_to_remove + 100 * kcs_to_remove + 10000 * zcs_to_remove})) + end + return { + ["ch_core:kcs_h"] = h_to_remove, + ["ch_core:kcs_kcs"] = kcs_to_remove, + ["ch_core:kcs_zcs"] = zcs_to_remove, + } + else + return nil + end +end + +--[[ + Všechny stacky s penězi v tabulce vyprázdní a vrátí jejich původní + celkovou hodnotu. + - stacks: table {ItemStack...} + - limit: int >= 0 or nil + returns: int >= 0 or nil +]] +function ch_core.vzit_vsechnu_hotovost(stacks) + local castka = 0 + for _, stack in ipairs(stacks) do + local stack_count = stack:get_count() + if stack_count > 0 then + local value_per_item = penize[stack:get_name()] + if value_per_item ~= nil then + castka = castka + value_per_item * stack_count + stack:clear() + end + end + end + return castka +end + +--[[ + Odečte ze stacků v tabulce peníze maximálně do zadaného limitu + a vrátí celkovou odečtenou částku, nebo nil, pokud se nepodaří + vrátit drobné. + - stacks: table {ItemStack...} + - limit: int >= 0 or nil + - strict: bool or nil (je-li true, vrátí nil, pokud nemůže odečíst přesně + částku „limit“) + returns: int >= 0 or nil +]] +function ch_core.vzit_hotovost(stacks, limit, strict) + -- Odečte ze stacků v tabulce peníze a vrátí celkovou částku. + if limit == nil then + return ch_core.vzit_vsechnu_hotovost(stacks) + end + limit = tonumber(limit) + if limit == nil or limit < 0 or math.floor(limit) ~= limit then + error("ch_core.vzit_hotovost(): limit must be a non-negative integer!") + end + local items = { + [""] = {count = 0, indices = {}}, + ["ch_core:kcs_h"] = {count = 0, indices = {}}, + ["ch_core:kcs_kcs"] = {count = 0, indices = {}}, + ["ch_core:kcs_zcs"] = {count = 0, indices = {}}, + } + for i, stack in ipairs(stacks) do + local name = stack:get_name() + local info = items[name] + if info ~= nil then + info.count = info.count + stack:get_count() + table.insert(info.indices, i) + end + end + local total_value = items["ch_core:kcs_h"].count + + items["ch_core:kcs_kcs"].count * penize["ch_core:kcs_kcs"] + + items["ch_core:kcs_zcs"].count * penize["ch_core:kcs_zcs"] + + if total_value <= limit then + if strict and total_value ~= limit then + return nil + end + + for name, info in pairs(items) do + if name ~= "" then + for _, i in ipairs(info.indices) do + stacks[i]:clear() + end + end + end + return total_value + end + + local new_stacks = {} -- {i = int, stack = ItemStack or false} + local next_empty_index = 1 + local rinfo = ch_core.rozmenit({ + ["ch_core:kcs_h"] = items["ch_core:kcs_h"].count, + ["ch_core:kcs_kcs"] = items["ch_core:kcs_kcs"].count, + ["ch_core:kcs_zcs"] = items["ch_core:kcs_zcs"].count, + }, limit) + for name, info in pairs(items) do + if name ~= "" then + local count_to_remove = rinfo[name] + if count_to_remove < 0 then + local stack_to_add = ItemStack(name.." "..(-count_to_remove)) + -- try to add to the existing stacks + local j = 1 + while not stack_to_add:is_empty() and j <= #info.indices do + local i = info.indices[j] + local new_stack = ItemStack(stacks[i]) + stack_to_add = new_stack:add_item(stack_to_add) + table.insert(new_stacks, {i = i, stack = new_stack}) + end + if not stack_to_add:is_empty() then + -- need an empty stack... + local empty_i = items[""].indices[next_empty_index] + if empty_i == nil then + return nil -- failure + end + table.insert(new_stacks, {i = empty_i, stack = stack_to_add}) + next_empty_index = next_empty_index + 1 + end + else + while count_to_remove > 0 do + for _, i in ipairs(info.indices) do + local current_stack = stacks[i] + local stack_count = current_stack:get_count() + if stack_count < count_to_remove then + count_to_remove = count_to_remove - stack_count + table.insert(new_stacks, {i = i, stack = ItemStack()}) + else + local new_stack = ItemStack(current_stack) + new_stack:take_item(count_to_remove) + table.insert(new_stacks, {i = i, stack = new_stack}) + count_to_remove = 0 + break + end + end + end + assert(count_to_remove == 0) + end + end + end + + -- commit the transaction + for _, pair in ipairs(new_stacks) do + stacks[pair.i]:replace(pair.stack) + end + return limit +end + +function ch_core.register_payment_method(name, pay_from_player, pay_to_player) + if payment_methods[name] ~= nil then + error("payment method "..name.." is already registered!") + end + if type(pay_from_player) ~= "function" or type(pay_to_player) ~= "function" then + error("ch_core.register_payment_method(): invalid type of arguments!") + end + payment_methods[name] = {pay_from = pay_from_player, pay_to = pay_to_player} +end + +local function build_methods_to_try(options, allow_bank, prefer_cash) + if options[1] ~= nil then + return options + end + local methods_to_consider = {} + if options.bank ~= false and allow_bank then + methods_to_consider.bank = true + end + if options.smartshop ~= false and options.shop ~= nil then + methods_to_consider.smartshop = true + elseif options.cash ~= false then + methods_to_consider.cash = true + end + + local methods_to_try = {} + if methods_to_consider.bank and not prefer_cash then + table.insert(methods_to_try, "bank") + methods_to_consider.bank = nil + end + if methods_to_consider.smartshop then + table.insert(methods_to_try, "smartshop") + methods_to_consider.smartshop = nil + end + if methods_to_consider.cash then + table.insert(methods_to_try, "cash") + methods_to_consider.cash = nil + end + if methods_to_consider.bank then + table.insert(methods_to_try, "bank") + methods_to_consider.bank = nil + end + for method, _ in pairs(methods_to_consider) do + table.insert(methods_to_try, method) + end + return methods_to_try +end + +local function pay_from_or_to(dir, player_name, amount, options) + if options == nil then options = {} end + local rezim = (ch_data.offline_charinfo[player_name] or {}).rezim_plateb or 0 + local methods_to_try + if dir == "from" then + methods_to_try = build_methods_to_try(options, rezim ~= 4, rezim >= 2) + else + methods_to_try = build_methods_to_try(options, true, rezim ~= 0 and rezim ~= 2) + end + local silent = options.simulation and options.silent + local errors = {} + local i = 1 + local method = methods_to_try[i] + while method ~= nil do + local pm = payment_methods[method] + if pm ~= nil then + local success, error_message + if dir == "from" then + success, error_message = pm.pay_from(player_name, amount, options) + else + success, error_message = pm.pay_to(player_name, amount, options) + end + if success then + if not silent then + minetest.log("action", "pay_"..dir.."("..player_name..", "..amount..") succeeded with method "..method) + end + return true, {method = method} + end + if error_message ~= nil then + table.insert(errors, error_message) + end + end + i = i + 1 + method = methods_to_try[i] + end + if #errors == 0 then + return false, "Nebyla nalezena žádná použitelná platební metoda." + end + if options.assert == true then + error("Payment assertion failed: pay_"..dir.."("..player_name..", "..amount.."): "..dump2({dir = dir, options = options, errors = errors, methods_to_try = methods_to_try})) + end + if not silent then + minetest.log("action", "pay_"..dir.."("..player_name..", "..amount..") failed! "..#methods_to_try.." methods has been tried. Errors: "..dump2(errors)) + end + return false, {errors = errors} +end + +function ch_core.pay_from(player_name, amount, options) + return pay_from_or_to("from", player_name, amount, options) +end + +function ch_core.pay_to(player_name, amount, options) + return pay_from_or_to("to", player_name, amount, options) +end + +--[[ +options: + + [method] : bool or nil, // je-li false, daná metoda nemá dovoleno běžet + a musí vrátit false bez chybového hlášení + assert : bool or nil, // je-li true a platba nebude uskutečněna + žádnou platební metodou, shodí server. Tato volba je obsluhována + přímo ch_core a platební metody by s ní neměly interferovat. + silent : bool or nil, // je-li true a je-li i simulation == true, + mělo by potlačit obvyklé logování, aby transakce zanechala co nejméně stop + simulation : bool or nil, // je-li true, jen vyzkouší, zda může uspět; + ve skutečnosti platbu neprovede a nikam nezaznamená + + player_inv : InvRef or nil, // platí pro metodu "cash"; specifikuje + inventář, se kterým se má zacházet jako s hráčovým/iným + listname : string or nil, // platí pro metodu "cash"; specifikuje + listname v inventáři; není-li zadáno, použije se "main" + + label : string or nil, // platí pro metodu "bank"; + udává poznámku, která se má uložit do záznamu o platebním převodu + + shop : shop_class or nil, // platí pro metodu "smartshop"; + odkazuje na objekt obchodního terminálu, který se má použít + namísto hráčova/ina inventáře + + Další platební metody mohou mít svoje vlastní parametry. +]] + + +local function cash_pay_from_player(player_name, amount, options) + if options.cash == false then return false end + local player_inv = options.player_inv + if player_inv == nil then + local player = minetest.get_player_by_name(player_name) + if player == nil then + return false, "Postava není ve hře" + end + player_inv = player:get_inventory() + end + local silent = options.simulation and options.silent + local listname = options.listname or "main" + local inv_list = player_inv:get_list(listname) + local hotovost_v_inv_pred = ch_core.vzit_hotovost(player_inv:get_list(listname)) or 0 + local ziskano = ch_core.vzit_hotovost(inv_list, amount) + if ziskano ~= amount then + if not silent then + minetest.log("action", player_name.." failed to pay "..amount.." in cash (got "..(ziskano or "nil")..")") + end + return false, "V inventáři není dost peněz v hotovosti." + end + if not options.simulation then + player_inv:set_list(listname, inv_list) + minetest.log("action", player_name.." payed "..amount.." in cash") + local hotovost_v_inv_po = ch_core.vzit_hotovost(inv_list) or 0 + if hotovost_v_inv_po ~= hotovost_v_inv_pred - amount then + error("ERROR in cash_pay_from_player: pred="..hotovost_v_inv_pred..", po="..hotovost_v_inv_po..", amount="..amount) + end + end + return true +end + +local function cash_pay_to_player(player_name, amount, options) + if options.cash == false then return false end + local player_inv = options.player_inv + if player_inv == nil then + local player = minetest.get_player_by_name(player_name) + if player == nil then + return false, "Postava není ve hře" + end + player_inv = player:get_inventory() + end + local silent = options.simulation and options.silent + local listname = options.listname or "main" + local inv_backup = player_inv:get_list(listname) + local hotovost_v_inv_pred = ch_core.vzit_hotovost(player_inv:get_list(listname)) or 0 + local hotovost = ch_core.hotovost(amount) + for _, stack in ipairs(hotovost) do + local remains = player_inv:add_item(listname, stack) + if not remains:is_empty() then + -- failure + player_inv:set_list(listname, inv_backup) + return false, "Plný inventář, platba v hotovosti se do něj nevejde." + end + end + local hotovost_v_inv_po = ch_core.vzit_hotovost(player_inv:get_list(listname)) or 0 + if hotovost_v_inv_po ~= hotovost_v_inv_pred + amount then + error("ERROR in cash_pay_to_player: pred="..hotovost_v_inv_pred..", po="..hotovost_v_inv_po..", amount="..amount) + end + if options.simulation then + player_inv:set_list(listname, inv_backup) + return true + end + if not silent then + minetest.log("action", "to "..player_name.." "..amount.." has been payed in cash") + end + return true +end + +ch_core.register_payment_method("cash", cash_pay_from_player, cash_pay_to_player) + +ch_core.close_submod("penize") diff --git a/ch_core/plaster.lua b/ch_core/plaster.lua new file mode 100644 index 0000000..1667b1a --- /dev/null +++ b/ch_core/plaster.lua @@ -0,0 +1,74 @@ +ch_core.open_submod("plaster", {}) +local colors = { + -- black = { +-- color = "222222", +-- description = "černá omítka", +-- }, + blue = { + color = "476092", + description = "modrá omítka", + }, + cyan = { + color = "77B39A", + description = "tyrkysová omítka", + }, + dark_green = { + color = "367342", + description = "tmavozelená omítka", + }, + dark_grey = { + color = "59534E", + description = "tmavě šedá omítka", + }, + grey = { + color = "ADACAA", + description = "šedá omítka", + }, + medium_amber_s50 = { + color = "BAA882", + description = "okrová omítka", + }, + orange = { + color = "FED2A3", + description = "oranžová omítka", + }, + pink = { + color = "FAC4B5", + description = "růžová omítka", + }, + red = { + color = "DD7156", + description = "červená omítka", + }, + green = { + -- color = "83E783", + color = "8FCE8D", + description = "zelená omítka", + }, + white = { + color = "FFFFFF", + description = "bílá omítka", + }, + yellow = { + color = "D9CD82", + description = "žlutá omítka", + }, +} + +for dye, data in pairs(colors) do + local def = { + description = data.description, + tiles = {"ch_core_clay.png^[multiply:#"..data.color}, + is_ground_content = false, + paramtype2 = "facedir", + groups = {cracky = 1, plaster = 1}, + sounds = default.node_sound_stone_defaults(), + } + core.register_node("ch_core:plaster_"..dye, def) + core.register_craft({output = "ch_core:plaster_"..dye, type = "shapeless", recipe = {"group:plaster", "dye:"..dye}}) +end +core.register_craft({ + output = "ch_core:plaster_grey 4", + type = "shapeless", + recipe = {"group:sand", "basic_materials:wet_cement", "default:clay", "default:clay"}}) +ch_core.close_submod("plaster", {}) diff --git a/ch_core/podnebi.lua b/ch_core/podnebi.lua new file mode 100644 index 0000000..c55799b --- /dev/null +++ b/ch_core/podnebi.lua @@ -0,0 +1,54 @@ +ch_core.open_submod("podnebi", {privs = true, chat = true}) + +local biomy = { + cold_desert = "bílá poušť", + coniferous_forest_dunes = "písek v jehličnatém lese", + coniferous_forest = "jehličnatý les", + deciduous_forest = "příjemný les", + deciduous_forest_shore = "pobřeží jehličnatého lesa", + desert = "poušť", + grassland_dunes = "písek u luk", + grassland = "louka", + icesheet = "ledovec", + rainforest_swamp = "močál", + rainforest = "tropický prales", + sandstone_desert = "pískovcová poušť", + savanna = "savana", + savanna_shore = "pobřeží savany", + snowy_grassland = "zimní louka", + taiga = "tajga", + tundra_beach = "pláž tundry", + tundra_highland = "vysočina", + tundra = "tundra", +} + +local def = { + description = "Vypíše údaje o podnebí na aktuální pozici", + privs = {server = true}, + func = function(player_name, param) + local player = minetest.get_player_by_name(player_name) + local player_pos = player and player:get_pos() + if not player_pos then + return false, "Vnitřní chyba serveru" + end + player_pos = vector.round(player_pos) + local info = minetest.get_biome_data(player_pos) + if not info then + return false, "Chybná pozice" + end + local biome_name, humidity, heat = minetest.get_biome_name(info.biome) or "unknown", info.heat or 0, info.humidity or 0 + if biomy[biome_name] ~= nil then + biome_name = biomy[biome_name] + elseif biome_name:sub(-6, -1) == "_under" and biomy[biome_name:sub(1, -7)] ~= nil then + biome_name = biomy[biome_name:sub(1, -7)].."/katakomby" + elseif biome_name:sub(-6, -1) == "_ocean" and biomy[biome_name:sub(1, -7)] ~= nil then + biome_name = biomy[biome_name:sub(1, -7)].."/voda" + end + ch_core.systemovy_kanal(player_name, minetest.pos_to_string(player_pos).." biom: "..biome_name..", průměrná teplota: "..math.round(heat / 100 * 50 - 5).." °C, vlhkost: "..math.round(humidity).." % (nemusí dávat smysl)") + return true + end, +} +minetest.register_chatcommand("podnebí", def) +minetest.register_chatcommand("podnebi", def) + +ch_core.close_submod("podnebi") diff --git a/ch_core/privs.lua b/ch_core/privs.lua new file mode 100644 index 0000000..3e2e2e8 --- /dev/null +++ b/ch_core/privs.lua @@ -0,0 +1,10 @@ +ch_core.open_submod("privs") +minetest.register_privilege("ch_events_moderator", "Rozšiřuje možnosti práce s oznámeními.") +-- právo značící registrovanou postavu +minetest.register_privilege("ch_registered_player", "Odlišuje registrované postavy od čerstvě založených.") +minetest.register_privilege("ch_trustful_player", "Poskytuje postavám důvěryhodných hráčů/ek výhody.") + +-- kouzelníci/ce nesmí vkládat do cizích inventářů +minetest.override_chatcommand("give", {privs = {give = true, protection_bypass = true, interact = true}}) + +ch_core.close_submod("privs") diff --git a/ch_core/pryc.lua b/ch_core/pryc.lua new file mode 100644 index 0000000..8523989 --- /dev/null +++ b/ch_core/pryc.lua @@ -0,0 +1,116 @@ +ch_core.open_submod("pryc", {data = true, events = true, lib = true, privs = true}) + +ch_core.register_event_type("pryc", { + description = "pryč od počítače", + access = "discard", + chat_access = "public", + default_text = "{PLAYER} jde pryč od počítače", +}) + +ch_core.register_event_type("zpet", { + description = "zpět u počítače", + access = "discard", + chat_access = "public", + default_text = "{PLAYER} je zpět u počítače", +}) + +local empty_table = {} + +local disrupt_pryc_silent = function(player, online_charinfo) + if not online_charinfo.pryc then + return false + end + online_charinfo.pryc = nil + + local player_name = player:get_player_name() + + -- remove HUD + local hud_id = online_charinfo.pryc_hud_id + if hud_id then + online_charinfo.pryc_hud_id = nil + player:hud_remove(hud_id) + end + -- remove titul + ch_core.set_temporary_titul(player_name, "pryč od počítače", false) + + return true +end + +local disrupt_pryc = function(player, online_charinfo) + if disrupt_pryc_silent(player, online_charinfo) then + -- announce + ch_core.add_event("zpet", nil, online_charinfo.player_name) + return true + else + return false + end +end + +function ch_core.je_pryc(player_name) + return ch_core.ifthenelse((ch_data.online_charinfo[player_name] or empty_table).pryc ~= nil, true, false) +end + +function ch_core.set_pryc(player_name, options) + local cod = ch_data.online_charinfo[player_name] + if not cod then + minetest.log("error", "Internal error: missing online_charinfo for character '"..player_name.."'!") + return false, "Interní chyba: chybí online_charinfo" + end + local player = minetest.get_player_by_name(player_name) + if not player then + minetest.log("error", "Internal error: missing player ref for character '"..player_name.."'!") + return false, "Interní chyba: chybí PlayerRef" + end + if cod.pryc then + minetest.log("warning", "Character '"..player_name.."' is already away from keyboard!") + return false, "Interní chyba: postava již je pryč od počítače!" + end + + if not options then + options = empty_table + end + local no_hud, silently = options.no_hud, options.silently + + if silently then + cod.pryc = disrupt_pryc_silent + else + cod.pryc = disrupt_pryc + end + + -- HUD + if not no_hud then + local hud_def = { + type = "image", + -- text = "ch_core_white_pixel.png^[invert:rgb^[opacity:192", + text = "ch_core_pryc.png", + position = { x = 0, y = 0 }, + scale = { x = -100, y = -100 }, + alignment = { x = 1, y = 1 }, + offset = { x = 0, y = 0}, + } + cod.pryc_hud_id = player:hud_add(hud_def) + end + + -- set titul + ch_core.set_temporary_titul(player_name, "pryč od počítače", true) + + -- announce + if not silently then + ch_core.add_event("pryc", nil, player_name) + end + + return true +end + +local def = { + privs = {}, + description = "", + func = function(player_name, param) + return ch_core.set_pryc(player_name, empty_table) + end +} +minetest.register_chatcommand("pop", def) +minetest.register_chatcommand("pryč", def) +minetest.register_chatcommand("pryc", def) + +ch_core.close_submod("pryc") diff --git a/ch_core/registrace.lua b/ch_core/registrace.lua new file mode 100644 index 0000000..d267d43 --- /dev/null +++ b/ch_core/registrace.lua @@ -0,0 +1,274 @@ +ch_core.open_submod("registrace", {chat = true, data = true, events = true, lib = true, nametag = true}) + +ch_core.register_event_type("reg_new", { + access = "admin", + description = "registrace (turistická postava)", + default_text = "nová postava {PLAYER} vstoupila do hry", + chat_access = "admin", +}) + +ch_core.register_event_type("reg_na_server", { + access = "public", + description = "přijetí na server", + chat_access = "public", + color = "#00ff00", +}) + +local survival_creative = {survival = true, creative = true} +local default_privs_to_reg_type = { + ch_registered_player = survival_creative, + creative = {new = true}, + fast = true, + give = {creative = true}, + hiking = survival_creative, + home = true, + interact = true, + peaceful_player = true, + railway_operator = survival_creative, + shout = true, + track_builder = survival_creative, + train_operator = survival_creative, +} +local reg_types = { + new = "turistická postava", + survival = "dělnický styl hry", + creative = "kouzelnický styl hry", +} +local function get_flashlight() + local stack = ItemStack("technic:flashlight") + stack:set_wear(1) + local meta = stack:get_meta() + meta:set_string("", "return {charge=30000}") + return stack +end +local function get_replacer() + local stack = ItemStack("replacer:replacer") + stack:set_wear(1) + local meta = stack:get_meta() + meta:set_string("", "return {charge=50000}") + return stack +end +local default_items = { + {stack = ItemStack("ch_extras:magic_wand"), creative = true}, + {stack = ItemStack("ch_extras:jumptool"), new = true, survival = true, creative = true}, + {stack = ItemStack("rotate:wrench_copper_cw"), survival = true, creative = true}, + {stack = get_replacer(), survival = true, creative = true}, + {stack = ItemStack("ch_extras:periskop"), new = true, survival = true, creative = true}, + {stack = ItemStack("ch_extras:sickle_steel"), survival = true}, + -- {stack = ItemStack("ch_extras:teleporter_unsellable"), new = true, survival = true}, + {stack = ItemStack("ch_extras:runa_navratu"), new = true, creative = true}, + {stack = ItemStack("advtrains_line_automation:jrad"), new = true}, + {stack = ItemStack("unified_inventory:bag_large"), survival = true, creative = true}, + {stack = ItemStack("bridger:scaffolding 100"), survival = true, creative = true}, + {stack = ItemStack("towercrane:base"), survival = true, creative = true}, + {stack = ItemStack("bike:bike"), new = true, survival = true, creative = false}, + {stack = ItemStack("anvil:hammer"), survival = true}, + --{stack = ItemStack("airtanks:bronze_tank"), survival = true, creative = true}, + {stack = ItemStack("ch_core:kcs_kcs 1000"), survival = true}, + {stack = ItemStack("ch_extras:lupa"), new = true, survival = true}, + {stack = ItemStack("orienteering:builder_compass_1"), survival = true, creative = true}, + {stack = ItemStack("basic_signs:vevystavbe"), survival = true, creative = true}, + {stack = ItemStack("ch_extras:teleporter_unsellable 100"), creative = true}, + {stack = ItemStack("binoculars:binoculars"), new = true}, + {stack = ItemStack("ch_core:chisel"), survival = true, creative = true}, + + {stack = get_flashlight(), min_index = 17, new = true, survival = true, creative = true}, + -- {stack = ItemStack("orienteering:map"), min_index = 18, new = true, survival = true, creative = true}, + {stack = ItemStack("orienteering:triangulator"), min_index = 18, survival = true, creative = true}, +} + +for i, item in ipairs(default_items) do + item.i = i + if item.min_index == nil then + item.min_index = 2 + end +end +table.sort(default_items, function(a, b) return a.min_index < b.min_index or (a.min_index == b.min_index and a.i < b.i) end) + +local function compute_initial_inventory(reg_type) + local empty_stack = ItemStack() + local initial_inventory = {} + local i_in = 1 + local item = default_items[1] + + for i_out = 1, 32 do + while item ~= nil and not item[reg_type] do + -- skip items not for the current reg_type + i_in = i_in + 1 + item = default_items[i_in] + end + if item == nil then + -- all items were assigned + break + end + if item.min_index > i_out then + initial_inventory[i_out] = empty_stack + else + initial_inventory[i_out] = item.stack + i_in = i_in + 1 + item = default_items[i_in] + end + end + return initial_inventory +end + +function ch_core.registrovat(player_name, reg_type, extra_privs) + if extra_privs == nil then + extra_privs = {} + elseif type(extra_privs) == "string" then + extra_privs = minetest.string_to_privs(extra_privs) + end + if type(extra_privs) ~= "table" then + error("Invalid extra_privs type: "..type(extra_privs)) + else + extra_privs = table.copy(extra_privs) + end + local offline_charinfo = ch_data.offline_charinfo[player_name] + if not offline_charinfo then + return false, "offline_charinfo not found!" + end + local reg_type_desc = reg_types[reg_type] + if not reg_type_desc then + return false, "unknown registration type "..reg_type + end + + -- compute privs + for priv, priv_setting in pairs(default_privs_to_reg_type) do + if priv_setting == true or (type(priv_setting) == "table" and priv_setting[reg_type]) then + extra_privs[priv] = true + end + end + + -- reset the bank account + if reg_type == "new" and ch_core.get_player_role(player_name) ~= "new" then + ch_core.overridable.reset_bank_account(player_name) + end + + print("Will compute initial inventory for reg_type \""..(reg_type or "nil").."\"") + local initial_inventory = compute_initial_inventory(reg_type) + print("Computed initial inventory for reg_type \""..(reg_type or "nil").."\": "..dump2({initial_inventory = initial_inventory, reg_type = reg_type})) + local player = minetest.get_player_by_name(player_name) + if not player then + return false, "the player is offline" + end + local inv = player:get_inventory() + local bags_inv = minetest.get_inventory({type = "detached", name = player_name.."_bags"}) + local empty_stack = ItemStack() + local old_lists = inv:get_lists() + minetest.log("action", "Will clear inventories of "..player_name..", old inventories: "..dump2(old_lists)) + old_lists.hand = nil + old_lists.main = nil + for inv_name, _ in pairs(old_lists) do + inv:set_list(inv_name, {}) + end + inv:set_list("main", initial_inventory) + local new_lists = inv:get_lists() + local bags_count = 0 + if bags_inv ~= nil then + for i = 1, 8 do + local bag = bags_inv:get_stack("bag"..i, 1) + if bag ~= nil and not bag:is_empty() then + bags_inv:set_stack("bag"..i, 1, empty_stack) + bags_count = bags_count + 1 + end + end + end + minetest.log("action", "Cleared inventories of "..player_name.." ("..bags_count.." bags), new inventories: "..dump2(new_lists)) + minetest.set_player_privs(player_name, extra_privs) + local new_privs = minetest.privs_to_string(minetest.get_player_privs(player_name)) + minetest.log("action", "Player "..player_name.." privs set to: "..new_privs) + local message = "Vaše registrace byla nastavena do režimu „"..reg_type_desc.."“." + if reg_type ~= "new" then + message = message.." Zkontrolujte si, prosím, svoje nová práva příkazem /práva." + end + ch_core.systemovy_kanal(player_name, message) + player:set_nametag_attributes(ch_core.compute_player_nametag(ch_data.online_charinfo[player_name], offline_charinfo)) + offline_charinfo.past_playtime = 0 + offline_charinfo.past_ap_playtime = 0 + offline_charinfo.ap_level = 1 + offline_charinfo.ap_xp = 0 + ch_data.save_offline_charinfo(player_name) + ch_core.ap_add(player_name, offline_charinfo) + if reg_type == "new" then + ch_core.add_event("reg_new", nil, player_name) + else + if reg_type == "survival" then + message = "dělnická postava {PLAYER} byla nově přijata na server" + elseif reg_type == "creative" then + message = "kouzelnická postava {PLAYER} byla nově přijata na server" + else + message = "postava {PLAYER} byla nově přijata na server ("..reg_type_desc..")" + end + ch_core.add_event("reg_na_server", message, player_name) + end + return true +end + +local function on_joinplayer(player_name) + local player = minetest.get_player_by_name(player_name) + if not player then + minetest.log("warning", "on_joinplayer: failed to fetch player object for registration of "..(player_name or "nil")) + end + ch_data.get_joining_online_charinfo(player) + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + if offline_charinfo.pending_registration_type == "" then + return + end + local result, error_message = ch_core.registrovat(player_name, offline_charinfo.pending_registration_type, offline_charinfo.pending_registration_privs) + if result then + offline_charinfo.pending_registration_privs = "" + offline_charinfo.pending_registration_type = "" + ch_data.save_offline_charinfo(player_name) + return + else + minetest.log("warning", "Registration of "..player_name.." failed: "..(error_message or "nil")) + end +end + +minetest.register_on_joinplayer(function(player, last_login) + local player_name = player:get_player_name() + local online_charinfo = ch_data.get_joining_online_charinfo(player) or {} + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + if offline_charinfo.pending_registration_type ~= "" and online_charinfo.news_role ~= "disconnect" and online_charinfo.news_role ~= "invalid_name" then + minetest.after(0.5, on_joinplayer, player_name) + end +end) + +local def = { + params = "<new|survival|creative> <jméno_postavy> [extra_privs]", + description = "registrovat", + privs = {server = true}, + func = function(player_name, param) + local reg_type, player_to_register, extra_privs = string.match(param, "^(%S+) (%S+) (%S+)$") + if not reg_type then + reg_type, player_to_register = string.match(param, "^(%S+) (%S+)$") + end + if not reg_type then + return false, "Neplatné zadání!" + end + player_to_register = ch_core.jmeno_na_prihlasovaci(player_to_register) + local offline_charinfo = ch_data.offline_charinfo[player_to_register] + if not offline_charinfo then + return false, "Postava "..player_to_register.." neexistuje!" + end + local online_charinfo = ch_data.online_charinfo[player_to_register] + if online_charinfo then + -- register immediately + local result, err_text = ch_core.registrovat(player_to_register, reg_type, extra_privs or "") + if result then + return true, "Postava registrována." + else + return false, "Registrace selhala; "..err_text + end + else + offline_charinfo.pending_registration_type = reg_type + offline_charinfo.pending_registration_privs = extra_privs or "" + ch_data.save_offline_charinfo(player_to_register) + return true, "Registrace naplánována." + end + end, +} + +minetest.register_chatcommand("registrovat", def) + +ch_core.close_submod("registrace") diff --git a/ch_core/rotation.lua b/ch_core/rotation.lua new file mode 100644 index 0000000..ff56501 --- /dev/null +++ b/ch_core/rotation.lua @@ -0,0 +1,272 @@ +ch_core.open_submod("rotation") + +--[[ +Funkce ch_rotation může být v definici každého bloku (node) a měla by být +použita při pokusu o otáčení bloku namísto on_rotate nebo jiných metod. +Musí podporovat dvě formy volání: + +1. ch_rotation(pos, node) +- vždy vrací tabulku, jednu z těchto tří forem: +a) {type = "none"} +b) {type = "facedir", facedir = int(>= 0 && <= 23), extra_data = int(>= 0 && <= 255)} +c) {type = "degrotate", degrotate = int(>=0 && <= 239), step = int(1..120), extra_data = int(>= 0 && <= 255)} + +2. ch_rotation(pos, node, rotation [, simulation = bool (default = false)]) +- pokusí se otočit blok na dané pozici podle požadavku; + pokud uspěje, nastaví také obsah "node" a "rotation" na hodnoty odpovídající + novému natočení bloku (které nemusejí přesně odpovídat požadavku) +- vrací true v případě úspěchu +- v případě selhání vrací false a "node" a "rotation" zůstávají beze změny +- rotation je tabulka ve formátu, který vrací předchozí forma funkce +- je-li simulation true, ve skutečnosti blok neotočí, ale provede plnou + simulaci (včetně např. volání metod can_dig sousedních bloků, je-li to potřeba) + +]] + +function ch_core.ch_rotation_facedir(pos, node, rotation, simulation) + assert(pos) + assert(node) + if rotation == nil then + local facedir = node.param2 % 32 + return {type = "facedir", facedir = math.min(facedir, 23), extra_data = node.param2 - facedir} + end + if rotation.type ~= "facedir" or rotation.facedir < 0 or rotation.facedir > 23 then return false end + node.param2 = rotation.facedir + rotation.extra_data + if not simulation then + minetest.swap_node(pos, node) + end + return true +end + +function ch_core.ch_rotation_4dir(pos, node, rotation, simulation) + assert(pos) + assert(node) + if rotation == nil then + local facedir = node.param2 % 4 + return {type = "facedir", facedir = facedir, extra_data = node.param2 - facedir} + end + if rotation.type ~= "facedir" or rotation.facedir < 0 or rotation.facedir > 3 then return false end + node.param2 = rotation.facedir + rotation.extra_data + if not simulation then + minetest.swap_node(pos, node) + end + return true +end + +local facedir_to_wallmounted = { + [0] = 4, [1] = 2, [2] = 5, [3] = 3, + [4] = 1, [5] = 2, [6] = 0, [7] = 3, + [8] = 0, [9] = 2, [10] = 1, [11] = 3, + [12] = 4, [13] = 1, [14] = 5, [15] = 0, + [16] = 4, [17] = 0, [18] = 5, [19] = 1, + [20] = 4, [21] = 3, [22] = 5, [23] = 2, +} +local wallmounted_to_facedir = { + [0] = 6, [1] = 4, [2] = 1, [3] = 3, [4] = 0, [5] = 2, +} + +function ch_core.ch_rotation_wallmounted(pos, node, rotation, simulation) + assert(pos) + assert(node) + if rotation == nil then + local wm = node.param2 % 8 + return {type = "facedir", facedir = wallmounted_to_facedir[math.min(wm, 5)] , extra_data = node.param2 - wm} + end + if rotation.type ~= "facedir" or facedir_to_wallmounted[rotation.facedir] == nil then return false end + node.param2 = facedir_to_wallmounted[rotation.facedir] + rotation.extra_data + if not simulation then + minetest.swap_node(pos, node) + end + return true +end + +function ch_core.ch_rotation_degrotate(pos, node, rotation, simulation) + assert(pos) + assert(node) + if rotation == nil then + local degrotate = node.param2 + if degrotate >= 240 then + degrotate = 239 + end + return {type = "degrotate", degrotate = degrotate, extra_data = 0} + end + if rotation.type ~= "degrotate" or rotation.degrotate < 0 or rotation.degrotate > 239 then return false end + node.param2 = rotation.degrotate + if not simulation then + minetest.swap_node(pos, node) + end + return true +end + +local function ch_rotation_4dir_generated(pos, node, rotation, simulation) + local dir = tonumber(string.sub(node.name, -1, -1)) + if rotation == nil then + if dir == nil or dir < 0 or dir > 3 then return {type = "none"} end + return {type = "facedir", facedir = dir, extra_data = node.param2} + end + if rotation.type ~= "facedir" or rotation.facedir < 0 or rotation.facedir > 3 or + dir == nil or dir < 0 or dir > 3 then return false end + node.name = node.name:sub(1, -2)..rotation.facedir + node.param2 = rotation.extra_data + if not simulation then + minetest.swap_node(pos, node) + end + return true +end + +local function transfer_value(from_v, from_i, to_v, to_i) + -- example: + -- transfer_value(from_vector, "+x", to_vector, "-z") + if #from_i ~= 2 or #to_i ~= 2 then + error("transfer_value(): invalid input: "..dump2({from_v = from_v, from_i = from_i, to_v = to_v, to_i = to_i})) + end + local n = from_v[from_i:sub(2, 2)] + if to_i:sub(1,1) ~= from_i:sub(1,1) then + n = -n + end + to_v[to_i:sub(2,2)] = n + return to_v +end + +local function rotate_aabb_0(aabb) + return +end + +local function rotate_aabb_1(aabb) + local o1, o3, o4, o6 = aabb[3], aabb[2], -aabb[4], aabb[6], -aabb[1] + aabb[1] = o1 + aabb[3] = o3 + aabb[4] = o4 + aabb[6] = o6 +end + +local function rotate_tiles_0(tiles) + return tiles +end + +--[[ +tiles: + +Y -Y +X -X +Z -Z +]] +local function rotate_tiles_1(tiles) + return { + tiles[1], + tiles[2], + tiles[5], + tiles[6], + tiles[4], + tiles[3], + } +end + +local function rotate_tiles_2(tiles) + return rotate_tiles_1(rotate_tiles_1(tiles)) +end + +local function rotate_tiles_3(tiles) + return rotate_tiles_1(rotate_tiles_1(rotate_tiles_1(tiles))) +end + +local function rotate_aabb_2(aabb) + rotate_aabb_1(aabb) + rotate_aabb_1(aabb) +end + +local function rotate_aabb_3(aabb) + rotate_aabb_1(aabb) + rotate_aabb_1(aabb) + rotate_aabb_1(aabb) +end + +local n_to_rotate = { + [0] = rotate_aabb_0, + [1] = rotate_aabb_1, + [2] = rotate_aabb_2, + [3] = rotate_aabb_3, +} + +local n_to_rotate_tiles = { + [0] = rotate_tiles_0, + [1] = rotate_tiles_1, + [2] = rotate_tiles_2, + [3] = rotate_tiles_3, +} + +local function get_rotated_node_box(fourdir, nodebox) + if nodebox.type ~= "fixed" then + return false + end + local rotate = assert(n_to_rotate[fourdir]) + local old_fixed = nodebox.fixed + local new_fixed + if type(old_fixed[1]) == "table" then + new_fixed = {} + for i, aabb in ipairs(old_fixed) do + local new_aabb = table.copy(aabb) + rotate(new_aabb) + new_fixed[i] = new_aabb + end + else + new_fixed = table.copy(old_fixed) + rotate(new_fixed) + end + return {type = "fixed", fixed = new_fixed} +end + +local function get_rotated_tiles(fourdir, tiles) + tiles = table.copy(tiles) + while #tiles < 6 do + tiles[#tiles + 1] = tiles[#tiles] + end + return n_to_rotate_tiles[fourdir](tiles) +end + +function ch_core.register_4dir_nodes(nodename_prefix, options, common_def, o0, o1, o2, o3) + local overrides = {[0] = o0, [1] = o1, [2] = o2, [3] = o3} + for i = 0, 3 do + local def = table.copy(common_def) + def.ch_rotation = ch_rotation_4dir_generated + if options.tiles then + local new_tiles = get_rotated_tiles(i, def.tiles) + if new_tiles ~= nil then + def.tiles = new_tiles + end + end + if options.node_box and def.node_box ~= nil then + local new_node_box = get_rotated_node_box(i, def.node_box) + if new_node_box ~= nil then + def.node_box = new_node_box + end + end + if options.selection_box and def.selection_box ~= nil then + local new_selection_box = get_rotated_node_box(i, def.selection_box) + if new_selection_box ~= nil then + def.selection_box = new_selection_box + end + end + if options.collision_box then + local new_collision_box = get_rotated_node_box(i, def.collision_box) + if new_collision_box ~= nil then + def.collision_box = new_collision_box + end + end + if options.drop and i ~= 0 then + def.drop = nodename_prefix.."0" + end + if common_def.groups ~= nil then + def.groups = table.copy(common_def.groups) + else + def.groups = {} + end + if i ~= 0 then + def.groups.not_in_creative_inventory = 1 + end + for k, v in pairs(overrides[i]) do + def[k] = v + end + minetest.register_node(nodename_prefix..i, def) + end + return true +end + +ch_core.close_submod("rotation") diff --git a/ch_core/shape_selector.lua b/ch_core/shape_selector.lua new file mode 100644 index 0000000..03b4297 --- /dev/null +++ b/ch_core/shape_selector.lua @@ -0,0 +1,432 @@ +ch_core.open_submod("shape_selector", {chat = true, formspecs = true, lib = true}) + +local ifthenelse = ch_core.ifthenelse +local F = minetest.formspec_escape + +local item_to_shape_selector_group = {} + +local function get_group_size(group) + if group.rows ~= nil and group.columns ~= nil then + return group.columns, group.rows + end + local count = #group.nodes + if group.rows ~= nil then + return math.ceil(count / group.rows), group.rows + elseif group.columns ~= nil then + return group.columns, math.ceil(count / group.columns) + else + if count <= 8 then + return count, 1 + else + return 8, math.ceil(count / 8) + end + end +end + +--[[ + Shape selector group definition: + { + -- povinné položky: + nodes = {nodespec, ...}, -- seznam bloků ve skupině; musí být sekvence, s výjimkou případu, že jsou + -- uvedeny obě vlastnosti columns a rows + -- volitelné položky: + columns = int, -- počet sloupců ve formspecu (>= 1), + rows = int, -- počet řádek ve formspecu (>= 1), + check_owner = bool, -- je-li true a má-li blok meta:get_string("owner"), povolí změnu jen + -- vlastníkovi/ici a postavám s právem protection_bypass + on_change = function(pos, old_node, new_node, player, nodespec), + -- callback volaný pro provedení změny (je-li nastaven); vrátí-li false, změna selhala + after_change = function(pos, old_node, new_node, player, nodespec), -- callback, který bude zavolaný *po* provedení změny + input_only = {name, ...}, -- seznam dalších bloků, které mohou být změněny na varianty této skupiny, + -- ale ne naopak; mohou být zadány pouze jmény bloků + } + + Každý nodespec může být: + - string (název bloku; není-li registrován, pozice bude vynechána) + - table: + { + -- povinné položky: + name = string, -- název bloku + -- volitelné položky: + param2 = 0..255 + 256 * (bitová maska 0..255), + -- základní hodnota udává hodnotu pro nastavení do param2; + -- maska udává, které bity se přímo nastaví podle spodního bajtu (bity 0) + -- a které se sloučí ze spodního bajtu a původní hodnoty param2 operací xor (bity 1); + -- není-li zadána, ponechá se původní hodnota param2 (odpovídá zadání 0xFF00) + oneway = bool, -- je-li true, zadaný blok se nebude registrovat do skupiny a konverze bude + -- prezentováno jako jednosměrná; rovněž se nepoužije při rozpoznávání + tooltip = string, -- vlastní text pro tooltip[] + label = string, -- vlastní text na tlačítko (musí být krátký, jinak nevypadá dobře) + } + - nil (jen pokud má skupina uvedeny obě vlastnosti 'columns' a 'rows') + + TODO: + [x] plná podpora pro param2 s maskou + [x] podpora pro 'tooltip' (je-li možná) + [x] podpora pro 'oneway' + [x] podpora pro barvené bloky (jako ch_extras:dice) + [x] rozpoznání současné varianty +]] + +function ch_core.register_shape_selector_group(def) + local has_columns = type(def.columns) == "number" + local has_rows = type(def.rows) == "number" + if type(def.nodes) ~= "table" then + error("Invalid type(def.nodes): "..type(def.nodes)) + end + if has_columns and def.columns < 1 then + error("Invalid number of columns: "..def.columns) + end + if has_rows and def.rows < 1 then + error("Invalid number of rows: "..def.rows) + end + + local new_group = {nodes = def.nodes} + if has_columns then new_group.columns = def.columns end + if has_rows then new_group.rows = def.rows end + local columns, rows = get_group_size(new_group) + local count = columns * rows + if def.check_owner then + new_group.check_owner = true + end + if def.after_change ~= nil then + new_group.after_change = def.after_change + end + if def.on_change ~= nil then + new_group.on_change = def.on_change + end + local new_nodes = {} + for i = 1, count do + local nodespec = def.nodes[i] + if nodespec ~= nil then + local name + if type(nodespec) ~= "table" then + name = nodespec + elseif not nodespec.oneway then + name = nodespec.name + end + if name ~= nil and new_nodes[name] == nil then + new_nodes[name] = true + if item_to_shape_selector_group[name] ~= nil then + error(name.." already has registered the shape selector!") + end + end + end + end + if def.input_only ~= nil then + for _, name in ipairs(def.input_only) do + if type(name) ~= "string" then + error("Invalid type of input_only member!") + end + if new_nodes[name] == nil then + new_nodes[name] = true + if item_to_shape_selector_group[name] ~= nil then + error(name.." already has registered the shape selector!") + end + end + end + end + -- local new_nodes_count = 0 + for name, _ in pairs(new_nodes) do + item_to_shape_selector_group[name] = new_group + -- new_nodes_count = new_nodes_count + 1 + if core.registered_nodes[name] == nil then + core.log("warning", name.." is used in a shape selector group, but it is an unknown node!") + end + end +end + +local function process_param2(current_param2, param2_spec) + if param2_spec == nil then + return current_param2 + end + local old_mask = bit.rshift(param2_spec, 8) + local new_mask = bit.bxor(old_mask, 0xFF) + return bit.bxor(bit.band(old_mask, current_param2), bit.band(new_mask, param2_spec)) +end + +--[[ +local function get_group_count(group) + local columns, rows = get_group_size(group) + return columns * rows +end +]] + +local function check_owner(pos, player_name) + if core.check_player_privs(player_name, "protection_bypass") then + return true + end + local owner = core.get_meta(pos):get_string("owner") + return owner == player_name or owner == "" +end + +local function record_owner_violation(pos, player_name) + ch_core.systemovy_kanal(player_name, "Tento blok patří postavě '".. + ch_core.prihlasovaci_na_zobrazovaci(core.get_meta(pos):get_string("owner")).."'!") +end + +local function formspec_callback(custom_state, player, formname, fields) + local player_name = player:get_player_name() + local inv = player:get_inventory() + local group = custom_state.group + local pos = custom_state.pos + local old_node = custom_state.old_node + local current_node = core.get_node(pos) + for k, _ in pairs(fields) do + if k:match("^chg_%d+$") then + local i = tonumber(k:sub(5, -1)) + local new_node_spec = group.nodes[i] + if new_node_spec ~= nil then + if core.is_protected(pos, player_name) then + core.record_protection_violation(pos, player_name) + return + end + if group.check_owner and not check_owner(pos, player_name) then + record_owner_violation(pos, player_name) + return + end + if old_node.name ~= current_node.name or old_node.param2 ~= current_node.param2 then + ch_core.systemovy_kanal(player_name, "Blok se změnil během výběru! Zkuste to znovu.") + return + end + local new_node + if type(new_node_spec) == "table" then + new_node = {name = new_node_spec.name, param2 = process_param2(current_node.param2, new_node_spec.param2)} + else + new_node = {name = new_node_spec, param2 = current_node.param2} + end + local change_node = new_node.name ~= current_node.name or new_node.param2 ~= current_node.param2 + if change_node then + if custom_state.wielded_item ~= nil and not core.is_creative_enabled(player_name) then + local expected_item = custom_state.wielded_item + local wielded_item = player:get_wielded_item() + if wielded_item:get_name() ~= expected_item:get_name() or wielded_item:get_wear() ~= expected_item:get_wear() then + ch_core.systemovy_kanal(player_name, "Dláto se změnilo během výběru! Zkuste to znovu.") + return + end + wielded_item:add_wear_by_uses(200) + player:set_wielded_item(wielded_item) + end + local change_result + if group.on_change ~= nil then + change_result = group.on_change(pos, old_node, + {name = new_node.name, param = old_node.param, param2 = new_node.param2}, player, new_node_spec) + end + if change_result == nil then + core.swap_node(pos, new_node) + elseif change_result == false then + change_node = false + end + end + fields.quit = "true" + core.close_formspec(player_name, formname) + inv:set_size("ch_shape_selector", 1) + inv:set_stack("ch_shape_selector", 1, ItemStack()) + if change_node and group.after_change ~= nil then + group.after_change(pos, old_node, core.get_node(pos), player, new_node_spec) + end + return + end + end + end +end + +function ch_core.show_shape_selector(player, pos, node, wielded_item) + local group = item_to_shape_selector_group[assert(node.name)] + if group == nil then + return false -- no selector group + end + local inv = player:get_inventory() + local columns, rows = get_group_size(group) + local count = columns * rows + local nodes = assert(group.nodes) + inv:set_list("ch_shape_selector", {}) + inv:set_size("ch_shape_selector", count) + local current_index + for i = 1, count do + local nodespec = nodes[i] + if nodespec ~= nil then + local node_name + if type(nodespec) ~= "table" then + node_name = nodespec + nodespec = {name = node_name, param2 = 0xFF00} + else + node_name = nodespec.name + end + local node_def = core.registered_nodes[node_name] + if node_def ~= nil then + local stack = ItemStack(node_name) + local meta = stack:get_meta() + local new_param2 = process_param2(node.param2, nodespec.param2) + if node_def.palette ~= nil and type(node_def.paramtype2) == "string" then + local palette_idx = core.strip_param2_color(new_param2, node_def.paramtype2) + if palette_idx ~= nil then + meta:set_int("palette_index", palette_idx) + end + end + if nodespec.label ~= nil then + meta:set_int("count_alignment", 14) -- middle bottom + meta:set_string("count_meta", nodespec.label) + end + inv:set_stack("ch_shape_selector", i, stack) + if current_index == nil and node_name == node.name and new_param2 == node.param2 then + current_index = i + end + end + end + end + + local width = 0.75 + 1.25 * math.max(4, columns) + local height = 1.5 + 1.25 * math.max(2, rows) + local formspec = { + ch_core.formspec_header({ + formspec_version = 6, + size = {width, height}, + listcolors = {"#00000000", "#00000000", "#00000000"}, + auto_background = true}), + "label[0.5,0.5;Změnit tvar či variantu]", + "button_exit["..(width - 0.975)..",0.25;0.5,0.5;close;X]", + "style_type[item_image_button;border=false;content_offset=(1024,1024)]", + } + local formspec2 = { + "list[current_player;ch_shape_selector;0.5,1;"..columns..","..rows..";]", + } + for row = 1, rows do + for col = 1, columns do + local i = columns * (row - 1) + col + local x, y = 1.25 * col - 0.75, 1.25 * row - 0.25 + local nodespec = nodes[i] + local nodespec_type = type(nodespec) + local node_name + if nodespec_type == "table" then + node_name = nodespec.name + else + node_name = nodespec + end + if node_name ~= nil and core.registered_nodes[node_name] ~= nil then + assert(nodespec ~= nil) + if current_index ~= nil and current_index == i then + table.insert(formspec, ("box[%f,%f;1.2,1.2;#00ff00]"):format(x - 0.1, y - 0.1)) + end + table.insert(formspec, ("image_button[%f,%f;1,1;blank.png;%s;]"):format(x, y, "chg_"..i.."_bg")) + table.insert(formspec2, ("item_image_button[%f,%f;1,1;%s;%s;]"):format(x, y, F(node_name), "chg_"..i)) + if nodespec_type == "table" then + if nodespec.tooltip ~= nil then + table.insert(formspec2, "tooltip[chg_"..i..";"..F(nodespec.tooltip).."]") + end + if nodespec.oneway then + table.insert(formspec2, ("vertlabel[%f,%f;↮]"):format(x + 0.2, y - 0.05)) + end + end + end + end + end + local custom_state = { + pos = assert(pos), + old_node = node, + nodes = nodes, + group = group, + } + if wielded_item ~= nil then + custom_state.wielded_item = wielded_item + end + formspec = table.concat(formspec)..table.concat(formspec2) + ch_core.show_formspec(player, "ch_core:shape_selector", formspec, formspec_callback, custom_state, {}) + return true +end + +function ch_core.get_shape_selector_group(node_name) + local group = item_to_shape_selector_group[assert(node_name)] + if group ~= nil then + return group.nodes, group.input_only + else + return nil, nil + end +end + +function ch_core.fill_shape_selector_equals_set(set, node_name) + local group = item_to_shape_selector_group[assert(node_name)] + if group == nil then + return false + end + if set ~= nil then + local columns, rows = get_group_size(group) + local count = columns * rows + local nodes = assert(group.nodes) + for i = 1, count do + local nodespec = nodes[i] + if nodespec ~= nil then + if type(nodespec) ~= "table" then + -- string + set[nodespec] = true + elseif not nodespec.oneway then + set[nodespec.name] = true + end + end + end + end + return true +end + +local function chisel_on_use(itemstack, user, pointed_thing) + if user == nil or pointed_thing.type ~= "node" then + return -- player and node are required + end + local pos = pointed_thing.under + local node = core.get_node(pos) + local group = item_to_shape_selector_group[node.name] + if group == nil then + return + end + local player_name = user:get_player_name() + if core.is_protected(pos, player_name) then + core.record_protection_violation(pos, player_name) + return + end + if group.check_owner and not check_owner(pos, player_name) then + record_owner_violation(pos, player_name) + return + end + ch_core.show_shape_selector(user, pos, node, itemstack) +end + +local def = { + description = "dláto", + inventory_image = "ch_core_chisel.png", + wield_image = "ch_core_chisel.png", + groups = {tool = 1}, + on_use = chisel_on_use, + _ch_help = "Levým klikem na podporované bloky můžete změnit jejich tvar nebo variantu,\nněkdy i barvu.", +} + +core.register_tool("ch_core:chisel", def) +core.register_craft({ + output = "ch_core:chisel", + recipe = { + {"default:steel_ingot", "", ""}, + {"default:steel_ingot", "", ""}, + {"default:stick", "", ""}, + }, +}) + +local function allow_player_inventory_action(player, action, inventory, inventory_info) + if action == "move" then + return ifthenelse( + inventory_info.from_list ~= "ch_shape_selector" and inventory_info.to_list ~= "ch_shape_selector", + inventory_info.count, + 0) + elseif action == "put" or action == "take" then + return ifthenelse(inventory_info.listname ~= "ch_shape_selector", inventory_info.stack:get_count(), 0) + end +end +local function on_joinplayer(player, last_login) + local inv = player:get_inventory() + if inv:get_size("ch_shape_selector") == 0 then + inv:set_size("ch_shape_selector", 1) + end +end + +core.register_allow_player_inventory_action(allow_player_inventory_action) +core.register_on_joinplayer(on_joinplayer) + +ch_core.close_submod("shape_selector") diff --git a/ch_core/shapes_db.lua b/ch_core/shapes_db.lua new file mode 100644 index 0000000..a2814ca --- /dev/null +++ b/ch_core/shapes_db.lua @@ -0,0 +1,1332 @@ +ch_core.open_submod("shapes_db", {lib = true}) + +local assembly_groups = ch_core.assembly_groups +local ifthenelse = ch_core.ifthenelse + +local function set(...) + local i, result, t = 1, {}, {...} + while t[i] ~= nil do + if type(t[i]) == "table" then + for k, v in pairs(t[i]) do + result[k] = v + end + else + result[t[i]] = true + end + i = i + 1 + end + return result +end + +local function union(...) + local i, result, t = 1, {}, {...} + while t[i] ~= nil do + for k, v in pairs(t[i]) do + result[k] = v + end + i = i + 1 + end + return result +end + +local materials_kp = set( + -- základní seznam materiálů na kotoučovou pilu (budou povoleny obvyklé tvary) +"artdeco:1a", +"artdeco:1b", +"artdeco:1c", +"artdeco:1d", +"artdeco:1e", +"artdeco:1f", +"artdeco:1g", +"artdeco:1h", +"artdeco:1i", +"artdeco:1j", +"artdeco:1k", +"artdeco:1l", +"artdeco:2a", +"artdeco:2b", +"artdeco:2c", +"artdeco:2d", +"artdeco:tile1", +"artdeco:tile2", +"artdeco:tile3", +"artdeco:tile4", +"artdeco:tile5", +"artdeco:brownwalltile", +"artdeco:greenwalltile", +"artdeco:ceilingtile", +"artdeco:decoblock1", +"artdeco:decoblock2", +"artdeco:decoblock3", +"artdeco:decoblock4", +"artdeco:decoblock5", +"artdeco:decoblock6", +"artdeco:stonewall", +"bakedclay:black", +"bakedclay:blue", +"bakedclay:brown", +"bakedclay:cyan", +"bakedclay:dark_green", +"bakedclay:dark_grey", +"bakedclay:green", +"bakedclay:grey", +"bakedclay:magenta", +"bakedclay:natural", +"bakedclay:orange", +"bakedclay:pink", +"bakedclay:red", +"bakedclay:violet", +"bakedclay:white", +"bakedclay:yellow", +"basic_materials:brass_block", +"basic_materials:cement_block", +"basic_materials:concrete_block", +"bridger:block_green", +"bridger:block_red", +"bridger:block_steel", +"bridger:block_white", +"bridger:block_yellow", +"building_blocks:Adobe", +"building_blocks:Tar", +"building_blocks:fakegrass", +"ch_core:plaster_blue", +"ch_core:plaster_cyan", +"ch_core:plaster_dark_green", +"ch_core:plaster_dark_grey", +"ch_core:plaster_green", +"ch_core:plaster_grey", +"ch_core:plaster_medium_amber_s50", +"ch_core:plaster_orange", +"ch_core:plaster_pink", +"ch_core:plaster_red", +"ch_core:plaster_white", +"ch_core:plaster_yellow", +"ch_extras:marble", +"ch_extras:particle_board_grey", +"ch_extras:scorched_tree", +"ch_extras:scorched_tree_noface", +"cottages:black", +"cottages:brown", +"cottages:red", +"cottages:reet", +"darkage:basalt", +"darkage:basalt_brick", +"darkage:basalt_cobble", +"darkage:chalk", +"darkage:glass", +"darkage:glow_glass", +"darkage:gneiss", +"darkage:gneiss_brick", +"darkage:gneiss_cobble", +"darkage:marble", +"darkage:marble_tile", +"darkage:ors", +"darkage:ors_brick", +"darkage:ors_cobble", +"darkage:schist", +"darkage:serpentine", +"darkage:slate", +"darkage:slate_brick", +"darkage:slate_cobble", +"darkage:slate_tile", +"darkage:stone_brick", +"default:acacia_wood", +"default:aspen_wood", +"default:brick", +"default:bronzeblock", +"default:cobble", +"default:copperblock", +"default:coral_skeleton", +"default:desert_cobble", +"default:desert_sandstone", +"default:desert_sandstone_block", +"default:desert_sandstone_brick", +"default:desert_stone", +"default:desert_stone_block", +"default:desert_stonebrick", +"default:dirt", +"default:goldblock", +"default:ice", +"default:junglewood", +"default:meselamp", +"default:obsidian", +"default:obsidian_block", +"default:obsidianbrick", +"default:pine_wood", +"default:silver_sandstone", +"default:silver_sandstone_block", +"default:silver_sandstone_brick", +"default:steelblock", +"default:stone", +"default:stone_block", +"default:stonebrick", +"default:wood", +"moreblocks:acacia_tree_noface", +"moreblocks:aspen_tree_noface", +"moreblocks:cactus_brick", +"moreblocks:cactus_checker", +"moreblocks:checker_stone_tile", +"moreblocks:circle_stone_bricks", +"moreblocks:coal_checker", +"moreblocks:coal_stone", +"moreblocks:coal_stone_bricks", +"moreblocks:cobble_compressed", +"moreblocks:copperpatina", +"moreblocks:desert_cobble_compressed", +"moreblocks:grey_bricks", +"moreblocks:iron_checker", +"moreblocks:iron_stone", +"moreblocks:iron_stone_bricks", +"moreblocks:jungletree_allfaces", +"moreblocks:jungletree_noface", +"moreblocks:pine_tree_noface", +"moreblocks:plankstone", +"moreblocks:stone_tile", +"moreblocks:tree_allfaces", +"moreblocks:tree_noface", +"moreblocks:wood_tile", +"moreblocks:wood_tile_center", +"moreblocks:wood_tile_full", +"moretrees:apple_tree_planks", +"moretrees:apple_tree_trunk_noface", +"moretrees:birch_planks", +"moretrees:birch_trunk_noface", +"moretrees:cedar_planks", +"moretrees:cedar_trunk_noface", +"moretrees:cherrytree_planks", +"moretrees:cherrytree_trunk_noface", +"moretrees:chestnut_tree_planks", +"moretrees:chestnut_tree_trunk_noface", +"moretrees:date_palm_planks", +"moretrees:date_palm_trunk_noface", +"moretrees:ebony_planks", +"moretrees:ebony_trunk_noface", +"moretrees:fir_planks", +"moretrees:fir_trunk_noface", +"moretrees:oak_planks", +"moretrees:oak_trunk_noface", +"moretrees:palm_trunk_noface", +"moretrees:plumtree_planks", +"moretrees:plumtree_trunk_noface", +"moretrees:poplar_planks", +"moretrees:poplar_trunk_noface", +"moretrees:rubber_tree_planks", +"moretrees:rubber_tree_trunk_noface", +"moretrees:sequoia_planks", +"moretrees:sequoia_trunk_noface", +"moretrees:spruce_planks", +"moretrees:spruce_trunk_noface", +"moretrees:willow_planks", +"moretrees:willow_trunk_noface", +"streets:asphalt_blue", +"streets:asphalt_red", +"summer:granite", +"summer:graniteA", +"summer:graniteB", +"summer:graniteBC", +"summer:graniteP", +"summer:graniteR", +"technic:blast_resistant_concrete", +"technic:carbon_steel_block", +"technic:cast_iron_block", +"technic:granite", +"technic:marble", +"technic:marble_bricks", +"technic:warning_block", +"technic:zinc_block", +"xdecor:wood_tile" +) + +local materials_no_kp = set( + -- materiály vyloučené z kotoučové pily (nebudou povoleny žádné tvary): + "moretrees:palm_planks" +) + +local materials_glass = set( + -- sklo (kromě středověkého): + "building_blocks:smoothglass", + "building_blocks:woodglass", + "default:glass", + "default:obsidian_glass", + "moreblocks:clean_glass", + "moreblocks:clean_glow_glass", + "moreblocks:clean_super_glow_glass", + "moreblocks:glow_glass", + "moreblocks:super_glow_glass" +) + +local materials_crumbly = set( + -- sypké materiály (např. písek, hlína, štěrk): + "artdeco:whitegardenstone", + "charcoal:charcoal_block", + "ch_extras:bright_gravel", + "ch_extras:railway_gravel", + "default:coalblock", + "default:desert_sand", + "default:dirt_with_coniferous_litter", + "default:dirt_with_grass", + "default:dirt_with_rainforest_litter", + "default:dry_dirt", + "default:dry_dirt_with_dry_grass", + "default:gravel", + "default:sand", + "default:silver_sand", + "default:snowblock", + "farming:hemp_block", + "farming:straw", + "summer:sabbia_mare" +) + +local materials_sns = set( + -- materiály s omezeným sortimentem tvarů: + "mobs:honey_block", + "moretrees:ebony_trunk_allfaces", + "moretrees:poplar_trunk_allfaces" +) + +local materials_wool = set( + -- vlna (+ split_stone_tile) + "wool:black", + "wool:blue", + "wool:brown", + "wool:cyan", + "wool:dark_green", + "wool:dark_grey", + "wool:green", + "wool:grey", + "wool:magenta", + "wool:orange", + "wool:pink", + "wool:red", + "wool:violet", + "wool:white", + "wool:yellow", + "moreblocks:split_stone_tile" -- the same shapes as wool +) + +local materials_cnc = set( + -- materiály na kotoučovou pilu +"bakedclay:black", +"bakedclay:blue", +"bakedclay:brown", +"bakedclay:cyan", +"bakedclay:dark_green", +"bakedclay:dark_grey", +"bakedclay:green", +"bakedclay:grey", +"bakedclay:magenta", +"bakedclay:natural", +"bakedclay:orange", +"bakedclay:pink", +"bakedclay:red", +"bakedclay:violet", +"bakedclay:white", +"bakedclay:yellow", +"basic_materials:brass_block", +"basic_materials:cement_block", +"basic_materials:concrete_block", +"building_blocks:fakegrass", +"cottages:black", +"cottages:brown", +"cottages:red", +"cottages:reet", +"darkage:slate_tile", +"default:acacia_wood", +"default:aspen_wood", +"default:brick", +"default:bronzeblock", +"default:cobble", +"default:copperblock", +"default:desert_cobble", +"default:desert_sandstone", +"default:desert_sandstone_block", +"default:desert_sandstone_brick", +"default:desert_stone", +"default:desert_stone_block", +"default:desert_stonebrick", +"default:dirt", +"default:glass", +"default:goldblock", +"default:ice", +"default:junglewood", +"default:leaves", +"default:meselamp", +"default:obsidian_block", +"default:obsidian_glass", +"default:pine_wood", +"default:silver_sandstone", +"default:silver_sandstone_block", +"default:silver_sandstone_brick", +"default:steelblock", +"default:stone", +"default:stone_block", +"default:stonebrick", +"default:tree", +"default:wood", +"farming:straw", +"moreblocks:cactus_brick", +"moreblocks:cactus_checker", +"moreblocks:copperpatina", +"moreblocks:glow_glass", +"moreblocks:grey_bricks", +"moreblocks:super_glow_glass", +"technic:blast_resistant_concrete", +"technic:cast_iron_block", +"technic:granite", +"technic:marble", +"technic:warning_block", +"technic:zinc_block" +) + +local materials_pillars = set( + -- materiály na pilíře + "bakedclay:black", + "basic_materials:brass_block", + "basic_materials:cement_block", + "basic_materials:concrete_block", + "bridger:block_white", + "bridger:block_steel", + "bridger:block_red", + "bridger:block_green", + "bridger:block_yellow", + "ch_core:plaster_blue", + "ch_core:plaster_cyan", + "ch_core:plaster_dark_green", + "ch_core:plaster_dark_grey", + "ch_core:plaster_green", + "ch_core:plaster_grey", + "ch_core:plaster_medium_amber_s50", + "ch_core:plaster_orange", + "ch_core:plaster_pink", + "ch_core:plaster_red", + "ch_core:plaster_white", + "ch_core:plaster_yellow", + "ch_extras:marble", + "darkage:basalt", + "darkage:basalt_brick", + "darkage:basalt_cobble", + "darkage:marble", + "darkage:ors_brick", + "darkage:serpentine", + "darkage:stone_brick", + "default:acacia_wood", + "default:aspen_wood", + "default:bronzeblock", + "default:copperblock", + "default:desert_cobble", + "default:desert_sandstone", + "default:desert_sandstone_brick", + "default:desert_stone", + "default:desert_stonebrick", + "default:goldblock", + "default:junglewood", + "default:obsidian", + "default:pine_wood", + "default:silver_sandstone", + "default:silver_sandstone_brick", + "default:steelblock", + "default:stone", + "default:stonebrick", + "default:wood", + "moreblocks:copperpatina", + "moretrees:date_palm_planks", + "moretrees:ebony_planks", + "moretrees:plumtree_planks", + "moretrees:poplar_planks", + "moretrees:spruce_planks", + "technic:cast_iron_block", + "technic:marble" +) + +local materials_for_arcades = set( + -- materiály na překlady zdí: + "bakedclay:black", + "bakedclay:blue", + "bakedclay:brown", + "bakedclay:cyan", + "bakedclay:dark_green", + "bakedclay:dark_grey", + "bakedclay:green", + "bakedclay:grey", + "bakedclay:magenta", + "bakedclay:natural", + "bakedclay:orange", + "bakedclay:pink", + "bakedclay:red", + "bakedclay:violet", + "bakedclay:white", + "bakedclay:yellow", + "basic_materials:cement_block", + "basic_materials:concrete_block", + "bridger:block_green", + "bridger:block_red", + "bridger:block_steel", + "bridger:block_white", + "bridger:block_yellow", + "ch_core:plaster_blue", + "ch_core:plaster_cyan", + "ch_core:plaster_dark_green", + "ch_core:plaster_dark_grey", + "ch_core:plaster_green", + "ch_core:plaster_grey", + "ch_core:plaster_medium_amber_s50", + "ch_core:plaster_orange", + "ch_core:plaster_pink", + "ch_core:plaster_red", + "ch_core:plaster_white", + "ch_core:plaster_yellow", + "ch_extras:marble", + "cottages:black", + "cottages:brown", + "cottages:red", + "cottages:reet", + "darkage:marble", + "default:acacia_wood", + "default:aspen_wood", + "default:copperblock", + "default:desert_stone", + "default:goldblock", + "default:ice", + "default:junglewood", + "default:meselamp", + "default:pine_wood", + "default:silver_sandstone", + "default:steelblock", + "default:stone", + "default:wood", + "summer:granite", + "summer:graniteA", + "summer:graniteB", + "summer:graniteBC", + "summer:graniteP", + "summer:graniteR", + "technic:marble", + "technic:warning_block" +) + +local materials_for_bank_slopes = set( + -- materiály podporující pobřežní svahy: + "basic_materials:cement_block", + "default:dirt", + "default:gravel", + "default:sand", + "summer:sabbia_mare" +) + +local materials_roof = set( + -- materiály podporující střešní tvary: + "bakedclay:black", + "bakedclay:blue", + "basic_materials:cement_block", + "basic_materials:concrete_block", + "cottages:black", + "cottages:brown", + "cottages:red", + "cottages:reet", + "darkage:glass", + "darkage:glow_glass", + "darkage:slate_tile", + "default:acacia_wood", + "default:aspen_wood", + "default:copperblock", + "default:junglewood", + "default:obsidian_glass", + "default:pine_wood", + "default:steelblock", + "default:wood", + "farming:straw", + "moreblocks:cactus_checker", + "moreblocks:copperpatina", + "moretrees:ebony_planks", + "technic:cast_iron_block", + "technic:zinc_block" +) + +local materials_zdlazba = set( + -- zámková dlažba: + "ch_extras:cervzdlazba", + "ch_extras:zdlazba" +) + +local materials_tombs = set( + -- materiály podporující náhrobky: + "bakedclay:black", + "bakedclay:blue", + "bakedclay:cyan", + "bakedclay:dark_green", + "bakedclay:dark_grey", + "bakedclay:green", + "bakedclay:grey", + "bakedclay:magenta", + "bakedclay:orange", + "bakedclay:pink", + "bakedclay:red", + "bakedclay:violet", + "bakedclay:white", + "basic_materials:concrete_block", + "default:desert_stone", + "default:goldblock", + "default:steelblock", + "default:stone", + "default:mossycobble", + "default:wood", + "ch_extras:marble", + "moreblocks:copperpatina", + "technic:granite", + "technic:marble" +) + +local materials_for_manholes = set( + -- materiály podporující oba druhy průlezů: + "bakedclay:black", + "bakedclay:blue", + "bakedclay:brown", + "bakedclay:cyan", + "bakedclay:dark_green", + "bakedclay:dark_grey", + "bakedclay:green", + "bakedclay:grey", + "bakedclay:magenta", + "bakedclay:natural", + "bakedclay:orange", + "bakedclay:pink", + "bakedclay:red", + "bakedclay:violet", + "bakedclay:white", + "bakedclay:yellow", + "basic_materials:brass_block", + "basic_materials:cement_block", + "basic_materials:concrete_block", + "building_blocks:Adobe", + "building_blocks:Tar", + "ch_core:plaster_blue", + "ch_core:plaster_cyan", + "ch_core:plaster_dark_green", + "ch_core:plaster_dark_grey", + "ch_core:plaster_green", + "ch_core:plaster_grey", + "ch_core:plaster_medium_amber_s50", + "ch_core:plaster_orange", + "ch_core:plaster_pink", + "ch_core:plaster_red", + "ch_core:plaster_white", + "ch_core:plaster_yellow", + "ch_extras:marble", + "ch_extras:particle_board_grey", + "darkage:basalt", + "darkage:basalt_brick", + "darkage:basalt_cobble", + "darkage:chalk", + "darkage:gneiss", + "darkage:gneiss_brick", + "darkage:gneiss_cobble", + "darkage:marble", + "darkage:marble_tile", + "darkage:ors", + "darkage:ors_brick", + "darkage:ors_cobble", + "darkage:schist", + "darkage:serpentine", + "darkage:slate", + "darkage:slate_brick", + "darkage:slate_cobble", + "darkage:slate_tile", + "darkage:stone_brick", + "default:brick", + "default:cobble", + "default:obsidian", + "default:obsidian_block", + "default:obsidianbrick", + "default:silver_sandstone", + "default:silver_sandstone_block", + "default:silver_sandstone_brick", + "default:steelblock", + "default:stone", + "default:stone_block", + "default:stonebrick", + "moreblocks:cactus_brick", + "moreblocks:cactus_checker", + "moreblocks:circle_stone_bricks", + "moreblocks:coal_checker", + "moreblocks:coal_stone", + "moreblocks:coal_stone_bricks", + "moreblocks:cobble_compressed", + "moreblocks:copperpatina", + "moreblocks:desert_cobble_compressed", + "moreblocks:iron_stone", + "moreblocks:iron_stone_bricks", + "streets:asphalt_blue", + "streets:asphalt_red", + "summer:granite", + "summer:graniteA", + "summer:graniteB", + "summer:graniteBC", + "summer:graniteP", + "summer:graniteR", + "technic:blast_resistant_concrete", + "technic:carbon_steel_block", + "technic:cast_iron_block", + "technic:granite", + "technic:marble", + "technic:marble_bricks", + "technic:warning_block", + "xdecor:wood_tile", + "ch_extras:cervzdlazba", + "ch_extras:zdlazba" +) + +--[[ +local materials_asphalt = set( + -- asfalt: + "building_blocks:Tar", + "streets:asphalt_blue", + "streets:asphalt_red" +) +]] + +local materials_for_manholes_only = set( + -- materiály podporující pouze jeden druh průlezu: + "default:wood", + "default:acacia_wood", + "default:aspen_wood", + "default:junglewood", + "default:pine_wood") + +local platform_materials = set( + -- materiály podporující nástupní hrany: + "basic_materials:cement_block", + "basic_materials:concrete_block", + "ch_core:plaster_blue", + "ch_core:plaster_cyan", + "ch_core:plaster_dark_green", + "ch_core:plaster_dark_grey", + "ch_core:plaster_green", + "ch_core:plaster_grey", + "ch_core:plaster_medium_amber_s50", + "ch_core:plaster_orange", + "ch_core:plaster_pink", + "ch_core:plaster_red", + "ch_core:plaster_white", + "ch_core:plaster_yellow", + "darkage:basalt", + "darkage:basalt_brick", + "darkage:gneiss_brick", + "darkage:marble", + "darkage:ors_brick", + "darkage:serpentine", + "darkage:slate_brick", + "darkage:stone_brick", + "default:desert_stone", + "default:desert_stone_block", + "default:steelblock", + "default:stone", + "default:stonebrick", + "default:stone_block", + "technic:marble" +) +local platform_materials_exceptions = set( + -- materiály podporující vybrané druhy nástupních hran: + "default:wood", + "default:junglewood", + "moretrees:oak_planks" +) + +local materials_for_diagfillers = set( + -- materiály podporující diagonální výplně: + "building_blocks:Tar", + "default:dry_dirt", + "ch_extras:bright_gravel", + "ch_extras:railway_gravel", + "default:gravel", + "streets:asphalt_blue", + "streets:asphalt_red" +) + +local materials_for_si_frames = set( + -- materiály podporující okenní rámy: + "default:acacia_wood", + "default:aspen_wood", + "default:junglewood", + "default:pine_wood", + "default:wood", + "bakedclay:black", + "bakedclay:blue", + "bakedclay:brown", + "bakedclay:cyan", + "bakedclay:dark_green", + "bakedclay:dark_grey", + "bakedclay:green", + "bakedclay:grey", + "bakedclay:magenta", + "bakedclay:natural", + "bakedclay:orange", + "bakedclay:pink", + "bakedclay:red", + "bakedclay:violet", + "bakedclay:white", + "bakedclay:yellow" +) + +local materials_all = union(materials_cnc, materials_kp, materials_no_kp, materials_glass, materials_crumbly, materials_sns, + materials_wool, materials_roof, materials_zdlazba, platform_materials, platform_materials_exceptions, + set("default:sandstone", "default:sandstonebrick", "default:sandstone_block", "ch_extras:rope_block")) + +local roof_slopes = set("_roof22", "_roof22_raised", "_roof22_3", "_roof22_raised_3", "_roof45", "_roof45_3") + +local alts_micro = set("", "_1", "_2", "_4", "_12", "_15") + +local alts_panel = set("", "_1", "_2", "_4", "_12", "_15", + "_special", -- vychýlená tyč I + "_l", -- vychýlená tyč L + "_l1", -- rohový panel do tvaru L + "_wide", + "_wide_1", + "_wall", + "_wall_flat", + "_element", + "_element_flat", + "_pole", + "_pole_flat", + "_pole_thin", + "_pole_thin_flat") + -- "_banister" (zatím jen pro kámen) + +local alts_slab = set("", "_quarter", "_three_quarter", "_1", "_2", "_14", "_15", + "_two_sides", --deska L (dvě strany) + "_three_sides", -- deska rohová (tři strany) + "_three_sides_u", -- deska U (tři strany) + "_triplet", -- trojitá deska + "_cube", -- kvádr + "_two_sides_half", -- deska L (dvě strany, seříznutá) + "_three_sides_half", -- deska rohová (tři strany, seříznutá) + "_rcover", -- kryt na koleje + "_table", -- stůl + "_bars" -- zábradlí či mříž +) + +local alts_slope = set("", "_half", "_half_raised", + "_inner", "_inner_half", "_inner_half_raised", "_inner_cut", "_inner_cut_half", "_inner_cut_half_raised", + "_outer", "_outer_half", "_outer_half_raised", "_outer_cut", "_outer_cut_half", "_cut", + "_slab", -- trojúhelník + "_tripleslope", + "_cut2", -- šikmá hradba + "_beveled", -- zkosená deska + "_valley" + -- , "_quarter" + -- , "_quarter_raised" +) + +local alts_stair = set("", "_inner", "_outer", "_alt", "_alt_1", "_alt_2", "_alt_4", + "_triple", -- schody (schod 1/3 m) + "_chimney", -- úzký komín + "_wchimney") -- široký komín/průlez + +local no_cnc = set( + -- vypnuté tvary soustruhu: + "technic_cnc_d45_slope_216", + "technic_cnc_element_end_double", + "technic_cnc_element_cross_double", + "technic_cnc_element_t_double", + "technic_cnc_element_edge_double", + "technic_cnc_element_straight_double", + "technic_cnc_element_end", + "technic_cnc_element_cross", + "technic_cnc_element_t", + "technic_cnc_element_edge", + "technic_cnc_element_straight", + "technic_cnc_stick", + "technic_cnc_cylinder_horizontal" +) + +local alts_cnc = set( + "technic_cnc_oblate_spheroid", + "technic_cnc_sphere", + "technic_cnc_cylinder", + "technic_cnc_twocurvededge", + "technic_cnc_onecurvededge", + "technic_cnc_spike", + "technic_cnc_pyramid", + "technic_cnc_arch216", + "technic_cnc_arch216_flange", + "technic_cnc_sphere_half", + "technic_cnc_block_fluted", + "technic_cnc_diagonal_truss", + "technic_cnc_diagonal_truss_cross", + "technic_cnc_opposedcurvededge", + "technic_cnc_cylinder_half", + "technic_cnc_cylinder_half_corner", + "technic_cnc_circle", + "technic_cnc_oct", + "technic_cnc_peek", +-- "technic_cnc_valley", + "technic_cnc_bannerstone" +) + +local alts_tombs = set("_0", "_1", "_2", "_5", "_7", "_8", "_9", "_10", "_12", "_13", "_14") + +local alts_fence = set("fence", "rail", "mesepost", "fencegate") + +local alts_advtrains = set("platform_high", "platform_low", "platform_45_high", "platform_45_low") + +local alts_diagfiller = set("_diagfiller22a", "_diagfiller22b", "_diagfiller45") + +local alts_si_frames = set("wfs", "wfq", "wfqd", "wfr") + +local wool_panels = set("", "_1", "_2", "_4", "_l", "_special") + +local wool_slabs = set("", "_quarter", "_three_quarter", "_1", "_2", "_14", "_15") + +local glass_micro = set("_1", "_2") + +local glass_panel = set("_1", "_2", "_special", "_l", "_wide_1") +local glass_slab = set("", "_1", "_2", "_two_sides", "_three_sides", "_three_sides_u", "_triplet", "_two_sides_half", "_three_sides_half") +local glass_slope = set("", "_half", "_half_raised", "_outer", "_outer_half", "_outer_half_raised", "_tripleslope", "_cut2", roof_slopes) + +local glass_no_cnc = set( + "technic_cnc_circle", + "technic_cnc_cylinder_half_corner", + "technic_cnc_diagonal_truss", + "technic_cnc_diagonal_truss_cross", + "technic_cnc_oblate_spheroid", + "technic_cnc_peek", + "technic_cnc_spike", + "technic_cnc_pyramid", + "technic_cnc_sphere_half", + "technic_cnc_sphere", + "technic_cnc_stick", + "technic_cnc_element_end_double", + "technic_cnc_element_cross_double", + "technic_cnc_element_t_double", + "technic_cnc_element_edge_double", + "technic_cnc_element_straight_double", + "technic_cnc_element_end", + "technic_cnc_element_cross", + "technic_cnc_element_t", + "technic_cnc_element_edge", + "technic_cnc_element_straight" +) + +local sns_micro = set("_1", "_2") +local sns_slabs = set("", "_quarter", "_three_quarter", "_1", "_2", "_14", "_15", "_rcover") + +local sns_slopes = set("", "_half", "_half_raised", + "_inner", "_inner_half", "_inner_half_raised", "_inner_cut", "_inner_cut_half", "_inner_cut_half_raised", + "_outer", "_outer_half", "_outer_half_raised", "_outer_cut", "_outer_cut_half", "_cut", + "_tripleslope") + +local crumbly_micro = sns_micro +local crumbly_slabs = sns_slabs +local crumbly_slopes = set(sns_slopes, "_quarter", "_quarter_raised") + +local wool_slopes = set("", "_half", "_half_raised") + +local bank_slopes = set( + "", "_cut", "_half", "_half_raised", + "_inner", "_inner_half", "_inner_half_raised", "_inner_cut", "_inner_cut_half", "_inner_cut_half_raised", + "_outer", "_outer_half", "_outer_half_raised") + +-- ============================================================================ +local rules = { + -- {materials, categories, alternates, true/false [, comment]} + -- materials, categories and alternates may be: + -- a) string (exact match) + -- b) table (query) + -- c) "*" (always match) + {materials_no_kp, set("micro", "panel", "slab", "slope", "stair"), "*", false}, + {"*", "cnc", no_cnc, false}, -- vypnuté tvary (nesmí se generovat pro žádný materiál) + {materials_all, "slab", "_1", true}, -- "slab/_1 pro všechny materiály kromě zakázaných" + {"default:stone", "*", "*", true}, -- kámen je referenční materiál, musí podporovat všechny tvary + + {materials_glass, { + micro = {glass_micro, true}, + panel = {glass_panel, true}, + slab = {glass_slab, true}, + slope = {glass_slope, true}, + stair = {"*", false}, -- no stairs... + cnc = {glass_no_cnc, false}, + }}, + {materials_wool, { + panel = {wool_panels, true}, + slab = {wool_slabs, true}, + slope = {wool_slopes, true}, + stair = {"", true}, -- single shape only + }}, + {materials_zdlazba, { + micro = {set("_1", "_2", "", "_15"), true}, + panel = {set("_1", "_special", "_wide_1"), true}, + slab = {sns_slabs, true}, + slope = {alts_slope, true}, + stair = {"_alt_1", true}, -- single shape only + }}, + {materials_kp, { + micro = {alts_micro, true}, + panel = {alts_panel, true}, + slab = {alts_slab, true}, + slope = {alts_slope, true}, + stair = {alts_stair, true}, + }}, + {materials_sns, { + micro = {sns_micro, true}, + slab = {sns_slabs, true}, + slope = {sns_slopes, true}, + }}, + {materials_crumbly, { + micro = {crumbly_micro, true}, + slab = {crumbly_slabs, true}, + slope = {crumbly_slopes, true}, + }}, + + {"streets:asphalt_yellow", "streets", "*", false}, + + -- speciality: + {materials_for_arcades, "slab", set("_arcade", "_arcade_flat"), true}, + {materials_for_bank_slopes, "bank_slope", bank_slopes, true}, + {materials_roof, "slope", roof_slopes, true}, -- roofs + {materials_pillars, "pillar", "*", true }, -- zatím bez omezení + {materials_tombs, "tombs", alts_tombs, true}, + {materials_for_manholes, "streets", set("manhole", "stormdrain"), true}, + {materials_for_manholes_only, "streets", "manhole", true}, + {materials_for_si_frames, "si_frames", alts_si_frames, true}, + {materials_for_diagfillers, "slope", alts_diagfiller, true}, + {set("default:gravel", "ch_extras:railway_gravel"), "slope", "_slab", true}, + + {platform_materials_exceptions , "advtrains", set("platform_high", "platform_low"), true}, + {platform_materials, "advtrains", alts_advtrains, true}, + {platform_materials, "slope", set("_quarter", "_quarter_raised"), true}, + + -- žlutý pískovec (omezený sortiment tvarů): + {set("default:sandstone", "default:sandstonebrick", "default:sandstone_block"), { + slab = {set("", "_1", "_triplet"), true}, + slope = {set("", "_half", "_half_raised"), true}, + }}, + {set("ch_extras:rope_block"), { + cnc = {set("technic_cnc_diagonal_truss", "technic_cnc_diagonal_truss_cross"), true}, + panel = {set("_l", "pole", "pole_flat", "_pole_thin", "_pole_thin_flat", "_special"), true}, + slab = {"_1", true}, + stair = {"chimney", true}, + }}, + + +-- CNC: + {"default:dirt", "cnc", + set("technic_cnc_oblate_spheroid", "technic_cnc_slope_upsdown", "technic_cnc_edge", "technic_cnc_inner_edge", + "technic_cnc_slope_edge_upsdown", "technic_cnc_slope_inner_edge_upsdown", "technic_cnc_stick", "technic_cnc_cylinder_horizontal"), + false}, -- zakázat vybrané tvary z hlíny + {materials_cnc, "cnc", alts_cnc, true}, -- výchozí pravidlo pro CNC +-- fence: + {"*", "fence", alts_fence, true}, -- [ ] TODO +-- catch-all rules: + {materials_all, set("micro", "panel", "slab", "slope", "stair", "cnc", "bank_slope", "advtrains", "pillar", "streets"), "*", false}, +} +-- ============================================================================ + + +-- Cache in format: +-- [category.."@"..alternate] = {[material] = true, ...} +local query_cache = {} + +local custom_list_shapes -- list of shapes for More Blocks + +function ch_core.get_stairsplus_custom_shapes(recipeitem) + if custom_list_shapes == nil then + error("custom_list_shapes are not initialized yet!") + end + if recipeitem == nil then + -- special case: return all possible shapes + return table.copy(custom_list_shapes) + end + local result = {} + for _, shape in ipairs(custom_list_shapes) do + if ch_core.is_shape_allowed(recipeitem, shape[1], shape[2]) then + table.insert(result, shape) + end + end + -- print("ch_core.get_stairsplus_custom_shapes(): "..#result.." shapes generated for "..recipeitem) + return ifthenelse(#result > 0, result, nil) -- return nil when no shapes are allowed +end + +function ch_core.get_materials_from_shapes_db(key) + local mset + if key == "advtrains" then + mset = union(platform_materials, platform_materials_exceptions) + elseif key == "si_frames" then + mset = materials_for_si_frames + elseif key == "streets" then + mset = union(materials_for_manholes, materials_for_manholes_only) + end + if mset == nil then + error("ch_core.get_materials_from_shapes_db(): unsupported key \""..key.."\"!") + end + local result = {} + for name, _ in pairs(mset) do + table.insert(result, name) + end + if not mset["default:stone"] then + table.insert(result, "default:stone") + end + return result +end + +function ch_core.init_stairsplus_custom_shapes(defs) + local key_set = {} + custom_list_shapes = {} + for category, cdef in pairs(defs) do + for alternate, _ in pairs(cdef) do + local key = category.."/"..alternate + if key_set[key] == nil then + key_set[key] = true + table.insert(custom_list_shapes, {category, alternate}) + end + end + end +end + +local function is_match(s, pattern) + if type(pattern) == "string" then + return pattern == "*" or s == pattern + elseif type(pattern) == "table" then + return pattern[s] ~= nil + elseif type(pattern) == "number" then + return s == tostring(pattern) + else + error("Invalid pattern type '"..type(pattern).."'!") + end +end + +function ch_core.is_shape_allowed(recipeitem, category, alternate) + if type(recipeitem) ~= "string" or type(category) ~= "string" or type(alternate) ~= "string" then + error("Invalid argument type: "..dump2({ + func = "ch_core.is_shape_allowed", + recipeitem = recipeitem, category = category, alternate = alternate, + ["type(recipeitem)"] = type(recipeitem), + ["type(category)"] = type(category), + ["type(alternate)"] = type(alternate), + })) + end + local shapeinfo = query_cache[category.."@"..alternate] + local result + if shapeinfo ~= nil then + result = shapeinfo[recipeitem] + if result ~= nil then + return result + end + end + -- perform full resolution: + for _, rule in ipairs(rules) do + if #rule == 4 then + -- starší formát pravidla + if is_match(category, rule[2]) and is_match(recipeitem, rule[1]) and is_match(alternate, rule[3]) then + -- match + result = ifthenelse(rule[4], true, false) + break + end + elseif #rule == 2 and type(rule[2]) == "table" then + -- novější formát pravidla + local subrule = rule[2][category] + if subrule ~= nil and is_match(recipeitem, rule[1]) then + if #subrule ~= 2 then + error("Invalid subrule found:"..dump2(rule)) + end + if is_match(alternate, subrule[1]) then + result = ifthenelse(subrule[2], true, false) + break + end + end + end + end + if result == nil then + error("Shapes DB resolution failed for "..recipeitem.."/"..category.."/"..alternate.."!") + -- result = true + -- minetest.log("warning", "Shapes DB resolution failed for "..recipeitem.."/"..category.."/"..alternate.."! Will allow the shape.") + end + if shapeinfo == nil then + shapeinfo = {} + query_cache[category.."@"..alternate] = shapeinfo + end + shapeinfo[recipeitem] = result + return result +end + +function ch_core.get_comboblock_index(v1, v2) + error("not implemented yet") +end + +local function get_single_texture(tiles) + if type(tiles) == "string" then + return tiles + elseif type(tiles) == "table" then + tiles = tiles[1] + if type(tiles) == "string" then + return tiles + elseif type(tiles) == "table" then + return tiles.name or tiles.image or "blank.png" + end + end + return "blank.png" +end + +local function get_six_textures(tiles) + if type(tiles) == "string" then + return {tiles, tiles, tiles, tiles, tiles, tiles} + elseif type(tiles) == "table" then + tiles = table.copy(tiles) + for i = 1, #tiles do + if type(tiles[i]) ~= "table" then + tiles[i] = {name = tiles[i]} + end + end + if #tiles < 6 then + for i = #tiles, 5 do + tiles[i + 1] = table.copy(tiles[i]) + end + end + return tiles + end + return { + {name = "blank.png"}, + {name = "blank.png"}, + {name = "blank.png"}, + {name = "blank.png"}, + {name = "blank.png"}, + {name = "blank.png"}, + } +end + +local groups_to_inherit = {"choppy", "crumbly", "snappy", "oddly_breakable_by_hand", "flammable"} + +local default_fence_defs = { + fence = true, + rail = true, + mesepost = false, + fencegate = true, +} + +local function assembly_name(matmod, prefix, matname, info, foreign_names) + if type(info) == "table" and info.name ~= nil then + return info.name + end + return ifthenelse(foreign_names, ":"..matmod, matmod)..":"..prefix..matname +end + +function ch_core.register_fence(material, defs) + local matmod, matname = string.match(material, "([^:]+):([^:]+)$") + local ndef = minetest.registered_nodes[material] + local empty_table = {} + if ndef == nil then + error("Fence material "..material.." is not a registered node!") + end + if matmod == nil or matname == nil then + error("Invalid material syntax: "..material.."!") + end + if defs == nil then + defs = default_fence_defs + end + + local info, def + local name + local tiles = ndef.tiles or {{name = "blank.png"}} + local texture = get_single_texture(tiles) + local groups = assembly_groups(nil, nil, ndef.groups, groups_to_inherit) + + -- FENCE + -- =============================================================================== + if defs.fence ~= nil and ch_core.is_shape_allowed(material, "fence", "fence") then + -- fence block ('fence') + -- example: default:fence_wood + info = ifthenelse(type(defs.fence) == "table", defs.fence, empty_table) + name = assembly_name(matmod, "fence_", matname, info, defs.foreign_names) + def = { + material = material, + groups = table.copy(groups), + sounds = ndef.sounds, + } + if info.texture ~= nil then + def.texture = defs.fence.texture + else + def.texture = "" + def.tiles = tiles + def.inventory_image = "default_fence_overlay.png^"..texture.."^default_fence_overlay.png^[makealpha:255,126,126" + def.wield_image = def.inventory_image + end + default.register_fence(name, def) + end + + -- FENCE RAIL + -- =============================================================================== + if defs.rail ~= nil and ch_core.is_shape_allowed(material, "fence", "rail") then + -- fence rail ('rail') + -- example: default:fence_rail_wood + info = ifthenelse(type(defs.rail) == "table", defs.rail, empty_table) + name = assembly_name(matmod, "fence_rail_", matname, info, defs.foreign_names) + def = { + material = material, + groups = table.copy(groups), + sounds = ndef.sounds, + } + if info.texture ~= nil then + def.texture = info.texture + else + def.texture = "" + def.tiles = tiles + def.inventory_image = "default_fence_rail_overlay.png^"..texture.."^default_fence_rail_overlay.png^[makealpha:255,126,126" + def.wield_image = def.inventory_image + end + default.register_fence_rail(name, def) + end + + -- MESE POST LIGHT + -- =============================================================================== + if defs.mesepost ~= nil and ch_core.is_shape_allowed(material, "fence", "mesepost") then + -- post_light ('post_light') + -- example: default:mese_post_light + info = ifthenelse(type(defs.mesepost) == "table", defs.mesepost, empty_table) + name = assembly_name(matmod, "mese_post_light_", matname, defs.mesepost, defs.foreign_names) + local mp_tiles = get_six_textures(ndef.tiles) + for i = 3, 4 do + mp_tiles[i].name = mp_tiles[i].name.."^default_mese_post_light_side.png^[makealpha:0,0,0" + end + for i = 5, 6 do + mp_tiles[i].name = mp_tiles[i].name.."^default_mese_post_light_side_dark.png^[makealpha:0,0,0" + end + def = { + material = material, + texture = info.texture or texture, + description = (ndef.description or "neznámý materiál")..": sloupek s meseovým světlem", + tiles = mp_tiles, + groups = table.copy(groups), + sounds = ndef.sounds, + } + default.register_mesepost(name, def) + end + if defs.fencegate ~= nil and ch_core.is_shape_allowed(material, "fence", "fencegate") then + -- fence gate ('gate') + -- example: doors:gate_wood_closed + info = ifthenelse(type(defs.fencegate) == "table", defs.fencegate, empty_table) + name = info.name or "doors:gate_"..matname + def = { + material = material, + texture = info.texture or texture, + groups = ch_core.assembly_groups({}, {oddly_breakable_by_hand = 2}, ndef.groups, groups_to_inherit), + } + doors.register_fencegate(name, def) + end +end + +ch_core.register_fence("default:wood", { + fence = true, + rail = true, + mesepost = {name = ":default:mese_post_light"}, + fencegate = true, + foreign_names = true, +}) + +local defs = { + fence = true, rail = true, mesepost = true, fencegate = true, foreign_names = true +} + +ch_core.register_fence("default:acacia_wood", defs) +ch_core.register_fence("default:junglewood", defs) +ch_core.register_fence("default:aspen_wood", defs) +ch_core.register_fence("default:pine_wood", defs) + +ch_core.close_submod("shapes_db") diff --git a/ch_core/sounds/chat3_bell.ogg b/ch_core/sounds/chat3_bell.ogg Binary files differnew file mode 100644 index 0000000..1f981bf --- /dev/null +++ b/ch_core/sounds/chat3_bell.ogg diff --git a/ch_core/stavby.lua b/ch_core/stavby.lua new file mode 100644 index 0000000..8e4bfdf --- /dev/null +++ b/ch_core/stavby.lua @@ -0,0 +1,303 @@ +ch_core.open_submod("stavby", {chat = true, events = true, lib = true}) + +--[[ +Datová struktura záznamu o stavbě: + +{ + key = string, -- pozice stavby v normalizovaném tvaru (klíč do tabulky staveb) + pos = {x = int y = int, z = int} -- pozice stavby ve formě vektoru celých čísel + + -- ostatní pole mohou být přímo upravována: + nazev = string, -- název stavby (nemusí být jedinečný) + druh = string, -- druh stavby (doplňuje název) + zalozil_a = string, -- postava, která stavby založila (přihlašovací tvar) + spravuje = string, -- postava, která stavbu spravuje (přihlašovací tvar) + urceni = string, -- jeden z řetězců z ch_core.urceni_staveb + stav = string, -- jeden z řetězců z ch_core.stavy_staveb + historie = { + [1...] = string, ... + } -- postupná historie změn + zamer = string, -- záměr stavby (text) + heslo = string || nil, -- do budoucna: vygenerované heslo u staveb nabízených k převzetí; kdo heslo zadá, dostane stavbu do správy + mapa = {x1, z1, [w, h]}, -- volitelné pole; definuje místo zobrazení na mapě (buď jen bod, nebo zónu) +} +]] + +minetest.register_privilege("ch_stavby_admin", { + description = "Umožňuje měnit všechny vlastnosti registrovaných staveb.", + give_to_singleplayer = true, + give_to_admin = true, +}) + +local worldpath = minetest.get_worldpath() + +ch_core.urceni_staveb = { + soukroma = "soukromá", + verejna = "veřejná", + chranena_oblast = "chráněná oblast", +} + +ch_core.stavy_staveb = { + k_povoleni = "čeká na stavební povolení", + rozestaveno = "rozestavěná", + k_schvaleni = "k schválení", + hotovo = "hotová", + rekonstrukce = "v rekonstrukci", + opusteno = "opuštěná", + k_smazani = "ke smazání", +} + +ch_core.urceni_staveb_r = table.key_value_swap(ch_core.urceni_staveb) +ch_core.stavy_staveb_r = table.key_value_swap(ch_core.stavy_staveb) + +local function pos_to_key(pos) + pos = vector.round(pos) + return string.format("%d,%d,%d", pos.x, pos.y, pos.z) +end + +local function key_to_pos(s) + local parts = s:split(",") + local result + if #parts == 3 then + result = vector.round(vector.new(assert(tonumber(parts[1])), assert(tonumber(parts[2])), assert(tonumber(parts[3])))) + end + return result +end + +local function verify_record(record) + if record == nil then + return "record is nil!" + end + local s + if type(record.key) ~= "string" then return "invalid key type: "..type(record.key) end + local pos = key_to_pos(record.key) + if pos == nil then return "invalid key (cannot unhash): "..record.key end + s = type(record.pos) + if s ~= "table" then return "invalid pos type: "..s end + if pos.x ~= record.pos.x or pos.y ~= record.pos.y or pos.z ~= record.pos.z then + return "invalid or inconsistent record.pos!" + end + s = type(record.nazev) + if s ~= "string" and s ~= "number" then return "invalid nazev type: "..s end + if record.nazev == "" then return "empty nazev!" end + s = type(record.zalozil_a) + if s ~= "string" and s ~= "number" then return "invalid zalozil_a type: "..s end + s = type(record.spravuje) + if s ~= "string" and s ~= "number" then return "invalid spravuje type: "..s end + s = record.urceni + if s == nil or ch_core.urceni_staveb[s or ""] == nil then + return "invalid urceni: "..(s or "nil") + end + s = record.stav + if s == nil or ch_core.stavy_staveb[s or ""] == nil then + return "invalid stav: "..(s or "nil") + end + s = type(record.historie) + if s ~= "table" then return "invalid historie type: "..s end + for i, v in ipairs(record.historie) do + if type(v) ~= "string" then return "invalid historie["..i.."] type: "..type(v) end + end + s = type(record.zamer) + if s ~= "string" and s ~= "number" then return "invalid zamer type: "..s end + return nil +end + +local function load_data() + local f = io.open(worldpath.."/ch_stavby.json") + local result + if f then + result = f:read("*a") + f:close() + if result ~= nil then + result = assert(minetest.parse_json(result)) + end + end + if result ~= nil then + local count = 0 + for key, record in pairs(result) do + local error_message = verify_record(record) + if error_message == nil and key ~= record.key then + error_message = "key mismatch! table key = <"..key..">, record key = <"..(record.key or "nil")..">" + end + if error_message ~= nil then + minetest.log("warning", "[ch_core/stavby] Verification error for stavba <"..key..">: "..error_message) + end + count = count + 1 + end + minetest.log("info", "[ch_core/stavby] "..count.." records loaded.") + return result + end + minetest.log("warning", "[ch_core/stavby] No data was loaded!") + return {} +end + +local data = load_data() + +function ch_core.stavby_add(pos, urceni, stav, nazev, druh, player_name, zamer) + local key = assert(pos_to_key(pos)) + if data[key] ~= nil then + return nil -- a duplicity! + end + if nazev == nil or nazev == "" then nazev = "Bez názvu" end + if druh == nil then druh = "" end + assert(player_name ~= nil) + local cas = ch_time.aktualni_cas() + local new_record = { + key = key, + pos = assert(key_to_pos(key)), + urceni = urceni, + nazev = nazev, + druh = druh, + zalozil_a = player_name, + spravuje = player_name, + stav = stav, + historie = { + string.format("%s založeno (správa: %s, stav: %s)", + cas:YYYY_MM_DD(), ch_core.prihlasovaci_na_zobrazovaci(player_name), assert(ch_core.stavy_staveb[stav])) + }, + zamer = zamer or "", + } + + local record_error = verify_record(new_record) + if record_error ~= nil then + error("stavby_add() internal error: "..record_error) + end + data[key] = new_record + return new_record +end + +function ch_core.stavby_get(key_or_pos) + if type(key_or_pos) ~= "string" then + key_or_pos = pos_to_key(key_or_pos) + end + return data[key_or_pos] +end + +function ch_core.stavby_get_all(filter, sorter, extra_argument) + local result = {} + if filter == nil then + filter = function() return true end + end + for _, record in pairs(data) do + if filter(record, extra_argument) then + table.insert(result, record) + end + end + if sorter ~= nil then + table.sort(result, function(a, b) return sorter(a, b, extra_argument) end) + end + return result +end + +function ch_core.stavby_move(key_from, pos) + local record = data[key_from] + local key_to = pos_to_key(pos) + if record == nil then return nil end + if data[key_to] ~= nil then return false end + record.key = key_to + record.pos = key_to_pos(key_to) + data[key_from] = nil + data[key_to] = record + return true +end + +function ch_core.stavby_remove(key_or_pos) + if type(key_or_pos) ~= "string" then + key_or_pos = pos_to_key(key_or_pos) + end + if data[key_or_pos] == nil then + return false + end + data[key_or_pos] = nil + return true +end + +local stavby_html_transtable = { + ["-"] = "m", + [","] = "x", +} + +function ch_core.stavby_save() + local json, error_message = minetest.write_json(data) + if json == nil then + error("ch_core.stavby_save(): serialization error: "..tostring(error_message or "nil")) + end + minetest.safe_file_write(worldpath.."/ch_stavby.json", json) + minetest.log("info", "ch_core/stavby: "..#data.." records saved ("..#json.." bytes).") + local html = {} + for key, stavba in pairs(data) do + local html_key = "st_"..key:gsub("[-,]", stavby_html_transtable) + local nazev = stavba.nazev + if stavba.druh ~= "" then + nazev = stavba.nazev.." ("..stavba.druh..")" + end + nazev = ch_core.formspec_hypertext_escape(nazev) + local mapa = stavba.mapa + + table.insert(html, "<div class=\""..stavba.stav.." "..stavba.urceni) + if mapa ~= nil and #mapa == 4 then + table.insert(html, " zona") + end + table.insert(html, "\" id=\""..html_key.."\" style=\"") + if mapa == nil then + table.insert(html, string.format("left:%dpx;top:%dpx;", stavba.pos.x, -stavba.pos.z)) + elseif #mapa == 2 then + table.insert(html, string.format("left:%dpx;top:%dpx;", mapa[1], -mapa[2])) + else + table.insert(html, string.format("left:%dpx;top:%dpx;width:%dpx;height:%dpx;", mapa[1], -(mapa[2] + mapa[4]), mapa[3], mapa[4])) + end + table.insert(html, "\" title=\"") + if stavba.urceni == "soukroma" then + table.insert(html, "<soukromá stavba> ") + elseif stavba.urceni == "chranena_oblast" then + table.insert(html, "<rezervovaná oblast> ") + end + table.insert(html, nazev.." spravuje: "..ch_core.prihlasovaci_na_zobrazovaci(stavba.spravuje).. + " stav: "..assert(ch_core.stavy_staveb[stavba.stav]).."\">"..nazev.."</div>\n") + end + minetest.safe_file_write(worldpath.."/ch_stavby.html", table.concat(html)) +end + +local function stavba_na_mape(admin_name, param) + local params = string.split(param, " ") + if #params ~= 1 and #params ~= 3 and #params ~= 5 then + return false, "Neplatný počet parametrů!" + end + local stavba = data[params[1]] + if stavba == nil then + return false, "Stavba s klíčem <"..params[1].."> nenalezena!" + end + if #params == 1 then + stavba.mapa = nil + else + local x1, z1 = tonumber(params[2]), tonumber(params[3]) + if x1 == nil or z1 == nil then + return false, "Neplatné zadání!" + end + if #params == 3 then + stavba.mapa = {x1, z1} + elseif #params == 5 then + local x2, z2 = tonumber(params[4]), tonumber(params[5]) + if x2 == nil or z2 == nil then + return false, "Neplatné zadání!" + end + if x2 < x1 then x1, x2 = x2, x1 end + if z2 < z1 then z1, z2 = z2, z1 end + stavba.mapa = {x1, z1, x2 - x1 + 1, z2 - z1 + 1} + end + end + ch_core.stavby_save() + return true, "Nastaveno." +end + +local def = { + params = "<pozice,stavby> [<x1> <z1> [<x2> <z2>]]", + description = "Pro správce/yni staveb: nastaví zobrazení stavby na mapě nebo toto nastavení zruší.", + privs = {ch_stavby_admin = true}, + func = stavba_na_mape, +} + +minetest.register_chatcommand("stavbanamapě", def) +minetest.register_chatcommand("stavbanamape", def) + +ch_core.close_submod("stavby") diff --git a/ch_core/teleportace.lua b/ch_core/teleportace.lua new file mode 100644 index 0000000..f4c4003 --- /dev/null +++ b/ch_core/teleportace.lua @@ -0,0 +1,578 @@ +ch_core.open_submod("teleportace", {privs = true, data = true, chat = true, lib = true, stavby = true, timers = true}) + +local ifthenelse = ch_core.ifthenelse + +local can_teleport_callbacks = {} + +-- LOCAL FUNCTIONS +local function get_entity_name(obj) + if obj.get_luaentity then + obj = obj:get_luaentity() + end + return obj.name or "unknown entity" +end + +local function get_finish_teleport_func(player_name, d, is_immediate) + return function() + -- je postava stále ještě ve hře? + local player = minetest.get_player_by_name(player_name) + local online_charinfo = ch_data.online_charinfo[player_name] + if player == nil or online_charinfo == nil then + return + end + local player_pos = player:get_pos() + player:set_properties{pointable = true} + ch_core.set_temporary_titul(player_name, "přemísťuje se", false) + + -- zopakovat testy přemístitelnosti + if not is_immediate then + if d.type ~= "admin" and ch_core.je_ve_vykonu_trestu ~= nil then + local trest = ch_core.je_ve_vykonu_trestu(player_name) + if trest ~= nil then + ch_core.systemovy_kanal(player_name, "Jste ve výkonu trestu odnětí svobody! Zbývající výše trestu: "..trest) + return false + end + end + end + + -- can_teleport? + local priority + if d.type == "admin" then + priority = 4 + elseif d.type == "force" then + priority = 3 + else + priority = 2 + end + for name, can_teleport in pairs(can_teleport_callbacks) do + if can_teleport(player, online_charinfo, priority) == false then + minetest.log("warning", "teleport_player() denied due to callback "..name) + return false, "Přemístění selhalo (technické informace: "..name..")" + end + end + + -- přehrát zvuk před přemístěním + if d.sound_before ~= nil then + minetest.sound_play(d.sound_before, {pos = player_pos, max_hear_distance = d.sound_before_max_hearing_distance or 50.0}) + end + + -- zavolat funkce před přemístěním + if d.callback_before ~= nil then + d.callback_before() + end + + -- zavříŧ formspec + if d.close_formspec == true then + minetest.close_formspec(player_name, "") + end + + -- vyřešit připojení postavy k objektu + local attach_object = player:get_attach() + if attach_object ~= nil then + local entity_name = get_entity_name(attach_object) + + if (d.type == "admin" or d.type == "force") and entity_name == "boats:boat" then + -- detach from the boat + player:set_detach() + player_api.player_attached[player_name] = false + player_api.set_animation(player, "stand" , 30) + else + if d.type == "player" then + ch_core.systemovy_kanal(player_name, "Přemístění selhalo") + end + minetest.log("action", player_name.." was not teleported to "..minetest.pos_to_string(d.target_pos)..".") + return false -- přemístění selhalo + end + end + + -- načíst cílovou oblast + minetest.load_area(vector.round(d.target_pos)) + + -- přemístit postavu + player:set_pos(d.target_pos) + + -- zrušit rychlost + if d.set_velocity ~= false then + player:add_velocity(vector.multiply(player:get_velocity(), -1.0)) + end + + -- nastavit natočení + if d.look_horizontal ~= nil then + player:set_look_horizontal(d.look_horizontal) + end + if d.look_vertical ~= nil then + player:set_look_vertical(d.look_vertical) + end + + -- zavolat funkci po přemístění + if d.callback_after ~= nil then + d.callback_after() + end + + -- přehrát zvuk po přemístění + if d.sound_after ~= nil then + minetest.sound_play(d.sound_after, {pos = d.target_pos, max_hear_distance = d.sound_after_max_hearing_distance or 50.0}) + end + + if d.type == "player" then + ch_core.systemovy_kanal(player_name, "Přemístění úspěšné") + end + minetest.log("action", player_name.." teleported to "..minetest.pos_to_string(d.target_pos)..".") + + return true + end +end + + + + + +-- PUBLIC FUNCTIONS +function ch_core.register_can_teleport(name, func) + -- func = function(player, online_charinfo, priority) + -- priority: 0 = very low priority, 1 = low priority, 2 = normal priority (player/normal), 3 = high priority (force), 4 = very high priority (admin) + -- should return: true or nil (can teleport), false (can't teleport) + if not name then + error("Name is required!") + end + can_teleport_callbacks[name] = func +end + +--[[ + d = { + type = string, // musí být jedno z: + // "admin": přemístění s nejvyšší prioritou, umí odpojovat od všech typů objektů a může k němu docházet i ve výkonu trestu + // "force": neinteraktivní přemístění s vyšší prioritou, umí odpojovat od objektů + // "player": interaktivní přemístění s normální prioritou, neumí odpojovat od objektů, vypisuje zprávy do četu + // "normal": neinteraktivní přemístění s normální prioritou, neumí odpojovat od objektů, nevypisuje zprávy + player = ObjRef or string, // postava k přemístění (objekt nebo jméno) + target_pos = vector, // cílová pozice + + delay = float or nil, // zpoždění; během zpoždění může postava přemístění zrušit např. pohybem + look_horizontal = float or nil, + look_vertical = float or nil, + sound_before = SimpleSoundSpec or nil, // zvuk k přehrání na výchozí pozici těsně před přemístěním, + sound_before_max_hearing_distance = float or nil, + sound_after = SimpleSoundSpec or nil, // zvuk k přehrání na cílové pozici těsně po přemístění, + sound_after_max_hearing_distance = float or nil, + callback_before = function or nil, // funkce k zavolání těsně před přemístěním + callback_after = function or nil, // funkce k zavolání těsně po přemístění + // nebude zavolána, pokud bude přemístění přerušeno + close_formspec = bool or nil, // je-li true, v momentě přemístění uzavře klientovi formspec (nedoporučuje se) + set_velocity = bool or nil, // je-li true nebo nil, po přemístění nastaví postavě nulovou rychlost + } +]] + +function ch_core.teleport_player(def) + local d = table.copy(def) + if d.type == nil or d.player == nil or d.target_pos == nil then + error("ch_core.teleport_player(): a required argument is missing: "..dump2(def)) + end + if d.type ~= "admin" and d.type ~= "force" and d.type ~= "player" and d.type ~= "normal" then + error("ch_core.teleport_player(): unsupported teleport type '"..d.type.."'!") + end + d.target_pos = vector.copy(d.target_pos) + + -- je postava ve hře? + local pinfo = ch_core.normalize_player(d.player) + if pinfo.role == "none" then + return false, "postava neexistuje" + end + local player = pinfo.player + local player_name = pinfo.player_name + local online_charinfo = ch_data.online_charinfo[player_name] + if player == nil or online_charinfo == nil then + return false, "postava není ve hře" + end + -- local player_pos = player:get_pos() + + -- mohu přemístit postavu? + if d.type ~= "admin" and ch_core.je_ve_vykonu_trestu ~= nil then + local trest = ch_core.je_ve_vykonu_trestu(player_name) + if trest ~= nil then + if d.type == "player" then + ch_core.systemovy_kanal(player_name, "Jste ve výkonu trestu odnětí svobody! Zbývající výše trestu: "..trest) + end + return false, "postava je ve výkonu trestu" + end + end + + -- zrušit stávající přemístění, je-li naplánováno + local timer_def = ch_core.get_ch_timer_info(online_charinfo, "teleportace") + if timer_def then + ch_core.cancel_teleport(player_name, false) + end + + -- přemístit se zpožděním? + local delay = d.delay + if delay ~= nil and delay > 0.0 then + timer_def = { + label = "přemístění", + func = get_finish_teleport_func(player_name, d, false), + hudbar_icon = (minetest.registered_items["basic_materials:energy_crystal_simple"] or {}).inventory_image, + hudbar_bar = "hudbars_bar_timer.png^[multiply:#0000ff", + + teleport_type = d.type, + start_pos = player and player:get_pos(), + target_pos = d.target_pos, + def = d, + } + if ch_core.start_ch_timer(online_charinfo, "teleportace", d.delay, timer_def) then + if d.type == "player" then + local delay_str = string.format("%.3f", delay) + delay_str = delay_str:gsub("%.", ",") + ch_core.systemovy_kanal(player_name, "Přemístění zahájeno (čas na přípravu: "..delay_str.." sekund).") + ch_core.set_temporary_titul(player_name, "přemísťuje se", true) + end + minetest.log("action", "Teleport of "..player_name.." to "..minetest.pos_to_string(d.target_pos).." started.") + player:set_properties{pointable = false} + return true + end + return false, "neznámý důvod" + end + + -- přemístit okamžitě? + local f = get_finish_teleport_func(player_name, d, true) + return f() +end + +--[[ + Zjistí, zda je postava přemísťována. +]] +function ch_core.is_teleporting(player_name) + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo == nil then + return false + end + local timer_def = ch_core.get_ch_timer_info(online_charinfo, "teleportace") + if timer_def == nil then + return false + end + return timer_def.teleport_type +end + +--[[ + Zruší probíhající přemístění. +]] +function ch_core.cancel_teleport(player_name, due_to_player_action) + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo == nil then + return false + end + local timer_def = ch_core.get_ch_timer_info(online_charinfo, "teleportace") + if timer_def == nil then + return false + end + ch_core.cancel_ch_timer(online_charinfo, "teleportace") + ch_core.set_temporary_titul(player_name, "přemísťuje se", false) + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + player:set_properties{pointable = true} + end + if timer_def.def.type == "player" then + ch_core.systemovy_kanal(player_name, ifthenelse(due_to_player_action, + "Přemístění zrušeno v důsledku akce hráče/ky nebo postavy.", + "Přemístění zrušeno.")) + end + return true +end + +--[[ + Předčasně dokončí probíhající přemístění. +]] +function ch_core.finish_teleport(player_name) + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo == nil then + return false + end + local timer_def = ch_core.get_ch_timer_info(online_charinfo, "teleportace") + if timer_def ~= nil then + ch_core.cancel_ch_timer(online_charinfo, "teleportace") + ch_core.set_temporary_titul(player_name, "přemísťuje se", false) + return timer_def.func() + else + return false + end +end + +-- /doma +local function doma(player_name, pos) + if not pos then + return false, "Chybná pozice!" + end + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + offline_charinfo.domov = minetest.pos_to_string(pos) + if not offline_charinfo.domov then + return false, "Pozice nebyla uložena!" + end + ch_data.save_offline_charinfo(player_name) + return true +end + +-- /domů +local function domu(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if not offline_charinfo then + return false, "Interní údaje nebyly nalezeny!" + elseif not offline_charinfo.domov then + return false, "Nejprve si musíte nastavit domovskou pozici příkazem /doma." + end + local new_pos = minetest.string_to_pos(offline_charinfo.domov) + if not new_pos then + return false, "Uložená pozice má neplatný formát!" + end + ch_core.teleport_player{ + type = "player", + player = player_name, + target_pos = new_pos, + delay = 30.0, + sound_before = ch_core.default_teleport_sound, + sound_after = ch_core.default_teleport_sound, + } + return true +end + +-- /stavím +local function stavim(player_name, pos) + if not pos then + return false, "Chybná pozice!" + end + local filter_result = {} + ch_core.stavby_get_all(function(record) + local distance = vector.distance(pos, record.pos) + if record.spravuje == player_name then + -- spravovaná stavba: max. vzdálenost 100 m, stav jakýkoliv kromě opuštěného + if distance <= 100 and record.stav ~= "opusteno" and record.stav ~= "k_smazani" then + filter_result[1] = record + end + else + -- nespravovaná stavba: max. vzdálenost 20 m, stav rozestaveno nebo rekonstrukce + if distance <= 20 and (record.stav == "rozestaveno" or record.stav == "rekonstrukce") then + filter_result[1] = record + end + end + end) + if filter_result[1] == nil then + return false, "Pro nastavení pozice příkazem /stavím musíte mít do 100 metrů vámi spravovanou stavbu, která není opuštěná nebo ke smazání, ".. + "nebo musí být do 20 metrů cizí stavba, která je rozestavěná nebo v rekonstrukci." + end + local cas = ch_time.aktualni_cas() + local dnes = cas:YYYY_MM_DD() + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + if not offline_charinfo then + return false, "Interní údaje nebyly nalezeny!" + end + local _old_pos, pos_date + if offline_charinfo.stavba ~= nil then + _old_pos, pos_date = offline_charinfo.stavba:match("^([^@]+)@([^@]+)$") + end + if pos_date == dnes then + return false, "Cílovou pozici příkazem /stavím lze nastavit jen jednou denně!" + end + offline_charinfo.stavba = core.pos_to_string(pos).."@"..dnes + ch_data.save_offline_charinfo(player_name) + core.log("action", player_name.." set their 'stavba' position to "..offline_charinfo.stavba) + return true, "Pozice pro příkaz /nastavbu nastavena. Pamatujte, že tuto pozici můžete změnit jen jednou denně." +end + +-- /nastavbu +local function nastavbu(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if not offline_charinfo then + return false, "Interní údaje nebyly nalezeny!" + elseif not offline_charinfo.stavba or offline_charinfo.stavba == "" then + return false, "Nejprve si musíte nastavit pozici stavby příkazem /stavím." + end + local new_pos, pos_date = offline_charinfo.stavba:match("^([^@]+)@([^@]+)$") + new_pos = new_pos and core.string_to_pos(new_pos) + if not new_pos or not pos_date then + return false, "Uložená pozice má neplatný formát!" + end + ch_core.teleport_player{ + type = "player", + player = player_name, + target_pos = new_pos, + delay = 20.0, + sound_before = ch_core.default_teleport_sound, + sound_after = ch_core.default_teleport_sound, + } + return true +end + +-- /začátek +local function zacatek(player_name, _param) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if not offline_charinfo then + return false, "Interní údaje nebyly nalezeny!" + end + local player = minetest.get_player_by_name(player_name) + local player_pos = player and player:get_pos() + if not player_pos then + return false + end + local i = tonumber(offline_charinfo.zacatek_kam) or 1 + local zacatek_pos = ch_core.positions["zacatek_"..i] or ch_core.positions["zacatek_1"] or vector.zero() + if vector.distance(player_pos, zacatek_pos) < 5 then + return false, "Jste příliš blízko počáteční pozice!" + end + ch_core.teleport_player{ + type = "player", + player = player_name, + target_pos = zacatek_pos, + delay = 30.0, + sound_before = ch_core.default_teleport_sound, + sound_after = ch_core.default_teleport_sound, + } + return true +end + +local function bn_nastavit(player) + local player_name = player:get_player_name() + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo == nil then + return false, "Interní údaje nebyly nalezeny!" + end + local player_pos = player:get_pos() + online_charinfo.bod_navratu = core.pos_to_string(player_pos).."|"..core.get_us_time() + return true, "Bod návratu úspěšně nastaven na: "..core.pos_to_string(vector.round(player_pos)) +end + +local function bn_zpet(player) + local player_name = player:get_player_name() + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo == nil then + return false, "Interní údaje nebyly nalezeny!" + end + local s = online_charinfo.bod_navratu or "" + local target_pos, set_ts = s:match("^([^|]+)|([^|]+)$") + if target_pos ~= nil then + target_pos = core.string_to_pos(target_pos) + end + if set_ts ~= nil then + set_ts = tonumber(set_ts) + end + if target_pos == nil or set_ts == nil then + return false, "V poslední době jste si nenastavili bod návratu." + end + local now = core.get_us_time() + local diff = (now - set_ts) * 1.0e-6 + if diff > 30 * 60 then + online_charinfo.bod_navratu = "" + return false, "V poslední době jste si nenastavili bod návratu." + elseif diff < 6 then + diff = 0 + end + ch_core.teleport_player{ + type = "player", + player = player_name, + target_pos = target_pos, + delay = math.max(1, diff / 60), + sound_before = ch_core.default_teleport_sound, + sound_after = ch_core.default_teleport_sound, + } + return true +end + +-- exportovat funkce pro ch_extras: +ch_core.bn_nastavit = bn_nastavit +ch_core.bn_zpet = bn_zpet + +local def + +-- /začátek +def = { + description = "Přenese vás na počáteční pozici.", + func = zacatek, +} +core.register_chatcommand("zacatek", def); +core.register_chatcommand("začátek", def); +core.register_chatcommand("zacatek", def); +core.register_chatcommand("yačátek", def); +core.register_chatcommand("yacatek", def); + +-- /doma +def = { + description = "Uloží domovskou pozici pro pozdější návrat příkazem /domů.", + privs = {home = true}, + func = function(player_name) + local player = minetest.get_player_by_name(player_name) + local pos = player and player:get_pos() + if not pos then + return false + else + local result, err = doma(player_name, pos) + if result then + return result, "Domovská pozice nastavena!" + else + return false, err + end + end + end +} +core.register_chatcommand("doma", table.copy(def)); + +-- /domů +def = { + description = "Přenese vás na domovskou pozici uloženou příkazem /doma.", + privs = {home = true}, + func = domu, +} +core.register_chatcommand("domů", def); +core.register_chatcommand("domu", def); + +-- /stavím +def = { + description = "Uloží pozici na stavbě pro pozdější návrat příkazem /nastavbu. Pozici lze změnit jen jednou denně.", + privs = {home = true}, + func = function(player_name) + local player = minetest.get_player_by_name(player_name) + local pos = player and player:get_pos() + if pos then + return stavim(player_name, pos) + else + return false + end + end +} +core.register_chatcommand("stavím", def) +core.register_chatcommand("stavim", def) + +-- /nastavbu +def = { + description = "Přenese vás na pozici na stavbě uloženou příkazem /stavím.", + privs = {home = true}, + func = nastavbu, +} +core.register_chatcommand("nastavbu", def) +core.register_chatcommand("na_stavbu", def) + +-- /bodnávratu +def = { + description = "Uloží vaši pozici jako bod návratu pro příkaz /zpět.", + privs = {home = true}, + func = function(player_name, param) + local player = core.get_player_by_name(player_name) + if player ~= nil then + return bn_nastavit(player) + end + end, +} +core.register_chatcommand("bn", def) +core.register_chatcommand("bodnávratu", def) +core.register_chatcommand("bodnavratu", def) + +-- /zpět +def = { + description = "Přenese vás na místo, které jste si v poslední půlhodině uložili příkazem /bodnávratu.", + privs = {home = true}, + func = function(player_name, param) + local player = core.get_player_by_name(player_name) + if player ~= nil then + return bn_zpet(player) + end + end, +} +core.register_chatcommand("zpět", def) +core.register_chatcommand("zpet", def) + +ch_core.close_submod("teleportace") diff --git a/ch_core/textures/ch_core_0.png b/ch_core/textures/ch_core_0.png Binary files differnew file mode 100644 index 0000000..e3162c4 --- /dev/null +++ b/ch_core/textures/ch_core_0.png diff --git a/ch_core/textures/ch_core_1.png b/ch_core/textures/ch_core_1.png Binary files differnew file mode 100644 index 0000000..b32c8ac --- /dev/null +++ b/ch_core/textures/ch_core_1.png diff --git a/ch_core/textures/ch_core_10.png b/ch_core/textures/ch_core_10.png Binary files differnew file mode 100644 index 0000000..f997169 --- /dev/null +++ b/ch_core/textures/ch_core_10.png diff --git a/ch_core/textures/ch_core_11.png b/ch_core/textures/ch_core_11.png Binary files differnew file mode 100644 index 0000000..e9fa64e --- /dev/null +++ b/ch_core/textures/ch_core_11.png diff --git a/ch_core/textures/ch_core_12.png b/ch_core/textures/ch_core_12.png Binary files differnew file mode 100644 index 0000000..c34bc1c --- /dev/null +++ b/ch_core/textures/ch_core_12.png diff --git a/ch_core/textures/ch_core_13.png b/ch_core/textures/ch_core_13.png Binary files differnew file mode 100644 index 0000000..4d439e9 --- /dev/null +++ b/ch_core/textures/ch_core_13.png diff --git a/ch_core/textures/ch_core_14.png b/ch_core/textures/ch_core_14.png Binary files differnew file mode 100644 index 0000000..c5af5eb --- /dev/null +++ b/ch_core/textures/ch_core_14.png diff --git a/ch_core/textures/ch_core_15.png b/ch_core/textures/ch_core_15.png Binary files differnew file mode 100644 index 0000000..8772147 --- /dev/null +++ b/ch_core/textures/ch_core_15.png diff --git a/ch_core/textures/ch_core_2.png b/ch_core/textures/ch_core_2.png Binary files differnew file mode 100644 index 0000000..288ab4c --- /dev/null +++ b/ch_core/textures/ch_core_2.png diff --git a/ch_core/textures/ch_core_3.png b/ch_core/textures/ch_core_3.png Binary files differnew file mode 100644 index 0000000..bad1356 --- /dev/null +++ b/ch_core/textures/ch_core_3.png diff --git a/ch_core/textures/ch_core_4.png b/ch_core/textures/ch_core_4.png Binary files differnew file mode 100644 index 0000000..8fddcc2 --- /dev/null +++ b/ch_core/textures/ch_core_4.png diff --git a/ch_core/textures/ch_core_5.png b/ch_core/textures/ch_core_5.png Binary files differnew file mode 100644 index 0000000..356a262 --- /dev/null +++ b/ch_core/textures/ch_core_5.png diff --git a/ch_core/textures/ch_core_6.png b/ch_core/textures/ch_core_6.png Binary files differnew file mode 100644 index 0000000..de540bd --- /dev/null +++ b/ch_core/textures/ch_core_6.png diff --git a/ch_core/textures/ch_core_7.png b/ch_core/textures/ch_core_7.png Binary files differnew file mode 100644 index 0000000..e2e4451 --- /dev/null +++ b/ch_core/textures/ch_core_7.png diff --git a/ch_core/textures/ch_core_8.png b/ch_core/textures/ch_core_8.png Binary files differnew file mode 100644 index 0000000..ba821f5 --- /dev/null +++ b/ch_core/textures/ch_core_8.png diff --git a/ch_core/textures/ch_core_9.png b/ch_core/textures/ch_core_9.png Binary files differnew file mode 100644 index 0000000..deb0510 --- /dev/null +++ b/ch_core/textures/ch_core_9.png diff --git a/ch_core/textures/ch_core_chessboard.png b/ch_core/textures/ch_core_chessboard.png Binary files differnew file mode 100644 index 0000000..84351ad --- /dev/null +++ b/ch_core/textures/ch_core_chessboard.png diff --git a/ch_core/textures/ch_core_chisel.png b/ch_core/textures/ch_core_chisel.png Binary files differnew file mode 100644 index 0000000..3cd15db --- /dev/null +++ b/ch_core/textures/ch_core_chisel.png diff --git a/ch_core/textures/ch_core_clay.png b/ch_core/textures/ch_core_clay.png Binary files differnew file mode 100644 index 0000000..01a9d05 --- /dev/null +++ b/ch_core/textures/ch_core_clay.png diff --git a/ch_core/textures/ch_core_dialog_bg.png b/ch_core/textures/ch_core_dialog_bg.png Binary files differnew file mode 100644 index 0000000..10cf2a1 --- /dev/null +++ b/ch_core/textures/ch_core_dialog_bg.png diff --git a/ch_core/textures/ch_core_dot.png b/ch_core/textures/ch_core_dot.png Binary files differnew file mode 100644 index 0000000..84d6a3f --- /dev/null +++ b/ch_core/textures/ch_core_dot.png diff --git a/ch_core/textures/ch_core_empty.png b/ch_core/textures/ch_core_empty.png Binary files differnew file mode 100644 index 0000000..3c72136 --- /dev/null +++ b/ch_core/textures/ch_core_empty.png diff --git a/ch_core/textures/ch_core_formspec_bg.png b/ch_core/textures/ch_core_formspec_bg.png Binary files differnew file mode 100644 index 0000000..f4f211e --- /dev/null +++ b/ch_core/textures/ch_core_formspec_bg.png diff --git a/ch_core/textures/ch_core_kcs_1h.png b/ch_core/textures/ch_core_kcs_1h.png Binary files differnew file mode 100644 index 0000000..0a39318 --- /dev/null +++ b/ch_core/textures/ch_core_kcs_1h.png diff --git a/ch_core/textures/ch_core_kcs_1kcs.png b/ch_core/textures/ch_core_kcs_1kcs.png Binary files differnew file mode 100644 index 0000000..e57a57e --- /dev/null +++ b/ch_core/textures/ch_core_kcs_1kcs.png diff --git a/ch_core/textures/ch_core_kcs_1zcs.png b/ch_core/textures/ch_core_kcs_1zcs.png Binary files differnew file mode 100644 index 0000000..11106e7 --- /dev/null +++ b/ch_core/textures/ch_core_kcs_1zcs.png diff --git a/ch_core/textures/ch_core_kladivo.png b/ch_core/textures/ch_core_kladivo.png Binary files differnew file mode 100644 index 0000000..f8ee561 --- /dev/null +++ b/ch_core/textures/ch_core_kladivo.png diff --git a/ch_core/textures/ch_core_koruna.png b/ch_core/textures/ch_core_koruna.png Binary files differnew file mode 100644 index 0000000..6ad4ebc --- /dev/null +++ b/ch_core/textures/ch_core_koruna.png diff --git a/ch_core/textures/ch_core_kouzhul.png b/ch_core/textures/ch_core_kouzhul.png Binary files differnew file mode 100644 index 0000000..881b085 --- /dev/null +++ b/ch_core/textures/ch_core_kouzhul.png diff --git a/ch_core/textures/ch_core_planks_128.png b/ch_core/textures/ch_core_planks_128.png Binary files differnew file mode 100644 index 0000000..ea6299a --- /dev/null +++ b/ch_core/textures/ch_core_planks_128.png diff --git a/ch_core/textures/ch_core_pryc.png b/ch_core/textures/ch_core_pryc.png Binary files differnew file mode 100644 index 0000000..3d72ecf --- /dev/null +++ b/ch_core/textures/ch_core_pryc.png diff --git a/ch_core/textures/ch_core_slunce.png b/ch_core/textures/ch_core_slunce.png Binary files differnew file mode 100644 index 0000000..bd2adb0 --- /dev/null +++ b/ch_core/textures/ch_core_slunce.png diff --git a/ch_core/textures/ch_core_white_frame32.png b/ch_core/textures/ch_core_white_frame32.png Binary files differnew file mode 100644 index 0000000..dbc1898 --- /dev/null +++ b/ch_core/textures/ch_core_white_frame32.png diff --git a/ch_core/textures/ch_core_white_pixel.png b/ch_core/textures/ch_core_white_pixel.png Binary files differnew file mode 100644 index 0000000..d057ee3 --- /dev/null +++ b/ch_core/textures/ch_core_white_pixel.png diff --git a/ch_core/textures/ch_core_wood_noise_128.png b/ch_core/textures/ch_core_wood_noise_128.png Binary files differnew file mode 100644 index 0000000..e9488ff --- /dev/null +++ b/ch_core/textures/ch_core_wood_noise_128.png diff --git a/ch_core/textures/ch_core_wood_noise_32.png b/ch_core/textures/ch_core_wood_noise_32.png Binary files differnew file mode 100644 index 0000000..2293c81 --- /dev/null +++ b/ch_core/textures/ch_core_wood_noise_32.png diff --git a/ch_core/textures/ch_core_wood_noise_64.png b/ch_core/textures/ch_core_wood_noise_64.png Binary files differnew file mode 100644 index 0000000..2e8bc96 --- /dev/null +++ b/ch_core/textures/ch_core_wood_noise_64.png diff --git a/ch_core/textures/flag_cz.png b/ch_core/textures/flag_cz.png Binary files differnew file mode 100644 index 0000000..02b33ea --- /dev/null +++ b/ch_core/textures/flag_cz.png diff --git a/ch_core/textures/flag_sk.png b/ch_core/textures/flag_sk.png Binary files differnew file mode 100644 index 0000000..20c4eb3 --- /dev/null +++ b/ch_core/textures/flag_sk.png diff --git a/ch_core/timers.lua b/ch_core/timers.lua new file mode 100644 index 0000000..59a0dcb --- /dev/null +++ b/ch_core/timers.lua @@ -0,0 +1,123 @@ +ch_core.open_submod("timers", {data = true, hud = true, chat = true}) + +ch_core.count_of_ch_timer_hudbars = 4 + +local default_timer_bar_icon = "default_snowball.png" +local default_timer_bar_bar = "hudbars_bar_timer.png" + +-- timer_def - podporované klíče: +-- - label -- textový popis pro HUD, může být prázdný řetězec (volitelná) +-- - func -- funkce bez parametrů, která má být spuštěna po vypršení časovače; může být nil +-- - hudbar_icon -- ikona pro HUD (volitelná) +-- - hudbar_bgicon -- ikona pro HUD na pozadí (volitelná) +-- - hudbar_bar -- textura pro HUD (volitelná) +-- - hudbar_text_color -- barva textu pro HUD (volitelná) +function ch_core.start_ch_timer(online_charinfo, timer_id, delay, timer_def) + local timers = online_charinfo.ch_timers + if not timers then + timers = {} + online_charinfo.ch_timers = timers + elseif timers[timer_id] then + return false -- timer is already running + end + if delay < 0.0 then + error("Negative delay at start_ch_timer()!") + end + local now = ch_core.cas + local new_timer = timer_def or {} + new_timer = table.copy(new_timer) + new_timer.started_at = now + new_timer.run_at = now + delay + timers[timer_id] = new_timer + local player = minetest.get_player_by_name(online_charinfo.player_name) + local hudbar_id = player and ch_core.try_alloc_hudbar(player) + if hudbar_id then + new_timer.hudbar = hudbar_id + new_timer.last_hud_value = math.ceil(delay) + hb.change_hudbar(player, + hudbar_id, + new_timer.last_hud_value, + new_timer.last_hud_value, + new_timer.hudbar_icon or default_timer_bar_icon, + new_timer.hudbar_bgicon, + new_timer.hudbar_bar or default_timer_bar_bar, + new_timer.label or "časovač", + new_timer.hudbar_text_color or 0xFFFFFF) + if delay >= 0.1 then + hb.unhide_hudbar(player, hudbar_id) + end + end + return true +end + +function ch_core.is_ch_timer_running(online_charinfo, timer_id) + local timers = online_charinfo.ch_timers + if timers and timers[timer_id] then + return true + else + return false + end +end + +function ch_core.get_ch_timer_info(online_charinfo, timer_id) + local timers = online_charinfo.ch_timers + return timers and timers[timer_id] +end + +function ch_core.cancel_ch_timer(online_charinfo, timer_id) + local timers = online_charinfo.ch_timers + local timer = timers and timers[timer_id] + if not timer then + return false + end + if timer.hudbar then + local player = minetest.get_player_by_name(online_charinfo.player_name) + hb.hide_hudbar(player, timer.hudbar) + ch_core.free_hudbar(player, timer.hudbar) + end + timers[timer_id] = nil + return true +end + +local def = { + params = "[početsekund]", + description = "Odpočítá zadaný počet sekund. Při použití bez parametru jen zruší probíhající odpočet.", + privs = {}, + func = function(player_name, param) + local online_charinfo = ch_data.online_charinfo[player_name] + if param == "" then + ch_core.cancel_ch_timer(online_charinfo, "custom_timer") + return true + end + local sekund = param:gsub(",", ".") + sekund = tonumber(sekund) + if not sekund or sekund < 0 then + return false, "Neplatné zadání!" + end + if not online_charinfo then + return false, "Vnitřní chyba serveru." + end + ch_core.cancel_ch_timer(online_charinfo, "custom_timer") + local timer_func = function() + local player = minetest.get_player_by_name(player_name) + if player ~= nil then + ch_core.systemovy_kanal(player_name, "Nastavený časovač vypršel.") + minetest.sound_play("chat3_bell", {to_player = player_name, gain = 1.0}, true) + end + end + local timer_def = { + label = "odpočet", + func = timer_func, + } + local watch_def = minetest.registered_items["orienteering:watch"] + if watch_def ~= nil and watch_def.inventory_image ~= nil then + timer_def.hudbar_icon = watch_def.inventory_image + end + ch_core.start_ch_timer(online_charinfo, "custom_timer", sekund, timer_def) + return true + end, +} +minetest.register_chatcommand("odpočítat", def) +minetest.register_chatcommand("odpocitat", def) + +ch_core.close_submod("timers") diff --git a/ch_core/udm.lua b/ch_core/udm.lua new file mode 100644 index 0000000..105dad9 --- /dev/null +++ b/ch_core/udm.lua @@ -0,0 +1,72 @@ +ch_core.open_submod("udm", {areas = true, data = true, lib = true}) + +local color_celoserverovy = minetest.get_color_escape_sequence("#ff8700") +local color_mistni = minetest.get_color_escape_sequence("#fff297") +-- local color_mistni_zblizka = minetest.get_color_escape_sequence("#64f231") -- 54cc29 +local color_soukromy = minetest.get_color_escape_sequence("#ff4cf3") +-- local color_sepot = minetest.get_color_escape_sequence("#fff297cc") +local color_systemovy = minetest.get_color_escape_sequence("#cccccc") +-- local color_reset = minetest.get_color_escape_sequence("#ffffff") + +function ch_core.udm_catch_chat(player_name, message) + local pinfo = ch_core.normalize_player(player_name) + if message == "test" then + minetest.chat_send_player(player_name, color_mistni..pinfo.viewname..": test "..color_systemovy.."[0 post.]") + minetest.after(1, minetest.chat_send_player, player_name, "* Správně! Takto byste napsali zprávu postavám ve vašem okolí. Za zprávou se vypsalo „[0 post.]“, ".. + "což znamená, že ve skutečnosti nikomu nedošla. Kdyby došla, řekněme, na dva další herní klienty, bylo by tam „[2 post.]“.\nMůžete pokročit k další ceduli.") + elseif message == "!ahoj" then + minetest.chat_send_player(player_name, color_celoserverovy..pinfo.viewname..": ahoj "..color_systemovy.."[1 post.]") + minetest.after(1, minetest.chat_send_player, player_name, color_celoserverovy.."Fiktivní postava: zdravím! "..color_systemovy.."(563 m)") + minetest.after(2, minetest.chat_send_player, player_name, "* Správně! Takto byste napsali zprávu na všechny herní klienty připojené k serveru. A rovněž vám odpověděla ".. + "Fiktivní postava (která ve skutečnosti neexistuje, je to jen ukázka). Za její zprávou se ukázalo (563 m), což by znamenalo, že se nachází ".. + "563 metrů od vás.\nNyní můžete pokročit k další ceduli.") -- Nyní zkuste zadat: \"Fik test + elseif message == "\"Fik test" or message == "\"Fiktivní_postava test" or message == "\"Fiktivni_postava test" then + minetest.chat_send_player(player_name, color_soukromy.."-> Fiktivní postava: test") + minetest.after(1, minetest.chat_send_player, player_name, color_soukromy.."Fiktivní postava: zpráva dorazila") + minetest.after(2, minetest.chat_send_player, player_name, "* Správně! Právě jste (jako) napsali Fiktivní postavě soukromou zprávu a ona vám odpověděla. ".. + "U další zprávy už můžete předponu jména vynechat, protože systém si pamatuje, komu jste psali soukromou zprávu naposledy.\n".. + "Nyní tedy zkuste zadat: \" test 2\nA nezapomeňte na mezeru mezi \" a t!") + elseif message == "\" test 2" then + minetest.chat_send_player(player_name, color_soukromy.."-> Fiktivní postava: test 2") + minetest.after(1, minetest.chat_send_player, player_name, color_soukromy.."Fiktivní postava: i tato zpráva dorazila") + minetest.after(2, minetest.chat_send_player, player_name, "* Správně!\n".. + "To je vše, co zatím potřebujete vědět o ovládání četu. Můžete pokračovat k další ceduli.") + else + minetest.chat_send_player(player_name, "* Napsali jste: „"..message.."“, což není to, co jste měli. Zkuste to, prosím, znovu, nebo opusťte zelenou plochu na podlaze.") + end + minetest.log("action", "UDM chat catched a message: >"..message.."<") + return true +end + +local function nastavit_udm_zachyceni(player_name, area_id) + local area_id_number = tonumber(area_id) + if area_id_number == nil or area_id_number ~= math.floor(area_id_number) or area_id_number <= 0 then + return false, "Chybné ID oblasti!" + end + local area = ch_core.areas[area_id_number] + if area == nil then + area = {} + ch_core.areas[area_id_number] = area + area.udm_catch_chat = true + elseif area.udm_catch_chat then + area.udm_catch_chat = false + ch_core.save_areas() + return true, "Zachycení zrušeno pro oblast č. "..area_id_number + else + area.udm_catch_chat = true + end + ch_core.save_areas() + return true, "Zachycení nastaveno pro oblast č. "..area_id_number +end + +local def = { + params = "<id_oblasti>", + description = "Pro danou oblast zapne/vypne funkci zachycení četu pro ÚDM", + privs = {server = true}, + func = nastavit_udm_zachyceni, +} + +minetest.register_chatcommand("nastavit_údm_zachycení", def) +minetest.register_chatcommand("nastavit_udm_zachyceni", def) + +ch_core.close_submod("udm") diff --git a/ch_core/vezeni.lua b/ch_core/vezeni.lua new file mode 100644 index 0000000..6a148d0 --- /dev/null +++ b/ch_core/vezeni.lua @@ -0,0 +1,376 @@ +ch_core.open_submod("vezeni", {chat = true, data = true, lib = true, privs = true, hud = true}) + +local trest_min = -100 +local trest_max = 1000000 + +local vezeni_data = { + min = assert(ch_core.positions.vezeni_min), + max = assert(ch_core.positions.vezeni_max), + stred = assert(ch_core.positions.vezeni_cil), + dvere = ch_core.positions.vezeni_dvere, +} +local send_to_prison_callbacks = {} + +local prohledavane_inventare = { + "main", + "craft", + "bag1", "bag2", "bag3", "bag4", "bag5", "bag6", "bag7", "bag8", +} + +local povolene_krumpace = {} + +for _, n in ipairs({"default:pick_steel", "default:pick_bronze", + "moreores:pick_silver", "moreores:pick_mithril", "default:pick_mese", "default:pick_diamond"}) do + povolene_krumpace[n] = ItemStack(n) +end + +--[[ +local function nacist_vezeni() + local offline_charinfo = ch_data.get_offline_charinfo("Administrace") + local s = offline_charinfo.data_vezeni + if s and s ~= "" then + ch_core.vezeni_data = minetest.deserialize(s, true) + vezeni_data = ch_core.vezeni_data + end + return offline_charinfo +end +nacist_vezeni() +]] + +function ch_core.je_ve_vezeni(pos) + return ch_core.pos_in_area(pos, vezeni_data.min, vezeni_data.max) +end + +function ch_core.vezeni_kontrola_krumpace(player) + if not player:is_player() then + return false + end + local inv = player:get_inventory() + if not inv then + return false + end + for _, listname in ipairs(prohledavane_inventare) do + for itemname, stack in pairs(povolene_krumpace) do + if inv:contains_item(listname, stack, false) then + minetest.log("action", "Player "..player:get_player_name().." already has a pickaxe "..itemname.." in the inventory list "..listname..".") + return true + end + end + end + player:set_wielded_item(povolene_krumpace["default:pick_steel"]) + minetest.log("action", "Player "..player:get_player_name().." was given a steel pickaxe at a prison check.") + return true +end + +-- PRISON STONE +local function on_construct(pos) + local meta = minetest.get_meta(pos) + if ch_core.je_ve_vezeni(pos) then + meta:set_string("infotext", "Vězeňský kámen. Těžte tento kámen železným nebo lepším krumpáčem pro odpykání si trestu.") + else + meta:set_string("infotext", "") + end +end +local box = { + type = "fixed", + fixed = {-0.4, -0.5, -0.4, 0.4, 0.4, 0.4} +} +local def = { + description = "vězeňský kámen", + drawtype = "nodebox", + node_box = box, + selection_box = box, + tiles = {"default_stone.png"}, + paramtype2 = "none", + groups = {cracky = 1}, + drop = "default:stone", + sounds = default.node_sound_stone_defaults(), + + can_dig = function(pos, player) + if not ch_core.je_ve_vezeni(pos) or minetest.check_player_privs(player, "server") then + return default.can_interact_with_node(player, pos) + end + local player_name = player:get_player_name() + local tool = player:get_wielded_item():get_name() or "" + + -- require a pickaxe + if not povolene_krumpace[tool] then + ch_core.systemovy_kanal(player_name, "Ke snížení trestu musíte tento kámen odtěžit železným nebo lepším krumpáčem!") + return false + end + + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + local trest_old = offline_charinfo.player.trest + ch_core.trest("", player_name, -1) + local trest_new = offline_charinfo.player.trest + if trest_new ~= trest_old then + ch_core.systemovy_kanal(player_name, "Váš trest: "..trest_old.." >> "..trest_new) + end + return false + end, + on_construct = on_construct, +} +minetest.register_node("ch_core:prison_stone", def) + +def = { + label = "Update prison stone infotext", + name = "ch_core:prison_stone_infotext", + nodenames = {"ch_core:prison_stone"}, + run_at_every_load = true, + action = on_construct, +} +minetest.register_lbm(def) + +local prison_bar_icon = "default_snowball.png" +local prison_bar_bgicon = nil +local prison_bar_bar = "hudbars_bar_prison.png" + +local function update_hudbar(online_charinfo, trest) + local player = minetest.get_player_by_name(online_charinfo.player_name) + if not player then + return false + end + local hudbar_id = online_charinfo.prison_hudbar + if not hudbar_id then + return false + end + if trest < 0 then + trest = 0 + end + local old_max = online_charinfo.last_hudbar_trest_max + local new_max + if not old_max or trest > old_max then + new_max = trest + end + return hb.change_hudbar(player, hudbar_id, trest, new_max) +end + +-- přesune online postavu do vězení +local function do_vezeni(player_name, online_charinfo) + -- callbacks + local player = minetest.get_player_by_name(player_name) + if not player then + minetest.log("warning", "do_vezeni(): Player '"..player_name.."' not found!") + return false + end + for _, f in ipairs(send_to_prison_callbacks) do + f(player) + end + + -- announcement + local offline_charinfo = ch_data.offline_charinfo[player_name] or {} + ch_core.set_temporary_titul(player_name, "ve vězení", true) + ch_core.systemovy_kanal(player_name, "Jste ve výkonu trestu odnětí svobody. Těžte vězeňský kámen železným nebo lepším krumpáčem pro odpykání si trestu. Současná výše trestu: "..(offline_charinfo.player.trest or 0)) + + -- teleport + if not ch_core.je_ve_vezeni(player:get_pos()) then + player:set_pos(vezeni_data.stred) + end + + -- HUD + local hudbar_id = online_charinfo.prison_hudbar + if not hudbar_id then + hudbar_id = ch_core.try_alloc_hudbar(player) + if hudbar_id then + online_charinfo.prison_hudbar = hudbar_id + end + end + if hudbar_id then + online_charinfo.last_hudbar_trest_max = offline_charinfo.player.trest or 0 + hb.change_hudbar(player, hudbar_id, online_charinfo.last_hudbar_trest_max, online_charinfo.last_hudbar_trest_max, prison_bar_icon, prison_bar_bgicon, prison_bar_bar, "trest", 0xFFFFFF) + update_hudbar(online_charinfo, offline_charinfo.trest or 0) + hb.unhide_hudbar(player, hudbar_id) + end + + -- close the door + local door = vezeni_data.dvere and doors.get(vezeni_data.dvere) + if door then + minetest.after(0.5, function() + door:close(nil) + end) + end + + -- Check for a pickaxe + minetest.after(5, function(pname) + local player_2 = minetest.get_player_by_name(pname) + if player_2 then + ch_core.vezeni_kontrola_krumpace(player_2) + end + end, player_name) +end + +local function propustit_z_vezeni(player_name, online_charinfo) + ch_core.set_temporary_titul(player_name, "ve vězení", false) + ch_core.systemovy_kanal(player_name, "Byl/a jste propuštěn/a z vězení. Nyní se můžete volně pohybovat a používat teleportační příkazy.") + + local hudbar_id = online_charinfo.prison_hudbar + if hudbar_id then + local player = minetest.get_player_by_name(player_name) + if player then + ch_core.free_hudbar(player, hudbar_id) + end + online_charinfo.prison_hudbar = nil + end + + local door = vezeni_data.dvere and doors.get(vezeni_data.dvere) + if door then + door:open(nil) + end +end + +local function on_joinplayer(player, last_login) + local player_name = player:get_player_name() + local online_charinfo = ch_data.get_joining_online_charinfo(player) + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + local trest = tonumber(offline_charinfo.player.trest or 0) + + if trest > 0 then + do_vezeni(player_name, online_charinfo) + end +end + +minetest.register_on_joinplayer(on_joinplayer) + +-- volajici = komu vypsat hlášení; "" = provést změnu potichu +function ch_core.trest(volajici, jmeno, vyse_trestu) + local message + local result = false + + if type(vyse_trestu) ~= "number" then + minetest.log("error", "Invalid call to ch_core.trest() with vyse_trestu of type "..type(vyse_trestu)) + return result + end + jmeno = ch_core.jmeno_na_prihlasovaci(jmeno) + if not minetest.player_exists(jmeno) then + message = "Postava "..jmeno.." neexistuje!" + else + local offline_charinfo = ch_data.get_offline_charinfo(jmeno) + local trest = offline_charinfo.player.trest + local nova_vyse = math.round(trest + vyse_trestu) + if nova_vyse < trest_min then + nova_vyse = trest_min + elseif nova_vyse > trest_max then + nova_vyse = trest_max + end + if trest == nova_vyse then + message = "Aktuální výše trestu postavy "..ch_core.prihlasovaci_na_zobrazovaci(jmeno)..": "..trest + else + message = "Výše trestu postavy "..ch_core.prihlasovaci_na_zobrazovaci(jmeno).." změněna: "..trest.." => "..nova_vyse + offline_charinfo.player.trest = nova_vyse + ch_data.save_offline_charinfo(jmeno, true) + minetest.log("action", message) + + local online_charinfo = ch_data.online_charinfo[jmeno] + if online_charinfo then + update_hudbar(online_charinfo, nova_vyse) + + if trest > 0 and nova_vyse <= 0 then + propustit_z_vezeni(jmeno, online_charinfo) + elseif trest <= 0 and nova_vyse > 0 then + do_vezeni(jmeno, online_charinfo) + end + end + end + result = true + end + + if message and volajici and volajici ~= "" then + ch_core.systemovy_kanal(volajici, message) + end + return result +end + +--[[ + Je-li postava ve výkonu trestu, vrací výši jejího trestu, + jinak vrátí nil. +]] +function ch_core.je_ve_vykonu_trestu(player_name) + local trest = ch_core.safe_get_4(ch_data.offline_charinfo, player_name, "player", "trest") + if trest ~= nil and trest > 0 then + return trest + else + return nil + end +end + +function ch_core.vykon_trestu(player, player_pos, us_time, online_charinfo) + if not ch_core.pos_in_area(player_pos, vezeni_data.min, vezeni_data.max) then + player:set_pos(vezeni_data.stred) + elseif us_time >= (online_charinfo.pristi_kontrola_krumpace or 0) then + online_charinfo.pristi_kontrola_krumpace = us_time + 900000000 + ch_core.vezeni_kontrola_krumpace(player) + end +end + +-- func(player) +function ch_core.register_before_send_to_prison(func) + if func then + table.insert(send_to_prison_callbacks, func) + return true + else + return false + end +end + +-- /trest +def = { + params = "<Jméno_Postavy> <výše_trestu>", + privs = {ban = true}, + description = "Uloží nebo promine postavě trest", + func = function(player_name, param) + local jmeno, vyse = param:match("^(%S+) ([-]?%d+)$") + if not jmeno then + return false, "Neplatný formát parametrů!" + end + vyse = tonumber(vyse) + return ch_core.trest(player_name, jmeno, vyse) + end, +} +minetest.register_chatcommand("trest", def) + +--[[ /umístit_vězení +def = { + params = "(<X1>, <Y1>, <Z1>) (<X2>, <Y2>, <Z2>)", + privs = {server = true}, + description = "Nastaví pozici vězení. Při nastavování musíte stát uvnitř nastavované oblasti.", + func = function(player_name, param) + local pos1, pos2 = minetest.string_to_area(param) + if not pos1 then + return false, "Neplatný formát parametrů!" + end + local player = minetest.get_player_by_name(player_name) + local stred = player:get_pos() + return ch_core.umistit_vezeni(player_name, pos1, pos2, stred) + end, +} +minetest.register_chatcommand("umístit_vězení", def) +minetest.register_chatcommand("umistit_vezeni", def)]] + +--[[ /dveře_vězení +def = { + params = "", + privs = {server = true}, + description = "Nastaví pozici vězeňských dveří", + func = function(player_name, param) + local player = minetest.get_player_by_name(player_name) + if not player then + return false, "postava nenalezena!" + end + local pos = player:get_pos() + if not pos then + return false, "pozice nenalezena!" + end + pos = vector.round(pos) + local door = doors.get(pos) + if not door then + return false, "CHYBA: na pozici "..minetest.pos_to_string(pos).." nejsou dveře!" + end + vezeni_data.dvere = pos + ulozit_vezeni() + return true, "Pozice dveří vězení nastavena na "..minetest.pos_to_string(pos) + end +} +minetest.register_chatcommand("dveře_vězení", def) +minetest.register_chatcommand("dvere_vezeni", def)]] + +ch_core.close_submod("vezeni") diff --git a/ch_core/vgroups.lua b/ch_core/vgroups.lua new file mode 100644 index 0000000..5b43fd8 --- /dev/null +++ b/ch_core/vgroups.lua @@ -0,0 +1,32 @@ +ch_core.open_submod("vgroups") + +local vgroups = {} +local private_vgroups = {} + +function ch_core.create_private_vgroup(vgroup, tbl) + if vgroups[vgroup] then + error("Vgroup "..vgroup.." demanded as private already exists!") + end + local result = tbl or {} + vgroups[vgroup] = result + private_vgroups[vgroup] = true + return result +end + +function ch_core.get_shared_vgroup(vgroup) + if private_vgroups[vgroup] then + error("Vgroup "..vgroup.." is private!") + end + local result = vgroups[vgroup] + if not result then + result = {} + vgroups[vgroup] = result + end + return result +end + +function ch_core.try_read_vgroup(vgroup) + return vgroups[vgroup] +end + +ch_core.close_submod("vgroups") diff --git a/ch_core/wielded_light.lua b/ch_core/wielded_light.lua new file mode 100644 index 0000000..6059474 --- /dev/null +++ b/ch_core/wielded_light.lua @@ -0,0 +1,32 @@ +ch_core.open_submod("wielded_light", {data = true, lib = true, nodes = true}) + +local valid_numbers = {} + +for i = 0, minetest.LIGHT_MAX do + valid_numbers[i] = true +end + +function ch_core.set_player_light(player_name, slot, light_level) + local ll_1 = light_level or minetest.LIGHT_MAX + local ll = tonumber(ll_1) + if not ll then + minetest.log("warning", "Invalid light_level: "..ll_1) + return false + end + local online_charinfo = ch_data.online_charinfo[player_name] + if not online_charinfo or not valid_numbers[ll] then + return false + end + local slots = online_charinfo.wielded_lights + if not slots then + slots = {} + online_charinfo.wielded_lights = slots + end + if ll == 0 then + slots[slot] = nil + else + slots[slot] = ll + end +end + +ch_core.close_submod("wielded_light") diff --git a/ch_data/api.md b/ch_data/api.md new file mode 100644 index 0000000..05ca9cc --- /dev/null +++ b/ch_data/api.md @@ -0,0 +1,109 @@ +# Veřejné funkce + +## Data + + ch_data.online_charinfo[player_name] + +Tabulka neperzistentních dat pro danou postavu, která je ve hře. Musí obsahovat minimálně následující položky: + +* formspec_version : int : nejvyšší verze formspec podporovaná klientem; 0, pokud údaj není k dispozici +* join_timestamp : int : časová známka vytvoření online_charinfo (vstupu postavy do hry), z get_us_time() +* lang_code : string : jazykový kód (obvykle "cs") +* news_role : string : co udělat po připojení (významy jednotlivých řetězců se mění) +* player_name : string : přihlašovací jméno + + ch_data.offline_charinfo[player_name] + +Tabulka perzistentních dat pro danou postavu. Vytváří se pro každou známou postavu. Musí obsahovat minimálně +položky z ch_data.initial_offline_charinfo. + + ch_data.initial_offline_charinfo + +Tabulka klíč/hodnota. Klíče specifikují klíče vyžadované v záznamech offline_charinfo. Hodnoty specifikují +výchozí hodnoty těchto klíčů, které se buď doplní při načtení, pokud nejsou k dispozici, nebo se doplní +při vytvoření nového záznamu offline_charinfo. Položky v této tabulce mohou být přepisovány z jiných +módů, ale tyto změny se uplatní teprve při vytváření nových záznamů offline_charinfo. + +## Funkce + + ch_data.get_flag(charinfo : table, flag_name : string, default_result : any) -> string or any + +Z daného charinfo (offline_charinfo) přečte znak odpovídající požadovanému příznaku. Není-li příznak +dostupný, vrací default_result, popř. " ". + + ch_data.get_flags(charinfo : table) -> table + +Z daného charinfo (offline_charinfo) přečte všechny příznaky a vrátí je ve formě tabulky. +Vhodné pro dumpování. + + ch_data.set_flag(charinfo, flag_name, value) -> bool + +V daném charinfo (offline_charinfo) nastaví hodnotu požadovaného příznaku. Pokud příznak neexistuje, vrátí false. + + ch_data.get_joining_online_charinfo(player) -> table + +Volá se z callbacků on_joinplayer; vrátí ch_data.online_charinfo[player_name]. Pokud není, inicializuje jej. +Může být použita víckrát po sobě. + + ch_data.get_leaving_online_charinfo(player) -> table + +Volá se z callbacků on_leaveplayer; vrátí ch_data.online_charinfo[player_name] a vyřadí ho z tabulky. +Pokud už bylo vyřazeno, vrátí poslední vyřazenou tabulku. To umožňuje pracovat s online daty postavy +po celou dobu jejího odpojování. + + ch_data.delete_offline_charinfo(player_name) -> bool, string + +Pokud postava není ve hře, smaže její ch_data.offline_charinfo[] se vším všudy a vrátí true. +Pokud postava je ve hře, nebo její offline_charinfo neexistuje, vrátí false a chybovou zprávu. + + ch_data.get_offline_charinfo(player_name) -> table + +Vrátí ch_data.offline_charinfo[player_name]. Pokud neexistuje, skončí s chybou. + + ch_data.get_or_add_offline_charinfo(player_name) -> table + +Vrátí ch_data.offline_charinfo[player_name]. Pokud neexistuje, inicializuje nové. + + ch_data.save_offline_charinfo(player_name) -> bool + +Uloží ch_data.offline_charinfo[player_name]. Vrátí false, pokud dané offline_charinfo neexistuje nebo je jméno postavy nepřijatelné. + + ch_data.clear_help(player) + +Pro danou postavu smaže ch_data.online_charinfo[player_name].navody (perzistentně). + + ch_data.should_show_help(player : PlayerRef, online_charinfo : table, item_name : string) -> table or nil + +Otestuje, zda podle online_charinfo má dané postavě být zobrazený v četu návod k položce daného názvu. +Pokud ano, nastaví příznak, aby se to znovu již nestalo, a vrátí definici daného předmětu, +z níž lze z položek description a _ch_help sestavit text k zobrazení. Jinak vrátí nil. + + ch_data.nastavit_shybani(player_name : string, shybat : bool) -> bool + +Pro danou postavu perzistentně nastaví shýbání. (Volá ch_data.save_offline_charinfo().) + +# Příkazy v četu + +## /delete_offline_charinfo + +Syntaxe: + +``/delete_offline_charinfo Jmeno_postavy`` + +Odstraní údaje o postavě uložené v systému ch_data. Postava nesmí být ve hře. Vyžaduje právo `server`. + +## /návodyznovu + +Syntaxe: + +``/návodyznovu`` + +Smaže údaje o tom, ke kterým předmětům již byly postavě zobrazeny nápovědy, takže budou znovu zobrazovány nápovědy ke všem předmětům. + +## /shýbat + +Syntaxe: + +``/shýbat <ano|ne>`` + +Trvale vypne či zapne shýbání postavy při držení Shiftu. diff --git a/ch_data/init.lua b/ch_data/init.lua new file mode 100644 index 0000000..6373461 --- /dev/null +++ b/ch_data/init.lua @@ -0,0 +1,764 @@ +ch_base.open_mod(core.get_current_modname()) + +local worldpath = core.get_worldpath() +local datapath = worldpath.."/ch_playerdata" +local playerlist_path = worldpath.."/ch_data_players" +local storage = core.get_mod_storage() +local old_online_charinfo = {} -- uchovává online_charinfo[] po odpojení postavy +local players_list, players_set = {}, {} -- seznam/množina všech známých hráčských postav (pro offline_charinfo) +local lc_to_player_name = {} -- pro jména existujících postav lowercase => loginname +local current_format_version = 2 + +ch_data = { + online_charinfo = {}, + offline_charinfo = {}, + supported_lang_codes = {cs = true, sk = true}, + -- Tato funkce může být přepsána. Rozhoduje, zda zadané jméno postavy je přijatelné. + is_acceptable_name = function(player_name) return true end, + initial_offline_charinfo = { + -- int (> 0) -- pro [ch_core/ap], udává aktuální úroveň postavy + ap_level = 1, -- musí být > 0 + -- int (> 0) -- pro [ch_core/ap], udává verzi systému AP (pro upgrade) + ap_version = 1, + -- int (>= 0) -- pro [ch_core/ap], udává celkový počet bodů aktivity postavy + ap_xp = 0, + -- int {0, 1} -- 0 = nic, 1 = předměty házet do koše + discard_drops = 0, + -- string -- pro [ch_core/teleport], pozice uložená příkazem /domů + domov = "", + -- int (>= 0) -- pro [ch_core/chat] udává aktuální doslech + doslech = 50, + -- int {0, 1} -- pro [ch_core] 0 = normální velikost, 1 = rozšířený inventář + extended_inventory = 0, + -- string -- pole příznaků + flags = "", + -- string YYYY-MM-DD nebo "" -- datum, kdy byla hráči/ce naposledy vypsána oznámení po přihlášení do hry (YYYY-MM-DD) + last_ann_shown_date = "1970-01-01", + -- int (>= 0) -- v sekundách od 1. 1. 2000 UTC; 0 značí neplatnou hodnotu + last_login = 0, + -- int {0, 1} -- 0 = shýbat se při stisku Shift; 1 = neshýbat se + neshybat = 0, + -- int {0, 1} -- 0 = krásná obloha ano, 1 ne + no_ch_sky = 0, + -- float -- v sekundách + past_ap_playtime = 0.0, + -- float -- v sekundách + past_playtime = 0.0, + -- string -- výčet dodatečných práv naplánovaných pro registraci postavy + pending_registration_privs = "", + -- string -- typ naplánované registrace postavy + pending_registration_type = "", + -- int -- pro [ch_bank] + rezim_plateb = 0, + -- int {0, 1} -- 0 => zobrazit, 1 => skrýt + skryt_body = 0, + -- int {0, 1} -- 0 => zobrazovat (výchozí), 1 => skrýt + skryt_hlad = 0, + -- int {0, 1} -- 0 => zobrazovat (výchozí), 1 => skrýt + skryt_zbyv = 0, + -- string -- pro [ch_core/teleport], pozice uložená příkazem /stavím + stavba = "", + -- string -- nastavení filtru událostí + ui_event_filter = "", + -- int (>= 1) -- číslo verze uložených dat (umožňuje upgrade) + version = current_format_version, + -- int {1, 2, 3} -- volba cíle pro /začátek: 1 => Začátek, 2 => Masarykovo náměstí, 3 => Hlavní nádraží + zacatek_kam = 1, + }, + initial_offline_playerinfo = { + -- int -- výše uloženého trestu (může být záporná) + trest = 0, + } +} + +-- POZNÁMKA: protože příznaky z následujícího pole se mapují na znaky v řetězci 'flags', nesmí se mazat ani zakomentovat! +-- Je však možno je přejmenovat při zachování pozice. +local flag_ids = { + "discard_drops", + "extended_inventory", + "neshybat", + "no_ch_sky", + "skryt_body", + "skryt_hlad", + "skryt_zbyv", +} +local flag_name_to_id = {} + +for i, flag in ipairs(flag_ids) do + local old_id = flag_name_to_id[flag] + if old_id ~= nil then + error("Flag '"..flag.."' has multiple IDs: "..old_id..", "..i.."!") + end + flag_name_to_id[flag] = i +end +ch_data.initial_offline_charinfo.flags = string.rep(" ", #flag_ids) + +local function add_player(player_name, new_offline_charinfo, player_info) + if players_set[player_name] then + return false + end + assert(new_offline_charinfo) + local lcase = string.lower(player_name) + -- add player to players_list (persistently): + table.insert(players_list, player_name) + core.safe_file_write(playerlist_path, assert(core.serialize(players_list))) + -- add player to players_set and lc_to_player_name: + players_set[player_name] = true + lc_to_player_name[lcase] = player_name + -- add new offline_charinfo: + ch_data.offline_charinfo[player_name] = new_offline_charinfo + -- add new offline_charinfo[].player: + new_offline_charinfo.player = player_info or table.copy(ch_data.initial_offline_playerinfo) + if new_offline_charinfo.player.name == nil then + new_offline_charinfo.player.name = player_name + end + return true +end + +local function delete_player(player_name) + -- check if the player exists: + if not players_set[player_name] then + return false + end + -- detach offline_playerinfo: + local offline_player_info = assert(ch_data.offline_charinfo[player_name].player) + local aliases = {} + for alias, offline_charinfo in pairs(ch_data.offline_charinfo) do + if alias ~= player_name and offline_charinfo.player.name == player_name then + offline_player_info = offline_charinfo.player + table.insert(aliases, alias) + end + end + if #aliases > 0 then + offline_player_info.name = aliases[1] + core.log("debug", "delete_player(): dotplayer of "..table.concat(aliases, ",").." corrected to "..player_info.name) + for _, alias in ipairs(aliases) do + ch_core.save_offline_playerinfo(alias) + end + end + -- remove player from players_list: + local lcase = string.lower(player_name) + for i, pname in ipairs(players_list) do + if pname == player_name then + table.remove(players_list, i) + break + end + end + -- save the player_list: + core.safe_file_write(playerlist_path, assert(core.serialize(players_list))) + -- remove player from players_set: + players_set[player_name] = nil + -- remove player from lc_to_player_name: + lc_to_player_name[lcase] = nil + -- remove player from offline_charinfo: + ch_data.offline_charinfo[player_name] = nil + return true +end + +function ch_data.get_flag(charinfo, flag_name, default_result) + local id = flag_name_to_id[flag_name] + if id ~= nil then + local result = (charinfo.flags or ""):sub(id, id) + if result ~= "" then + return result + end + end + return default_result or " " +end + +function ch_data.get_flags(charinfo) + local flags = charinfo.flags or "" + local result = {} + for id, name in ipairs(flag_ids) do + local value = flags:sub(id, id) + if value == "" then + value = " " + end + result[name] = value + end + return result +end + +function ch_data.set_flag(charinfo, flag_name, value) + local id = flag_name_to_id[flag_name] + if id == nil then + return false + end + value = (tostring(value).." "):sub(1,1) + local flags = charinfo.flags or "" + if flags:len() < id then + flags = flags..string.rep(" ", id - flags:len() - 1)..value + else + flags = flags:sub(1, id - 1)..value..flags:sub(id + 1, -1) + end + charinfo.flags = flags + return true +end + +function ch_data.correct_player_name_casing(name) + return lc_to_player_name[string.lower(name)] +end + +function ch_data.get_joining_online_charinfo(player) + assert(core.is_player(player)) + local player_name = player:get_player_name() + local result = ch_data.online_charinfo[player_name] + if result ~= nil then + return result + end + local player_info = core.get_player_information(player_name) + local now = core.get_us_time() + result = { + areas = {{ + id = 0, + name = "Český hvozd", + type = 1, + }}, + -- časová známka vytvoření online_charinfo (vstupu postavy do hry) + join_timestamp = now, + -- jazykový kód (obvykle "cs") + lang_code = player_info.lang_code or "", + -- úroveň osvětlení postavy + light_level = 0, + -- časová známka pro úroveň osvětlení postavy + light_level_timestamp = now, + -- verze protokolu + protocol_version = player_info.protocol_version or 0, + -- tabulka již zobrazených nápověd (bude deserializována níže) + navody = {}, + -- co udělat těsně po připojení + news_role = "new_player", + -- přihlašovací jméno + player_name = player_name, + } + + -- news_role: + --[[ + 5.5.x => formspec_version = 5, protocol_version = 40 + 5.6.x => formspec_version = 6, protocol_version = 41 + 5.7.x => formspec_version = 6, protocol_version = 42 + 5.8.0 => formspec_version = 7, protocol_version = 43 + 5.9.0 => formspec_version = ?, protocol_version = ? + 5.10.0 => formspec_version = 8, protocol_version = 46 + ]] + if result.protocol_version < 42 then + result.news_role = "disconnect" + elseif not ch_data.supported_lang_codes[result.lang_code] and not core.check_player_privs(player, "server") then + result.news_role = "invalid_locale" + elseif core.check_player_privs(player, "ch_registered_player") then + result.news_role = "player" + elseif not ch_data.is_acceptable_name(player_name) then + result.news_role = "invalid_name" + else + result.news_role = "new_player" + end + + ch_data.online_charinfo[player_name] = result + local prev_online_charinfo = old_online_charinfo[player_name] + if prev_online_charinfo ~= nil then + old_online_charinfo[player_name] = nil + if prev_online_charinfo.leave_timestamp ~= nil then + result.prev_leave_timestamp = prev_online_charinfo.leave_timestamp + end + end + core.log("action", "JOIN PLAYER(" .. player_name ..") at "..now.." with lang_code \""..result.lang_code.. + "\", formspec_version = "..tostring(player_info.formspec_version)..", protocol_version = ".. + result.protocol_version..", news_role = "..result.news_role..", ip_address = "..tostring(player_info.address)) + + -- deserializovat návody: + local meta = player:get_meta() + local s = meta:get_string("navody") + if s and s ~= "" then + result.navody = core.deserialize(s, true) or result.navody + end + + if result.news_role ~= "invalid_name" then + ch_data.get_or_add_offline_charinfo(player_name) + end + --[[ + TODO: + if core.is_creative_enabled(player_name) then + result.is_creative = true + if ch_core.set_immortal then + ch_core.set_immortal(player, true) + end + end + else + core.log("error", "Player object not available for "..player_name.." in get_joining_online_charinfo()!") + end + ]] + return result +end + +function ch_data.get_leaving_online_charinfo(player) + assert(core.is_player(player)) + local player_name = player:get_player_name() + local result = ch_data.online_charinfo[player_name] + if result ~= nil then + result.leave_timestamp = core.get_us_time() + old_online_charinfo[player_name] = result + ch_data.online_charinfo[player_name] = nil + return result + else + return old_online_charinfo[player_name] + end +end + +function ch_data.delete_offline_charinfo(player_name) + if ch_data.online_charinfo[player_name] ~= nil then + return false, "Postava je ve hře!" + end + if not delete_player(player_name) then + return false, "Mazání selhalo." + end + local success, errmsg = os.remove(worldpath.."/ch_playerdata/"..player_name) + if success then + return true, "Úspěšně smazáno." + else + return false, "Mazání souboru selhalo: "..(errmsg or "nil") + end +end + +function ch_data.get_offline_charinfo(player_name) + local result = ch_data.offline_charinfo[player_name] + if result == nil then + error("Offline charinfo not found for player '"..player_name.."'!") + end + return result +end + +function ch_data.get_or_add_offline_charinfo(player_name) + local result = ch_data.offline_charinfo[player_name] + if result == nil then + add_player(player_name, table.copy(ch_data.initial_offline_charinfo)) + result = assert(ch_data.offline_charinfo[player_name]) + core.log("action", "[ch_data] Offline charinfo initialized for "..player_name) + ch_data.save_offline_charinfo(player_name) + end + return result +end + +local debug_flag = false + +function ch_data.save_offline_charinfo(player_name, include_playerinfo) + if players_set[player_name] == nil then + return false + end + local data = ch_data.offline_charinfo[player_name] + if data == nil then + return false + end + core.safe_file_write(datapath.."/"..player_name, assert(core.serialize(data))) + if include_playerinfo and data.player.name ~= player_name then + local dotplayer_name = data.player.name + assert(ch_data.offline_charinfo[dotplayer_name]) + assert(ch_data.offline_charinfo[dotplayer_name].player.name == dotplayer_name) + return ch_data.save_offline_charinfo(dotplayer_name) + end + return true +end + +function ch_data.save_offline_playerinfo(player_name) + if players_set[player_name] == nil then + return false + end + local data = ch_data.offline_charinfo[player_name] + if data == nil then + return false + end + local dotplayer_name = assert(data.player.name) + assert(ch_data.offline_charinfo[dotplayer_name]) + assert(ch_data.offline_charinfo[dotplayer_name].player.name == dotplayer_name) + return ch_data.save_offline_charinfo(dotplayer_name) +end + +local function on_joinplayer(player, last_login) + ch_data.get_joining_online_charinfo(player) +end + +local function on_leaveplayer(player) + ch_data.get_leaving_online_charinfo(player) +end + +core.register_on_joinplayer(on_joinplayer) +core.register_on_leaveplayer(on_leaveplayer) + +local function upgrade_offline_charinfo(player_name, data) + local old_version = data.version + if data.version <= 1 then + data.player.trest = data.trest or 0 + data.trest = nil + end + data.version = current_format_version + core.log("info", "Offline_charinfo["..player_name.."] upgraded from version "..old_version.." to the current version "..data.version..".") + return true +end + +-- Load and initialize: + +core.mkdir(datapath) +local function initialize() + local f = io.open(playerlist_path) + if f then + local text = f:read("*a") + f:close() + if text ~= nil and text ~= "" then + local new_players = core.deserialize(text, true) + if type(new_players) == "table" then + core.log("action", "[ch_data] "..#new_players.." known players.") + players_list = new_players + players_set = {} + for _, player_name in ipairs(players_list) do + players_set[player_name] = true + end + end + end + end + for _, player_name in ipairs(players_list) do + f = io.open(datapath.."/"..player_name) + if f then + local text = f:read("*a") + f:close() + if text ~= nil and text ~= "" then + local data = core.deserialize(text, true) + if type(data) == "table" then + ch_data.offline_charinfo[player_name] = data + lc_to_player_name[string.lower(player_name)] = player_name + end + end + end + if ch_data.offline_charinfo[player_name] == nil then + core.log("error", "[ch_data] deserialization of offline_charinfo["..player_name.."] failed!") + end + end + for player_name, poc in pairs(ch_data.offline_charinfo) do + -- vivify/upgrade/correct offline_charinfo: + for key, value in pairs(ch_data.initial_offline_charinfo) do + if poc[key] == nil then + poc[key] = value + core.log("warning", "Missing offline_charinfo key "..player_name.."/"..key.." vivified.") + end + end + -- correct invalid past_playtime: + if poc.past_playtime < 0 then + core.log("warning", "Invalid past_playtime for "..player_name.." ("..poc.past_playtime..") corrected to zero!") + poc.past_playtime = 0 -- correction of invalid data + end + -- vivify .player table (including .player.name) + if poc.player == nil or poc.player.name == nil then + poc.player = {name = player_name} + end + end + -- link .player tables according to .player.name: + for player_name, poc in pairs(ch_data.offline_charinfo) do + local dotplayer_name = assert(poc.player.name) + if dotplayer_name ~= player_name then + if players_set[dotplayer_name] and ch_data.offline_charinfo[dotplayer_name] then + poc.player = assert(ch_data.offline_charinfo[dotplayer_name].player) + else + error("offline_charinfo["..player_name.."] looks for player data of '"..dotplayer_name.."', but it doesn't exist!") + end + end + end + -- upgrade old data: + for player_name, poc in pairs(ch_data.offline_charinfo) do + if poc.version < current_format_version then + upgrade_offline_charinfo(player_name, poc) + end + end +end + +initialize() +initialize = nil + +-- Obsluhy událostí: +-- ====================================================================================================================== +local function on_joinplayer(player, last_login) + local player_name = player:get_player_name() + local online_charinfo = ch_data.get_joining_online_charinfo(player) + local offline_charinfo = ch_data.get_offline_charinfo(player_name) + online_charinfo.doslech = offline_charinfo.doslech + + if offline_charinfo ~= nil then + offline_charinfo.last_login = os.time() - 946684800 + ch_data.save_offline_charinfo(player_name) + -- lc_to_player_name[string.lower(player_name)] = player_name + end + + return true +end + +local function save_playtime(online_charinfo, offline_charinfo) + if offline_charinfo == nil then + return 0, 0, 0 + end + local now = core.get_us_time() + local past_playtime = offline_charinfo.past_playtime or 0 + local current_playtime = math.max(0, 1.0e-6 * (now - online_charinfo.join_timestamp)) + local total_playtime = past_playtime + current_playtime + + offline_charinfo.past_playtime = total_playtime + ch_data.save_offline_charinfo(online_charinfo.player_name) + online_charinfo.join_timestamp = nil + return past_playtime, current_playtime, total_playtime +end + +local function on_leaveplayer(player, timedout) + local player_name = player:get_player_name() + local online_info = ch_data.get_leaving_online_charinfo(player) + + if online_info.join_timestamp then + local past_playtime, current_playtime, total_playtime = save_playtime(online_info, ch_data.offline_charinfo[player_name]) + print("PLAYER(" .. player_name .."): played seconds: " .. current_playtime .. " / " .. total_playtime) + end +end + +local function on_shutdown() + for player_name, online_info in pairs(table.copy(ch_data.online_charinfo)) do + if online_info.join_timestamp then + local past_playtime, current_playtime, total_playtime + past_playtime, current_playtime, total_playtime = save_playtime(online_info, ch_data.offline_charinfo[player_name]) + print("PLAYER(" .. player_name .."): played seconds: " .. current_playtime .. " / " .. total_playtime) + end + end +end + +local function on_placenode(pos, newnode, placer, oldnode, itemstack, pointed_thing) + if core.is_player(placer) then + local player_name = placer:get_player_name() + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo ~= nil then + online_charinfo.last_placenode_ustime = core.get_us_time() + end + end +end + +local function on_dignode(pos, oldnode, digger) + if core.is_player(digger) then + local player_name = digger:get_player_name() + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo ~= nil then + online_charinfo.last_dignode_ustime = core.get_us_time() + end + end +end + +function ch_data.clear_help(player) + local player_name = player:get_player_name() + local online_charinfo = ch_data.online_charinfo[player_name] + if online_charinfo then + online_charinfo.navody = {} + player:get_meta():set_string("navody", core.serialize(online_charinfo.navody)) + return true + else + return false + end +end + +--[[ + Otestuje, zda podle online_charinfo má dané postavě být zobrazený + v četu návod k položce daného názvu. Pokud ano, nastaví příznak, aby se + to znovu již nestalo, a vrátí definici daného předmětu, + z níž lze z položek description a _ch_help sestavit text k zobrazení. +]] +function ch_data.should_show_help(player, online_charinfo, item_name) + local def = core.registered_items[item_name] + if def and def._ch_help then + if def._ch_help_group then + item_name = def._ch_help_group + end + local navody = online_charinfo.navody + if not navody then + navody = {[item_name] = 1} + online_charinfo.navody = navody + player:get_meta():set_string("navody", core.serialize(navody)) + return def.description ~= nil and def + end + if not navody[item_name] then + navody[item_name] = 1 + player:get_meta():set_string("navody", core.serialize(navody)) + return def.description ~= nil and def + end + end + return nil +end + +function ch_data.nastavit_shybani(player_name, shybat) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo == nil then + core.log("error", "ch_data.nastavit_shybani(): Expected offline charinfo for player "..player_name.." not found!") + return false + end + local new_state + if shybat then + new_state = 0 + else + new_state = 1 + end + offline_charinfo.neshybat = new_state + ch_data.save_offline_charinfo(player_name) + return true +end + +core.register_on_joinplayer(on_joinplayer) +core.register_on_leaveplayer(on_leaveplayer) +core.register_on_shutdown(on_shutdown) +core.register_on_dignode(on_dignode) +core.register_on_placenode(on_placenode) + +local def = { + description = "Smaže údaje o tom, ke kterým předmětům již byly postavě zobrazeny nápovědy, takže budou znovu zobrazovány nápovědy ke všem předmětům.", + func = function(player_name, param) + if ch_data.clear_help(core.get_player_by_name(player_name)) then + return true, "Údaje smazány." + else + core.log("error", "/návodyznovu: vnitřní chyba serveru ("..player_name..")!") + return false, "Vnitřní chyba serveru" + end + end, +} +core.register_chatcommand("návodyznovu", def) +core.register_chatcommand("navodyznovu", def) + +def = { + description = "Odstraní údaje o postavě uložené v systému ch_data. Postava nesmí být ve hře.", + privs = {server = true}, + func = function(player_name, param) + local offline_charinfo = ch_data.offline_charinfo[param] + if not offline_charinfo then + return false, "Data o "..param.." nenalezena!" + end + if ch_data.delete_offline_charinfo(param) then + return true, "Data o "..param.." smazána." + else + return false, "Při odstraňování nastala chyba." + end + end, +} + +core.register_chatcommand("delete_offline_charinfo", def) + +def = { + description = "Trvale vypne či zapne shýbání postavy při držení Shiftu.", + params = "<ano|ne>", + func = function(player_name, param) + if param ~= "ano" and param ~= "ne" then + return false, "Chybná syntaxe." + end + if ch_data.nastavit_shybani(player_name, param == "ano") then + core.chat_send_player(player_name, "*** Shýbání postavy "..(param == "ne" and "vypnuto" or "zapnuto")..".") + return true + else + core.log("error", "/shybat: Expected offline charinfo for player "..player_name.." not found!") + return false, "Vnitřní chyba serveru: Data postavy nenalezena." + end + end, +} + +core.register_chatcommand("shýbat", def) +core.register_chatcommand("shybat", def) + +local function merge_playerinfos(player_name_a, player_name_b) + if player_name_a == player_name_b then + return false, "'"..player_name_a.."' a '"..player_name_b.."' reprezentují tutéž postavu!" + end + local oci_a = ch_data.offline_charinfo[player_name_a] + local oci_b = ch_data.offline_charinfo[player_name_b] + if oci_a == nil then + return false, "Postava '"..player_name_a.."' neexistuje!" + end + if oci_b == nil then + return false, "Postava '"..player_name_b.."' neexistuje!" + end + local dotplayer_a = assert(oci_a.player.name) + local dotplayer_b = assert(oci_b.player.name) + if dotplayer_a == dotplayer_b then + return false, "Postavy '"..player_name_a.."' a '"..player_name_b.."' již patří stejné/mu hráči/ce '"..dotplayer_a.."'." + end + local aliases = {dotplayer_b} + for alias, offline_charinfo in pairs(ch_data.offline_charinfo) do + if alias ~= dotplayer_b and offline_charinfo.player.name == dotplayer_b then + table.insert(aliases, alias) + end + end + ch_data.save_offline_charinfo(dotplayer_b) + for _, alias in ipairs(aliases) do + ch_data.offline_charinfo[alias].player = oci_a.player + ch_data.save_offline_charinfo(alias) + core.log("action", "[MERGE] Player info of .player "..dotplayer_a.." assigned to player "..alias..".") + end + return true, "Postavy "..table.concat(aliases, ",").." přiřazeny hráči/ce "..dotplayer_a.."." +end + +local function set_main_player(player_name) + local oci_a = ch_data.offline_charinfo[player_name] + if oci_a == nil then + return false, "Postava '"..player_name.."' neexistuje!" + end + local old_main = assert(oci_a.player.name) + if old_main == player_name then + return false, "Postava '"..player_name.."' již je hlavní." + end + local aliases = {} + for alias, offline_charinfo in pairs(ch_data.offline_charinfo) do + if alias ~= old_main and offline_charinfo.player.name == old_main then + table.insert(aliases, alias) + end + end + oci_a.player.name = player_name + ch_data.save_offline_charinfo(old_main) + for _, alias in ipairs(aliases) do + ch_data.save_offline_charinfo(alias) + end + return true, "Postava "..player_name.." nastavena jako hlavní (původní hlavní postava: "..old_main..")." +end + +function ch_data.get_player_characters(player_name) + local offline_charinfo = ch_data.offline_charinfo[player_name] + if offline_charinfo == nil then + return nil + end + local main_name = offline_charinfo.player.name + local result = {} + for name, oci in pairs(ch_data.offline_charinfo) do + if oci.player.name == main_name then + table.insert(result, name) + end + end + if #result > 1 then + table.sort(result, function(a, b) return (a == main_name and b ~= main_name) or a < b end) -- TODO: better sorting + end + return result, main_name +end + +def = { + description = "Sloučí hráčská data odpovídající postavě B s hráčskými daty odpovídajícími postavě A", + params = "<Jmeno_postavy_hlavni_A> <Jmeno_postavy_vedlejsi_B>", + privs = {server = true}, + func = function(admin_name, param) + local a, b = string.match(param, "^([^ ]+) +([^ ]+)$") + if a == nil or b == nil then + return false, "Chybné zadání!" + end + local result, message = merge_playerinfos(a, b) + return result, message + end, +} + +core.register_chatcommand("připojit_postavu", def) +core.register_chatcommand("pripojit_postavu", def) + +def = { + description = "Změní hlavní postavu hráče/ky", + params = "<Nova_hlavni_postava>", + privs = {server = true}, + func = function(admin_name, param) + local result, message = set_main_player(param) + return result, message + end, +} + +core.register_chatcommand("hlavní_postava", def) +core.register_chatcommand("hlavni_postava", def) + +ch_base.close_mod(core.get_current_modname()) diff --git a/ch_data/license.txt b/ch_data/license.txt new file mode 100644 index 0000000..6295ca7 --- /dev/null +++ b/ch_data/license.txt @@ -0,0 +1,15 @@ +License of source code +---------------------- + +MIT License + +Copyright (c) 2025 Singularis <singularis@volny.cz> + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Licenses of media (models, sounds, textures) +------------------------------------ diff --git a/ch_data/mod.conf b/ch_data/mod.conf new file mode 100644 index 0000000..a9f8eb8 --- /dev/null +++ b/ch_data/mod.conf @@ -0,0 +1,3 @@ +name = ch_data +description = Player data API from Český hvozd +depends = ch_base diff --git a/ch_time/api.md b/ch_time/api.md new file mode 100644 index 0000000..946d028 --- /dev/null +++ b/ch_time/api.md @@ -0,0 +1,179 @@ +# Veřejné funkce + +## Nastavení a callbacky + + ch_time.get_time_shift() -> int + ch_time.set_time_shift(new_shift) + +Získá/nastaví časový posun v sekundách, který se přičítá k výstupu os.time() k získání správné aktuální časové známky. +Výchozí posun je 0. + + ch_time.get_time_speed_during_day() + ch_time.set_time_speed_during_day(new_value) + +Získá/nastaví rychlost plynutí herního času během dne. Hodnota může být nil, v takovém případě se při úsvitu rychlost času nemění. +Výchozí hodnota je nil. + + ch_time.get_time_speed_during_night() + ch_time.set_time_speed_during_night(new_value) + +Získá/nastaví rychlost plynutí herního času během noci. Hodnota může být nil, v takovém případě se při soumraku rychlost času nemění. +Výchozí hodnota je nil. + + ch_time.get_rwtime_callback() + ch_time.set_rwtime_callback(new_callback) + +Získá/nastaví callback volaný pro získání aktuálního železničního času. Je-li nil, železniční čas není dostupný. Výchozí hodnota je nil. +Callback se bude volat bez parametrů a musí vracet tabulku v tomto formátu: + +{ + secs = int, -- železniční čas v číselné formě (to_secs()) + string = string, -- železniční čas v textové formě bez cyklu (jen minuty a sekundy) + string_extended = string, -- železniční čas v úplné textové formě (tzn. včetně cyklu) +} + +## Třída "Cas" + +Objekty této třídy vrací funkce ch_time.aktualni_cas() a ch_time.na_strukturu(). Reprezentují konkrétní časový okamžik +v kombinaci s příslušným místním a UTC časem. + + Cas:den_v_tydnu_cislo() -> int + +Vrací den v týdnu jako číslo od 1 (pondělí) do 7 (neděle) + + Cas:den_v_tydnu_nazev() -> string + +Vrací název dne v týdnu malými písmeny ("pondělí" až "neděle") + + Cas:nazev_mesice(pad) -> string + +pad musí být 1, nebo 2. Vrací název měsíce v uvedeném pádě. + + Cas:den_v_roce() -> int + +Vrací den v roce (1 až 366) + + Cas:posun_cislo() -> int + +Vrací posun místního času proti UTC v počtu hodin. + + Cas:posun_text() -> string + +Vrací posun místního času proti UTC v textovém formátu "+HH:MM". + + Cas:znamka32() -> int + +Vrací celý počet sekund od 1. 1. 2020 UTC (může být záporný), v rozsahu typu int32_t. + + Cas:YYYY_MM_DD() -> string + +Vrací místní čas ve formátu "YYYY-MM-DD". + + Cas:YYYY_MM_DD_HH_MM_SS() -> string + +Vrací místní čas ve formátu "YYYY-MM-DD HH:MM:SS". + + Cas:YYYY_MM_DD_HH_MM_SS_ZZZ() -> string + +Vrací místní čas ve formátu "YYYY-MM-DD HH:MM:SS +HH:MM". + + Cas:YYYY_MM_DD_HH_MM_SSZZZ() -> string + +Vrací místní čas ve formátu "YYYY-MM-DD HH:MM:SS+HH:MM". + + Cas:YYYY_MM_DDTHH_MM_SSZZZ() -> string + +Vrací místní čas ve formátu "YYYY-MM-DDTHH:MM:SS+HH:MM" (tzn. zcela bez mezer). + + Cas:HH_MM_SS() + +Vrací místní čas ve formátu "HH:MM:SS". + + Cas:UTC_YYYY_MM_DD() + +Vrací UTC čas ve formátu "YYYY-MM-DD". + + Cas:UTC_YYYY_MM_DD_HH_MM_SS() + +Vrací UTC čas ve formátu "YYYY-MM-DD HH:MM:SS". + + Cas:za_n_sekund(n) + +Vrací nový objekt třídy Cas reprezentující časový okamžik o n sekund později než tento objekt; +n může být záporné, v takovém případě bude okamžik o -n sekund dříve. + +## Funkce pro práci s reálným časem + + ch_time.aktualni_cas() + +Vrací objekt třídy Cas reprezentující aktuální čas. V podstatě jde o synonymum k ch_time.na_strukturu(nil). + + ch_time.time() -> int + +Vrací hodnotu os.time() posunutou o posun zadaný funkcí ch_time.set_time_shift(new_shift). + + ch_time.na_strukturu(time) + +Vrací objekt třídy Cas reprezentující zadaný čas (ve stejném formátu jako výstup ch_time.time()). +Kromě metod nabízí ještě následující datové prvky: + +{ + time = int, -- původní zadaná časová známka + utc = table, -- struktura vrácená příkazem os.date() reprezentující UTC čas; prvky jsou např.: year, month, day, hour, min, sec, yday + lt = lt, -- struktura vrácená příkazem os.date() reprezentující místní čas + rok = int, -- rok (místní) + mesic = int, -- měsíc 1 až 12 (místní) + den = int, -- den 1 až 31 (místní) + hodina = int, -- hodina 0 až 23 (místní) + minuta = int, -- minuta 0 až 59 (místní) + sekunda = int, -- sekunda 0 až 59 (místní) + je_letni_cas = bool, -- indikátor, zda místní čas je letní +} + + ch_time.znamka32(time) + +Vrátí časovou známku v rozsahu typu 'int32_t' jako počet sekund od 1. 1. 2020 UTC. +Rozsah je od 1951-12-13 20:45:53 UTC do 2088-01-19 03:14:07 UTC. +Není-li zadaná časová známka (nil), použije aktuální čas. + +## Funkce pro práci s herním časem + + ch_time.herni_cas() -> table + +Vrací herní čas ve struktuře s následujícími prvky: + +{ + day_count = int -- návratová hodnota funkce minetest.get_day_count() + timeofday = float -- hodnota podle funkce minetest.get_timeofday() + hodina = int (0..23) -- hodina ve hře (celá) + minuta = int (0..59) -- minuta ve hře (celá) + sekunda = int (0..59) -- sekunda ve hře (celá) + daynight_ratio = float + natural_light = int (0..15) + time_speed = float -- návratová hodnota core.settings:get("time_speed") +} + + ch_time.herni_cas_nastavit(h, m, s) -> table + +Nastaví herní čas na zadanou hodnotu uvedenou ve formátu hodin, minut a sekund. +Vrací výstup ch_time.herni_cas() po provedení nastavení. +Součástí nastavení může být změna rychlosti plynutí času. + +# Příkazy v četu + +## /posunčasu + +Syntaxe: + +``/posunčasu <celé_číslo>`` + +Nastaví posun času, volá funkci ch_time.set_time_shift(). Účinek je okamžitý. +Vyžaduje právo *server*. + +## /čas + +Syntaxe: + +``/čas [utc|utc+|m[ístní]|m[ístní]+|h[erní]|h[erní]+|ž[elezniční]|ž[elezniční]+]`` + +Vypíše požadovaný druh času. Není-li druh zadán, vypíše herní čas. diff --git a/ch_time/init.lua b/ch_time/init.lua new file mode 100644 index 0000000..a8e5627 --- /dev/null +++ b/ch_time/init.lua @@ -0,0 +1,518 @@ +ch_base.open_mod(core.get_current_modname()) + +local storage = core.get_mod_storage() +local time_shift = storage:get_int("time_shift") +local epoch = 1577836800 -- 1. 1. 2020 UTC +-- local epoch2 = 2208988800 -- 1. 1. 2040 UTC + +local time_speed_during_day, time_speed_during_night +local rwtime_callback + +ch_time = {} + +local dst_points_03 = { + [2022] = 1648342800, + [2023] = 1679792400, + [2024] = 1711846800, + [2025] = 1743296400, + [2026] = 1774746000, + [2027] = 1806195600, + [2028] = 1837645200, + [2029] = 1869094800, + [2030] = 1901149200, +} +local dst_points_10 = { + [2022] = 1667091600, -- 2 => 1 + [2023] = 1698541200, + [2024] = 1729990800, + [2025] = 1761440400, + [2026] = 1792890000, + [2027] = 1824944400, + [2028] = 1856394000, + [2029] = 1887843600, + [2030] = 1919293200, +} + +local nazvy_mesicu = { + {"leden", "ledna", "led", "LED"}, + {"únor", "února", "úno", "ÚNO"}, + {"březen", "března", "bře", "BŘE"}, + {"duben", "dubna", "dub", "DUB"}, + {"květen", "května", "kvě", "KVĚ"}, + {"červen", "června", "čer", "ČER"}, + {"červenec", "července", "čvc", "ČVC"}, + {"srpen", "srpna", "srp", "SRP"}, + {"září", "září", "zář", "ZÁŘ"}, + {"říjen", "října", "říj", "ŘÍJ"}, + {"listopad", "listopadu", "lis", "LIS"}, + {"prosinec", "prosince", "pro", "PRO"}, +} + +local dny_v_tydnu = { + "pondělí", + "úterý", + "středa", + "čtvrtek", + "pátek", + "sobota", + "neděle", +} + +local function ifthenelse(cond, t, f) + if cond then return t else return f end +end + + +-- Nastavení +-- =================================== +function ch_time.get_time_shift() + return time_shift +end + +function ch_time.set_time_shift(new_shift) + storage:set_int("time_shift", new_shift) + time_shift = storage:get_int("time_shift") +end + +function ch_time.get_time_speed_during_day() + return time_speed_during_day +end + +function ch_time.get_time_speed_during_night() + return time_speed_during_night +end + +function ch_time.set_time_speed_during_day(new_value) + time_speed_during_day = new_value +end + +function ch_time.set_time_speed_during_night(new_value) + time_speed_during_night = new_value +end + +function ch_time.get_rwtime_callback() + return rwtime_callback +end + +function ch_time.set_rwtime_callback(new_callback) + --[[ callback musí vracet strukturu v tomto formátu: + secs = int, + string = string, + string_extended = string, + ]] + rwtime_callback = new_callback +end + +local Cas = {} + +function Cas:den_v_tydnu_cislo() + local n = self.lt.wday - 1 + return ifthenelse(n == 0, 7, n) +end + +function Cas:den_v_tydnu_nazev() + return dny_v_tydnu[self:den_v_tydnu_cislo()] +end + +function Cas:nazev_mesice(pad) + return nazvy_mesicu[self.lt.month][pad] +end + +function Cas:den_v_roce() + return self.lt.yday +end + +function Cas:posun_cislo() + return ifthenelse(self.je_letni_cas, 2, 1) +end + +function Cas:posun_text() + return ifthenelse(self.je_letni_cas, "+02:00", "+01:00") +end + +function Cas:znamka32() + return ch_time.znamka32(self.time) +end + +function Cas:YYYY_MM_DD() + return string.format("%04d-%02d-%02d", self.rok, self.mesic, self.den) +end + +function Cas:YYYY_MM_DD_HH_MM_SS() + return string.format("%04d-%02d-%02d %02d:%02d:%02d", self.rok, self.mesic, self.den, self.hodina, self.minuta, self.sekunda) +end + +function Cas:YYYY_MM_DD_HH_MM_SS_ZZZ() + return string.format("%04d-%02d-%02d %02d:%02d:%02d %s", + self.rok, self.mesic, self.den, self.hodina, self.minuta, self.sekunda, self:posun_text()) +end + +function Cas:YYYY_MM_DD_HH_MM_SSZZZ() + return string.format("%04d-%02d-%02d %02d:%02d:%02d%s", + self.rok, self.mesic, self.den, self.hodina, self.minuta, self.sekunda, self:posun_text()) +end + +function Cas:YYYY_MM_DDTHH_MM_SSZZZ() + return string.format("%04d-%02d-%02dT%02d:%02d:%02d%s", + self.rok, self.mesic, self.den, self.hodina, self.minuta, self.sekunda, self:posun_text()) +end + +function Cas:HH_MM_SS() + return string.format("%02d:%02d:%02d", self.hodina, self.minuta, self.sekunda) +end + +function Cas:UTC_YYYY_MM_DD() + local u = self.utc + return string.format("%04d-%02d-%02d", u.year, u.month, u.day) +end + +function Cas:UTC_YYYY_MM_DD_HH_MM_SS() + local u = self.utc + return string.format("%04d-%02d-%02d %02d:%02d:%02d", u.year, u.month, u.day, u.hour, u.min, u.sec) +end + +function Cas:za_n_sekund(n) + return ch_time.na_strukturu(self.time + n) +end + +-- API +-- ============================================ + +-- Zformátuje čas stejným způsobem jako os.time(), ale podle českého locale. +function ch_time.date(format, time) + if format == nil then return nil end + local st = ch_time.na_strukturu(time) + local dvt_nazev = st:den_v_tydnu_nazev() + local values = { + ["%a"] = dvt_nazev:sub(1, 2), + ["%A"] = dvt_nazev, + ["%b"] = nazvy_mesicu[st.mesic][3], + ["%B"] = nazvy_mesicu[st.mesic][1], + ["%c"] = st:YYYY_MM_DD_HH_MM_SS(), + ["%d"] = string.format("%02d", st.den), + ["%H"] = string.format("%02d", st.hodina), + ["%I"] = string.format("%02d", st.hodina % 12), + ["%M"] = string.format("%02d", st.minuta), + ["%m"] = string.format("%02d", st.mesic), + ["%p"] = ifthenelse(st.hodina < 12, "dop", "odp"), + ["%S"] = string.format("%02d", st.sekunda), + ["%w"] = tostring(st:den_v_tydnu_cislo()), + ["%x"] = st:YYYY_MM_DD(), + ["%X"] = st:HH_MM_SS(), + ["%Y"] = tostring(st.rok), + ["%y"] = string.format("%02d", st.rok % 100), + ["%%"] = "%", + } + format = string.gsub(format, "%%[aAbBcdHImMpSwxXyY%%]", values) + return format +end + +-- Vrací počet sekund od začátku epochy, posunutý o nastavený time_shift. +function ch_time.time() + return os.time() + time_shift +end + +--[[ + Vrátí strukturu popisující zadaný časový okamžik. + time = int, -- čas vrácený z ch_time.time() nebo posunutý +]] +function ch_time.na_strukturu(time) + if time == nil then + time = ch_time.time() + end + local utc_time = os.date("!*t", time) + local je_letni_cas + if utc_time.month < 7 then + local dst_point = dst_points_03[utc_time.year] + je_letni_cas = dst_point ~= nil and time >= dst_point + else + local dst_point = dst_points_10[utc_time.year] + je_letni_cas = dst_point ~= nil and time < dst_point + end + local lt = os.date("!*t", time + 3600 * ifthenelse(je_letni_cas, 2, 1)) + local result = { + time = time, + utc = utc_time, -- year, month, day, hour, min, sec, yday + lt = lt, -- local time + rok = lt.year, + mesic = lt.month, + den = lt.day, + hodina = lt.hour, + minuta = lt.min, + sekunda = lt.sec, + je_letni_cas = je_letni_cas, + } + return setmetatable(result, {__index = Cas}) +end + +--[[ + Vrátí strukturu popisující aktuální časový okamžik. +]] +function ch_time.aktualni_cas() + return ch_time.na_strukturu(ch_time.time()) +end + +--[[ + Vrátí časovou známku v rozsahu typu 'int32_t' jako počet sekund od 1. 1. 2020 UTC. + Rozsah je od 1951-12-13 20:45:53 UTC do 2088-01-19 03:14:07 UTC. + Není-li zadaný čas, použije aktuální čas. +]] +function ch_time.znamka32(time) + if time == nil then + time = ch_time.time() + end + local result = time - epoch + if result > 2147483647 then + return 2147483647 + elseif result < -2147483647 then + return -2147483647 + else + return result + end +end + + +--[[ +Vrátí herní čas ve struktuře: +{ + day_count = int -- návratová hodnota funkce minetest.get_day_count() + timeofday = float -- hodnota podle funkce minetest.get_timeofday() + hodina = int (0..23) -- hodina ve hře (celá) + minuta = int (0..59) -- minuta ve hře (celá) + sekunda = int (0..59) -- sekunda ve hře (celá) + daynight_ratio = float + natural_light = int (0..15) + time_speed = float -- návratová hodnota core.settings:get("time_speed") +} +]] +function ch_time.herni_cas() + local timeofday = core.get_timeofday() + if timeofday == nil then return nil end + local sekundy_celkem = math.floor(timeofday * 86400) + local minuty_celkem = math.floor(sekundy_celkem / 60) + local hodiny_celkem = math.floor(minuty_celkem / 60) + local result = { + day_count = core.get_day_count(), + timeofday = timeofday, + hodina = hodiny_celkem, + minuta = minuty_celkem % 60, + sekunda = sekundy_celkem % 60, + time_speed = tonumber(core.settings:get("time_speed")), + } + if type(result.time_speed) ~= "number" then + core.log("warning", "ch_time.herni_cas(): invalid type of time_speed!") + end + + if 367 < minuty_celkem and minuty_celkem < 1072 then + -- den + result.day_night_ratio, result.natural_light = 1, 15 + elseif minuty_celkem < 282 or minuty_celkem > 1158 then + -- noc + result.day_night_ratio, result.natural_light = 0, 2 + elseif minuty_celkem < 500 then + -- úsvit + result.day_night_ratio = (minuty_celkem - 282) / 86.0 + if minuty_celkem < 295 then + result.natural_light = 3 + elseif minuty_celkem < 305 then + result.natural_light = 4 + elseif minuty_celkem < 312 then + result.natural_light = 5 + elseif minuty_celkem < 319 then + result.natural_light = 6 + elseif minuty_celkem < 325 then + result.natural_light = 7 + elseif minuty_celkem < 331 then + result.natural_light = 8 + elseif minuty_celkem < 336 then + result.natural_light = 9 + elseif minuty_celkem < 341 then + result.natural_light = 10 + elseif minuty_celkem < 346 then + result.natural_light = 11 + elseif minuty_celkem < 351 then + result.natural_light = 12 + elseif minuty_celkem < 359 then + result.natural_light = 13 + elseif minuty_celkem < 367 then + result.natural_light = 14 + else + result.natural_light = 15 + end + else + -- soumrak + result.day_night_ratio = (1158 - minuty_celkem) / 86.0 + if minuty_celkem < 1080 then + result.natural_light = 14 + elseif minuty_celkem < 1088 then + result.natural_light = 13 + elseif minuty_celkem < 1093 then + result.natural_light = 12 + elseif minuty_celkem < 1098 then + result.natural_light = 11 + elseif minuty_celkem < 1103 then + result.natural_light = 10 + elseif minuty_celkem < 1108 then + result.natural_light = 9 + elseif minuty_celkem < 1114 then + result.natural_light = 8 + elseif minuty_celkem < 1120 then + result.natural_light = 7 + elseif minuty_celkem < 1127 then + result.natural_light = 6 + elseif minuty_celkem < 1134 then + result.natural_light = 5 + elseif minuty_celkem < 1144 then + result.natural_light = 4 + elseif minuty_celkem < 1157 then + result.natural_light = 3 + else + result.natural_light = 2 + end + end + return result +end + +--[[ +Nastaví herní čas na hodnotu uvedenou ve formátu hodin, minut a sekund. +]] +function ch_time.herni_cas_nastavit(h, m, s) + assert(h) + assert(m) + assert(s) + local novy_timeofday = (3600 * h + 60 * m + s) / 86400.0 + if novy_timeofday < 0 then + novy_timeofday = 0.0 + elseif novy_timeofday > 1 then + novy_timeofday = 1.0 + end + local puvodni = ch_time.herni_cas() + local puvodni_timeofday = puvodni.timeofday + core.set_timeofday(novy_timeofday) + local byla_noc = puvodni_timeofday < 0.2292 or puvodni_timeofday > 0.791666 + local je_noc = novy_timeofday < 0.2292 or novy_timeofday > 0.791666 + if byla_noc and not je_noc then + -- Ráno + if time_speed_during_day ~= nil then + core.settings:set("time_speed", tostring(time_speed_during_day)) + end + elseif not byla_noc and je_noc then + -- Noc + if time_speed_during_night ~= nil then + core.settings:set("time_speed", tostring(time_speed_during_night)) + end + end + local novy = ch_time.herni_cas() + core.log("action", string.format("Time of day set from ((%d):%d:%d:%d => (%d):%d:%d:%d); speed: %f => %f\n", + puvodni.day_count, puvodni.hodina, puvodni.minuta, puvodni.sekunda, novy.day_count, novy.hodina, novy.minuta, novy.sekunda, + puvodni.time_speed or 0.0, novy.time_speed or 0.0)) + return novy +end + +-- Příkazy v četu +-- ======================================================== + +local def = { + description = "Nastaví posun zobrazovaného času.", + privs = {server = true}, + func = function(player_name, param) + local n = tonumber(param) + if not n then + return false, "Chybné zadán!!" + end + n = math.round(n) + ch_time.set_time_shift(n) + core.chat_send_player(player_name, "*** Posun nastaven: "..n) + end, +} + +core.register_chatcommand("posunčasu", def) +core.register_chatcommand("posuncasu", def) +-- core.register_chatcommand("set_time_shift", def) + +local vypsat_cas_param_table = { + u = function() + local cas = ch_time.aktualni_cas() + return string.format("%02d:%02d UTC", cas.utc.hour, cas.utc.min) + end, + ["u+"] = function() + local cas = ch_time.aktualni_cas() + return cas:UTC_YYYY_MM_DD_HH_MM_SS().." UTC" + end, + m = function() + local cas = ch_time.aktualni_cas() + return cas:HH_MM_SS().." "..cas:posun_text() + end, + ["m+"] = function() + local cas = ch_time.aktualni_cas() + return cas:YYYY_MM_DD_HH_MM_SS_ZZZ() + end, + h = function() + local cas = ch_time.herni_cas() + return string.format("%02d:%02d herního času", cas.hodina, cas.minuta) + end, + ["h+"] = function() + local cas = ch_time.herni_cas() + return string.format("%02d:%02d:%02d herního času (herní den %d)", cas.hodina, cas.minuta, cas.sekunda, cas.day_count) + end, + ["ž"] = function() + if rwtime_callback == nil then + return "železniční čas není dostupný" + else + local rwtime = rwtime_callback() + return "železniční čas: "..assert(rwtime.string) + end + end, + ["ž+"] = function() + if rwtime_callback == nil then + return "železniční čas není dostupný" + else + local rwtime = rwtime_callback() + return "železniční čas: "..rwtime.string_extended.." ("..rwtime.secs..")" + end + end, +} +vypsat_cas_param_table[""] = vypsat_cas_param_table["h"] +vypsat_cas_param_table["utc"] = vypsat_cas_param_table["u"] +vypsat_cas_param_table["utc+"] = vypsat_cas_param_table["u+"] +vypsat_cas_param_table["místní"] = vypsat_cas_param_table["m"] +vypsat_cas_param_table["mistni"] = vypsat_cas_param_table["m"] +vypsat_cas_param_table["místní+"] = vypsat_cas_param_table["m+"] +vypsat_cas_param_table["mistni+"] = vypsat_cas_param_table["m+"] +vypsat_cas_param_table["herni"] = vypsat_cas_param_table["h"] +vypsat_cas_param_table["herní"] = vypsat_cas_param_table["h"] +vypsat_cas_param_table["herni+"] = vypsat_cas_param_table["h+"] +vypsat_cas_param_table["herní+"] = vypsat_cas_param_table["h+"] +vypsat_cas_param_table["železniční"] = vypsat_cas_param_table["ž"] +vypsat_cas_param_table["zeleznicni"] = vypsat_cas_param_table["ž"] +vypsat_cas_param_table["železniční+"] = vypsat_cas_param_table["ž+"] +vypsat_cas_param_table["zeleznicni+"] = vypsat_cas_param_table["ž+"] + +local function vypsat_cas(player_name, param) + if type(player_name) == "table" then + -- API hack: + local t = player_name + if t[1] ~= nil then + ch_core.systemovy_kanal(t[1], t[2]) + end + return t[3], t[4] + end + local f = vypsat_cas_param_table[param] + if f ~= nil then + ch_core.systemovy_kanal(player_name, f()) + return true + else + return false, "Nerozpoznaný parametr: "..param + end +end + +def = { + params = "[utc|utc+|m[ístní]|m[ístní]+|h[erní]|h[erní]+|ž[elezniční]|ž[elezniční]+]", + description = "Vypíše požadovaný druh času (a případně data). Výchozí je „h“ (herní čas).", + privs = {}, + func = vypsat_cas, +} +core.register_chatcommand("čas", def) +core.register_chatcommand("cas", def) + +ch_base.close_mod(core.get_current_modname()) diff --git a/ch_time/license.txt b/ch_time/license.txt new file mode 100644 index 0000000..6295ca7 --- /dev/null +++ b/ch_time/license.txt @@ -0,0 +1,15 @@ +License of source code +---------------------- + +MIT License + +Copyright (c) 2025 Singularis <singularis@volny.cz> + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Licenses of media (models, sounds, textures) +------------------------------------ diff --git a/ch_time/mod.conf b/ch_time/mod.conf new file mode 100644 index 0000000..9830cc6 --- /dev/null +++ b/ch_time/mod.conf @@ -0,0 +1,3 @@ +name = ch_time +description = Time API from Český hvozd +depends = ch_base |