local S = minetest.get_translator("lzr_editor") local FS = function(...) return minetest.formspec_escape(S(...)) end local NS = function(s) return end local F = minetest.formspec_escape -- Text color the special autosave level will be highlighted -- in the save/load dialog local AUTOSAVE_HIGHLIGHT = "#FF8060" -- List of items to give to player on editor entry local INITIAL_EDITOR_ITEMS = { -- hotbar slots "lzr_tools:ultra_pickaxe", "screwdriver2:screwdriver", "lzr_teleporter:teleporter_off", "lzr_laser:emitter_on", "lzr_laser:detector", "lzr_laser:mirror", "lzr_laser:crate", "lzr_treasure:chest_wood_locked", -- rest of inventory "lzr_tools:ultra_bucket", "lzr_laser:emit_toggler", "lzr_laser:barricade", "lzr_laser:emitter_takable_on", "lzr_laser:detector_takable", "lzr_laser:mirror_takable", "lzr_laser:crate_takable", "lzr_treasure:chest_wood_unlocked", } lzr_editor = {} lzr_editor.first_time = true -- The level state holds the metadata for -- the level that is being currently edited. -- INITIAL_LEVEL_STATE is the default state -- and the state when the editor starts. local INITIAL_LEVEL_STATE = { name = "", file_name = "", size = lzr_globals.DEFAULT_LEVEL_SIZE, wall = lzr_globals.DEFAULT_WALL_NODE, window = lzr_globals.DEFAULT_WINDOW_NODE, ceiling = lzr_globals.DEFAULT_CEILING_NODE, floor = lzr_globals.DEFAULT_FLOOR_NODE, ambient = lzr_ambience.DEFAULT_AMBIENCE, sky = lzr_globals.DEFAULT_SKY, npc_texts = lzr_globals.DEFAULT_NPC_TEXTS, weather = lzr_globals.DEFAULT_WEATHER, } local level_state = table.copy(INITIAL_LEVEL_STATE) -- Used to store the current contents for the -- textlist contents of the custom level file list. -- (this assumes singleplayer) local level_file_textlist_state = {} -- Give useful starter items to player local function give_initial_items(player) local inv = player:get_inventory() -- Clear inventory first inv:set_list("main", {}) -- Add itmes local errored = false for i=1, #INITIAL_EDITOR_ITEMS do local item = INITIAL_EDITOR_ITEMS[i] local leftover = inv:add_item("main", item) if not leftover:is_empty() then minetest.log("error", "[lzr_editor] Could not give initial item "..item.." (leftover="..leftover:to_string().."). ".. "get_size('main')="..inv:get_size("main").."; get_list('main')="..dump(inv:get_list("main"))) errored = true end end if errored then minetest.log("error", "[lzr_editor] Could not give one or more initial items") end end -- Returns true if pos is within the current bounds -- of the actively edited level. function lzr_editor.is_in_level_bounds(pos) local offset = table.copy(level_state.size) offset = vector.offset(offset, -1, -1, -1) return vector.in_area(pos, lzr_globals.LEVEL_POS, vector.add(lzr_globals.LEVEL_POS, offset)) end -- Enter editor state (if not already in it). -- Returns true if state changed, false if already in editor. function lzr_editor.enter_editor(player) local state = lzr_gamestate.get_state() if state == lzr_gamestate.EDITOR then return false else lzr_levels.clear_playfield(level_state.size) level_state = table.copy(INITIAL_LEVEL_STATE) local pos = lzr_globals.LEVEL_POS local size = level_state.size lzr_levels.build_room({pos=pos, size=size, spawn_pos=pos, yaw=0}) lzr_gui.show_level_bounds(player, pos, size) lzr_gamestate.set_state(lzr_gamestate.EDITOR) return true end end local check_for_slash = function(str) if string.match(str, "[/\\]") then return true else return false end end local save_level = function(level_name, is_autosave) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false end if check_for_slash(level_name) then return false end minetest.mkdir(minetest.get_worldpath().."/levels") local filename = minetest.get_worldpath().."/levels/"..level_name..".mts" local size = vector.subtract(level_state.size, vector.new(1, 1, 1)) -- TODO: Don't save the lasers local ok = minetest.create_schematic(lzr_globals.LEVEL_POS, vector.add(lzr_globals.LEVEL_POS, size), {}, filename, {}) local csv_filename = minetest.get_worldpath().."/levels/"..level_name..".csv" local npc_texts_csv = "" if level_state.npc_texts and level_state.npc_texts.goldie then npc_texts_csv = level_state.npc_texts.goldie or "" end local csv_contents = lzr_csv.write_csv({{ level_name..".mts", level_state.name, level_state.wall .. "|" .. level_state.window .. "|" .. level_state.floor .. "|" .. level_state.ceiling, level_state.ambient, level_state.sky, npc_texts_csv, level_state.weather, }}) local ok_csv = minetest.safe_file_write(csv_filename, csv_contents) if ok and ok_csv then minetest.log("action", "[lzr_editor] Level written to "..filename.." and "..csv_filename) if not is_autosave then level_state.file_name = level_name end return true, filename, csv_filename elseif ok then minetest.log("error", "[lzr_editor] Level written to "..filename..", but failed to write CSV!") return false, filename, csv_filename else minetest.log("error", "[lzr_editor] Failed to write level to "..filename) return false, filename, csv_filename end end -- Exit editor state (if not already outside of editor). -- Returns true if state changed, false if already out of editor. local function exit_editor(player) level_state.file_name = "" local state = lzr_gamestate.get_state() if state ~= lzr_gamestate.EDITOR then return false else lzr_levels.go_to_menu() return true end end minetest.register_chatcommand("editor_save", { description = S("Save current level"), params = S(""), func = function(name, param) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") end if param == "" then return false, S("No level name provided.") end local level_name = param if check_for_slash(level_name) then return false, S("Level name must not contain slash or backslash!") end local ok, filename, filename2 = save_level(level_name) if ok and filename and filename2 then return true, S("Level saved to @1 and @2.", filename, filename2) elseif of and filename then return true, S("Level saved to @1, but could not write metadata to @2.", filename, filename2) else return false, S("Error writing level file!") end end, }) -- Returns true if the given file exists, false otherwise. -- * path: Path to file (without file name) -- * filename: File name of file (without path) local file_exists = function(path, filename) local levels = minetest.get_dir_list(path, false) for l=1, #levels do if levels[l] == filename then return true end end return false end local load_level = function(level_name, player) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false end if check_for_slash(level_name) then return false end local filename = level_name..".mts" local ok = file_exists(minetest.get_worldpath().."/levels", filename) if not ok then return false end local schem = minetest.read_schematic(minetest.get_worldpath().."/levels/"..filename, {write_yslice_prob="none"}) if schem then local csv_file = io.open(minetest.get_worldpath().."/levels/"..level_name..".csv", "r") if csv_file then local csv_string = csv_file:read("*a") csv_file:close() local csv_parsed = lzr_csv.parse_csv(csv_string) if csv_parsed and #csv_parsed >= 1 then level_state.name = csv_parsed[1][2] local bounds = csv_parsed[1][3] local exploded_bounds = string.split(bounds, "|") if exploded_bounds then level_state.wall = exploded_bounds[1] level_state.window = exploded_bounds[2] level_state.floor = exploded_bounds[3] level_state.ceiling = exploded_bounds[4] end level_state.ambient = csv_parsed[1][4] level_state.sky = csv_parsed[1][5] or lzr_globals.DEFAULT_SKY level_state.npc_texts = csv_parsed[1][6] if level_state.npc_texts then level_state.npc_texts = { goldie = level_state.npc_texts } else level_state.npc_texts = lzr_globals.DEFAULT_NPC_TEXTS end level_state.weather = csv_parsed[1][7] or lzr_globals.DEFAULT_WEATHER else minetest.log("error", "[lzr_editor] Error parsing CSV file for "..level_name) end else minetest.log("error", "[lzr_editor] No CSV file found for "..level_name) end level_state.size = table.copy(schem.size) level_state.name = level_state.name or "" level_state.wall = level_state.wall or lzr_globals.DEFAULT_WALL_NODE level_state.window = level_state.window or lzr_globals.DEFAULT_WINDOW_NODE level_state.ceiling = level_state.ceiling or lzr_globals.DEFAULT_CEILING_NODE level_state.floor = level_state.floor or lzr_globals.DEFAULT_FLOOR_NODE level_state.ambient = level_state.ambient or lzr_ambience.DEFAULT_AMBIENCE level_state.sky = level_state.sky or lzr_globals.DEFAULT_SKY level_state.npc_texts = level_state.npc_texts or lzr_globals.DEFAULT_NPC_TEXTS level_state.weather = level_state.weather or lzr_globals.DEFAULT_WEATHER local bounding_nodes = { node_wall = level_state.wall, node_window = level_state.window, node_ceiling = level_state.ceiling, node_floor = level_state.floor, } lzr_levels.prepare_and_build_custom_level(schem, nil, nil, bounding_nodes) lzr_gui.show_level_bounds(player, lzr_globals.LEVEL_POS, schem.size) if lzr_ambience.ambience_exists(level_state.ambient) then lzr_ambience.set_ambience(level_state.ambient) else level_state.ambient = "none" lzr_ambience.set_ambience("none") end if lzr_sky.sky_exists(level_state.sky) then lzr_sky.set_sky(level_state.sky) else lzr_sky.set_sky(lzr_globals.DEFAULT_SKY) end if lzr_weather.weather_exists(level_state.weather) then lzr_weather.set_weather(level_state.weather) else lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER) end level_state.file_name = level_name minetest.log("action", "[lzr_editor] Level loaded from "..filename) return true else minetest.log("error", "[lzr_editor] Failed to read level from "..filename) return false end end minetest.register_chatcommand("editor_load", { description = S("Load level"), params = S(""), func = function(name, param) local player = minetest.get_player_by_name(name) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") end if param == "" then return false, S("No level name provided.") end local level_name = param if check_for_slash(level_name) then return false, S("Level name must not contain slash or backslash!") end local ok = file_exists(minetest.get_worldpath().."/levels", level_name..".mts") if not ok then return false, S("Level file does not exist!") end local ok = load_level(level_name, player) if ok then return true, S("Level loaded.") else return false, S("Error reading level file!") end end, }) minetest.register_chatcommand("editor", { description = S("Start or exit level editor"), params = S("[ enter | exit ]"), func = function(name, param) local player = minetest.get_player_by_name(name) if param == "" or param == "enter" then local ok = lzr_editor.enter_editor(player) if ok then return true else return false, S("Already in level editor!") end elseif param == "exit" then local ok = exit_editor(player) if ok then return true else return false, S("Not in level editor!") end end return false end, }) -- Unlimited node placement in editor mode minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack) if placer and placer:is_player() then return lzr_gamestate.get_state() == lzr_gamestate.EDITOR end end) -- Don't pick node up if the item is already in the inventory local old_handle_node_drops = minetest.handle_node_drops function minetest.handle_node_drops(pos, drops, digger) if not digger or not digger:is_player() or lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return old_handle_node_drops(pos, drops, digger) end local inv = digger:get_inventory() if inv then for _, item in ipairs(drops) do if not inv:contains_item("main", item, true) then inv:add_item("main", item) end end end end lzr_gamestate.register_on_enter_state(function(state) if state == lzr_gamestate.EDITOR then local player = minetest.get_player_by_name("singleplayer") local message = S("Welcome to the Level Editor!").."\n".. S("See LEVEL_EDITOR.md for instructions.") lzr_player.set_editor_inventory(player) lzr_gui.set_editor_gui(player) lzr_ambience.set_ambience(lzr_ambience.DEFAULT_AMBIENCE) lzr_sky.set_sky(lzr_globals.DEFAULT_SKY) lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER) give_initial_items(player) lzr_privs.grant_edit_privs(player) if lzr_editor.first_time then minetest.chat_send_player("singleplayer", message) lzr_editor.first_time = false end end end) lzr_gamestate.register_on_exit_state(function(state) if state == lzr_gamestate.EDITOR then minetest.log("action", "[lzr_editor] Autosaving level on editor exit") save_level(lzr_globals.AUTOSAVE_NAME, true) local player = minetest.get_player_by_name("singleplayer") if player then lzr_privs.revoke_edit_privs(player) end end end) local show_settings_dialog = function(player) -- Shorthand function to get the current dropdown index of -- a multiple-choice item (ambience, sky, weather) as well -- as a string to insert in the dropdown[] formspec element local get_current_thing = function(thing_type, thing_getter) local thing_list = "" local current_thing = 1 local things = thing_getter() for t=1, #things do -- Construct string for dropdown[] thing_list = thing_list .. F(things[t]) -- Current thing found! if things[t] == level_state[thing_type] then current_thing = t end -- Append comma except at end if t < #things then thing_list = thing_list .. "," end end return thing_list, current_thing end local ambient_list, current_ambient = get_current_thing("ambient", lzr_ambience.get_ambiences) local sky_list, current_sky = get_current_thing("sky", lzr_sky.get_skies) local weather_list, current_weather = get_current_thing("weather", lzr_weather.get_weathers) -- TODO: Use this string when we have a parrot model local goldie_speech = NS("Goldie speech") local form = "formspec_version[4]".. "size[9,11.7]".. "box[0,0;9,0.8;#00000080]".. "label[0.4,0.4;"..FS("Level settings").."]".. "field[0.5,1.3;8,0.6;level_name;"..FS("Name")..";"..F(level_state.name).."]".. "label[0.5,2.3;Size]".. "field[1.6,2.3;1,0.6;level_size_x;"..FS("X")..";"..F(tostring(level_state.size.x)).."]".. "field[2.62,2.3;1,0.6;level_size_y;"..FS("Y")..";"..F(tostring(level_state.size.y)).."]".. "field[3.63,2.3;1,0.6;level_size_z;"..FS("Z")..";"..F(tostring(level_state.size.z)).."]".. "field[0.5,3.3;8,0.6;level_wall;"..FS("Wall node")..";"..F(level_state.wall).."]".. "field[0.5,4.3;8,0.6;level_window;"..FS("Window node")..";"..F(level_state.window).."]".. "field[0.5,5.3;8,0.6;level_floor;"..FS("Floor node")..";"..F(level_state.floor).."]".. "field[0.5,6.3;8,0.6;level_ceiling;"..FS("Ceiling node")..";"..F(level_state.ceiling).."]".. "field[0.5,7.3;8,0.6;level_npc_goldie;"..FS("Information block text")..";"..F(level_state.npc_texts.goldie).."]".. "label[0.5,8.1;"..FS("Ambience").."]".. "dropdown[0.5,8.3;3.5,0.6;level_ambient;"..ambient_list..";"..current_ambient..";false]".. "label[0.5,9.2;"..FS("Sky").."]".. "dropdown[0.5,9.4;3.5,0.6;level_sky;"..sky_list..";"..current_sky..";false]".. "label[5,9.2;"..FS("Weather").."]".. "dropdown[5,9.4;3.5,0.6;level_weather;"..weather_list..";"..current_weather..";false]".. "button_exit[0.5,10.5;3.5,0.7;level_ok;"..FS("OK").."]".. "button_exit[5,10.5;3.5,0.7;level_cancel;"..FS("Cancel").."]".. "field_close_on_enter[level_ok;true]" minetest.show_formspec(player:get_player_name(), "lzr_editor:level_settings", form) end local show_save_load_dialog = function(player, save, level_name) if not level_name then level_name = "" end local txt_caption local txt_action local action if save then txt_caption = S("Save level as …") txt_action = S("Save") action = "save" else txt_caption = S("Load level …") txt_action = S("Load") action = "load" end local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false) local levels = {} for l=1, #levels_files do if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then local name = string.sub(levels_files[l], 1, -5) table.insert(levels, name) end end table.sort(levels) level_file_textlist_state = levels local levels_str = "" for l=1, #levels do local entry = levels[l] if entry == lzr_globals.AUTOSAVE_NAME then entry = AUTOSAVE_HIGHLIGHT .. entry end levels_str = levels_str .. entry if l < #levels then levels_str = levels_str .. "," end end local level_list_elem = "" if #levels > 0 then level_list_elem = "label[0.5,1.5;"..FS("File list:").."]" .. "textlist[0.5,2;8,4;level_list;"..levels_str.."]" end local form = "formspec_version[4]".. "size[9,9]".. "box[0,0;9,0.8;#00000080]".. "label[0.4,0.4;"..F(txt_caption).."]".. level_list_elem.. "field[0.5,6.6;7,0.6;file_name;"..FS("File name")..";"..minetest.formspec_escape(level_name).."]".. "label[7.7,6.9;.mts]".. "button_exit[0.5,7.7;3.5,0.7;"..action..";"..F(txt_action).."]".. "button_exit[5,7.7;3.5,0.7;cancel;"..FS("Cancel").."]".. "field_close_on_enter["..action..";true]" local formname if save then formname = "lzr_editor:level_save" else formname = "lzr_editor:level_load" end minetest.show_formspec(player:get_player_name(), formname, form) end minetest.register_on_player_receive_fields(function(player, formname, fields) local pname = player:get_player_name() if formname == "lzr_editor:level_settings" then if fields.level_cancel or (not fields.level_ok and not fields.key_enter) then return end local invalid = false if not fields.level_name or not tonumber(fields.level_size_x) or not tonumber(fields.level_size_y) or not tonumber(fields.level_size_z) or not fields.level_floor or not fields.level_ceiling or not fields.level_window or not fields.level_wall or not fields.level_ambient or not fields.level_sky or not fields.level_npc_goldie or not fields.level_weather then return end local old_level_size = table.copy(level_state.size) level_state.name = fields.level_name level_state.size.x = math.floor(tonumber(fields.level_size_x)) level_state.size.y = math.floor(tonumber(fields.level_size_y)) level_state.size.z = math.floor(tonumber(fields.level_size_z)) for _, axis in pairs({"x","y","z"}) do if level_state.size[axis] > lzr_globals.PLAYFIELD_SIZE[axis]-1 then level_state.size[axis] = lzr_globals.PLAYFIELD_SIZE[axis]-1 end if level_state.size[axis] < lzr_globals.PLAYFIELD_SIZE_MIN[axis]-1 then level_state.size[axis] = lzr_globals.PLAYFIELD_SIZE_MIN[axis]-1 end end local old_floor = level_state.floor local old_window = level_state.window local old_ceiling = level_state.ceiling local old_wall = level_state.wall level_state.floor = fields.level_floor level_state.window = fields.level_window level_state.ceiling = fields.level_ceiling level_state.wall = fields.level_wall level_state.ambient = fields.level_ambient level_state.sky = fields.level_sky level_state.npc_texts = {} level_state.npc_texts.goldie = fields.level_npc_goldie level_state.weather = fields.level_weather local rebuild_room = false local nodes = {} nodes.node_floor = level_state.floor nodes.node_window = level_state.window nodes.node_ceiling = level_state.ceiling nodes.node_wall = level_state.wall if old_floor ~= nodes.floor or old_window ~= nodes.window or old_ceiling ~= nodes.ceiling or old_wall ~= nodes.wall then for k,v in pairs(nodes) do if not minetest.registered_nodes[v] then nodes[k] = lzr_globals.DEFAULT_WALL_NODE level_state[string.sub(k, 6)] = lzr_globals.DEFAULT_WALL_NODE end end rebuild_room = true end if not vector.equals(level_state.size, old_level_size) then rebuild_room = true end if rebuild_room then minetest.log("action", "[lzr_editor] Autosaving level on level rebuild") save_level(lzr_globals.AUTOSAVE_NAME, true) lzr_levels.resize_room(old_level_size, level_state.size, nodes) rebuild_room = true lzr_gui.show_level_bounds(player, lzr_globals.LEVEL_POS, level_state.size) end if lzr_ambience.ambience_exists(level_state.ambient) then lzr_ambience.set_ambience(level_state.ambient) else lzr_ambience.set_ambience("none") end if lzr_sky.sky_exists(level_state.sky) then lzr_sky.set_sky(level_state.sky) else lzr_sky.set_sky(lzr_globals.DEFAULT_SKY) end if lzr_weather.weather_exists(level_state.weather) then lzr_weather.set_weather(level_state.weather) else lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER) end return elseif formname == "lzr_editor:level_save" or formname == "lzr_editor:level_load" then if fields.level_list then local evnt = minetest.explode_textlist_event(fields.level_list) if evnt.type == "CHG" or evnt.type == "DBL" then local file = level_file_textlist_state[evnt.index] if file then if formname == "lzr_editor:level_save" then show_save_load_dialog(player, true, file) else show_save_load_dialog(player, false, file) end return end end elseif (fields.load or (formname == "lzr_editor:level_load" and fields.key_enter)) and fields.file_name then if fields.file_name == "" then minetest.chat_send_player(pname, S("No level name provided.")) return end if check_for_slash(fields.file_name) then minetest.chat_send_player(pname, S("File name must not contain slash or backslash!")) return false end local exists = file_exists(minetest.get_worldpath().."/levels", fields.file_name..".mts") if not exists then minetest.chat_send_player(pname, S("Level file does not exist!")) return end local ok, filename = load_level(fields.file_name, player) if ok then minetest.chat_send_player(pname, S("Level loaded.")) else minetest.chat_send_player(pname, S("Error reading level file!")) end elseif (fields.save or (formname == "lzr_editor:level_save" and fields.key_enter)) and fields.file_name then if fields.file_name == "" then minetest.chat_send_player(pname, S("No level name provided.")) return end if check_for_slash(fields.file_name) then minetest.chat_send_player(pname, S("File name must not contain slash or backslash!")) return false end local ok, filename, filename2 = save_level(fields.file_name) if ok and filename and filename2 then minetest.chat_send_player(pname, S("Level saved to @1 and @2.", filename, filename2)) elseif ok and filename then minetest.chat_send_player(pname, S("Level saved to @1, but could not write metadata to @2.", filename, filename2)) else minetest.chat_send_player(pname, S("Error writing level file!")) end end return end if fields.__lzr_level_editor_exit then exit_editor() return elseif fields.__lzr_level_editor_settings then show_settings_dialog(player) return elseif fields.__lzr_level_editor_save then show_save_load_dialog(player, true, level_state.file_name) return elseif fields.__lzr_level_editor_load then show_save_load_dialog(player, false, level_state.file_name) return elseif fields.__lzr_level_editor_get_item then lzr_getitem.show_formspec(pname) return end end) -- Returns list of all custom level file names, sorted. -- The file name suffix is omitted. lzr_editor.get_custom_levels = function() local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false) local levels = {} for l=1, #levels_files do if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then local name = string.sub(levels_files[l], 1, -5) table.insert(levels, name) end end table.sort(levels) return levels end -- Returns proper level name of a custom level. -- The proper level name is read from a matching CSV file. -- If no such file is found, the empty string is returned. -- * level_name: Level file name without suffix -- * with_fallback: If true, will return "Untitled ()" -- instead of the empty string if the level name is empty or undefined lzr_editor.get_custom_level_name = function(level_name, with_fallback) local csv_file = io.open(minetest.get_worldpath().."/levels/"..level_name..".csv", "r") local pname if csv_file then local csv_string = csv_file:read("*a") csv_file:close() local csv_parsed = lzr_csv.parse_csv(csv_string) if csv_parsed and #csv_parsed >= 1 then pname = csv_parsed[1][2] end end if not pname then pname = "" end if pname == "" and with_fallback then return S("Untitled (@1)", level_name) end return pname end -- Expose invisible blocks in editor mode local invis_display_timer = 0 local INVIS_DISPLAY_UPDATE_TIME = 1 local INVIS_DISPLAY_RANGE = 8 minetest.register_globalstep(function(dtime) local state = lzr_gamestate.get_state() if state ~= lzr_gamestate.EDITOR then return end invis_display_timer = invis_display_timer + dtime if invis_display_timer < INVIS_DISPLAY_UPDATE_TIME then return end invis_display_timer = 0 local player = minetest.get_player_by_name("singleplayer") if not player then return end local pos = vector.round(player:get_pos()) local r = INVIS_DISPLAY_RANGE local vm = minetest.get_voxel_manip() local emin, emax = vm:read_from_map(vector.offset(pos, -r, -r, -r), vector.offset(pos, r, r, r)) local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax}) local data = vm:get_data() for x=pos.x-r, pos.x+r do for y=pos.y-r, pos.y+r do for z=pos.z-r, pos.z+r do local vi = area:index(x,y,z) local nodename = minetest.get_name_from_content_id(data[vi]) local texture if nodename == "lzr_core:barrier" then texture = "lzr_core_barrier.png" elseif nodename == "lzr_core:rain_membrane" then texture = "lzr_core_rain_membrane.png" end if texture then minetest.add_particle({ pos = {x=x, y=y, z=z}, texture = texture, glow = minetest.LIGHT_MAX, size = 8, expirationtime = 1.1, }) end end end end end)