local ALTITUDE = 600 -- average altitude of islands local ALTITUDE_AMPLITUDE = 400 -- rough island altitude variance (plus or minus) local GENERATE_ORES = true -- set to true for island core stone to contain patches of dirt and sand etc. local LOWLAND_BIOMES = false --or -- If true then determine an island's biome using the biome at altitude "LOWLAND_BIOME_ALTITUDE" --minetest.get_modpath("ethereal") ~= nil -- Ethereal has an alpine biome above altitude 40, so default to lowland biomes local LOWLAND_BIOME_ALTITUDE = 10 -- Higher than beaches, lower than mountains (See LOWLAND_BIOMES) local VINE_COVERAGE = 0.3 -- set to 0 to turn off vines local REEF_RARITY = 0.015 -- Chance of a viable island having a reef or atoll local TREE_RARITY = 0.08 -- Chance of a viable island having a giant tree growing out of it local PORTAL_RARITY = 0.04 -- Chance of a viable island having some ancient portalstone on it (If portals API available and ENABLE_PORTALS is true) local BIOLUMINESCENCE = true or -- Allow giant trees variants which have glowing parts minetest.get_modpath("glowtest") ~= nil or minetest.get_modpath("ethereal") ~= nil or minetest.get_modpath("glow") ~= nil or minetest.get_modpath("nsspf") ~= nil or minetest.get_modpath("nightscape") ~= nil or minetest.get_modpath("moonflower") ~= nil -- a world using any of these mods is OK with bioluminescence local ENABLE_PORTALS = true -- Whether to allow players to build portals to islands. Portals require the Nether mod. local EDDYFIELD_SIZE = 1 -- size of the "eddy field-lines" that smaller islands follow local ISLANDS_SEED = 1000 -- You only need to change this if you want to try different island layouts without changing the map seed -- Some lists of known node aliases (any nodes which can't be found won't be used). local NODENAMES_STONE = {"mapgen_stone", "mcl_core:stone", "default:stone", "main:stone"} local NODENAMES_WATER = {"mapgen_water_source", "mcl_core:water_source", "default:water_source", "main:water"} local NODENAMES_ICE = {"mapgen_ice", "mcl_core:ice", "pedology:ice_white", "default:ice", "main:ice"} local NODENAMES_GRAVEL = {"mapgen_gravel", "mcl_core:gravel", "default:gravel", "main:gravel"} local NODENAMES_GRASS = {"mapgen_dirt_with_grass", "mcl_core:dirt_with_grass", "default:dirt_with_grass", "main:grass"} -- currently only used with games that don't register biomes, e.g. Hades Revisted local NODENAMES_DIRT = {"mapgen_dirt", "mcl_core:dirt", "default:dirt", "main:dirt"} -- currently only used with games that don't register biomes, e.g. Hades Revisted local NODENAMES_SILT = {"mapgen_silt", "default:silt", "aotearoa:silt", "darkage:silt", "mapgen_sand", "mcl_core:sand", "default:sand", "main:sand"} -- silt isn't a thing yet, but perhaps one day it will be. Use sand for the bottom of ponds in the meantime. local NODENAMES_VINES = {"mcl_core:vine", "vines:side_end", "ethereal:vine", "main:vine"} -- ethereal vines don't grow, so only select that if there's nothing else. local NODENAMES_HANGINGVINE = {"vines:vine_end"} local NODENAMES_HANGINGROOT = {"vines:root_end"} local NODENAMES_TREEWOOD = {"mcl_core:tree", "default:tree", "mapgen_tree", "main:tree"} local NODENAMES_TREELEAVES = {"mcl_core:leaves", "default:leaves", "mapgen_leaves", "main:leaves"} local NODENAMES_FRAMEGLASS = {"xpanes:obsidian_pane_flat", "xpanes:pane_flat", "default:glass", "xpanes:pane_natural_flat", "mcl_core:glass", "walls:window"} local NODENAMES_WOOD = {"default:wood", "mcl_core:wood", "main:wood"} local MODNAME = minetest.get_current_modname() local VINES_REQUIRED_HUMIDITY = 49 local VINES_REQUIRED_TEMPERATURE = 40 local ICE_REQUIRED_TEMPERATURE = 8 local DEBUG = false -- dev logging local DEBUG_GEOMETRIC = false -- turn off noise from island shapes local DEBUG_SKYTREES = false -- dev logging -- OVERDRAW can be set to 1 to cause a y overdraw of one node above the chunk, to avoid creating a dirt "surface" -- at the top of the chunk that trees mistakenly grow on when the chunk is decorated. -- However, it looks like that tree problem has been solved by either engine or biome updates, and overdraw causes -- it's own issues (e.g. nodeId_top not getting set correctly), so I'm leaving overdraw off (i.e. zero) until I -- notice problems requiring it. local OVERDRAW = 0 local S = minetest.get_translator(MODNAME) cloudlands = {} -- API functions can be accessed via this global: -- cloudlands.get_island_details(minp, maxp) -- returns an array of island-information-tables, y is ignored. -- cloudlands.find_nearest_island(x, z, search_radius) -- returns a single island-information-table, or nil -- cloudlands.get_height_at(x, z, [island-information-tables]) -- returns (y, isWater), or nil if no island here cloudlands.coreTypes = { { territorySize = 200, coresPerTerritory = 3, radiusMax = 96, depthMax = 50, thicknessMax = 8, frequency = 0.1, pondWallBuffer = 0.03, requiresNexus = true, exclusive = false }, { territorySize = 60, coresPerTerritory = 1, radiusMax = 40, depthMax = 40, thicknessMax = 4, frequency = 0.1, pondWallBuffer = 0.06, requiresNexus = false, exclusive = true }, { territorySize = 30, coresPerTerritory = 3, radiusMax = 16, -- I feel this and depthMax should be bigger, say 18, and territorySize increased to 34 to match, but I can't change it any more or existing worlds will mismatch along previously emerged chunk boundaries depthMax = 16, thicknessMax = 2, frequency = 0.1, pondWallBuffer = 0.11, -- larger values will make ponds smaller and further from island edges, so it should be as low as you can get it without the ponds leaking over the edge. A small leak-prone island is at (3160, -2360) on seed 1 requiresNexus = false, exclusive = true } } if minetest.get_biome_data == nil then error(MODNAME .. " requires Minetest v5.0 or greater", 0) end local function fromSettings(settings_name, default_value) local result if type(default_value) == "number" then result = tonumber(minetest.settings:get(settings_name) or default_value) elseif type(default_value) == "boolean" then result = minetest.settings:get_bool(settings_name, default_value) end return result end -- override any settings with user-specified values before these values are needed ALTITUDE = fromSettings(MODNAME .. "_altitude", ALTITUDE) ALTITUDE_AMPLITUDE = fromSettings(MODNAME .. "_altitude_amplitude", ALTITUDE_AMPLITUDE) GENERATE_ORES = fromSettings(MODNAME .. "_generate_ores", GENERATE_ORES) VINE_COVERAGE = fromSettings(MODNAME .. "_vine_coverage", VINE_COVERAGE * 100) / 100 LOWLAND_BIOMES = fromSettings(MODNAME .. "_use_lowland_biomes", LOWLAND_BIOMES) TREE_RARITY = fromSettings(MODNAME .. "_giant_tree_rarety", TREE_RARITY * 100) / 100 BIOLUMINESCENCE = fromSettings(MODNAME .. "_bioluminescence", BIOLUMINESCENCE) ENABLE_PORTALS = fromSettings(MODNAME .. "_enable_portals", ENABLE_PORTALS) local noiseparams_eddyField = { offset = -1, scale = 2, spread = {x = 350 * EDDYFIELD_SIZE, y = 350 * EDDYFIELD_SIZE, z= 350 * EDDYFIELD_SIZE}, seed = ISLANDS_SEED, --WARNING! minetest.get_perlin() will add the server map's seed to this value octaves = 2, persistence = 0.7, lacunarity = 2.0, } local noiseparams_heightMap = { offset = 0, scale = ALTITUDE_AMPLITUDE, spread = {x = 160, y = 160, z= 160}, seed = ISLANDS_SEED, --WARNING! minetest.get_perlin() will add the server map's seed to this value octaves = 3, persistence = 0.5, lacunarity = 2.0, } local DENSITY_OFFSET = 0.7 local noiseparams_density = { offset = DENSITY_OFFSET, scale = .3, spread = {x = 25, y = 25, z= 25}, seed = 1000, --WARNING! minetest.get_perlin() will add the server map's seed to this value octaves = 4, persistence = 0.5, lacunarity = 2.0, } local SURFACEMAP_OFFSET = 0.5 local noiseparams_surfaceMap = { offset = SURFACEMAP_OFFSET, scale = .5, spread = {x = 40, y = 40, z= 40}, seed = ISLANDS_SEED, --WARNING! minetest.get_perlin() will add the server map's seed to this value octaves = 4, persistence = 0.5, lacunarity = 2.0, } local noiseparams_skyReef = { offset = .3, scale = .9, spread = {x = 3, y = 3, z= 3}, seed = 1000, octaves = 2, persistence = 0.5, lacunarity = 2.0, } local noiseAngle = -15 --degrees to rotate eddyField noise, so that the vertical and horizontal tendencies are off-axis local ROTATE_COS = math.cos(math.rad(noiseAngle)) local ROTATE_SIN = math.sin(math.rad(noiseAngle)) local noise_eddyField local noise_heightMap local noise_density local noise_surfaceMap local noise_skyReef local worldSeed local nodeId_ignore = minetest.CONTENT_IGNORE local nodeId_air local nodeId_stone local nodeId_grass local nodeId_dirt local nodeId_water local nodeId_ice local nodeId_silt local nodeId_gravel local nodeId_vine local nodeName_vine local nodeName_ignore = minetest.get_name_from_content_id(nodeId_ignore) local REQUIRED_DENSITY = 0.4 local randomNumbers = {} -- array of 0-255 random numbers with values between 0 and 1 (inclusive) local data = {} -- reuse the massive VoxelManip memory buffers instead of creating on every on_generate() local biomes = {} -- optional region specified in settings to restrict islands too local region_restrictions = false local region_min_x, region_min_z, region_max_x, region_max_z = -32000, -32000, 32000, 32000 -- optional biomes specified in settings to restrict islands too local limit_to_biomes = nil local limit_to_biomes_altitude = nil --[[============================== Math functions ==============================]]-- -- avoid having to perform table lookups each time a common math function is invoked local math_min, math_max, math_floor, math_sqrt, math_cos, math_sin, math_abs, math_pow, PI = math.min, math.max, math.floor, math.sqrt, math.cos, math.sin, math.abs, math.pow, math.pi local function clip(value, minValue, maxValue) if value <= minValue then return minValue elseif value >= maxValue then return maxValue else return value end end local function round(value) return math_floor(0.5 + value) end --[[============================== Interop functions ==============================]]-- local get_heat, get_humidity = minetest.get_heat, minetest.get_humidity local biomeinfoAvailable = minetest.get_modpath("biomeinfo") ~= nil and minetest.global_exists("biomeinfo") local isMapgenV6 = minetest.get_mapgen_setting("mg_name") == "v6" if isMapgenV6 then if not biomeinfoAvailable then -- The biomeinfo mod by Wuzzy can be found at https://repo.or.cz/minetest_biomeinfo.git minetest.log("warning", MODNAME .. " detected mapgen v6: Full mapgen v6 support requires adding the biomeinfo mod.") else get_heat = function(pos) return biomeinfo.get_v6_heat(pos) * 100 end get_humidity = function(pos) return biomeinfo.get_v6_humidity(pos) * 100 end end end local interop = {} -- returns the id of the first nodename in the list that resolves to a node id, or nodeId_ignore if not found interop.find_node_id = function (node_contender_names) local result = nodeId_ignore for _,contenderName in ipairs(node_contender_names) do local nonAliasName = minetest.registered_aliases[contenderName] or contenderName if minetest.registered_nodes[nonAliasName] ~= nil then result = minetest.get_content_id(nonAliasName) end --if DEBUG then minetest.log("info", contenderName .. " returned " .. result .. " (" .. minetest.get_name_from_content_id(result) .. ")") end if result ~= nodeId_ignore then return result end end return result end -- returns the name of the first nodename in the list that resolves to a node id, or 'ignore' if not found interop.find_node_name = function (node_contender_names) return minetest.get_name_from_content_id(interop.find_node_id(node_contender_names)) end interop.get_first_element_in_table = function(tbl) for k,v in pairs(tbl) do return v end return nil end -- returns the top-texture name of the first nodename in the list that's a registered node, or nil if not found interop.find_node_texture = function (node_contender_names) local result = nil local nodeName = minetest.get_name_from_content_id(interop.find_node_id(node_contender_names)) if nodeName ~= nil then local node = minetest.registered_nodes[nodeName] if node ~= nil then result = node.tiles if type(result) == "table" then result = result["name"] or interop.get_first_element_in_table(result) end -- incase it's not a string if type(result) == "table" then result = result["name"] or interop.get_first_element_in_table(result) end -- incase multiple tile definitions end end return result end -- returns the node name of the clone node. interop.register_clone = function(node_name, clone_name) local node = minetest.registered_nodes[node_name] if node == nil then minetest.log("error", "cannot clone " .. node_name) return nil else if clone_name == nil then clone_name = MODNAME .. ":" .. string.gsub(node.name, ":", "_") end if minetest.registered_nodes[clone_name] == nil then if DEBUG then minetest.log("info", "attempting to register: " .. clone_name) end local clone = {} for key, value in pairs(node) do clone[key] = value end clone.name = clone_name minetest.register_node(clone_name, clone) --minetest.log("info", clone_name .. " id: " .. minetest.get_content_id(clone_name)) --minetest.log("info", clone_name .. ": " .. dump(minetest.registered_nodes[clone_name])) end return clone_name end end -- converts "modname:nodename" into (modname, nodename), if no colon is found then modname is nil interop.split_nodename = function(nodeName) local result_modname = nil local result_nodename = nodeName local pos = nodeName:find(':') if pos ~= nil then result_modname = nodeName:sub(0, pos - 1) result_nodename = nodeName:sub(pos + 1) end return result_modname, result_nodename end -- returns a unique id for the biome, normally this is numeric but with mapgen v6 it can be a string name. interop.get_biome_key = function(pos) if isMapgenV6 and biomeinfoAvailable then return biomeinfo.get_v6_biome(pos) else return minetest.get_biome_data(pos).biome end end -- returns true if filename is a file that exists. interop.file_exists = function(filename) local f = io.open(filename, "r") if f == nil then return false else f:close() return true end end -- returns a written book item (technically an item stack), or nil if no books mod available interop.write_book = function(title, author, text, description) local stackName_writtenBook if minetest.get_modpath("mcl_books") then stackName_writtenBook = "mcl_books:written_book" text = title .. "\n\n" .. text -- MineClone2 books doen't show a title (or author) elseif minetest.get_modpath("book") ~= nil then stackName_writtenBook = "book:book_written" text = "\n\n" .. text -- Crafter books put the text immediately under the title elseif minetest.get_modpath("default") ~= nil then stackName_writtenBook = "default:book_written" else return nil end local book_itemstack = ItemStack(stackName_writtenBook) local book_data = {} book_data.title = title book_data.text = text book_data.owner = author book_data.author = author book_data.description = description book_data.page = 1 book_data.page_max = 1 book_data.generation = 0 book_data["book.book_title"] = title -- Crafter book title book_data["book.book_text"] = text -- Crafter book text book_itemstack:get_meta():from_table({fields = book_data}) return book_itemstack end --[[============================== Portals ==============================]]-- local addDetail_ancientPortal = nil if ENABLE_PORTALS and minetest.get_modpath("nether") ~= nil and minetest.global_exists("nether") and nether.register_portal ~= nil then -- The Portals API is available -- Register a player-buildable portal to Hallelujah Mountains. -- returns a position on the island which is suitable for a portal to be placed, or nil if none can be found -- player_name is optional, allowing a player to spawn a remote portal in their own protected areas. local function find_potential_portal_location_on_island(island_info, player_name) local result = nil if island_info ~= nil then local searchRadius = island_info.radius * 0.6 -- islands normally don't reach their full radius, and lets not put portals too near the edge local coreList = cloudlands.get_island_details( {x = island_info.x - searchRadius, z = island_info.z - searchRadius}, {x = island_info.x + searchRadius, z = island_info.z + searchRadius} ) -- Deterministically sample the island for a low location that isn't water. -- Seed the prng so this function always returns the same coords for the island local prng = PcgRandom(island_info.x * 65732 + island_info.z * 729 + minetest.get_mapgen_setting("seed") * 3) local positions = {} for attempt = 1, 15 do -- how many attempts we'll make at finding a good location local angle = (prng:next(0, 10000) / 10000) * 2 * PI local distance = math_sqrt(prng:next(0, 10000) / 10000) * searchRadius if attempt == 1 then distance = 0 end -- Always sample the middle of the island, as it's the safest fallback location local x = round(island_info.x + math_cos(angle) * distance) local z = round(island_info.z + math_sin(angle) * distance) local y, isWater = cloudlands.get_height_at(x, z, coreList) if y ~= nil then local weight = 0 if not isWater then weight = weight + 1 end -- avoid putting portals in ponds if y >= island_info.y + ALTITUDE then weight = weight + 2 end -- avoid putting portals down the sides of eroded cliffs positions[#positions + 1] = {x = x, y = y + 1, z = z, weight = weight} end end -- Order the locations by how good they are local compareFn = function(pos_a, pos_b) if pos_a.weight > pos_b.weight then return true end if pos_a.weight == pos_b.weight and pos_a.y < pos_b.y then return true end -- I can't justify why I think lower positions are better. I'm imagining portals nested in valleys rather than on ridges. return false end table.sort(positions, compareFn) -- nether.volume_is_natural() was deprecated in favor of nether.volume_is_natural_and_unprotected() local volume_is_natural_and_unprotected = nether.volume_is_natural_and_unprotected or nether.volume_is_natural -- Now the locations are sorted by how good they are, find the first/best that doesn't -- grief a player build. -- Ancient Portalstone has is_ground_content set to true, so we won't have to worry about old/broken -- portal frames interfering with the results of nether.volume_is_natural_and_unprotected() for _, position in ipairs(positions) do -- Unfortunately, at this point we don't know the orientation of the portal, so use worst case local minp = {x = position.x - 2, y = position.y, z = position.z - 2} local maxp = {x = position.x + 3, y = position.y + 4, z = position.z + 3} if volume_is_natural_and_unprotected(minp, maxp, player_name) then result = position break end end end return result end -- returns nil if no suitable location could be found, otherwise returns (portal_pos, island_info) -- player_name is optional, allowing a player to spawn a remote portal in their own protected areas. local function find_nearest_island_location_for_portal(surface_x, surface_z, player_name) local result = nil local island = cloudlands.find_nearest_island(surface_x, surface_z, 75) if island == nil then island = cloudlands.find_nearest_island(surface_x, surface_z, 150) end if island == nil then island = cloudlands.find_nearest_island(surface_x, surface_z, 400) end if island ~= nil then result = find_potential_portal_location_on_island(island, player_name) end return result, island end -- Ideally the Nether mod will provide a block obtainable by exploring the Nether which is -- earmarked for mods like this one to use for portals, but until this happens I'll create -- our own tempory placeholder "portalstone". -- The Portals API is currently provided by nether, which depends on default, so we can assume default textures are available local portalstone_end = "default_furnace_top.png^(default_ice.png^[opacity:120)^[multiply:#668" -- this gonna look bad with non-default texturepacks, hopefully Nether mod will provide a real block local portalstone_side = "[combine:16x16:0,0=default_furnace_top.png:4,0=default_furnace_top.png:8,0=default_furnace_top.png:12,0=default_furnace_top.png:^(default_ice.png^[opacity:120)^[multiply:#668" minetest.register_node("cloudlands:ancient_portalstone", { description = S("Ancient Portalstone"), tiles = {portalstone_end, portalstone_end, portalstone_side, portalstone_side, portalstone_side, portalstone_side}, paramtype2 = "facedir", sounds = default.node_sound_stone_defaults(), groups = {cracky = 1, level = 2}, on_blast = function() --[[blast proof]] end }) minetest.register_ore({ ore_type = "scatter", ore = "cloudlands:ancient_portalstone", wherein = "nether:rack", clust_scarcity = 32 * 32 * 32, clust_num_ores = 6, clust_size = 3, y_max = nether.DEPTH_CEILING or nether.DEPTH, y_min = nether.DEPTH_FLOOR or -32000, }) local _ = {name = "air", prob = 0} local A = {name = "air", prob = 255, force_place = true} local PU = {name = "cloudlands:ancient_portalstone", param2 = 0, prob = 255, force_place = true} local PW = {name = "cloudlands:ancient_portalstone", param2 = 12, prob = 255, force_place = true} local PN = {name = "cloudlands:ancient_portalstone", param2 = 4, prob = 255, force_place = true} minetest.register_decoration({ name = "Ancient broken portal", deco_type = "schematic", place_on = "nether:rack", sidelen = 80, fill_ratio = 0.00018, biomes = {"nether_caverns"}, y_max = nether.DEPTH_CEILING or nether.DEPTH, y_min = nether.DEPTH_FLOOR or -32000, schematic = { size = {x = 4, y = 4, z = 1}, data = { PN, A, PW, PN, PU, A, A, PU, A, _, _, PU, _, _, _, PU }, yslice_prob = { {ypos = 3, prob = 92}, {ypos = 1, prob = 30}, } }, place_offset_y = 1, flags = "force_placement,all_floors", rotation = "random" }) -- this uses place_schematic() without minetest.after(), so should be called after vm:write_to_map() addDetail_ancientPortal = function(core) if (core.radius < 8 or PORTAL_RARITY == 0) then return false end -- avoid portals hanging off the side of small islands local fastHash = 3 fastHash = (37 * fastHash) + 9354 -- to keep this probability distinct from reefs and atols fastHash = (37 * fastHash) + ISLANDS_SEED fastHash = (37 * fastHash) + core.x fastHash = (37 * fastHash) + core.z fastHash = (37 * fastHash) + math_floor(core.radius) fastHash = (37 * fastHash) + math_floor(core.depth) if (PORTAL_RARITY * 10000) < math_floor((math_abs(fastHash)) % 10000) then return false end local portalPos = find_potential_portal_location_on_island(core, nil) if portalPos ~= nil then local orientation = (fastHash % 2) * 90 portalPos.y = portalPos.y - ((core.x + core.z) % 3) -- partially bury some ancient portals minetest.place_schematic( portalPos, { size = {x = 4, y = 5, z = 1}, data = { PN, PW, PW, PN, PU, _, _, PU, PU, _, _, PU, PU, _, _, PU, PN, PW, PW, PN }, }, orientation, { -- node replacements ["default:obsidian"] = "cloudlands:ancient_portalstone", }, true ) end end nether.register_portal("cloudlands_portal", { shape = nether.PortalShape_Traditional, frame_node_name = "cloudlands:ancient_portalstone", wormhole_node_color = 2, -- 2 is blue particle_color = "#77F", particle_texture = { name = "nether_particle_anim1.png", animation = { type = "vertical_frames", aspect_w = 7, aspect_h = 7, length = 1, }, scale = 1.5 }, title = S("Hallelujah Mountains Portal"), book_of_portals_pagetext = S("Construction requires 14 blocks of ancient portalstone. We have no knowledge of how portalstones were created, the means to craft them are likely lost to time, so our only source has been to scavenge the Nether for the remnants of ancient broken portals. A finished frame is four blocks wide, five blocks high, and stands vertically, like a doorway.") .. "\n\n" .. S("The only portal we managed to scavenge enough portalstone to build took us to a land of floating islands. There were hills and forests and even water up there, but the edges are a perilous drop — a depth of which we cannot even begin to plumb."), is_within_realm = function(pos) -- return true if pos is in the cloudlands -- I'm doing this based off height for speed, so it sometimes gets it wrong when the -- Hallelujah mountains start reaching the ground. if noise_heightMap == nil then cloudlands.init() end local largestCoreType = cloudlands.coreTypes[1] -- the first island type is the biggest/thickest local island_bottom = ALTITUDE - (largestCoreType.depthMax * 0.66) + round(noise_heightMap:get2d({x = pos.x, y = pos.z})) return pos.y > math_max(40, island_bottom) end, find_realm_anchorPos = function(surface_anchorPos, player_name) -- Find the nearest island and obtain a suitable surface position on it local destination_pos, island = find_nearest_island_location_for_portal(surface_anchorPos.x, surface_anchorPos.z, player_name) if island ~= nil then -- Allow any existing or player-positioned portal on the island to be linked to -- first before resorting to the island's default portal position local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal( "cloudlands_portal", {x = island.x, y = 100000, z = island.z}, -- Using 100000 for y to ensure the position is in the cloudlands realm and so find_nearest_working_portal() will only returns island portals. island.radius * 0.9, -- Islands normally don't reach their full radius. Ensure this distance limit encompasses any location find_nearest_island_location_for_portal() can return. 0 -- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are in the Cloudlands realm) ) if existing_portal_location ~= nil then return existing_portal_location, existing_portal_orientation end end return destination_pos end, find_surface_anchorPos = function(realm_anchorPos) -- This function isn't needed since find_surface_target_y() will be used by default, -- but by implementing it I can look for any existing nearby portals before falling -- back to find_surface_target_y. -- Using -100000 for y to ensure the position is outside the cloudlands realm and so -- find_nearest_working_portal() will only returns ground portals. -- a y_factor of 0 makes the search ignore the -100000 altitude of the portals (as -- long as they are outside the cloudlands realm) local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal("cloudlands_portal", {x = realm_anchorPos.x, y = -100000, z = realm_anchorPos.z}, 150, 0) if existing_portal_location ~= nil then return existing_portal_location, existing_portal_orientation else local y = nether.find_surface_target_y(realm_anchorPos.x, realm_anchorPos.z, "cloudlands_portal") return {x = realm_anchorPos.x, y = y, z = realm_anchorPos.z} end end, on_ignite = function(portalDef, anchorPos, orientation) -- make some sparks fly on ignition local p1, p2 = portalDef.shape:get_p1_and_p2_from_anchorPos(anchorPos, orientation) local pos = vector.divide(vector.add(p1, p2), 2) local textureName = portalDef.particle_texture if type(textureName) == "table" then textureName = textureName.name end local velocity if orientation == 0 then velocity = {x = 0, y = 0, z = 7} else velocity = {x = 7, y = 0, z = 0} end local particleSpawnerDef = { amount = 180, time = 0.15, minpos = {x = pos.x - 1, y = pos.y - 1.5, z = pos.z - 1}, maxpos = {x = pos.x + 1, y = pos.y + 1.5, z = pos.z + 1}, minvel = velocity, maxvel = velocity, minacc = {x = 0, y = 0, z = 0}, maxacc = {x = 0, y = 0, z = 0}, minexptime = 0.1, maxexptime = 0.5, minsize = 0.3 * portalDef.particle_texture_scale, maxsize = 0.8 * portalDef.particle_texture_scale, collisiondetection = false, texture = textureName .. "^[colorize:#99F:alpha", animation = portalDef.particle_texture_animation, glow = 8 } minetest.add_particlespawner(particleSpawnerDef) velocity = vector.multiply(velocity, -1) particleSpawnerDef.minvel, particleSpawnerDef.maxvel = velocity, velocity minetest.add_particlespawner(particleSpawnerDef) end }) end --[[============================== SkyTrees ==============================]]-- -- If splitting SkyTrees into a seperate mod, perhaps schemlib would be of help - https://forum.minetest.net/viewtopic.php?t=18084 if not minetest.global_exists("SkyTrees") then -- If SkyTrees added into other mods, this may have already been defined local TREE1_FILE = 'cloudlands_tree1.mts' local TREE2_FILE = 'cloudlands_tree2.mts' local BARK_SUFFIX = '_bark' local GLOW_SUFFIX = '_glow' SkyTrees = { -- Order the trees in this schematicInfo array from the largest island requirements to smallest -- The data in each schematicInfo must exactly match what's in the .mts file or things will break schematicInfo = { { filename = TREE1_FILE, size = {x = 81, y = 106, z = 111}, center = {x = 37, y = 11, z = 73}, requiredIslandDepth = 20, requiredIslandRadius = 40, nodesWithConstructor = { {x=35, y=69, z=1}, {x=61, y=51, z=2}, {x=36, y=68, z=2}, {x=68, y=48, z=3}, {x=61, y=50, z=4}, {x=71, y=50, z=5}, {x=58, y=52, z=5}, {x=65, y=50, z=9}, {x=72, y=53, z=11}, {x=41, y=67, z=12}, {x=63, y=48, z=13}, {x=69, y=52, z=13}, {x=33, y=66, z=14}, {x=39, y=68, z=15}, {x=72, y=68, z=15}, {x=40, y=67, z=16}, {x=39, y=66, z=17}, {x=68, y=45, z=19}, {x=69, y=44, z=20}, {x=72, y=55, z=20}, {x=66, y=56, z=20}, {x=58, y=66, z=20}, {x=71, y=58, z=21}, {x=68, y=45, z=22}, {x=70, y=51, z=22}, {x=73, y=55, z=22}, {x=36, y=62, z=22}, {x=70, y=67, z=22}, {x=21, y=65, z=23}, {x=22, y=66, z=23}, {x=53, y=66, z=23}, {x=70, y=68, z=23}, {x=73, y=54, z=24}, {x=75, y=57, z=24}, {x=37, y=63, z=24}, {x=7, y=68, z=24}, {x=69, y=56, z=25}, {x=34, y=58, z=25}, {x=66, y=62, z=25}, {x=64, y=66, z=25}, {x=6, y=67, z=25}, {x=3, y=68, z=25}, {x=68, y=56, z=26}, {x=65, y=57, z=26}, {x=61, y=63, z=26}, {x=31, y=59, z=27}, {x=48, y=62, z=27}, {x=50, y=63, z=27}, {x=78, y=65, z=27}, {x=78, y=52, z=28}, {x=68, y=57, z=28}, {x=76, y=57, z=28}, {x=31, y=60, z=28}, {x=15, y=63, z=28}, {x=16, y=63, z=28}, {x=66, y=64, z=28}, {x=60, y=65, z=28}, {x=61, y=76, z=28}, {x=63, y=76, z=28}, {x=69, y=59, z=29}, {x=51, y=65, z=29}, {x=72, y=57, z=30}, {x=20, y=60, z=30}, {x=21, y=61, z=30}, {x=49, y=65, z=30}, {x=52, y=53, z=31}, {x=72, y=57, z=31}, {x=36, y=58, z=31}, {x=63, y=60, z=31}, {x=54, y=63, z=31}, {x=45, y=65, z=31}, {x=79, y=66, z=31}, {x=62, y=70, z=31}, {x=55, y=103, z=31}, {x=52, y=53, z=32}, {x=68, y=60, z=32}, {x=19, y=61, z=32}, {x=53, y=63, z=32}, {x=37, y=64, z=32}, {x=21, y=65, z=32}, {x=56, y=65, z=32}, {x=59, y=71, z=32}, {x=35, y=74, z=32}, {x=23, y=75, z=32}, {x=35, y=58, z=33}, {x=62, y=60, z=33}, {x=18, y=63, z=33}, {x=73, y=67, z=33}, {x=37, y=74, z=33}, {x=65, y=75, z=33}, {x=38, y=2, z=34}, {x=67, y=52, z=34}, {x=71, y=60, z=34}, {x=25, y=63, z=34}, {x=19, y=64, z=34}, {x=32, y=66, z=34}, {x=66, y=72, z=34}, {x=41, y=81, z=34}, {x=45, y=93, z=34}, {x=54, y=99, z=34}, {x=38, y=5, z=35}, {x=68, y=48, z=35}, {x=69, y=51, z=35}, {x=48, y=53, z=35}, {x=37, y=57, z=35}, {x=77, y=58, z=35}, {x=32, y=60, z=35}, {x=20, y=61, z=35}, {x=27, y=61, z=35}, {x=33, y=65, z=35}, {x=58, y=65, z=35}, {x=58, y=72, z=35}, {x=60, y=73, z=35}, {x=30, y=74, z=35}, {x=41, y=74, z=35}, {x=41, y=87, z=35}, {x=22, y=58, z=36}, {x=64, y=58, z=36}, {x=39, y=70, z=36}, {x=36, y=77, z=36}, {x=44, y=83, z=36}, {x=40, y=86, z=36}, {x=35, y=56, z=37}, {x=65, y=59, z=37}, {x=66, y=62, z=37}, {x=62, y=67, z=37}, {x=39, y=68, z=37}, {x=40, y=86, z=37}, {x=53, y=88, z=37}, {x=43, y=97, z=37}, {x=52, y=99, z=37}, {x=37, y=3, z=38}, {x=35, y=55, z=38}, {x=38, y=56, z=38}, {x=25, y=57, z=38}, {x=65, y=57, z=38}, {x=71, y=61, z=38}, {x=33, y=65, z=38}, {x=61, y=65, z=38}, {x=50, y=66, z=38}, {x=38, y=68, z=38}, {x=46, y=97, z=38}, {x=44, y=100, z=38}, {x=51, y=102, z=38}, {x=29, y=42, z=39}, {x=27, y=43, z=39}, {x=70, y=48, z=39}, {x=72, y=52, z=39}, {x=23, y=57, z=39}, {x=26, y=57, z=39}, {x=28, y=58, z=39}, {x=55, y=58, z=39}, {x=73, y=59, z=39}, {x=65, y=65, z=39}, {x=41, y=68, z=39}, {x=42, y=81, z=39}, {x=55, y=88, z=39}, {x=43, y=91, z=39}, {x=45, y=100, z=39}, {x=23, y=57, z=40}, {x=29, y=57, z=40}, {x=76, y=58, z=40}, {x=73, y=59, z=40}, {x=78, y=59, z=40}, {x=31, y=60, z=40}, {x=64, y=64, z=40}, {x=41, y=67, z=40}, {x=42, y=75, z=40}, {x=37, y=78, z=40}, {x=42, y=92, z=40}, {x=51, y=101, z=40}, {x=48, y=105, z=40}, {x=75, y=59, z=41}, {x=55, y=63, z=41}, {x=35, y=68, z=41}, {x=35, y=69, z=41}, {x=35, y=71, z=41}, {x=34, y=42, z=42}, {x=29, y=55, z=42}, {x=50, y=61, z=42}, {x=34, y=65, z=42}, {x=57, y=88, z=42}, {x=48, y=89, z=42}, {x=49, y=89, z=42}, {x=27, y=22, z=43}, {x=26, y=28, z=43}, {x=31, y=46, z=43}, {x=66, y=52, z=43}, {x=49, y=57, z=43}, {x=56, y=57, z=43}, {x=41, y=69, z=43}, {x=36, y=52, z=44}, {x=63, y=54, z=44}, {x=51, y=55, z=44}, {x=57, y=56, z=44}, {x=69, y=57, z=44}, {x=64, y=65, z=44}, {x=55, y=90, z=44}, {x=30, y=42, z=45}, {x=31, y=52, z=45}, {x=51, y=54, z=45}, {x=24, y=57, z=45}, {x=70, y=62, z=45}, {x=39, y=69, z=45}, {x=35, y=80, z=45}, {x=29, y=81, z=45}, {x=44, y=85, z=45}, {x=41, y=86, z=45}, {x=33, y=9, z=46}, {x=28, y=44, z=46}, {x=50, y=54, z=46}, {x=47, y=55, z=46}, {x=45, y=56, z=46}, {x=45, y=58, z=46}, {x=47, y=58, z=46}, {x=30, y=63, z=46}, {x=27, y=81, z=46}, {x=28, y=81, z=46}, {x=40, y=86, z=46}, {x=29, y=16, z=47}, {x=32, y=10, z=48}, {x=66, y=49, z=48}, {x=29, y=52, z=48}, {x=53, y=54, z=48}, {x=55, y=54, z=48}, {x=61, y=58, z=48}, {x=59, y=61, z=48}, {x=50, y=63, z=48}, {x=26, y=82, z=48}, {x=43, y=85, z=48}, {x=48, y=86, z=48}, {x=31, y=19, z=49}, {x=30, y=46, z=49}, {x=63, y=51, z=49}, {x=41, y=53, z=49}, {x=31, y=60, z=49}, {x=67, y=1, z=50}, {x=37, y=8, z=50}, {x=40, y=30, z=50}, {x=43, y=57, z=50}, {x=59, y=57, z=50}, {x=60, y=57, z=50}, {x=29, y=61, z=50}, {x=34, y=63, z=50}, {x=49, y=65, z=50}, {x=65, y=3, z=51}, {x=45, y=29, z=51}, {x=41, y=58, z=51}, {x=42, y=60, z=51}, {x=46, y=64, z=51}, {x=47, y=67, z=51}, {x=52, y=68, z=51}, {x=69, y=51, z=52}, {x=53, y=55, z=52}, {x=45, y=62, z=52}, {x=64, y=2, z=53}, {x=3, y=3, z=53}, {x=10, y=6, z=53}, {x=31, y=14, z=53}, {x=37, y=35, z=53}, {x=43, y=48, z=53}, {x=71, y=50, z=53}, {x=52, y=54, z=53}, {x=43, y=57, z=53}, {x=55, y=57, z=53}, {x=52, y=67, z=53}, {x=48, y=72, z=53}, {x=5, y=1, z=54}, {x=9, y=4, z=54}, {x=62, y=4, z=54}, {x=33, y=8, z=54}, {x=42, y=29, z=54}, {x=42, y=32, z=54}, {x=43, y=34, z=54}, {x=41, y=39, z=54}, {x=41, y=57, z=54}, {x=34, y=61, z=54}, {x=58, y=2, z=55}, {x=59, y=3, z=55}, {x=38, y=7, z=55}, {x=40, y=12, z=55}, {x=38, y=39, z=55}, {x=33, y=46, z=55}, {x=28, y=54, z=55}, {x=29, y=55, z=55}, {x=30, y=57, z=55}, {x=54, y=58, z=55}, {x=52, y=63, z=55}, {x=37, y=7, z=56}, {x=55, y=8, z=56}, {x=33, y=45, z=56}, {x=58, y=0, z=57}, {x=9, y=5, z=57}, {x=34, y=7, z=57}, {x=54, y=8, z=57}, {x=17, y=9, z=57}, {x=32, y=12, z=57}, {x=37, y=39, z=57}, {x=41, y=45, z=57}, {x=31, y=46, z=57}, {x=49, y=50, z=57}, {x=50, y=56, z=57}, {x=46, y=59, z=57}, {x=48, y=66, z=57}, {x=51, y=67, z=57}, {x=15, y=3, z=58}, {x=8, y=10, z=58}, {x=41, y=11, z=58}, {x=40, y=13, z=58}, {x=42, y=45, z=58}, {x=50, y=51, z=58}, {x=20, y=5, z=59}, {x=19, y=7, z=59}, {x=22, y=8, z=59}, {x=23, y=9, z=59}, {x=40, y=13, z=59}, {x=33, y=14, z=59}, {x=42, y=41, z=59}, {x=20, y=6, z=60}, {x=9, y=8, z=60}, {x=46, y=8, z=60}, {x=34, y=39, z=60}, {x=30, y=52, z=60}, {x=43, y=57, z=60}, {x=18, y=5, z=61}, {x=11, y=10, z=61}, {x=36, y=36, z=61}, {x=47, y=55, z=61}, {x=38, y=56, z=61}, {x=61, y=59, z=61}, {x=56, y=60, z=61}, {x=36, y=6, z=62}, {x=55, y=7, z=62}, {x=26, y=10, z=62}, {x=29, y=13, z=62}, {x=46, y=13, z=62}, {x=57, y=60, z=62}, {x=18, y=7, z=63}, {x=30, y=11, z=63}, {x=53, y=13, z=63}, {x=45, y=14, z=63}, {x=36, y=32, z=63}, {x=46, y=41, z=63}, {x=29, y=43, z=63}, {x=29, y=44, z=63}, {x=29, y=46, z=63}, {x=29, y=50, z=63}, {x=30, y=52, z=63}, {x=46, y=54, z=63}, {x=19, y=6, z=64}, {x=54, y=8, z=64}, {x=16, y=11, z=64}, {x=42, y=16, z=64}, {x=36, y=25, z=64}, {x=37, y=27, z=64}, {x=36, y=28, z=64}, {x=37, y=29, z=64}, {x=40, y=33, z=64}, {x=30, y=36, z=64}, {x=43, y=39, z=64}, {x=62, y=61, z=64}, {x=21, y=6, z=65}, {x=24, y=6, z=65}, {x=53, y=10, z=65}, {x=52, y=12, z=65}, {x=27, y=17, z=65}, {x=39, y=17, z=65}, {x=29, y=19, z=65}, {x=32, y=22, z=65}, {x=28, y=42, z=65}, {x=60, y=61, z=65}, {x=24, y=6, z=66}, {x=26, y=6, z=66}, {x=19, y=12, z=66}, {x=28, y=20, z=66}, {x=31, y=26, z=66}, {x=39, y=55, z=66}, {x=42, y=6, z=67}, {x=24, y=7, z=67}, {x=20, y=14, z=67}, {x=41, y=21, z=67}, {x=28, y=22, z=67}, {x=29, y=46, z=67}, {x=34, y=52, z=67}, {x=45, y=17, z=68}, {x=42, y=25, z=68}, {x=28, y=43, z=68}, {x=46, y=44, z=68}, {x=29, y=7, z=69}, {x=49, y=12, z=69}, {x=29, y=43, z=69}, {x=48, y=9, z=70}, {x=45, y=17, z=70}, {x=36, y=9, z=71}, {x=47, y=10, z=71}, {x=25, y=11, z=71}, {x=45, y=17, z=71}, {x=42, y=46, z=71}, {x=34, y=47, z=71}, {x=35, y=48, z=71}, {x=45, y=10, z=72}, {x=25, y=12, z=72}, {x=45, y=35, z=72}, {x=45, y=43, z=72}, {x=36, y=52, z=72}, {x=39, y=55, z=72}, {x=26, y=19, z=73}, {x=27, y=21, z=73}, {x=26, y=27, z=73}, {x=26, y=29, z=73}, {x=43, y=31, z=73}, {x=28, y=36, z=73}, {x=42, y=41, z=73}, {x=34, y=46, z=73}, {x=39, y=59, z=73}, {x=24, y=9, z=74}, {x=48, y=9, z=74}, {x=35, y=48, z=74}, {x=35, y=51, z=74}, {x=42, y=53, z=74}, {x=33, y=57, z=74}, {x=30, y=60, z=74}, {x=47, y=8, z=75}, {x=22, y=12, z=75}, {x=45, y=18, z=75}, {x=27, y=30, z=75}, {x=45, y=33, z=75}, {x=36, y=49, z=75}, {x=36, y=1, z=76}, {x=45, y=7, z=76}, {x=21, y=14, z=76}, {x=44, y=23, z=76}, {x=29, y=35, z=76}, {x=38, y=40, z=76}, {x=39, y=42, z=76}, {x=33, y=58, z=76}, {x=34, y=1, z=77}, {x=21, y=7, z=77}, {x=18, y=11, z=77}, {x=26, y=23, z=77}, {x=43, y=25, z=77}, {x=41, y=32, z=77}, {x=36, y=41, z=77}, {x=39, y=47, z=77}, {x=35, y=56, z=77}, {x=35, y=1, z=78}, {x=26, y=3, z=78}, {x=34, y=3, z=78}, {x=18, y=9, z=78}, {x=27, y=23, z=78}, {x=51, y=33, z=78}, {x=41, y=37, z=78}, {x=36, y=1, z=79}, {x=25, y=2, z=79}, {x=18, y=8, z=79}, {x=15, y=10, z=79}, {x=14, y=11, z=79}, {x=27, y=23, z=79}, {x=28, y=25, z=79}, {x=45, y=32, z=79}, {x=33, y=34, z=79}, {x=34, y=34, z=79}, {x=37, y=55, z=79}, {x=40, y=62, z=79}, {x=27, y=0, z=80}, {x=31, y=18, z=80}, {x=30, y=26, z=80}, {x=34, y=61, z=80}, {x=20, y=7, z=81}, {x=51, y=7, z=81}, {x=25, y=8, z=81}, {x=53, y=8, z=81}, {x=42, y=10, z=81}, {x=56, y=12, z=81}, {x=21, y=15, z=81}, {x=37, y=28, z=81}, {x=36, y=29, z=81}, {x=37, y=29, z=81}, {x=44, y=35, z=81}, {x=22, y=7, z=82}, {x=26, y=8, z=82}, {x=29, y=8, z=82}, {x=44, y=9, z=82}, {x=42, y=10, z=82}, {x=32, y=13, z=82}, {x=13, y=14, z=82}, {x=29, y=22, z=82}, {x=31, y=25, z=82}, {x=35, y=27, z=82}, {x=27, y=60, z=82}, {x=41, y=64, z=82}, {x=20, y=8, z=83}, {x=57, y=8, z=83}, {x=24, y=9, z=83}, {x=58, y=9, z=83}, {x=36, y=22, z=83}, {x=32, y=24, z=83}, {x=47, y=8, z=84}, {x=56, y=8, z=84}, {x=59, y=11, z=84}, {x=45, y=13, z=84}, {x=58, y=13, z=84}, {x=17, y=14, z=84}, {x=23, y=14, z=84}, {x=56, y=14, z=84}, {x=29, y=19, z=84}, {x=36, y=19, z=84}, {x=27, y=59, z=84}, {x=35, y=6, z=85}, {x=9, y=8, z=85}, {x=41, y=11, z=85}, {x=50, y=13, z=85}, {x=33, y=58, z=85}, {x=34, y=58, z=85}, {x=33, y=7, z=86}, {x=18, y=10, z=86}, {x=9, y=12, z=86}, {x=41, y=12, z=87}, {x=41, y=60, z=87}, {x=9, y=2, z=88}, {x=7, y=5, z=88}, {x=5, y=10, z=88}, {x=41, y=11, z=88}, {x=62, y=11, z=88}, {x=42, y=68, z=88}, {x=37, y=6, z=89}, {x=66, y=8, z=89}, {x=9, y=10, z=89}, {x=19, y=10, z=89}, {x=58, y=12, z=89}, {x=45, y=62, z=89}, {x=7, y=5, z=90}, {x=67, y=5, z=90}, {x=7, y=9, z=90}, {x=31, y=11, z=90}, {x=62, y=11, z=90}, {x=1, y=2, z=91}, {x=5, y=5, z=91}, {x=69, y=5, z=91}, {x=62, y=8, z=91}, {x=58, y=9, z=91}, {x=63, y=10, z=91}, {x=35, y=7, z=92}, {x=62, y=9, z=92}, {x=33, y=13, z=92}, {x=36, y=62, z=92}, {x=37, y=3, z=93}, {x=37, y=6, z=93}, {x=64, y=6, z=93}, {x=32, y=10, z=93}, {x=34, y=14, z=93}, {x=39, y=57, z=93}, {x=41, y=67, z=93}, {x=33, y=9, z=94}, {x=38, y=57, z=94}, {x=41, y=69, z=94}, {x=40, y=1, z=95}, {x=34, y=7, z=97}, {x=33, y=9, z=97}, {x=33, y=10, z=102}, {x=33, y=7, z=105}, {x=35, y=9, z=107} } }, { filename = TREE2_FILE, size = {x = 62, y = 65, z = 65}, center = {x = 30, y = 12, z = 36}, requiredIslandDepth = 16, requiredIslandRadius = 24, nodesWithConstructor = { {x=35, y=53, z=1}, {x=33, y=59, z=1}, {x=32, y=58, z=3}, {x=31, y=57, z=5}, {x=40, y=58, z=6}, {x=29, y=57, z=7}, {x=39, y=51, z=8}, {x=52, y=53, z=8}, {x=32, y=53, z=9}, {x=25, y=58, z=9}, {x=51, y=51, z=10}, {x=47, y=50, z=11}, {x=50, y=55, z=11}, {x=28, y=57, z=11}, {x=26, y=39, z=12}, {x=30, y=39, z=12}, {x=24, y=40, z=12}, {x=53, y=52, z=12}, {x=29, y=57, z=12}, {x=43, y=59, z=12}, {x=26, y=39, z=13}, {x=36, y=48, z=13}, {x=27, y=39, z=14}, {x=39, y=48, z=14}, {x=33, y=50, z=14}, {x=43, y=50, z=14}, {x=24, y=59, z=14}, {x=41, y=49, z=15}, {x=33, y=12, z=16}, {x=36, y=46, z=16}, {x=50, y=51, z=16}, {x=46, y=57, z=16}, {x=36, y=45, z=17}, {x=27, y=46, z=17}, {x=22, y=48, z=17}, {x=45, y=50, z=17}, {x=31, y=38, z=18}, {x=32, y=38, z=18}, {x=39, y=46, z=18}, {x=51, y=51, z=18}, {x=31, y=11, z=19}, {x=32, y=38, z=19}, {x=39, y=41, z=19}, {x=45, y=57, z=19}, {x=29, y=58, z=19}, {x=28, y=60, z=20}, {x=38, y=40, z=21}, {x=30, y=58, z=21}, {x=31, y=13, z=22}, {x=20, y=41, z=22}, {x=22, y=43, z=22}, {x=20, y=48, z=22}, {x=22, y=39, z=23}, {x=49, y=50, z=23}, {x=52, y=52, z=23}, {x=53, y=53, z=23}, {x=32, y=55, z=23}, {x=36, y=59, z=23}, {x=31, y=60, z=23}, {x=25, y=46, z=24}, {x=40, y=56, z=24}, {x=34, y=58, z=24}, {x=38, y=58, z=24}, {x=32, y=39, z=25}, {x=40, y=46, z=25}, {x=39, y=55, z=25}, {x=36, y=45, z=26}, {x=12, y=7, z=28}, {x=34, y=33, z=28}, {x=31, y=36, z=28}, {x=37, y=41, z=28}, {x=14, y=60, z=28}, {x=19, y=13, z=29}, {x=12, y=43, z=29}, {x=8, y=45, z=29}, {x=31, y=46, z=29}, {x=39, y=47, z=29}, {x=13, y=60, z=29}, {x=22, y=63, z=29}, {x=51, y=9, z=30}, {x=32, y=39, z=30}, {x=33, y=40, z=30}, {x=34, y=44, z=30}, {x=22, y=1, z=31}, {x=24, y=2, z=31}, {x=20, y=7, z=31}, {x=51, y=9, z=31}, {x=16, y=12, z=31}, {x=34, y=27, z=31}, {x=22, y=43, z=31}, {x=27, y=44, z=31}, {x=23, y=51, z=31}, {x=42, y=58, z=31}, {x=9, y=60, z=31}, {x=22, y=5, z=32}, {x=22, y=6, z=32}, {x=50, y=10, z=32}, {x=53, y=11, z=32}, {x=41, y=15, z=32}, {x=43, y=15, z=32}, {x=31, y=21, z=32}, {x=31, y=28, z=32}, {x=12, y=42, z=32}, {x=15, y=42, z=32}, {x=13, y=48, z=32}, {x=37, y=49, z=32}, {x=18, y=59, z=32}, {x=52, y=9, z=33}, {x=40, y=10, z=33}, {x=43, y=10, z=33}, {x=22, y=11, z=33}, {x=27, y=11, z=33}, {x=50, y=11, z=33}, {x=22, y=15, z=33}, {x=36, y=29, z=33}, {x=33, y=37, z=33}, {x=9, y=42, z=33}, {x=14, y=42, z=33}, {x=18, y=43, z=33}, {x=23, y=43, z=33}, {x=33, y=49, z=33}, {x=43, y=53, z=33}, {x=54, y=53, z=33}, {x=31, y=55, z=33}, {x=23, y=58, z=33}, {x=43, y=10, z=34}, {x=44, y=10, z=34}, {x=32, y=12, z=34}, {x=46, y=13, z=34}, {x=28, y=29, z=34}, {x=20, y=42, z=34}, {x=39, y=50, z=34}, {x=51, y=52, z=34}, {x=54, y=52, z=34}, {x=35, y=55, z=34}, {x=51, y=56, z=34}, {x=35, y=5, z=35}, {x=34, y=8, z=35}, {x=33, y=10, z=35}, {x=49, y=10, z=35}, {x=43, y=14, z=35}, {x=36, y=35, z=35}, {x=30, y=47, z=35}, {x=9, y=48, z=35}, {x=39, y=51, z=35}, {x=56, y=52, z=35}, {x=40, y=56, z=35}, {x=13, y=59, z=35}, {x=26, y=62, z=35}, {x=28, y=13, z=36}, {x=38, y=17, z=36}, {x=38, y=20, z=36}, {x=27, y=26, z=36}, {x=38, y=35, z=36}, {x=24, y=39, z=36}, {x=6, y=43, z=36}, {x=13, y=57, z=36}, {x=48, y=7, z=37}, {x=33, y=8, z=37}, {x=50, y=9, z=37}, {x=36, y=11, z=37}, {x=27, y=20, z=37}, {x=27, y=22, z=37}, {x=38, y=24, z=37}, {x=33, y=34, z=37}, {x=9, y=42, z=37}, {x=14, y=42, z=37}, {x=25, y=42, z=37}, {x=53, y=50, z=37}, {x=33, y=53, z=37}, {x=54, y=59, z=37}, {x=28, y=21, z=38}, {x=39, y=34, z=38}, {x=24, y=35, z=38}, {x=8, y=43, z=38}, {x=6, y=47, z=38}, {x=48, y=51, z=38}, {x=61, y=53, z=38}, {x=26, y=57, z=38}, {x=27, y=57, z=38}, {x=32, y=59, z=38}, {x=29, y=62, z=38}, {x=38, y=62, z=38}, {x=33, y=7, z=39}, {x=34, y=9, z=39}, {x=28, y=23, z=39}, {x=34, y=37, z=39}, {x=19, y=42, z=39}, {x=55, y=50, z=39}, {x=47, y=51, z=39}, {x=11, y=54, z=39}, {x=9, y=60, z=39}, {x=33, y=61, z=39}, {x=33, y=4, z=40}, {x=30, y=11, z=40}, {x=39, y=13, z=40}, {x=36, y=23, z=40}, {x=22, y=38, z=40}, {x=54, y=49, z=40}, {x=53, y=50, z=40}, {x=23, y=54, z=40}, {x=28, y=57, z=40}, {x=29, y=57, z=40}, {x=31, y=29, z=41}, {x=27, y=34, z=41}, {x=30, y=37, z=41}, {x=42, y=38, z=41}, {x=12, y=42, z=41}, {x=15, y=42, z=41}, {x=44, y=44, z=41}, {x=28, y=57, z=41}, {x=55, y=57, z=41}, {x=9, y=59, z=41}, {x=30, y=10, z=42}, {x=26, y=15, z=42}, {x=31, y=15, z=42}, {x=34, y=17, z=42}, {x=28, y=36, z=42}, {x=38, y=44, z=42}, {x=42, y=44, z=42}, {x=46, y=44, z=42}, {x=32, y=47, z=42}, {x=52, y=47, z=42}, {x=39, y=55, z=42}, {x=54, y=56, z=42}, {x=34, y=59, z=42}, {x=40, y=11, z=43}, {x=30, y=14, z=43}, {x=28, y=16, z=43}, {x=34, y=31, z=43}, {x=11, y=43, z=43}, {x=14, y=43, z=43}, {x=28, y=47, z=43}, {x=57, y=50, z=43}, {x=61, y=54, z=43}, {x=30, y=58, z=43}, {x=34, y=59, z=43}, {x=7, y=61, z=43}, {x=41, y=10, z=44}, {x=29, y=15, z=44}, {x=36, y=39, z=44}, {x=6, y=43, z=44}, {x=30, y=47, z=44}, {x=57, y=50, z=44}, {x=38, y=10, z=45}, {x=42, y=10, z=45}, {x=11, y=43, z=45}, {x=14, y=43, z=45}, {x=46, y=44, z=45}, {x=32, y=45, z=45}, {x=55, y=45, z=45}, {x=3, y=48, z=45}, {x=31, y=57, z=45}, {x=41, y=3, z=46}, {x=40, y=7, z=46}, {x=28, y=11, z=46}, {x=23, y=13, z=46}, {x=19, y=43, z=46}, {x=24, y=9, z=47}, {x=39, y=9, z=47}, {x=43, y=12, z=47}, {x=5, y=43, z=47}, {x=42, y=43, z=47}, {x=46, y=43, z=47}, {x=24, y=47, z=47}, {x=60, y=52, z=47}, {x=24, y=54, z=47}, {x=37, y=57, z=47}, {x=11, y=60, z=47}, {x=27, y=9, z=48}, {x=27, y=11, z=48}, {x=22, y=14, z=48}, {x=15, y=44, z=48}, {x=51, y=45, z=48}, {x=23, y=49, z=48}, {x=59, y=53, z=48}, {x=9, y=56, z=48}, {x=33, y=59, z=48}, {x=41, y=14, z=49}, {x=8, y=43, z=49}, {x=10, y=43, z=49}, {x=39, y=43, z=49}, {x=34, y=44, z=49}, {x=47, y=44, z=49}, {x=48, y=44, z=49}, {x=24, y=51, z=49}, {x=10, y=55, z=49}, {x=32, y=59, z=49}, {x=20, y=61, z=49}, {x=11, y=63, z=49}, {x=25, y=8, z=50}, {x=22, y=10, z=50}, {x=42, y=14, z=50}, {x=10, y=43, z=50}, {x=43, y=43, z=50}, {x=61, y=46, z=50}, {x=39, y=54, z=50}, {x=24, y=12, z=51}, {x=50, y=44, z=51}, {x=52, y=45, z=51}, {x=54, y=45, z=51}, {x=2, y=46, z=51}, {x=8, y=51, z=51}, {x=7, y=52, z=51}, {x=37, y=58, z=51}, {x=22, y=50, z=52}, {x=25, y=55, z=52}, {x=39, y=58, z=52}, {x=20, y=7, z=53}, {x=40, y=43, z=53}, {x=58, y=45, z=53}, {x=60, y=50, z=53}, {x=22, y=55, z=53}, {x=28, y=56, z=53}, {x=50, y=62, z=53}, {x=54, y=45, z=54}, {x=61, y=46, z=54}, {x=30, y=47, z=54}, {x=30, y=49, z=54}, {x=53, y=53, z=54}, {x=18, y=55, z=54}, {x=51, y=56, z=54}, {x=46, y=62, z=54}, {x=21, y=56, z=55}, {x=24, y=56, z=55}, {x=38, y=61, z=55}, {x=19, y=49, z=56}, {x=46, y=52, z=56}, {x=47, y=53, z=56}, {x=59, y=47, z=57}, {x=26, y=57, z=57}, {x=45, y=43, z=58}, {x=15, y=50, z=58}, {x=11, y=51, z=58}, {x=50, y=44, z=59}, {x=53, y=47, z=59}, {x=43, y=49, z=59}, {x=18, y=50, z=59}, {x=18, y=51, z=60}, {x=38, y=45, z=61}, {x=50, y=47, z=61}, {x=41, y=48, z=61} }, } }, MODNAME = minetest.get_current_modname() -- don't hardcode incase it's copied into other mods } -- Must be called during mod load time, as it uses minetest.register_node() -- (add an optional dependency for any mod where the tree & leaf textures might be -- sourced from, to ensure they are loaded before this is called) SkyTrees.init = function() SkyTrees.minimumIslandRadius = 100000 SkyTrees.minimumIslandDepth = 100000 SkyTrees.maximumYOffset = 0 SkyTrees.maximumHeight = 0 SkyTrees.nodeName_sideVines = interop.find_node_name(NODENAMES_VINES) SkyTrees.nodeName_hangingVine = interop.find_node_name(NODENAMES_HANGINGVINE) SkyTrees.nodeName_hangingRoot = interop.find_node_name(NODENAMES_HANGINGROOT) for i,tree in pairs(SkyTrees.schematicInfo) do local fullFilename = minetest.get_modpath(SkyTrees.MODNAME) .. DIR_DELIM .. tree.filename if not interop.file_exists(fullFilename) then -- remove the schematic from the list SkyTrees.schematicInfo[i] = nil else SkyTrees.minimumIslandRadius = math_min(SkyTrees.minimumIslandRadius, tree.requiredIslandRadius) SkyTrees.minimumIslandDepth = math_min(SkyTrees.minimumIslandDepth, tree.requiredIslandDepth) SkyTrees.maximumYOffset = math_max(SkyTrees.maximumYOffset, tree.center.y) SkyTrees.maximumHeight = math_max(SkyTrees.maximumHeight, tree.size.y) tree.theme = {} SkyTrees.schematicInfo[tree.filename] = tree -- so schematicInfo of trees can be indexed by name end end local function generate_woodTypes(nodeName_templateWood, overlay, barkoverlay, nodesuffix, description, dropsTemplateWood) local trunkNode = minetest.registered_nodes[nodeName_templateWood] local newTrunkNode = {} for key, value in pairs(trunkNode) do newTrunkNode[key] = value end newTrunkNode.name = SkyTrees.MODNAME .. ":" .. nodesuffix newTrunkNode.description = description if newTrunkNode.paramtype2 == nil then newTrunkNode.paramtype2 = "facedir" end if newTrunkNode.on_dig ~= nil and minetest.get_modpath("main") then newTrunkNode.on_dig = nil -- Crafter has special trunk auto-digging logic that doesn't make sense for giant trees end if dropsTemplateWood then newTrunkNode.drop = nodeName_templateWood if newTrunkNode.groups == nil then newTrunkNode.groups = {} end newTrunkNode.groups.not_in_creative_inventory = 1 else newTrunkNode.drop = nil end local tiles = trunkNode.tiles if type(tiles) == "table" then newTrunkNode.tiles = {} for key, value in pairs(tiles) do newTrunkNode.tiles[key] = value .. overlay end else newTrunkNode.tiles = tiles .. overlay end local newBarkNode = {} for key, value in pairs(newTrunkNode) do newBarkNode[key] = value end newBarkNode.name = newBarkNode.name .. BARK_SUFFIX newBarkNode.description = S("Bark of @1", newBarkNode.description) -- .drop: leave the bark nodes dropping the trunk wood tiles = trunkNode.tiles if type(tiles) == "table" then newBarkNode.tiles = { tiles[#tiles] .. barkoverlay } end --minetest.log("info", newTrunkNode.name .. ": " .. dump(newTrunkNode)) minetest.register_node(newTrunkNode.name, newTrunkNode) minetest.register_node(newBarkNode.name, newBarkNode) return newTrunkNode.name end local function generate_leafTypes(nodeName_templateLeaf, overlay, nodesuffix, description, dropsTemplateLeaf, glowVariantBrightness) local leafNode = minetest.registered_nodes[nodeName_templateLeaf] local newLeafNode = {} for key, value in pairs(leafNode) do newLeafNode[key] = value end newLeafNode.name = SkyTrees.MODNAME .. ":" .. nodesuffix newLeafNode.description = description newLeafNode.sunlight_propagates = true -- soo many leaves they otherwise blot out the sun. if dropsTemplateLeaf then newLeafNode.drop = nodeName_templateLeaf if newLeafNode.groups == nil then newLeafNode.groups = {} end newLeafNode.groups.not_in_creative_inventory = 1 else newLeafNode.drop = nil end local tiles = leafNode.tiles if type(tiles) == "table" then newLeafNode.tiles = {} for key, value in pairs(tiles) do newLeafNode.tiles[key] = value .. overlay end else newLeafNode.tiles = tiles .. overlay end minetest.register_node(newLeafNode.name, newLeafNode) if glowVariantBrightness ~= nil and glowVariantBrightness > 0 and BIOLUMINESCENCE then local glowingLeafNode = {} for key, value in pairs(newLeafNode) do glowingLeafNode[key] = value end glowingLeafNode.name = newLeafNode.name .. GLOW_SUFFIX glowingLeafNode.description = S("Glowing @1", description) glowingLeafNode.light_source = glowVariantBrightness minetest.register_node(glowingLeafNode.name, glowingLeafNode) end return newLeafNode.name end local templateWood = interop.find_node_name(NODENAMES_TREEWOOD) if templateWood == 'ignore' then SkyTrees.disabled = "Could not find any tree nodes" return end local normalwood = generate_woodTypes(templateWood, "", "", "tree", S("Giant tree"), true) local darkwood = generate_woodTypes(templateWood, "^[colorize:black:205", "^[colorize:black:205", "darkwood", S("Giant Ziricote"), false) local deadwood = generate_woodTypes(templateWood, "^[colorize:#EFE6B9:110", "^[colorize:#E8D0A0:110", "deadbleachedwood", S("Dead bleached wood"), false) -- make use of the bark blocks to introduce some color variance in the tree local templateLeaf = interop.find_node_name(NODENAMES_TREELEAVES) if templateLeaf == 'ignore' then SkyTrees.disabled = "Could not find any treeleaf nodes" return end local greenleaf1 = generate_leafTypes(templateLeaf, "", "leaves", S("Leaves of a giant tree"), true) -- drops templateLeaf because these look close enough to the original leaves that we won't clutter the game & creative-menu with tiny visual variants that other recipes/parts of the game won't know about local greenleaf2 = generate_leafTypes(templateLeaf, "^[colorize:#00FF00:16", "leaves2", S("Leaves of a giant tree"), false) local greenleaf3 = generate_leafTypes(templateLeaf, "^[colorize:#90FF60:28", "leaves3", S("Leaves of a giant tree"), false) local whiteblossom1 = generate_leafTypes(templateLeaf, "^[colorize:#fffdfd:alpha", "blossom_white1", S("Blossom"), false) local whiteblossom2 = generate_leafTypes(templateLeaf, "^[colorize:#fff0f0:alpha", "blossom_white2", S("Blossom"), false) local pinkblossom = generate_leafTypes(templateLeaf, "^[colorize:#FFE3E8:alpha", "blossom_whitepink", S("Blossom"), false, 5) local sakurablossom1 = generate_leafTypes(templateLeaf, "^[colorize:#ea327c:alpha", "blossom_red", S("Sakura blossom"), false, 5) local sakurablossom2 = generate_leafTypes(templateLeaf, "^[colorize:#ffc3dd:alpha", "blossom_pink", S("Sakura blossom"), false) local wisteriaBlossom1 = generate_leafTypes(templateLeaf, "^[colorize:#8087ec:alpha", "blossom_wisteria1", S("Wisteria blossom"), false) local wisteriaBlossom2 = generate_leafTypes(templateLeaf, "^[colorize:#ccc9ff:alpha", "blossom_wisteria2", S("Wisteria blossom"), false, 7) local tree = SkyTrees.schematicInfo[TREE1_FILE] if tree ~= nil then tree.defaultThemeName = "Green foliage" tree.theme[tree.defaultThemeName] = { relativeProbability = 5, trunk = normalwood, leaves1 = greenleaf1, leaves2 = greenleaf2, leaves_special = greenleaf3, vineflags = { leaves = true, hanging_leaves = true }, init = function(self, position) -- if it's hot and humid then add vines local viney = get_heat(position) >= VINES_REQUIRED_TEMPERATURE and get_humidity(position) >= VINES_REQUIRED_HUMIDITY if viney then local flagSeed = position.x * 3 + position.z + ISLANDS_SEED self.vineflags.hanging_leaves = (flagSeed % 10) <= 3 or (flagSeed % 10) >= 8 self.vineflags.leaves = (flagSeed % 10) <= 5 self.vineflags.bark = (flagSeed % 10) <= 2 self.vineflags.hanging_bark = (flagSeed % 10) <= 1 end end } tree.theme["Haunted"] = { relativeProbability = 2, trunk = darkwood, vineflags = { hanging_roots = true }, hasHeart = false, hasSoil = false, init = function(self, position) -- 60% of these trees are a hanging roots variant self.vineflags.hanging_roots = (position.x * 3 + position.y + position.z + ISLANDS_SEED) % 10 < 60 end } tree.theme["Dead"] = { relativeProbability = 0, -- 0 because this theme will be chosen based on location, rather than chance. trunk = deadwood, hasHeart = false } tree.theme["Sakura"] = { relativeProbability = 2, trunk = darkwood, leaves1 = sakurablossom2, leaves2 = whiteblossom2, leaves_special = sakurablossom1, init = function(self, position) -- 40% of these trees are a glowing variant self.glowing = (position.x * 3 + position.z + ISLANDS_SEED) % 10 <= 3 and BIOLUMINESCENCE self.leaves_special = sakurablossom1 if self.glowing then self.leaves_special = sakurablossom1 .. GLOW_SUFFIX end end } end tree = SkyTrees.schematicInfo[TREE2_FILE] if tree ~= nil then -- copy the green leaves theme from tree1 tree.defaultThemeName = "Green foliage" tree.theme[tree.defaultThemeName] = SkyTrees.schematicInfo[TREE1_FILE].theme["Green foliage"] tree.theme["Wisteria"] = { relativeProbability = 2.5, trunk = normalwood, leaves1 = greenleaf1, leaves2 = wisteriaBlossom1, leaves_special = wisteriaBlossom2, vineflags = { leaves = true, hanging_leaves = true, hanging_bark = true }, init = function(self, position) -- 40% of these trees are a glowing variant self.glowing = (position.x * 3 + position.z + ISLANDS_SEED) % 10 <= 3 and BIOLUMINESCENCE self.leaves_special = wisteriaBlossom2 if self.glowing then self.leaves_special = wisteriaBlossom2 .. GLOW_SUFFIX end -- if it's hot and humid then allow vines on the trunk as well self.vineflags.bark = get_heat(position) >= VINES_REQUIRED_TEMPERATURE and get_humidity(position) >= VINES_REQUIRED_HUMIDITY end } tree.theme["Blossom"] = { relativeProbability = 1.5, trunk = normalwood, leaves1 = whiteblossom1, leaves2 = whiteblossom2, leaves_special = normalwood..BARK_SUFFIX, init = function(self, position) -- 30% of these trees are a glowing variant self.glowing = (position.x * 3 + position.z + ISLANDS_SEED) % 10 <= 2 and BIOLUMINESCENCE self.leaves_special = normalwood..BARK_SUFFIX if self.glowing then self.leaves_special = pinkblossom .. GLOW_SUFFIX end end } end -- fill in any omitted fields in the themes with default values for _,treeInfo in pairs(SkyTrees.schematicInfo) do for _,theme in pairs(treeInfo.theme) do if theme.bark == nil then theme.bark = theme.trunk .. BARK_SUFFIX end if theme.leaves1 == nil then theme.leaves1 = 'ignore' end if theme.leaves2 == nil then theme.leaves2 = 'ignore' end if theme.leaves_special == nil then theme.leaves_special = theme.leaves1 end if theme.vineflags == nil then theme.vineflags = {} end if theme.relativeProbability == nil then theme.relativeProbability = 1.0 end if theme.glowing == nil then theme.glowing = false end if theme.hasSoil == nil then theme.hasSoil = true end if theme.hasHeart == nil then theme.hasHeart = true end end end -- The heart of the Tree -- The difference between a living tree and and a haunted/darkened husk -- -- Ideally trees would slowly fizzlefade to/from the Haunted theme depending on -- whether a player steals or restores the heart, meaning a house hollowed out inside -- a living tree would need the heart to still be kept inside it, perhaps on its -- own pedestal (unless wanting an Addam's Family treehouse). local heartwoodTexture = minetest.registered_nodes[templateWood].tiles if type(heartwoodTexture) == "table" then heartwoodTexture = heartwoodTexture[1] end local heartwoodGlow = minetest.LIGHT_MAX -- plants can grow under the heart of the Tree if not BIOLUMINESCENCE then heartwoodGlow = 0 end -- :( minetest.register_node( SkyTrees.MODNAME .. ":HeartWood", { tiles = { heartwoodTexture }, description = S("Heart of the Tree"), groups = {oddly_breakable_by_hand = 3, handy = 1}, _mcl_hardness = 0.4, drawtype = "nodebox", paramtype = "light", light_source = heartwoodGlow, -- plants can grow under the heart of the Tree node_box = { type = "fixed", fixed = { --[[ Original heart {-0.38, -0.38, -0.38, 0.38, 0.38, 0.38}, {0.15, 0.15, 0.15, 0.5, 0.5, 0.5}, {-0.5, 0.15, 0.15, -0.15, 0.5, 0.5}, {-0.5, 0.15, -0.5, -0.15, 0.5, -0.15}, {0.15, 0.15, -0.5, 0.5, 0.5, -0.15}, {0.15, -0.5, -0.5, 0.5, -0.15, -0.15}, {-0.5, -0.5, -0.5, -0.15, -0.15, -0.15}, {-0.5, -0.5, 0.15, -0.15, -0.15, 0.5}, {0.15, -0.5, 0.15, 0.5, -0.15, 0.5} ]] {-0.38, -0.38, -0.38, 0.38, 0.38, 0.38}, {-0.5, -0.2, -0.2, 0.5, 0.2, 0.2}, {-0.2, -0.5, -0.2, 0.2, 0.5, 0.2}, {-0.2, -0.2, -0.5, 0.2, 0.2, 0.5} } } } ) end -- this is hack to work around how place_schematic() never invalidates its cache -- a unique schematic filename is generated for each unique theme SkyTrees.getMalleatedFilename = function(schematicInfo, themeName) -- create a unique id for the theme local theme = schematicInfo.theme[themeName] local flags = 0 if theme.glowing then flags = flags + 1 end if theme.vineflags.leaves then flags = flags + 2 end if theme.vineflags.hanging_leaves then flags = flags + 4 end if theme.vineflags.bark then flags = flags + 8 end if theme.vineflags.hanging_bark then flags = flags + 16 end if theme.vineflags.hanging_roots then flags = flags + 32 end if theme.hasSoil then flags = flags + 64 end if theme.hasHeart then flags = flags + 128 end local uniqueId = themeName .. flags if schematicInfo.malleatedFilenames == nil then schematicInfo.malleatedFilenames = {} end if schematicInfo.malleatedFilenames[uniqueId] == nil then local malleationCount = 0 for _ in pairs(schematicInfo.malleatedFilenames) do malleationCount = malleationCount + 1 end local malleatedFilename = minetest.get_modpath(SkyTrees.MODNAME) .. DIR_DELIM for i = 1, malleationCount do malleatedFilename = malleatedFilename .. '.' .. DIR_DELIM -- should work on both Linux and Windows end malleatedFilename = malleatedFilename .. schematicInfo.filename schematicInfo.malleatedFilenames[uniqueId] = malleatedFilename end --minetest.log("info", "Malleated file name for " .. uniqueId .. " is " .. schematicInfo.malleatedFilenames[uniqueId]) return schematicInfo.malleatedFilenames[uniqueId] end -- Returns true if a tree in this location would be dead -- (checks for desert) SkyTrees.isDead = function(position) local heat = get_heat(position) local humidity = get_humidity(position) if humidity <= 10 or (humidity <= 20 and heat >= 80) then return true end local biomeId = interop.get_biome_key(position) local biome = biomes[biomeId] if biome ~= nil and biome.node_top ~= nil then local modname, nodename = interop.split_nodename(biome.node_top) if string.find(nodename, "sand") or string.find(nodename, "desert") then return true end end end -- Returns the name of a suitable theme -- Picks a theme from the schematicInfo automatically, based on the themes' relativeProbability, and location. SkyTrees.selectTheme = function(position, schematicInfo, choiceSeed) local deadThemeName = "Dead" if schematicInfo.theme[deadThemeName] ~= nil then -- Tree is dead and bleached in desert biomes if SkyTrees.isDead(position) then return deadThemeName end end if choiceSeed == nil then choiceSeed = 0 end -- Use a known PRNG implementation local prng = PcgRandom( position.x * 65732 + position.z * 729 + schematicInfo.size.x * 3 + choiceSeed ) local sumProbabilities = 0 for _,theme in pairs(schematicInfo.theme) do sumProbabilities = sumProbabilities + theme.relativeProbability end local selection = prng:next(0, sumProbabilities * 1000) / 1000 if DEBUG_SKYTREES then minetest.log("info", "Skytrees x: "..position.x.." y: ".. position.y .. " sumProbabilities: " .. sumProbabilities .. ", selection: " .. selection) end sumProbabilities = 0 for themeName,theme in pairs(schematicInfo.theme) do if selection <= sumProbabilities + theme.relativeProbability then return themeName else sumProbabilities = sumProbabilities + theme.relativeProbability end end error(SkyTrees.MODNAME .. " - SkyTrees.selectTheme failed to find a theme", 0) return schematicInfo.defaultThemeName end -- position is a vector {x, y, z} -- rotation must be either 0, 90, 180, or 270 -- schematicInfo must be one of the items in SkyTrees.schematicInfo[] -- topsoil [optional] is the biome's "node_top" - the ground node of the region. SkyTrees.placeTree = function(position, rotation, schematicInfo, themeName, topsoil) if SkyTrees.disabled ~= nil then error(SkyTrees.MODNAME .. " - SkyTrees are disabled: " .. SkyTrees.disabled, 0) return end -- returns a new position vector, rotated around (0, 0) to match the schematic rotation (provided the schematic_size is correct!) local function rotatePositon(position, schematic_size, rotation) local result = vector.new(position) if rotation == 90 then result.x = position.z result.z = schematic_size.x - position.x - 1 elseif rotation == 180 then result.x = schematic_size.x - position.x - 1 result.z = schematic_size.z - position.z - 1 elseif rotation == 270 then result.x = schematic_size.z - position.z - 1 result.z = position.x end return result end local rotatedCenter = rotatePositon(schematicInfo.center, schematicInfo.size, rotation) local treePos = vector.subtract(position, rotatedCenter) if themeName == nil then themeName = SkyTrees.selectTheme(position, schematicInfo) end local theme = schematicInfo.theme[themeName] if theme == nil then error(MODNAME .. ' called SkyTrees.placeTree("' .. schematicInfo.filename .. '") with invalid theme: ' .. themeName, 0) end if theme.init ~= nil then theme.init(theme, position) end if theme.hasSoil then if topsoil == nil then topsoil = 'ignore' if minetest.get_biome_data == nil then error(SkyTrees.MODNAME .. " requires Minetest v5.0 or greater, or to have minor modifications to support v0.4.x", 0) end local treeBiome = biomes[interop.get_biome_key(position)] if treeBiome ~= nil and treeBiome.node_top ~= nil then topsoil = treeBiome.node_top end end else topsoil = 'ignore' end local nodeName_heart = SkyTrees.MODNAME .. ":HeartWood" if not theme.hasHeart then nodeName_heart = 'ignore' end -- theme.init() may have changed the vineflags, so update the replacement node names if theme.vineflags.hanging_leaves == true and SkyTrees.nodeName_hangingVine == 'ignore' then theme.vineflags.leaves = true end -- if there are no hanging vines then substitute side_vines if theme.vineflags.leaves == true then theme.leaf_vines = SkyTrees.nodeName_sideVines else theme.leaf_vines = 'ignore' end if theme.vineflags.bark == true then theme.bark_vines = SkyTrees.nodeName_sideVines else theme.bark_vines = 'ignore' end if theme.vineflags.hanging_leaves == true then theme.hanging_leaf_vines = SkyTrees.nodeName_hangingVine else theme.hanging_leaf_vines = 'ignore' end if theme.vineflags.hanging_bark == true then theme.hanging_bark_vines = SkyTrees.nodeName_hangingVine else theme.hanging_bark_vines = 'ignore' end if theme.vineflags.hanging_roots == true and SkyTrees.nodeName_hangingRoot ~= 'ignore' then theme.hanging_bark_vines = SkyTrees.nodeName_hangingRoot end local replacements = { ['treebark\r\n\r\n~~~ Cloudlands_tree mts by Dr.Frankenstone: Amateur Arborist ~~~\r\n\r\n'] = theme.bark, -- because this node name is always replaced, it can double as space for a text header in the file. ['default:tree'] = theme.trunk, ['default:leaves'] = theme.leaves1, ['leaves_alt'] = theme.leaves2, ['leaves_special'] = theme.leaves_special, ['leaf_vines'] = theme.leaf_vines, ['bark_vines'] = theme.bark_vines, ['hanging_leaf_vines'] = theme.hanging_leaf_vines, ['hanging_bark_vines'] = theme.hanging_bark_vines, ['default:dirt'] = topsoil, ['heart'] = nodeName_heart } if minetest.global_exists("schemlib") then -- Use schemlib instead minetest.place_schematic(), to avoid bugs in place_schematic() local filename = minetest.get_modpath(SkyTrees.MODNAME) .. DIR_DELIM .. schematicInfo.filename local plan_obj = schemlib.plan.new() plan_obj:read_from_schem_file(filename, replacements) plan_obj.data.ground_y = -1 -- prevent read_from_schem_file() from automatically adjusting the height when it encounters dirt in the schematic (SkyTrees sometimes have dirt up in their nooks) plan_obj.data.facedir = round(rotation / 90) rotatedCenter = plan_obj:get_world_pos(vector.add(vector.multiply(schematicInfo.center, -1), -1), position) -- this function performs the rotation I require, even if it's named/intended for something else. plan_obj.data.anchor_pos = rotatedCenter if DEBUG_SKYTREES then minetest.log("info", "building tree at " .. dump(position) .. "rotated to " .. dump(treePos) .. "rotatedCenter " .. dump(rotatedCenter) .. ", " .. schematicInfo.filename) end plan_obj:set_status("build") else -- fall back on minetest.place_schematic() local malleatedFilename = SkyTrees.getMalleatedFilename(schematicInfo, themeName) if DEBUG_SKYTREES then minetest.log("info", "placing tree at " .. dump(position) .. "rotated to " .. dump(treePos) .. "rotatedCenter " .. dump(rotatedCenter) .. ", " .. schematicInfo.filename) end -- Defering minetest.place_schematic() until after the lua emerge seems to reduce the likelyhood of -- having it draw the tree with pieces missing. minetest.after( 0.1, function(treePos, malleatedFilename, rotation, replacements, schematicInfo) minetest.place_schematic(treePos, malleatedFilename, rotation, replacements, true) -- minetest.place_schematic() doesn't invoke node constructors, so use set_node() for any nodes requiring construction for i, schematicCoords in pairs(schematicInfo.nodesWithConstructor) do if rotation ~= 0 then schematicCoords = rotatePositon(schematicCoords, schematicInfo.size, rotation) end local nodePos = vector.add(treePos, schematicCoords) local nodeToConstruct = minetest.get_node(nodePos) if nodeToConstruct.name == "air" or nodeToConstruct.name == nodeName_ignore then --this is now normal - e.g. if vines are set to 'ignore' then the nodeToConstruct won't be there. --minetest.log("error", "nodesWithConstructor["..i.."] does not match schematic " .. schematicInfo.filename .. " at " .. nodePos.x..","..nodePos.y..","..nodePos.z.." rotation "..rotation) else minetest.set_node(nodePos, nodeToConstruct) end end end, treePos, malleatedFilename, rotation, replacements, schematicInfo ) end end end SkyTrees.init() --[[============================== Initialization and Mapgen ==============================]]-- local function init_mapgen() -- invoke get_perlin() here, since it can't be invoked before the environment -- is created because it uses the world's seed value. noise_eddyField = minetest.get_perlin(noiseparams_eddyField) noise_heightMap = minetest.get_perlin(noiseparams_heightMap) noise_density = minetest.get_perlin(noiseparams_density) noise_surfaceMap = minetest.get_perlin(noiseparams_surfaceMap) noise_skyReef = minetest.get_perlin(noiseparams_skyReef) local prng = PcgRandom(122456 + ISLANDS_SEED) for i = 0,255 do randomNumbers[i] = prng:next(0, 0x10000) / 0x10000 end if isMapgenV6 then biomes["Normal"] = {node_top="mapgen_dirt_with_grass", node_filler="mapgen_dirt", node_stone="mapgen_stone"} biomes["Desert"] = {node_top="mapgen_desert_sand", node_filler="mapgen_desert_sand", node_stone="mapgen_desert_stone"} biomes["Jungle"] = {node_top="mapgen_dirt_with_grass", node_filler="mapgen_dirt", node_stone="mapgen_stone"} biomes["Tundra"] = {node_top="mapgen_dirt_with_snow", node_filler="mapgen_dirt", node_stone="mapgen_stone"} biomes["Taiga"] = {node_top="mapgen_dirt_with_snow", node_filler="mapgen_dirt", node_stone="mapgen_stone"} else for k,v in pairs(minetest.registered_biomes) do biomes[minetest.get_biome_id(k)] = v end end if DEBUG then minetest.log("info", "registered biomes: " .. dump(biomes)) end nodeId_air = minetest.get_content_id("air") nodeId_stone = interop.find_node_id(NODENAMES_STONE) nodeId_grass = interop.find_node_id(NODENAMES_GRASS) nodeId_dirt = interop.find_node_id(NODENAMES_DIRT) nodeId_water = interop.find_node_id(NODENAMES_WATER) nodeId_ice = interop.find_node_id(NODENAMES_ICE) nodeId_silt = interop.find_node_id(NODENAMES_SILT) nodeId_gravel = interop.find_node_id(NODENAMES_GRAVEL) nodeId_vine = interop.find_node_id(NODENAMES_VINES) nodeName_vine = minetest.get_name_from_content_id(nodeId_vine) local regionRectStr = minetest.settings:get(MODNAME .. "_limit_rect") if type(regionRectStr) == "string" then local minXStr, minZStr, maxXStr, maxZStr = string.match(regionRectStr, '(-?[%d%.]+)[,%s]+(-?[%d%.]+)[,%s]+(-?[%d%.]+)[,%s]+(-?[%d%.]+)') if minXStr ~= nil then local minX, minZ, maxX, maxZ = tonumber(minXStr), tonumber(minZStr), tonumber(maxXStr), tonumber(maxZStr) if minX ~= nil and maxX ~= nil and minX < maxX then region_min_x, region_max_x = minX, maxX end if minZ ~= nil and maxZ ~= nil and minZ < maxZ then region_min_z, region_max_z = minZ, maxZ end end end local limitToBiomesStr = minetest.settings:get(MODNAME .. "_limit_biome") if type(limitToBiomesStr) == "string" and string.len(limitToBiomesStr) > 0 then limit_to_biomes = limitToBiomesStr:lower() end limit_to_biomes_altitude = tonumber(minetest.settings:get(MODNAME .. "_limit_biome_altitude")) region_restrictions = region_min_x > -32000 or region_min_z > -32000 or region_max_x < 32000 or region_max_z < 32000 or limit_to_biomes ~= nil end -- Updates coreList to include all cores of type coreType within the given bounds local function addCores(coreList, coreType, x1, z1, x2, z2) -- this function is used by the API functions, so may be invoked without our on_generated -- being called cloudlands.init() for z = math_floor(z1 / coreType.territorySize), math_floor(z2 / coreType.territorySize) do for x = math_floor(x1 / coreType.territorySize), math_floor(x2 / coreType.territorySize) do -- Use a known PRNG implementation, to make life easier for Amidstest local prng = PcgRandom( x * 8973896 + z * 7467838 + worldSeed + 8438 + ISLANDS_SEED ) local coresInTerritory = {} for i = 1, coreType.coresPerTerritory do local coreX = x * coreType.territorySize + prng:next(0, coreType.territorySize - 1) local coreZ = z * coreType.territorySize + prng:next(0, coreType.territorySize - 1) -- there's strong vertical and horizontal tendency in 2-octave noise, -- so rotate it a little to avoid it lining up with the world axis. local noiseX = ROTATE_COS * coreX - ROTATE_SIN * coreZ local noiseZ = ROTATE_SIN * coreX + ROTATE_COS * coreZ local eddyField = noise_eddyField:get2d({x = noiseX, y = noiseZ}) if (math_abs(eddyField) < coreType.frequency) then local nexusConditionMet = not coreType.requiresNexus if not nexusConditionMet then -- A 'nexus' is a made up name for a place where the eddyField is flat. -- There are often many 'field lines' leading out from a nexus. -- Like a saddle in the perlin noise the height "coreType.frequency" local eddyField_orthA = noise_eddyField:get2d({x = noiseX + 2, y = noiseZ}) local eddyField_orthB = noise_eddyField:get2d({x = noiseX, y = noiseZ + 2}) if math_abs(eddyField - eddyField_orthA) + math_abs(eddyField - eddyField_orthB) < 0.02 then nexusConditionMet = true end end if nexusConditionMet then local radius = (coreType.radiusMax + prng:next(0, coreType.radiusMax) * 2) / 3 -- give a 33%/66% weighting split between max-radius and random local depth = (coreType.depthMax + prng:next(0, coreType.depthMax) * 2) / 2 -- ERROR!! fix this bug! should be dividing by 3. But should not change worldgen now, so adjust depthMax of islands so nothing changes when bug is fixed? local thickness = prng:next(0, coreType.thicknessMax) if coreX >= x1 and coreX < x2 and coreZ >= z1 and coreZ < z2 then local spaceConditionMet = not coreType.exclusive if not spaceConditionMet then -- see if any other cores occupy this space, and if so then -- either deny the core, or raise it spaceConditionMet = true local minDistSquared = radius * radius * .7 for _,core in ipairs(coreList) do if core.type.radiusMax == coreType.radiusMax then -- We've reached the cores of the current type. We can't exclude based on all -- cores of the same type as we can't be sure neighboring territories will have been generated. break end if (core.x - coreX)*(core.x - coreX) + (core.z - coreZ)*(core.z - coreZ) <= minDistSquared + core.radius * core.radius then spaceConditionMet = false break end end if spaceConditionMet then for _,core in ipairs(coresInTerritory) do -- We can assume all cores of the current type are being generated in this territory, -- so we can exclude the core if it overlaps one already in this territory. if (core.x - coreX)*(core.x - coreX) + (core.z - coreZ)*(core.z - coreZ) <= minDistSquared + core.radius * core.radius then spaceConditionMet = false break end end end end if spaceConditionMet then -- all conditions met, we've located a new island core --minetest.log("Adding core "..x..","..y..","..z..","..radius) local y = round(noise_heightMap:get2d({x = coreX, y = coreZ})) local newCore = { x = coreX, y = y, z = coreZ, radius = radius, thickness = thickness, depth = depth, type = coreType, } coreList[#coreList + 1] = newCore coresInTerritory[#coreList + 1] = newCore end else -- We didn't test coreX,coreZ against x1,z1,x2,z2 immediately and save all -- that extra work, as that would break the determinism of the prng calls. -- i.e. if the area was approached from a different direction then a -- territory might end up with a different list of cores. -- TODO: filter earlier but advance prng? end end end end end end end -- removes any islands that fall outside region restrictions specified in the options local function removeUnwantedIslands(coreList) local testBiome = limit_to_biomes ~= nil local get_biome_name = nil if testBiome then -- minetest.get_biome_name() was added in March 2018, we'll ignore the -- limit_to_biomes option on versions of Minetest that predate this get_biome_name = minetest.get_biome_name testBiome = get_biome_name ~= nil if get_biome_name == nil then minetest.log("warning", MODNAME .. " ignoring " .. MODNAME .. "_limit_biome option as Minetest API version too early to support get_biome_name()") limit_to_biomes = nil end end for i = #coreList, 1, -1 do local core = coreList[i] local coreX = core.x local coreZ = core.z if coreX < region_min_x or coreX > region_max_x or coreZ < region_min_z or coreZ > region_max_z then table.remove(coreList, i) elseif testBiome then local biomeAltitude if (limit_to_biomes_altitude == nil) then biomeAltitude = ALTITUDE + core.y else biomeAltitude = limit_to_biomes_altitude end local biomeName = get_biome_name(minetest.get_biome_data({x = coreX, y = biomeAltitude, z = coreZ}).biome) if not string.match(limit_to_biomes, biomeName:lower()) then table.remove(coreList, i) end end end end -- gets an array of all cores which may intersect the (minp, maxp) area -- y is ignored cloudlands.get_island_details = function(minp, maxp) local result = {} for _,coreType in pairs(cloudlands.coreTypes) do addCores( result, coreType, minp.x - coreType.radiusMax, minp.z - coreType.radiusMax, maxp.x + coreType.radiusMax, maxp.z + coreType.radiusMax ) end -- remove islands only after cores have all generated to avoid the restriction -- settings from rearranging islands. if region_restrictions then removeUnwantedIslands(result) end return result end cloudlands.find_nearest_island = function(x, z, search_radius) local coreList = {} for _,coreType in pairs(cloudlands.coreTypes) do addCores( coreList, coreType, x - (search_radius + coreType.radiusMax), z - (search_radius + coreType.radiusMax), x + (search_radius + coreType.radiusMax), z + (search_radius + coreType.radiusMax) ) end -- remove islands only after cores have all generated to avoid the restriction -- settings from rearranging islands. if region_restrictions then removeUnwantedIslands(coreList) end local result = nil for _,core in ipairs(coreList) do local distance = math.hypot(core.x - x, core.z - z) if distance >= core.radius then core.distance = 1 + distance - core.radius else -- distance is fractional core.distance = distance / (core.radius + 1) end if result == nil or core.distance < result.distance then result = core end end return result end -- coreList can be left as null, but if you wish to sample many heights in a small area -- then use cloudlands.get_island_details() to get the coreList for that area and save -- having to recalculate it during each call to get_height_at(). cloudlands.get_height_at = function(x, z, coreList) local result, isWater = nil, false if coreList == nil then local pos = {x = x, z = z} coreList = cloudlands.get_island_details(pos, pos) end for _,core in ipairs(coreList) do -- duplicates the code from renderCores() to find surface height -- See the renderCores() version for explanatory comments local horz_easing local distanceSquared = (x - core.x)*(x - core.x) + (z - core.z)*(z - core.z) local radiusSquared = core.radius * core.radius local noise_weighting = 1 local shapeType = math_floor(core.depth + core.radius + core.x) % 5 if shapeType < 2 then -- convex, see renderCores() implementatin for comments horz_easing = 1 - distanceSquared / radiusSquared elseif shapeType == 2 then -- conical, see renderCores() implementatin for comments horz_easing = 1 - math_sqrt(distanceSquared) / core.radius else -- concave, see renderCores() implementatin for comments local radiusRoot = math_sqrt(core.radius) local squared = 1 - distanceSquared / radiusSquared local distance = math_sqrt(distanceSquared) local distance_normalized = distance / core.radius local root = 1 - math_sqrt(distance) / radiusRoot horz_easing = math_min(1, 0.8*distance_normalized*squared + 1.2*(1-distance_normalized)*root) noise_weighting = 0.63 end if core.radius + core.depth > 80 then noise_weighting = 0.6 end if core.radius + core.depth > 120 then noise_weighting = 0.35 end local surfaceNoise = noise_surfaceMap:get2d({x = x, y = z}) if DEBUG_GEOMETRIC then surfaceNoise = SURFACEMAP_OFFSET end local coreTop = ALTITUDE + core.y local surfaceHeight = coreTop + round(surfaceNoise * 3 * (core.thickness + 1) * horz_easing) if result == nil or math_max(coreTop, surfaceHeight) > result then local coreBottom = math_floor(coreTop - (core.thickness + core.depth)) local yBottom = coreBottom if result ~= nil then yBottom = math_max(yBottom, result + 1) end for y = math_max(coreTop, surfaceHeight), yBottom, -1 do local vert_easing = math_min(1, (y - coreBottom) / core.depth) local densityNoise = noise_density:get3d({x = x, y = y - coreTop, z = z}) densityNoise = noise_weighting * densityNoise + (1 - noise_weighting) * DENSITY_OFFSET if DEBUG_GEOMETRIC then densityNoise = DENSITY_OFFSET end if densityNoise * ((horz_easing + vert_easing) / 2) >= REQUIRED_DENSITY then result = y isWater = surfaceNoise < 0 break --[[abandoned because do we need to calc the bottom of ponds? It also needs the outer code refactored to work if not isWater then -- we've found the land height break else -- find the pond bottom, since the water level is already given by (ALTITUDE + island.y) local surfaceDensity = densityNoise * ((horz_easing + 1) / 2) local onTheEdge = math_sqrt(distanceSquared) + 1 >= core.radius if onTheEdge or surfaceDensity > (REQUIRED_DENSITY + core.type.pondWallBuffer) then break end end]] end end end end return result, isWater end local function setCoreBiomeData(core) local pos = {x = core.x, y = ALTITUDE + core.y, z = core.z} if LOWLAND_BIOMES then pos.y = LOWLAND_BIOME_ALTITUDE end core.biomeId = interop.get_biome_key(pos) core.biome = biomes[core.biomeId] core.temperature = get_heat(pos) core.humidity = get_humidity(pos) if core.temperature == nil then core.temperature = 50 end if core.humidity == nil then core.humidity = 50 end if core.biome == nil then -- Some games don't use the biome list, so come up with some fallbacks core.biome = {} core.biome.node_top = minetest.get_name_from_content_id(nodeId_grass) core.biome.node_filler = minetest.get_name_from_content_id(nodeId_dirt) end end local function addDetail_vines(decoration_list, core, data, area, minp, maxp) if VINE_COVERAGE > 0 and nodeId_vine ~= nodeId_ignore then local y = ALTITUDE + core.y if y >= minp.y and y <= maxp.y then -- if core.biome is nil then renderCores() never rendered it, which means it -- doesn't instersect this draw region. if core.biome ~= nil and core.humidity >= VINES_REQUIRED_HUMIDITY and core.temperature >= VINES_REQUIRED_TEMPERATURE then local nodeId_top local nodeId_filler local nodeId_stoneBase local nodeId_dust if core.biome.node_top == nil then nodeId_top = nodeId_stone else nodeId_top = minetest.get_content_id(core.biome.node_top) end if core.biome.node_filler == nil then nodeId_filler = nodeId_stone else nodeId_filler = minetest.get_content_id(core.biome.node_filler) end if core.biome.node_stone == nil then nodeId_stoneBase = nodeId_stone else nodeId_stoneBase = minetest.get_content_id(core.biome.node_stone) end if core.biome.node_dust == nil then nodeId_dust = nodeId_stone else nodeId_dust = minetest.get_content_id(core.biome.node_dust) end local function isIsland(nodeId) return (nodeId == nodeId_filler or nodeId == nodeId_top or nodeId == nodeId_stoneBase or nodeId == nodeId_dust or nodeId == nodeId_silt or nodeId == nodeId_water) end local function findHighestNodeFace(y, solidIndex, emptyIndex) -- return the highest y value (or maxp.y) where solidIndex is part of an island -- and emptyIndex is not local yOffset = 1 while y + yOffset <= maxp.y and isIsland(data[solidIndex + yOffset * area.ystride]) and not isIsland(data[emptyIndex + yOffset * area.ystride]) do yOffset = yOffset + 1 end return y + yOffset - 1 end local radius = round(core.radius) local xCropped = math_min(maxp.x, math_max(minp.x, core.x)) local zStart = math_max(minp.z, core.z - radius) local vi = area:index(xCropped, y, zStart) for z = 0, math_min(maxp.z, core.z + radius) - zStart do local searchIndex = vi + z * area.zstride if isIsland(data[searchIndex]) then -- add vines to east face if randomNumbers[(zStart + z + y) % 256] <= VINE_COVERAGE then for x = xCropped + 1, maxp.x do if not isIsland(data[searchIndex + 1]) then local yhighest = findHighestNodeFace(y, searchIndex, searchIndex + 1) decoration_list[#decoration_list + 1] = {pos={x=x, y=yhighest, z= zStart + z}, node={name = nodeName_vine, param2 = 3}} break end searchIndex = searchIndex + 1 end end -- add vines to west face if randomNumbers[(zStart + z + y + 128) % 256] <= VINE_COVERAGE then searchIndex = vi + z * area.zstride for x = xCropped - 1, minp.x, -1 do if not isIsland(data[searchIndex - 1]) then local yhighest = findHighestNodeFace(y, searchIndex, searchIndex - 1) decoration_list[#decoration_list + 1] = {pos={x=x, y=yhighest, z= zStart + z}, node={name = nodeName_vine, param2 = 2}} break end searchIndex = searchIndex - 1 end end end end local zCropped = math_min(maxp.z, math_max(minp.z, core.z)) local xStart = math_max(minp.x, core.x - radius) local zstride = area.zstride vi = area:index(xStart, y, zCropped) for x = 0, math_min(maxp.x, core.x + radius) - xStart do local searchIndex = vi + x if isIsland(data[searchIndex]) then -- add vines to north face (make it like moss - grows better on the north side) if randomNumbers[(xStart + x + y) % 256] <= (VINE_COVERAGE * 1.2) then for z = zCropped + 1, maxp.z do if not isIsland(data[searchIndex + zstride]) then local yhighest = findHighestNodeFace(y, searchIndex, searchIndex + zstride) decoration_list[#decoration_list + 1] = {pos={x=xStart + x, y=yhighest, z=z}, node={name = nodeName_vine, param2 = 5}} break end searchIndex = searchIndex + zstride end end -- add vines to south face (make it like moss - grows better on the north side) if randomNumbers[(xStart + x + y + 128) % 256] <= (VINE_COVERAGE * 0.8) then searchIndex = vi + x for z = zCropped - 1, minp.z, -1 do if not isIsland(data[searchIndex - zstride]) then local yhighest = findHighestNodeFace(y, searchIndex, searchIndex - zstride) decoration_list[#decoration_list + 1] = {pos={x=xStart + x, y=yhighest, z=z}, node={name = nodeName_vine, param2 = 4}} break end searchIndex = searchIndex - zstride end end end end end end end end -- A rare formation of rocks circling or crowning an island -- returns true if voxels were changed local function addDetail_skyReef(decoration_list, core, data, area, minp, maxp) local coreTop = ALTITUDE + core.y local overdrawTop = maxp.y + OVERDRAW local reefAltitude = math_floor(coreTop - 1 - core.thickness / 2) local reefMaxHeight = 12 local reefMaxUnderhang = 4 if (maxp.y < reefAltitude - reefMaxUnderhang) or (minp.y > reefAltitude + reefMaxHeight) then --no reef here return false end local isReef = core.radius < core.type.radiusMax * 0.4 -- a reef can't extend beyond radiusMax, so needs a small island local isAtoll = core.radius > core.type.radiusMax * 0.8 if not (isReef or isAtoll) then return false end local fastHash = 3 fastHash = (37 * fastHash) + core.x fastHash = (37 * fastHash) + core.z fastHash = (37 * fastHash) + math_floor(core.radius) fastHash = (37 * fastHash) + math_floor(core.depth) if ISLANDS_SEED ~= 1000 then fastHash = (37 * fastHash) + ISLANDS_SEED end local rarityAdj = 1 if core.type.requiresNexus and isAtoll then rarityAdj = 4 end -- humongous islands are very rare, and look good as an atoll if (REEF_RARITY * rarityAdj * 1000) < math_floor((math_abs(fastHash)) % 1000) then return false end local coreX = core.x --save doing a table lookup in the loop local coreZ = core.z --save doing a table lookup in the loop -- Use a known PRNG implementation local prng = PcgRandom( coreX * 8973896 + coreZ * 7467838 + worldSeed + 32564 ) local reefUnderhang local reefOuterRadius = math_floor(core.type.radiusMax) local reefInnerRadius = prng:next(core.type.radiusMax * 0.5, core.type.radiusMax * 0.7) local reefWidth = reefOuterRadius - reefInnerRadius local noiseOffset = 0 if isReef then reefMaxHeight = round((core.thickness + 4) / 2) reefUnderhang = round(reefMaxHeight / 2) noiseOffset = -0.1 end if isAtoll then -- a crown attached to the island reefOuterRadius = math_floor(core.radius * 0.8) reefWidth = math_max(4, math_floor(core.radius * 0.15)) reefInnerRadius = reefOuterRadius - reefWidth reefUnderhang = 0 if maxp.y < reefAltitude - reefUnderhang then return end -- no atoll here end local reefHalfWidth = reefWidth / 2 local reefMiddleRadius = (reefInnerRadius + reefOuterRadius) / 2 local reefOuterRadiusSquared = reefOuterRadius * reefOuterRadius local reefInnerRadiusSquared = reefInnerRadius * reefInnerRadius local reefMiddleRadiusSquared = reefMiddleRadius * reefMiddleRadius local reefHalfWidthSquared = reefHalfWidth * reefHalfWidth -- get the biome details for this core local nodeId_first local nodeId_second local nodeId_top local nodeId_filler if core.biome == nil then setCoreBiomeData(core) end -- We can't assume the core biome has already been resolved, core might not have been big enough to enter the draw region if core.biome.node_top == nil then nodeId_top = nodeId_stone else nodeId_top = minetest.get_content_id(core.biome.node_top) end if core.biome.node_filler == nil then nodeId_filler = nodeId_stone else nodeId_filler = minetest.get_content_id(core.biome.node_filler) end if core.biome.node_dust ~= nil then nodeId_first = minetest.get_content_id(core.biome.node_dust) nodeId_second = nodeId_top else nodeId_first = nodeId_top nodeId_second = nodeId_filler end local zStart = round(math_max(core.z - reefOuterRadius, minp.z)) local zStop = round(math_min(core.z + reefOuterRadius, maxp.z)) local xStart = round(math_max(core.x - reefOuterRadius, minp.x)) local xStop = round(math_min(core.x + reefOuterRadius, maxp.x)) local yCenter = math_min(math_max(reefAltitude, minp.y), maxp.y) local pos = {} local dataBufferIndex = area:index(xStart, yCenter, zStart) local vi = -1 for z = zStart, zStop do local zDistSquared = (z - coreZ) * (z - coreZ) pos.y = z for x = xStart, xStop do local distanceSquared = (x - coreX) * (x - coreX) + zDistSquared if distanceSquared < reefOuterRadiusSquared and distanceSquared > reefInnerRadiusSquared then pos.x = x local offsetEase = math_abs(distanceSquared - reefMiddleRadiusSquared) / reefHalfWidthSquared local fineNoise = noise_skyReef:get2d(pos) local reefNoise = (noiseOffset* offsetEase) + fineNoise + 0.2 * noise_surfaceMap:get2d(pos) if (reefNoise > 0) then local distance = math_sqrt(distanceSquared) local ease = 1 - math_abs(distance - reefMiddleRadius) / reefHalfWidth local yStart = math_max(math_floor(reefAltitude - ease * fineNoise * reefUnderhang), minp.y) local yStop = math_min(math_floor(reefAltitude + ease * reefNoise * reefMaxHeight), overdrawTop) for y = yStart, yStop do vi = dataBufferIndex + (y - yCenter) * area.ystride if data[vi] == nodeId_air then if y == yStop then data[vi] = nodeId_first elseif y == yStop - 1 then data[vi] = nodeId_second else data[vi] = nodeId_filler end end end end end dataBufferIndex = dataBufferIndex + 1 end dataBufferIndex = dataBufferIndex + area.zstride - (xStop - xStart + 1) end return vi >= 0 end -- A rarely occuring giant tree growing from the center of the island -- returns true if tree was added local function addDetail_skyTree(decoration_list, core, minp, maxp) if (core.radius < SkyTrees.minimumIslandRadius) or (core.depth < SkyTrees.minimumIslandDepth) then --no tree here return false end local coreTop = ALTITUDE + core.y local treeAltitude = math_floor(coreTop + core.thickness) if (maxp.y < treeAltitude - SkyTrees.maximumYOffset) or (minp.y > treeAltitude + SkyTrees.maximumHeight) then --no tree here return false elseif SkyTrees.disabled ~= nil then -- can't find nodes/textures in this game that are needed to build trees return false end local coreX = core.x --save doing a table lookups local coreZ = core.z --save doing a table lookups local fastHash = 3 fastHash = (37 * fastHash) + coreX fastHash = (37 * fastHash) + coreZ fastHash = (37 * fastHash) + math_floor(core.radius) fastHash = (37 * fastHash) + math_floor(core.depth) fastHash = (37 * fastHash) + ISLANDS_SEED fastHash = (37 * fastHash) + 76276 -- to keep this probability distinct from reefs and atols if (TREE_RARITY * 1000) < math_floor((math_abs(fastHash)) % 1000) then return false end -- choose a tree that will fit on the island local tree local skipLargeTree = (fastHash % 10) < 3 -- to allow small trees a chance to spawn on large islands if skipLargeTree then if SkyTrees.isDead({x = coreX, y = treeAltitude, z = coreZ}) then -- small tree currently doesn't have a dead theme, so don't skip the large tree skipLargeTree = false end end for i, treeType in pairs(SkyTrees.schematicInfo) do if i == 1 and skipLargeTree then -- 'continue', to allow small trees a chance to spawn on large islands elseif (core.radius >= treeType.requiredIslandRadius) and (core.depth >= treeType.requiredIslandDepth) then tree = treeType break end end local maxOffsetFromCenter = core.radius - (tree.requiredIslandRadius - 4) -- 4 is an arbitrary number, to allow trees to get closer to the edge -- Use a known PRNG implementation local prng = PcgRandom( coreX * 8973896 + coreZ * 7467838 + worldSeed + 43786 ) local treeAngle = 90 * prng:next(0, 3) local treePos = { x = coreX + math_floor((prng:next(-maxOffsetFromCenter, maxOffsetFromCenter) + prng:next(-maxOffsetFromCenter, maxOffsetFromCenter)) / 2), y = treeAltitude, z = coreZ + math_floor((prng:next(-maxOffsetFromCenter, maxOffsetFromCenter) + prng:next(-maxOffsetFromCenter, maxOffsetFromCenter)) / 2) } if minetest.global_exists("schemlib") then -- This check is skipped when not using schemlib, because while redrawing the tree multiple times - every time a chunk it -- touches gets emitted - might be slower, it helps work around the bugs in minetest.place_schematic() where large schematics -- are spawned incompletely. -- The bug in question: https://forum.minetest.net/viewtopic.php?f=6&t=22136 -- (it isn't an issue if schemlib is used) if (maxp.y < treePos.y) or (minp.y > treePos.y) or (maxp.x < treePos.x) or (minp.x > treePos.x) or (maxp.z < treePos.z) or (minp.z > treePos.z) then -- Now that we know the exact position of the tree, we know it's spawn point is not in this chunk. -- In the interests of only drawing trees once, we only invoke placeTree when the chunk containing treePos is emitted. return false end end if tree.theme["Dead"] == nil then if SkyTrees.isDead(treePos) then -- Trees in this location should be dead, but this tree doesn't have a dead theme, so don't put a tree here return false end end if core.biome == nil then setCoreBiomeData(core) end -- We shouldn't assume the core biome has already been resolved, it might be below the emerged chunk and unrendered if core.biome.node_top == nil then -- solid stone isn't fertile enough for giant trees, and there's a solid stone biome in MT-Game: tundra_highland return false end if DEBUG_SKYTREES then minetest.log("info", "core x: "..coreX.." y: ".. coreZ .. " treePos: " .. treePos.x .. ", y: " .. treePos.y) end SkyTrees.placeTree(treePos, treeAngle, tree, nil, core.biome.node_top) return true end ------------------------------------------------------------------------------ -- Secrets section ------------------------------------------------------------------------------ -- We might not need this stand-in cobweb, but unless we go overboard on listing many -- optional dependencies we won't know whether there's a proper cobweb available to -- use until after it's too late to register this one. local nodeName_standinCobweb = MODNAME .. ":cobweb" minetest.register_node( nodeName_standinCobweb, { tiles = { -- [Ab]Use the crack texture to avoid needing to include a cobweb texture -- crack_anylength.png is required by the engine, so all games will have it. "crack_anylength.png^[verticalframe:5:4^[brighten" }, description = S("Cobweb"), groups = {snappy = 3, liquid = 3, flammable = 3, not_in_creative_inventory = 1}, drawtype = "plantlike", walkable = false, liquid_viscosity = 8, liquidtype = "source", liquid_alternative_flowing = nodeName_standinCobweb, liquid_alternative_source = nodeName_standinCobweb, liquid_renewable = false, liquid_range = 0, sunlight_propagates = true, paramtype = "light" } ) local nodeName_egg = "secret:fossilized_egg" local eggTextureBaseName = interop.find_node_texture({"default:jungleleaves", "mcl_core:jungleleaves", "ethereal:frost_leaves", "main:leaves"}) -- [Ab]Use a leaf texture. Originally this was to avoid needing to include an egg texture (extra files) and -- exposing that the mod contains secrets, however both those reasons are obsolete and the mod could have textures -- added in future local eggTextureName = eggTextureBaseName.."^[colorize:#280040E0^[noalpha" -- Since "secret:fossilized_egg" doesn't use this mod's name for the prefix, we can't assume -- another mod isn't also using/providing it if minetest.registered_nodes[nodeName_egg] == nil then local fossilSounds = nil local nodeName_stone = interop.find_node_name(NODENAMES_STONE) if nodeName_stone ~= nodeName_ignore then fossilSounds = minetest.registered_nodes[nodeName_stone].sounds end minetest.register_node( ":"..nodeName_egg, { tiles = { eggTextureName }, description = S("Fossilized Egg"), groups = { oddly_breakable_by_hand = 3, -- MTG handy = 1, -- MCL stone = 1, -- Crafter needs to know the material in order to be breakable by hand not_in_creative_inventory = 1 }, _mcl_hardness = 0.4, sounds = fossilSounds, drawtype = "nodebox", paramtype = "light", node_box = { type = "fixed", fixed = { {-0.066666, -0.5, -0.066666, 0.066666, 0.5, 0.066666}, -- column1 {-0.133333, -0.476667, -0.133333, 0.133333, 0.42, 0.133333}, -- column2 {-0.2, -0.435, -0.2, 0.2, 0.31, 0.2 }, -- column3 {-0.2, -0.36, -0.28, 0.2, 0.16667, 0.28 }, -- side1 {-0.28, -0.36, -0.2, 0.28, 0.16667, 0.2 } -- side2 } } } ) end -- Allow the player to craft their egg into an egg in a display case local nodeName_eggDisplay = nodeName_egg .. "_display" local nodeName_frameGlass = interop.find_node_name(NODENAMES_FRAMEGLASS) local woodTexture = interop.find_node_texture(NODENAMES_WOOD) local frameTexture = nil if woodTexture ~= nil then -- perhaps it's time for cloudlands to contain textures. frameTexture = "([combine:16x16:0,0="..woodTexture.."\\^[colorize\\:black\\:170:1,1="..woodTexture.."\\^[colorize\\:#0F0\\:255\\^[resize\\:14x14^[makealpha:0,255,0)" --frameTexture = "([combine:16x16:0,0="..woodTexture.."\\^[resize\\:16x16\\^[colorize\\:black\\:170:1,1="..woodTexture.."\\^[colorize\\:#0F0\\:255\\^[resize\\:14x14^[makealpha:0,255,0)" --frameTexture = "(("..woodTexture.."^[colorize:black:170)^([combine:16x16:1,1="..woodTexture.."\\^[resize\\:14x14\\^[colorize\\:#0F0\\:255))" end -- Since "secret:fossilized_egg_display" doesn't use this mod's name as the prefix, we shouldn't -- assume another mod isn't also using/providing it. if frameTexture ~= nil and nodeName_frameGlass ~= nodeName_ignore and minetest.registered_nodes[nodeName_eggDisplay] == nil then minetest.register_node( ":"..nodeName_eggDisplay, { tiles = { eggTextureName .. "^" .. frameTexture }, description = S("Fossil Display"), groups = { oddly_breakable_by_hand = 3, glass = 1, -- Crafter needs to know the material in order to be breakable by hand not_in_creative_inventory = 1}, _mcl_hardness = 0.2, drop = "", sounds = minetest.registered_nodes[nodeName_frameGlass].sounds, drawtype = "nodebox", paramtype = "light", node_box = { type = "fixed", fixed = { {-0.066666, -0.5, -0.066666, 0.066666, 0.4375, 0.066666}, -- column1 {-0.133333, -0.5, -0.133333, 0.133333, 0.375, 0.133333}, -- column2 {-0.2, -0.4375, -0.2, 0.2, 0.285, 0.2 }, -- column3 {-0.2, -0.36, -0.28, 0.2, 0.14, 0.28 }, -- side1 {-0.28, -0.36, -0.2, 0.28, 0.14, 0.2 }, -- side2 -- corner frame (courtesy of NodeBox Editor Abuse mod) {-0.4375, 0.4375, 0.4375, 0.4375, 0.5, 0.5}, {-0.4375, -0.5, 0.4375, 0.4375, -0.4375, 0.5}, {-0.5, -0.5, 0.4375, -0.4375, 0.5, 0.5}, {0.4375, -0.5, 0.4375, 0.5, 0.5, 0.5}, {-0.5, 0.4375, -0.4375, -0.4375, 0.5, 0.4375}, {-0.5, -0.5, -0.4375, -0.4375, -0.4375, 0.4375}, {0.4375, 0.4375, -0.4375, 0.5, 0.5, 0.4375}, {0.4375, -0.5, -0.4375, 0.5, -0.4375, 0.4375}, {-0.5, 0.4375, -0.5, 0.5, 0.5, -0.4375}, {-0.5, -0.5, -0.5, 0.5, -0.4375, -0.4375}, {0.4375, -0.4375, -0.5, 0.5, 0.4375, -0.4375}, {-0.5, -0.4375, -0.5, -0.4375, 0.4375, -0.4375} } }, after_destruct = function(pos,node) minetest.set_node(pos, {name = nodeName_egg, param2 = node.param2}) end, } ) if minetest.get_modpath("xpanes") ~= nil then minetest.register_craft({ output = nodeName_eggDisplay, recipe = { {"group:stick", "group:pane", "group:stick"}, {"group:pane", nodeName_egg, "group:pane"}, {"group:stick", "group:pane", "group:stick"} } }) else -- Game doesn't have glass panes, so just use glass minetest.register_craft({ output = nodeName_eggDisplay, recipe = { {"group:stick", nodeName_frameGlass, "group:stick"}, {nodeName_frameGlass, nodeName_egg, nodeName_frameGlass}, {"group:stick", nodeName_frameGlass, "group:stick"} } }) end end local nodeId_egg = minetest.get_content_id(nodeName_egg) local nodeId_airStandIn = minetest.get_content_id(interop.register_clone("air")) -- defer assigning the following until all mods are loaded local nodeId_bed_top local nodeId_bed_bottom local nodeId_torch local nodeId_chest local nodeId_bookshelf local nodeId_junk local nodeId_anvil local nodeId_workbench local nodeId_cobweb local nodeName_bookshelf local isMineCloneBookshelf local function addDetail_secrets(decoration_list, core, data, area, minp, maxp) -- if core.biome is nil then renderCores() never rendered it, which means it -- doesn't instersect this draw region. if core.biome ~= nil and core.radius > 18 and core.depth > 20 and core.radius + core.depth > 60 then local territoryX = math_floor(core.x / core.type.territorySize) local territoryZ = math_floor(core.z / core.type.territorySize) local isPolarOutpost = (core.temperature <= 5) and (core.x % 3 == 0) and noise_surfaceMap:get2d({x = core.x, y = core.z - 8}) >= 0 --make sure steps aren't under a pond local isAncientBurrow = core.humidity >= 60 and core.temperature >= 50 -- only allow a checkerboard pattern of territories to help keep the secrets -- spread out, rather than bunching up too much with climate if ((territoryX + territoryZ) % 2 == 0) and (isPolarOutpost or isAncientBurrow) then local burrowRadius = 7 local burrowHeight = 5 local burrowDepth = 12 local burrowFloor = ALTITUDE + core.y - burrowDepth local radiusSquared = burrowRadius * burrowRadius local function carve(originp, destp, pattern, height, floorId, floorDistance) local direction = vector.direction(originp, destp) local vineSearchDirection = {} if direction.x > 0 then vineSearchDirection.x = -1 else vineSearchDirection.x = 1 end if direction.z > 0 then vineSearchDirection.z = -1 else vineSearchDirection.z = 1 end local vinePlacements = {} local function placeVine(vi, pos, only_place_on_nodeId) if data[vi] == nodeId_air then local faces = {} local facing local function vineCanGrowOnIt(node_id) return node_id ~= nodeId_air and node_id ~= nodeId_airStandIn and (node_id == only_place_on_nodeId or only_place_on_nodeId == nil) end if vineCanGrowOnIt(data[vi + vineSearchDirection.x]) and pos.x + vineSearchDirection.x >= minp.x and pos.x + vineSearchDirection.x <= maxp.x then if vineSearchDirection.x > 0 then facing = 2 else facing = 3 end faces[#faces + 1] = {solid_vi = vi + vineSearchDirection.x, facing = facing} end if vineCanGrowOnIt(data[vi + vineSearchDirection.z * area.zstride]) and pos.z + vineSearchDirection.z >= minp.z and pos.z + vineSearchDirection.z <= maxp.z then if vineSearchDirection.z > 0 then facing = 4 else facing = 5 end faces[#faces + 1] = {solid_vi = vi + vineSearchDirection.z * area.zstride, facing = facing} end local faceInfo = nil if #faces == 1 then faceInfo = faces[1] elseif #faces == 2 then local ratio = math.abs(direction.x) / (math.abs(direction.x) + math.abs(direction.z)) if randomNumbers[(pos.x + pos.y + pos.z) % 256] <= ratio then faceInfo = faces[1] else faceInfo = faces[2] end end if faceInfo ~= nil and (only_place_on_nodeId == nil or only_place_on_nodeId == data[faceInfo.solid_vi]) and (data[faceInfo.solid_vi] ~= nodeId_airStandIn) then -- find the highest y value (or maxp.y) where solid_vi is solid -- and vi is not local solid_vi = faceInfo.solid_vi local yOffset = 1 while (pos.y + yOffset <= maxp.y + 1) and (data[solid_vi + yOffset * area.ystride] ~= nodeId_air) and (data[vi + yOffset * area.ystride] == nodeId_air) and (only_place_on_nodeId == nil or only_place_on_nodeId == data[solid_vi + yOffset * area.ystride]) do yOffset = yOffset + 1 end -- defer final vine placement until all nodes have been carved vinePlacements[#vinePlacements + 1] = function(decoration_list) -- retest that the vine is still going in air and still attached to a solid node local solidNode = data[solid_vi + (yOffset - 1) * area.ystride] if solidNode ~= nodeId_airStandIn and solidNode ~= nodeId_air and data[vi] == nodeId_air then decoration_list[#decoration_list + 1] = {pos={x=pos.x, y=pos.y + yOffset - 1, z=pos.z}, node={name = nodeName_vine, param2 = faceInfo.facing}} end end end end end local stampedIndexes = {} local function stamp(pos, pattern, height, node_id, isAir_callback) local callbackClosures = {} local index = -1 for y = pos.y, pos.y + height - 1 do if y >= minp.y and y <= maxp.y then if index == -1 then index = area:index(pos.x, y, pos.z) else index = index + area.ystride end for _,voxel in ipairs(pattern) do local x = pos.x + voxel.x local z = pos.z + voxel.z if x >= minp.x and x <= maxp.x and z >= minp.z and z <= maxp.z then local vi = index + voxel.x + voxel.z * area.zstride if data[vi] == nodeId_air then if isAir_callback ~= nil then callbackClosures[#callbackClosures + 1] = function() isAir_callback(pos, vi, x, y, z) end end else data[vi] = node_id stampedIndexes[#stampedIndexes + 1] = vi end end end end end for _,callback in ipairs(callbackClosures) do callback() end end local function excavate(pos, add_floor, add_vines, add_cobwebs) local function onAirNode(stampPos, node_vi, node_x, node_y, node_z) if node_y > stampPos.y and node_y + 1 <= maxp.y then -- place vines above the entrance, for concealment placeVine(node_vi + area.ystride, {x=node_x, y=node_y + 1, z=node_z}) else -- place vines on the floor - perhaps explorers can climb to the burrow placeVine(node_vi, {x=node_x, y=node_y, z=node_z}, floorId) end end local onAirNodeCallback = onAirNode local fill = nodeId_airStandIn if not add_vines or nodeId_vine == nodeId_ignore then onAirNodeCallback = nil end if add_cobwebs and nodeId_cobweb ~= nodeId_ignore then fill = nodeId_cobweb end stamp(pos, pattern, height, fill, onAirNodeCallback) if add_floor and floorId ~= nil then stamp({x=pos.x, y=pos.y - 1, z=pos.z}, pattern, 1, floorId, onAirNodeCallback) end end local addVines = core.humidity >= VINES_REQUIRED_HUMIDITY and core.temperature >= VINES_REQUIRED_TEMPERATURE if floorDistance == nil then floorDistance = 0 end local distance = round(vector.distance(originp, destp)) local step = vector.divide(vector.subtract(destp, originp), distance) local pos = vector.new(originp) local newPos = vector.new(originp) excavate(originp, 0 >= floorDistance, false) for i = 1, distance do newPos.x = newPos.x + step.x if round(newPos.x) ~= pos.x then pos.x = round(newPos.x) excavate(pos, i >= floorDistance, addVines, i <= floorDistance - 1 and i >= floorDistance - 2) end newPos.y = newPos.y + step.y if round(newPos.y) ~= pos.y then pos.y = round(newPos.y) excavate(pos, i >= floorDistance, addVines, i <= floorDistance - 1 and i >= floorDistance - 2) end newPos.z = newPos.z + step.z if round(newPos.z) ~= pos.z then pos.z = round(newPos.z) excavate(pos, i >= floorDistance, addVines, i <= floorDistance - 1 and i >= floorDistance - 2) end end -- We only place vines after entire burrow entrance has been carved, to avoid placing -- vines on blocks which will later be removed. for _,vineFunction in ipairs(vinePlacements) do vineFunction(decoration_list) end -- Replace airStandIn with real air. -- This two-pass process was neccessary because the vine placing algorithm used -- the presense of air to determine if a rock was facing outside and should have a vine. -- Single-pass solutions result in vines inside the tunnel (where I'd rather overgrowth spawned) for _,stampedIndex in ipairs(stampedIndexes) do if data[stampedIndex] == nodeId_airStandIn then data[stampedIndex] = nodeId_air end end end local function placeNode(x, y, z, node_id) if (x >= minp.x and x <= maxp.x and z >= minp.z and z <= maxp.z and y >= minp.y and y <= maxp.y) then data[area:index(x, y, z)] = node_id end end local function posInBounds(pos) return pos.x >= minp.x and pos.x <= maxp.x and pos.z >= minp.z and pos.z <= maxp.z and pos.y >= minp.y and pos.y <= maxp.y end local zStart = math_max(core.z - burrowRadius, minp.z) local xStart = math_max(core.x - burrowRadius, minp.x) local xStop = math_min(core.x + burrowRadius, maxp.x) local yStart = math_max(burrowFloor, minp.y) -- dig burrow local dataBufferIndex = area:index(xStart, yStart, zStart) for z = zStart, math_min(core.z + burrowRadius, maxp.z) do for x = xStart, xStop do local distanceSquared = (x - core.x)*(x - core.x) + (z - core.z)*(z - core.z) if distanceSquared < radiusSquared then local horz_easing = 1 - distanceSquared / radiusSquared for y = math_max(minp.y, burrowFloor + math_floor(1.4 - horz_easing)), math_min(maxp.y, burrowFloor + 1 + math_min(burrowHeight - 1, math_floor(0.8 + burrowHeight * horz_easing))) do data[dataBufferIndex + (y - yStart) * area.ystride] = nodeId_air end end dataBufferIndex = dataBufferIndex + 1 end dataBufferIndex = dataBufferIndex + area.zstride - (xStop - xStart + 1) end local floorId if core.biome.node_top == nil then floorId = nil else floorId = minetest.get_content_id(core.biome.node_top) end if isAncientBurrow then -- island overlaps can only happen at territory edges when a coreType has exclusive=true, so -- angle the burrow entrance toward the center of the terrority to avoid any overlapping islands. local territoryCenter = vector.new( core.type.territorySize * math.floor(core.x / core.type.territorySize) + math.floor(0.5 + core.type.territorySize / 2), burrowFloor, core.type.territorySize * math.floor(core.z / core.type.territorySize) + math.floor(0.5 + core.type.territorySize / 2) ) local burrowStart = vector.new(core.x, burrowFloor, core.z) local direction = vector.direction(burrowStart, territoryCenter) local directionOffsetZ = 4 if direction.z < 0 then directionOffsetZ = -directionOffsetZ end burrowStart.z = burrowStart.z + directionOffsetZ -- start the burrow enterance off-center burrowStart.x = burrowStart.x + 2 -- start the burrow enterance off-center direction = vector.direction(burrowStart, territoryCenter) if vector.length(direction) == 0 then direction = vector.direction({x=0, y=0, z=0}, {x=2, y=0, z=1}) end local path = vector.add(vector.multiply(direction, core.radius), {x=0, y=-4,z=0}) local floorStartingFrom = 4 + math.floor(0.5 + core.radius * 0.3) -- carve burrow entrance local pattern = {{x=0,z=0}, {x=-1,z=0}, {x=1,z=0}, {x=0,z=-1}, {x=0,z=1}} carve(burrowStart, vector.add(burrowStart, path), pattern, 2, floorId, floorStartingFrom) -- place egg in burrow local eggX = core.x local eggZ = core.z - directionOffsetZ * 0.75 -- move the egg away from where the burrow entrance is carved placeNode(eggX, burrowFloor, eggZ, nodeId_egg) if nodeId_gravel ~= nodeId_ignore then placeNode(eggX, burrowFloor - 1, eggZ, nodeId_gravel) end if nodeId_cobweb ~= nodeId_ignore then placeNode(core.x - 6, burrowFloor + 3, core.z - 1, nodeId_cobweb) placeNode(core.x + 4, burrowFloor + 4, core.z + 3, nodeId_cobweb) placeNode(core.x + 6, burrowFloor + 1, core.z - 3, nodeId_cobweb) end else -- Only attempt this if it can contain beds and a place to store the diary. if (nodeId_bookshelf ~= nodeId_ignore or nodeId_chest ~= nodeId_ignore) and nodeId_bed_top ~= nodeId_ignore and nodeId_bed_bottom ~= nodeId_ignore then -- carve stairs to the surface local stairsStart = vector.new(core.x - 3, burrowFloor, core.z - 7) local stairsbottom = vector.add(stairsStart, {x=0,y=0,z=1}) local stairsMiddle1 = vector.add(stairsStart, {x=8,y=8,z=0}) local stairsMiddle2 = vector.add(stairsMiddle1, {x=0,y=0,z=-1}) local stairsEnd = vector.add(stairsMiddle2, {x=-20,y=20,z=0}) carve(stairsEnd, stairsMiddle2, {{x=0,z=0}}, 3, floorId, 0) carve(stairsMiddle1, stairsStart, {{x=0,z=0}}, 2, floorId, 0) local pattern = {{x=0,z=0}, {x=1,z=0}, {x=0,z=2}, {x=0,z=1}, {x=1,z=1}} carve(stairsbottom, stairsbottom, pattern, 2, floorId, 0) -- fill the outpost placeNode(core.x + 2, burrowFloor, core.z + 5, nodeId_bed_top) placeNode(core.x + 2, burrowFloor, core.z + 4, nodeId_bed_bottom) placeNode(core.x + 2, burrowFloor, core.z + 2, nodeId_bed_top) placeNode(core.x + 2, burrowFloor, core.z + 1, nodeId_bed_bottom) placeNode(core.x + 4, burrowFloor, core.z + 2, nodeId_bed_top) placeNode(core.x + 4, burrowFloor, core.z + 1, nodeId_bed_bottom) if (nodeId_torch ~= nodeId_ignore) then decoration_list[#decoration_list + 1] = { pos={x=core.x, y=burrowFloor + 2, z=core.z + 6}, node={name = minetest.get_name_from_content_id(nodeId_torch), param2 = 4} } end if nodeId_junk ~= nodeId_ignore then placeNode(core.x - 4, burrowFloor + 1, core.z + 5, nodeId_junk) end if nodeId_anvil ~= nodeId_ignore then placeNode(core.x - 6, burrowFloor + 1, core.z, nodeId_anvil) end if nodeId_workbench ~= nodeId_ignore then placeNode(core.x - 5, burrowFloor, core.z + 2, nodeId_workbench) end if nodeId_cobweb ~= nodeId_ignore then placeNode(core.x + 4, burrowFloor + 4, core.z - 3, nodeId_cobweb) end local bookshelf_pos local invBookshelf = nil local invChest = nil if nodeId_chest ~= nodeId_ignore then local pos = {x = core.x - 3, y = burrowFloor + 1, z = core.z + 6} local nodeName_chest = minetest.get_name_from_content_id(nodeId_chest) local nodeNameAtPos = minetest.get_node(pos).name -- falls back on the nodeNameAtPos:find("chest") check to avoid a race-condition where if the -- chest is opened while nearby areas are being generated, the opened chest may be replaced with -- a new empty closed one. if nodeNameAtPos ~= nodeName_chest and not nodeNameAtPos:find("chest") then minetest.set_node(pos, {name = nodeName_chest}) end if posInBounds(pos) then data[area:index(pos.x, pos.y, pos.z)] = nodeId_chest invChest = minetest.get_inventory({type = "node", pos = pos}) end end if nodeId_bookshelf ~= nodeId_ignore then local pos = {x = core.x - 2, y = burrowFloor + 1, z = core.z + 6} bookshelf_pos = pos if minetest.get_node(pos).name ~= nodeName_bookshelf then minetest.set_node(pos, {name = nodeName_bookshelf}) end if posInBounds(pos) then data[area:index(pos.x, pos.y, pos.z)] = nodeId_bookshelf if not isMineCloneBookshelf then -- mineclone bookshelves are decorational (like Minecraft) and don't contain anything invBookshelf = minetest.get_inventory({type = "node", pos = pos}) end end end if invBookshelf ~= nil or invChest ~= nil then -- create diary local groundDesc = S("rock") if core.biome.node_filler ~= nil then local earthNames = string.lower(core.biome.node_filler) .. string.lower(core.biome.node_top) if string.match(earthNames, "ice") or string.match(earthNames, "snow") or string.match(earthNames, "frozen") then groundDesc = S("ice") end end local book_itemstack = interop.write_book( S("Weddell Outpost, November 21"), -- title S("Bert Shackleton"), -- owner/author S([[The aerostat is lost. However, salvage attempts throughout the night managed to save most provisions before it finally broke apart and fell. ---====--- This island is highly exposed and the weather did not treat the tents well. We have enlarged a sheltered crag in the @1, but it is laborous work and the condition of some of the party is becoming cause for concern. Quite a journey is now required, we cannot stay - nobody will look for us here. McNish is attempting to strengthen the gliders. ---====---]], groundDesc), S("Diary of Bert Shackleton") -- description ) if book_itemstack ~= nil then if invBookshelf == nil then -- mineclone bookshelves are decorational like Minecraft, put the book in the chest instead -- (also testing for nil invBookshelf because it can happen. Weird race condition??) if invChest ~= nil then invChest:add_item("main", book_itemstack) end else -- add the book to the bookshelf and manually trigger update_bookshelf() so its -- name will reflect the new contents. invBookshelf:add_item("books", book_itemstack) local dummyPlayer = {} dummyPlayer.get_player_name = function() return "server" end dummyPlayer.is_fake_player = true dummyPlayer.is_player = function() return false end -- it's unclear whether this should return false for fake players minetest.registered_nodes[nodeName_bookshelf].on_metadata_inventory_put(bookshelf_pos, "books", 1, book_itemstack, dummyPlayer) end end end if invChest ~= nil then -- leave some junk from the expedition in the chest local stack local function addIfFound(item_aliases, amount) for _,name in ipairs(item_aliases) do if minetest.registered_items[name] ~= nil then stack = ItemStack(name .. " " .. amount) invChest:add_item("main", stack) break end end end addIfFound({"mcl_tools:pick_iron", "default:pick_steel", "main:ironpick"}, 1) addIfFound({"binoculars:binoculars"}, 1) addIfFound(NODENAMES_WOOD, 10) addIfFound({"mcl_torches:torch", "default:torch", "torch:torch"}, 3) end end end end end end local function init_secrets() nodeId_bed_top = interop.find_node_id({"beds:bed_top", "bed:bed_front"}) nodeId_bed_bottom = interop.find_node_id({"beds:bed_bottom", "bed:bed_back"}) nodeId_torch = interop.find_node_id({"mcl_torches:torch_wall", "default:torch_wall", "torch:wall"}) nodeId_chest = interop.find_node_id({"chest", "mcl_chests:chest", "default:chest", "utility:chest"}) nodeId_junk = interop.find_node_id({"xdecor:barrel", "cottages:barrel", "homedecor:copper_pans", "vessels:steel_bottle", "mcl_flowerpots:flower_pot"}) nodeId_anvil = interop.find_node_id({"castle:anvil", "cottages:anvil", "mcl_anvils:anvil", "default:anvil", "main:anvil" }) -- "default:anvil" and "main:anvil" aren't a thing, but perhaps one day. nodeId_workbench = interop.find_node_id({"homedecor:table", "xdecor:workbench", "mcl_crafting_table:crafting_table", "default:table", "random_buildings:bench", "craftingtable:craftingtable"}) -- "default:table" isn't a thing, but perhaps one day. nodeId_cobweb = interop.find_node_id({"mcl_core:cobweb", "xdecor:cobweb", "homedecor:cobweb_plantlike", "default:cobweb", "main:cobweb"}) local mineCloneBookshelfName = "mcl_books:bookshelf" nodeId_bookshelf = interop.find_node_id({mineCloneBookshelfName, "default:bookshelf"}) nodeName_bookshelf = minetest.get_name_from_content_id(nodeId_bookshelf) isMineCloneBookshelf = nodeName_bookshelf == mineCloneBookshelfName --[[ if nodeId_cobweb ~= nodeId_ignore then -- This game has proper cobwebs, replace any cobwebs this mod may have generated -- previously (when a cobweb mod wasn't included) with the proper cobwebs. minetest.register_alias(nodeName_standinCobweb, minetest.get_name_from_content_id(nodeId_cobweb)) elseif minetest.registered_nodes[nodeName_standinCobweb] ~= nil then -- use a stand-in cobweb created by this mod nodeId_cobweb = minetest.get_content_id(nodeName_standinCobweb) end ]]-- end ------------------------------------------------------------------------------ -- End of secrets section ------------------------------------------------------------------------------ local function renderCores(cores, minp, maxp, blockseed) local voxelsWereManipulated = false local vm, emerge_min, emerge_max = minetest.get_mapgen_object("voxelmanip") vm:get_data(data) -- put all nodes except the ground surface in this array local area = VoxelArea:new{MinEdge=emerge_min, MaxEdge=emerge_max} local overdrawTop = maxp.y + OVERDRAW local currentBiomeId = -1 local nodeId_dust local nodeId_top local nodeId_filler local nodeId_stoneBase local nodeId_pondBottom local depth_top local depth_filler local fillerFallsWithGravity local floodableDepth for z = minp.z, maxp.z do local dataBufferIndex = area:index(minp.x, minp.y, z) for x = minp.x, maxp.x do for _,core in pairs(cores) do local coreTop = ALTITUDE + core.y local distanceSquared = (x - core.x)*(x - core.x) + (z - core.z)*(z - core.z) local radius = core.radius local radiusSquared = radius * radius if distanceSquared <= radiusSquared then -- get the biome details for this core if core.biome == nil then setCoreBiomeData(core) end if currentBiomeId ~= core.biomeId then if core.biome.node_top == nil then nodeId_top = nodeId_stone else nodeId_top = minetest.get_content_id(core.biome.node_top) end if core.biome.node_filler == nil then nodeId_filler = nodeId_stone else nodeId_filler = minetest.get_content_id(core.biome.node_filler) end if core.biome.node_stone == nil then nodeId_stoneBase = nodeId_stone else nodeId_stoneBase = minetest.get_content_id(core.biome.node_stone) end if core.biome.node_dust == nil then nodeId_dust = nodeId_ignore else nodeId_dust = minetest.get_content_id(core.biome.node_dust) end if core.biome.node_riverbed == nil then nodeId_pondBottom = nodeId_silt else nodeId_pondBottom = minetest.get_content_id(core.biome.node_riverbed) end if core.biome.depth_top == nil then depth_top = 1 else depth_top = core.biome.depth_top end if core.biome.depth_filler == nil then depth_filler = 3 else depth_filler = core.biome.depth_filler end fillerFallsWithGravity = core.biome.node_filler ~= nil and minetest.registered_items[core.biome.node_filler].groups.falling_node == 1 --[[Commented out as unnecessary, as a supporting node will be added, but uncommenting this will make the strata transition less noisey. if fillerFallsWithGravity then -- the filler node is affected by gravity and can fall if unsupported, so keep that layer thinner than -- core.thickness when possible. --depth_filler = math_min(depth_filler, math_max(1, core.thickness - 1)) end--]] floodableDepth = 0 if nodeId_top ~= nodeId_stone and minetest.registered_items[core.biome.node_top].floodable then -- nodeId_top is a node that water floods through, so we can't have ponds appearing at this depth floodableDepth = depth_top end currentBiomeId = core.biomeId end -- decide on a shape local horz_easing local noise_weighting = 1 local shapeType = math_floor(core.depth + radius + core.x) % 5 if shapeType < 2 then -- convex -- squared easing function, e = 1 - x² horz_easing = 1 - distanceSquared / radiusSquared elseif shapeType == 2 then -- conical -- linear easing function, e = 1 - x horz_easing = 1 - math_sqrt(distanceSquared) / radius else -- concave -- root easing function blended/scaled with square easing function, -- x = normalised distance from center of core -- a = 1 - x² -- b = 1 - √x -- e = 0.8*a*x + 1.2*b*(1 - x) local radiusRoot = core.radiusRoot if radiusRoot == nil then radiusRoot = math_sqrt(radius) core.radiusRoot = radiusRoot end local squared = 1 - distanceSquared / radiusSquared local distance = math_sqrt(distanceSquared) local distance_normalized = distance / radius local root = 1 - math_sqrt(distance) / radiusRoot horz_easing = math_min(1, 0.8*distance_normalized*squared + 1.2*(1-distance_normalized)*root) -- this seems to be a more delicate shape that gets wiped out by the -- density noise, so lower that noise_weighting = 0.63 end if radius + core.depth > 80 then -- larger islands shapes have a slower easing transition, which leaves large areas -- dominated by the density noise, so reduce the density noise when the island is large. -- (the numbers here are arbitrary) if radius + core.depth > 120 then noise_weighting = 0.35 else noise_weighting = math_min(0.6, noise_weighting) end end local surfaceNoise = noise_surfaceMap:get2d({x = x, y = z}) if DEBUG_GEOMETRIC then surfaceNoise = SURFACEMAP_OFFSET end local surface = round(surfaceNoise * 3 * (core.thickness + 1) * horz_easing) -- if you change this formular then update maxSufaceRise in on_generated() local coreBottom = math_floor(coreTop - (core.thickness + core.depth)) local noisyDepthOfFiller = depth_filler if noisyDepthOfFiller >= 3 then noisyDepthOfFiller = noisyDepthOfFiller + math_floor(randomNumbers[(x + z) % 256] * 3) - 1 end local yBottom = math_max(minp.y, coreBottom - 4) -- the -4 is for rare instances when density noise pushes the bottom of the island deeper local yBottomIndex = dataBufferIndex + area.ystride * (yBottom - minp.y) -- equivalent to yBottomIndex = area:index(x, yBottom, z) local topBlockIndex = -1 local bottomBlockIndex = -1 local vi = yBottomIndex local densityNoise = nil for y = yBottom, math_min(overdrawTop, coreTop + surface) do local vert_easing = math_min(1, (y - coreBottom) / core.depth) -- If you change the densityNoise calculation, remember to similarly update the copy of this calculation in the pond code densityNoise = noise_density:get3d({x = x, y = y - coreTop, z = z}) -- TODO: Optimize this!! densityNoise = noise_weighting * densityNoise + (1 - noise_weighting) * DENSITY_OFFSET if DEBUG_GEOMETRIC then densityNoise = DENSITY_OFFSET end if densityNoise * ((horz_easing + vert_easing) / 2) >= REQUIRED_DENSITY then if vi > topBlockIndex then topBlockIndex = vi end if bottomBlockIndex < 0 and y > minp.y then bottomBlockIndex = vi end -- if y==minp.y then we don't know for sure this is the lowest block if y > coreTop + surface - depth_top and data[vi] == nodeId_air then data[vi] = nodeId_top elseif y >= coreTop + surface - (depth_top + noisyDepthOfFiller) then data[vi] = nodeId_filler else data[vi] = nodeId_stoneBase end end vi = vi + area.ystride end -- ensure nodeId_top blocks also cover the rounded sides of islands (which may be lower -- than the flat top), then dust the top surface. if topBlockIndex >= 0 then voxelsWereManipulated = true -- we either have the highest block, or overdrawTop - but we don't want to set overdrawTop nodes to nodeId_top -- (we will err on the side of caution when we can't distinguish the top of a island's side from overdrawTop) if overdrawTop >= coreTop + surface or vi > topBlockIndex + area.ystride then if topBlockIndex > yBottomIndex and data[topBlockIndex - area.ystride] ~= nodeId_air and data[topBlockIndex + area.ystride] == nodeId_air then -- We only set a block to nodeId_top if there's a block under it "holding it up" as -- it's better to leave 1-deep noise as stone/whatever. data[topBlockIndex] = nodeId_top end if nodeId_dust ~= nodeId_ignore and data[topBlockIndex + area.ystride] == nodeId_air then -- Delay writing dust to the data buffer until after decoration so avoid preventing tree growth etc if core.dustLocations == nil then core.dustLocations = {} end core.dustLocations[#core.dustLocations + 1] = topBlockIndex + area.ystride end end if fillerFallsWithGravity and bottomBlockIndex >= 0 and data[bottomBlockIndex] == nodeId_filler then -- the bottom node is affected by gravity and can fall if unsupported, put some support in data[bottomBlockIndex] = nodeId_stoneBase end end -- add ponds of water, trying to make sure they're not on an edge. -- (the only time a pond needs to be rendered when densityNoise is nil (i.e. when there was no land at this x, z), -- is when the pond is at minp.y - i.e. the reason no land was rendered is it was below minp.y) if surfaceNoise < 0 and (densityNoise ~= nil or (coreTop + surface < minp.y and coreTop >= minp.y)) and nodeId_water ~= nodeId_ignore then local pondWallBuffer = core.type.pondWallBuffer local pondBottom = nodeId_filler local pondWater = nodeId_water if radius > 18 and core.depth > 15 and nodeId_pondBottom ~= nodeId_ignore then -- only give ponds a sandbed when islands are large enough for it not to stick out the side or bottom pondBottom = nodeId_pondBottom end if core.temperature <= ICE_REQUIRED_TEMPERATURE and nodeId_ice ~= nodeId_ignore then pondWater = nodeId_ice end if densityNoise == nil then -- Rare edge case. If the pond is at minp.y, then no land has been rendered, so -- densityNoise hasn't been calculated. Calculate it now. densityNoise = noise_density:get3d({x = x, y = minp.y, z = z}) densityNoise = noise_weighting * densityNoise + (1 - noise_weighting) * DENSITY_OFFSET if DEBUG_GEOMETRIC then densityNoise = DENSITY_OFFSET end end local surfaceDensity = densityNoise * ((horz_easing + 1) / 2) local onTheEdge = math_sqrt(distanceSquared) + 1 >= radius for y = math_max(minp.y, coreTop + surface), math_min(overdrawTop, coreTop - floodableDepth) do if surfaceDensity > REQUIRED_DENSITY then vi = dataBufferIndex + area.ystride * (y - minp.y) -- this is the same as vi = area:index(x, y, z) if surfaceDensity > (REQUIRED_DENSITY + pondWallBuffer) and not onTheEdge then data[vi] = pondWater if y > minp.y then data[vi - area.ystride] = pondBottom end --remove any dust above ponds if core.dustLocations ~= nil and core.dustLocations[#core.dustLocations] == vi + area.ystride then core.dustLocations[#core.dustLocations] = nil end else -- make sure there are some walls to keep the water in if y == coreTop then data[vi] = nodeId_top -- to let isIsland() know not to put vines here (only seems to be an issue when pond is 2 deep or more) else data[vi] = nodeId_filler end end end end end end end dataBufferIndex = dataBufferIndex + 1 end end local decorations = {} for _,core in ipairs(cores) do addDetail_vines(decorations, core, data, area, minp, maxp) voxelsWereManipulated = addDetail_skyReef(decorations, core, data, area, minp, maxp) or voxelsWereManipulated addDetail_secrets(decorations, core, data, area, minp, maxp) end if voxelsWereManipulated then vm:set_data(data) if GENERATE_ORES then minetest.generate_ores(vm) end minetest.generate_decorations(vm) for _,core in ipairs(cores) do addDetail_skyTree(decorations, core, minp, maxp) end for _,decoration in ipairs(decorations) do local nodeAtPos = minetest.get_node(decoration.pos) if nodeAtPos.name == "air" or nodeAtPos.name == nodeName_ignore then minetest.set_node(decoration.pos, decoration.node) end end local dustingInProgress = false for _,core in ipairs(cores) do if core.dustLocations ~= nil then if not dustingInProgress then vm:get_data(data) dustingInProgress = true end nodeId_dust = minetest.get_content_id(core.biome.node_dust) for _, location in ipairs(core.dustLocations) do if data[location] == nodeId_air and data[location - area.ystride] ~= nodeId_air then data[location] = nodeId_dust end end end end if dustingInProgress then vm:set_data(data) end -- Lighting is a problem. Two problems really... -- -- Problem 1: -- We can't use the usual lua mapgen lighting trick of flags="nolight" e.g.: -- minetest.set_mapgen_params({mgname = "singlenode", flags = "nolight"}) -- (https://forum.minetest.net/viewtopic.php?t=19836) -- -- because the mod is designed to run with other mapgens. So we must set the light -- values to zero at islands before calling calc_lighting() to propegate lighting -- down from above. -- -- This causes lighting bugs if we zero the whole emerge_min-emerge_max area because -- it leaves hard black at the edges of the emerged area (calc_lighting must assume -- a value of zero for light outside the region, and be blending that in) -- -- But we can't simply zero only the minp-maxp area instead, because then calc_lighting -- reads the daylight values out of the overdraw area and blends those in, cutting -- up shadows with lines of daylight along chunk boundaries. -- -- The correct solution is to zero and calculate the whole emerge_min-emerge_max area, -- but only write the calculated lighting information from minp-maxp back into the map, -- however the API doesn't appear to provide a fast way to do that. -- -- Workaround: zero an area that extends into the overdraw region, but keeps a gap around -- the edges to preserve and allow the real light values to propegate in. Then when -- calc_lighting is called it will have daylight (or existing values) at the emerge boundary -- but not near the chunk boundary. calc_lighting is able to take the edge lighting into -- account instead of assuming zero. It's not a perfect solution, but allows shading without -- glaringly obvious lighting artifacts, and the minor ill effects should only affect the -- islands and be corrected any time lighting is updated. -- -- -- Problem 2: -- We don't want islands to blacken the landscape below them in shadow. -- -- Workaround 1: Instead of zeroing the lighting before propegating from above, set it -- to 2, so that shadows are never pitch black. Shadows will still go back to pitch black -- though if lighting gets recalculated, e.g. player places a torch then removes it. -- -- Workaround 2: set the bottom of the chunk to full daylight, ensuring that full -- daylight is what propegates down below islands. This has the problem of causing a -- bright horizontal band of light where islands approach a chunk floor or ceiling, -- but Hallelujah Mountains already had that issue due to having propagate_shadow -- turned off when calling calc_lighting. This workaround has the same drawback, but -- does a much better job of preventing undesired shadows. local shadowGap = 1 local brightMin = {x = emerge_min.x + shadowGap, y = minp.y , z = emerge_min.z + shadowGap} local brightMax = {x = emerge_max.x - shadowGap, y = minp.y + 1, z = emerge_max.z - shadowGap} local darkMin = {x = emerge_min.x + shadowGap, y = minp.y + 1, z = emerge_min.z + shadowGap} local darkMax = {x = emerge_max.x - shadowGap, y = maxp.y , z = emerge_max.z - shadowGap} vm:set_lighting({day=2, night=0}, darkMin, darkMax) vm:calc_lighting() vm:set_lighting({day=15, night=0}, brightMin, brightMax) vm:write_to_map() -- seems to be unnecessary when other mods that use vm are running for _,core in ipairs(cores) do -- place any schematics which should be placed after the landscape if addDetail_ancientPortal ~= nil then addDetail_ancientPortal(core) end end end end cloudlands.init = function() if noise_eddyField == nil then init_mapgen() init_secrets() end if noise_eddyField == nil then -- See comment in init_mapgen() about when this can be called minetest.log("warning", "cloudlands.init() unable to init - was probably invoked before the the environment was created") end end local function on_generated(minp, maxp, blockseed) local memUsageT0 local osClockT0 = os.clock() if DEBUG then memUsageT0 = collectgarbage("count") end local largestCoreType = cloudlands.coreTypes[1] -- the first island type is the biggest/thickest local maxCoreThickness = largestCoreType.thicknessMax local maxCoreDepth = largestCoreType.radiusMax * 3 / 2 -- todo: not sure why this is radius based and not maxDepth based?? local maxSufaceRise = 3 * (maxCoreThickness + 1) if minp.y > ALTITUDE + (ALTITUDE_AMPLITUDE + maxSufaceRise + 10) or -- the 10 is an arbitrary number because sometimes the noise values exceed their normal range. maxp.y < ALTITUDE - (ALTITUDE_AMPLITUDE + maxCoreThickness + maxCoreDepth + 10) then -- Hallelujah Mountains don't generate here return end local cores = cloudlands.get_island_details(minp, maxp) if DEBUG then minetest.log("info", "Cores for on_generated(): " .. #cores) for _,core in pairs(cores) do minetest.log("core ("..core.x..","..core.y..","..core.z..") r"..core.radius) end end if #cores > 0 then -- voxelmanip has mem-leaking issues, avoid creating one if we're not going to need it renderCores(cores, minp, maxp, blockseed) if DEBUG then minetest.log( "info", MODNAME .. " took " .. round((os.clock() - osClockT0) * 1000) .. "ms for " .. #cores .. " cores. Uncollected memory delta: " .. round(collectgarbage("count") - memUsageT0) .. " KB" ) end end end minetest.register_on_generated(on_generated) minetest.register_on_mapgen_init( -- invoked after mods initially run but before the environment is created, while the mapgen is being initialized function(mgparams) worldSeed = mgparams.seed --if DEBUG then minetest.set_mapgen_params({mgname = "singlenode"--[[, flags = "nolight"]]}) end end )