mapgen_mantle.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. --[[
  2. Nether mod for minetest
  3. This file contains helper functions for generating the Mantle
  4. (AKA center region), which are moved into a separate file to keep the
  5. size of mapgen.lua manageable.
  6. Copyright (C) 2021 Treer
  7. Permission to use, copy, modify, and/or distribute this software for
  8. any purpose with or without fee is hereby granted, provided that the
  9. above copyright notice and this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
  11. WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
  12. WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
  13. BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
  14. OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  15. WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  16. ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  17. SOFTWARE.
  18. ]]--
  19. local debugf = nether.debug
  20. local mapgen = nether.mapgen
  21. local S = nether.get_translator
  22. local BASALT_COLUMN_UPPER_LIMIT = mapgen.BASALT_COLUMN_UPPER_LIMIT
  23. local BASALT_COLUMN_LOWER_LIMIT = mapgen.BASALT_COLUMN_LOWER_LIMIT
  24. -- 2D noise for basalt formations
  25. local np_basalt = {
  26. offset =-0.85,
  27. scale = 1,
  28. spread = {x = 46, y = 46, z = 46},
  29. seed = 1000,
  30. octaves = 5,
  31. persistence = 0.5,
  32. lacunarity = 2.6,
  33. flags = "eased"
  34. }
  35. -- Buffers and objects we shouldn't recreate every on_generate
  36. local nobj_basalt = nil
  37. local nbuf_basalt = {}
  38. -- Content ids
  39. local c_air = minetest.get_content_id("air")
  40. local c_netherrack_deep = minetest.get_content_id("nether:rack_deep")
  41. local c_glowstone = minetest.get_content_id("nether:glowstone")
  42. local c_lavasea_source = minetest.get_content_id("nether:lava_source") -- same as lava but with staggered animation to look better as an ocean
  43. local c_lava_crust = minetest.get_content_id("nether:lava_crust")
  44. local c_basalt = minetest.get_content_id("nether:basalt")
  45. -- Math funcs
  46. local math_max, math_min, math_abs, math_floor = math.max, math.min, math.abs, math.floor -- avoid needing table lookups each time a common math function is invoked
  47. function random_unit_vector()
  48. return vector.normalize({
  49. x = math.random() - 0.5,
  50. y = math.random() - 0.5,
  51. z = math.random() - 0.5
  52. })
  53. end
  54. -- returns the smallest component in the vector
  55. function vector_min(v)
  56. return math_min(v.x, math_min(v.y, v.z))
  57. end
  58. -- Mantle mapgen functions (AKA Center region)
  59. -- Returns (absolute height, fractional distance from ceiling or sea floor)
  60. -- the fractional distance from ceiling or sea floor is a value between 0 and 1 (inclusive)
  61. -- Note it may find the most relevent sea-level - not necesssarily the one you are closest
  62. -- to, since the space above the sea reaches much higher than the depth below the sea.
  63. mapgen.find_nearest_lava_sealevel = function(y)
  64. -- todo: put oceans near the bottom of chunks to improve ability to generate tunnels to the center
  65. -- todo: constrain y to be not near the bounds of the nether
  66. -- todo: add some random adj at each level, seeded only by the level height
  67. local sealevel = math.floor((y + 100) / 200) * 200
  68. --local sealevel = math.floor((y + 80) / 160) * 160
  69. --local sealevel = math.floor((y + 120) / 240) * 240
  70. local cavern_limits_fraction
  71. local height_above_sea = y - sealevel
  72. if height_above_sea >= 0 then
  73. cavern_limits_fraction = math_min(1, height_above_sea / 95)
  74. else
  75. -- approaches 1 much faster as the lava sea is shallower than the cavern above it
  76. cavern_limits_fraction = math_min(1, -height_above_sea / 40)
  77. end
  78. return sealevel, cavern_limits_fraction
  79. end
  80. mapgen.add_basalt_columns = function(data, area, minp, maxp)
  81. -- Basalt columns are structures found in lava oceans, and the only way to obtain
  82. -- nether basalt.
  83. -- Their x, z position is determined by a 2d noise map and a 2d slice of the cave
  84. -- noise (taken at lava-sealevel).
  85. local x0, y0, z0 = minp.x, math_max(minp.y, nether.DEPTH_FLOOR), minp.z
  86. local x1, y1, z1 = maxp.x, math_min(maxp.y, nether.DEPTH_CEILING), maxp.z
  87. local yStride = area.ystride
  88. local yCaveStride = x1 - x0 + 1
  89. local cavePerlin = mapgen.get_cave_point_perlin()
  90. nobj_basalt = nobj_basalt or minetest.get_perlin_map(np_basalt, {x = yCaveStride, y = yCaveStride})
  91. local nvals_basalt = nobj_basalt:get_2d_map_flat({x=minp.x, y=minp.z}, {x=yCaveStride, y=yCaveStride}, nbuf_basalt)
  92. local nearest_sea_level, _ = mapgen.find_nearest_lava_sealevel(math_floor((y0 + y1) / 2))
  93. local leeway = mapgen.CENTER_CAVERN_LIMIT * 0.18
  94. for z = z0, z1 do
  95. local noise2di = 1 + (z - z0) * yCaveStride
  96. for x = x0, x1 do
  97. local basaltNoise = nvals_basalt[noise2di]
  98. if basaltNoise > 0 then
  99. -- a basalt column is here
  100. local abs_sealevel_cave_noise = math_abs(cavePerlin:get_3d({x = x, y = nearest_sea_level, z = z}))
  101. -- Add Some quick deterministic noise to the column heights
  102. -- This is probably not good noise, but it doesn't have to be.
  103. local fastNoise = 17
  104. fastNoise = 37 * fastNoise + y0
  105. fastNoise = 37 * fastNoise + z
  106. fastNoise = 37 * fastNoise + x
  107. fastNoise = 37 * fastNoise + math_floor(basaltNoise * 32)
  108. local columnHeight = basaltNoise * 18 + ((fastNoise % 3) - 1)
  109. -- columns should drop below sealevel where lava rivers are flowing
  110. -- i.e. anywhere abs_sealevel_cave_noise < BASALT_COLUMN_LOWER_LIMIT
  111. -- And we'll also have it drop off near the edges of the lava ocean so that
  112. -- basalt columns can only be found by the player reaching a lava ocean.
  113. local lowerClip = (math_min(math_max(abs_sealevel_cave_noise, BASALT_COLUMN_LOWER_LIMIT - leeway), BASALT_COLUMN_LOWER_LIMIT + leeway) - BASALT_COLUMN_LOWER_LIMIT) / leeway
  114. local upperClip = (math_min(math_max(abs_sealevel_cave_noise, BASALT_COLUMN_UPPER_LIMIT - leeway), BASALT_COLUMN_UPPER_LIMIT + leeway) - BASALT_COLUMN_UPPER_LIMIT) / leeway
  115. local columnHeightAdj = lowerClip * -upperClip -- all are values between 1 and -1
  116. columnHeight = columnHeight + math_floor(columnHeightAdj * 12 - 12)
  117. local vi = area:index(x, y0, z) -- Initial voxelmanip index
  118. for y = y0, y1 do -- Y loop first to minimise tcave & lava-sea calculations
  119. if y < nearest_sea_level + columnHeight then
  120. local id = data[vi] -- Existing node
  121. if id == c_lava_crust or id == c_lavasea_source or (id == c_air and y > nearest_sea_level) then
  122. -- Avoid letting columns extend beyond the central region.
  123. -- (checking node ids saves having to calculate abs_cave_noise_adjusted here
  124. -- to test it against CENTER_CAVERN_LIMIT)
  125. data[vi] = c_basalt
  126. end
  127. end
  128. vi = vi + yStride
  129. end
  130. end
  131. noise2di = noise2di + 1
  132. end
  133. end
  134. end
  135. -- returns an array of points from pos1 and pos2 which deviate from a straight line
  136. -- but which don't venture too close to a chunk boundary
  137. function generate_waypoints(pos1, pos2, minp, maxp)
  138. local segSize = 10
  139. local maxDeviation = 7
  140. local minDistanceFromChunkWall = 5
  141. local pathVec = vector.subtract(pos2, pos1)
  142. local pathVecNorm = vector.normalize(pathVec)
  143. local pathLength = vector.distance(pos1, pos2)
  144. local minBound = vector.add(minp, minDistanceFromChunkWall)
  145. local maxBound = vector.subtract(maxp, minDistanceFromChunkWall)
  146. local result = {}
  147. result[1] = pos1
  148. local segmentCount = math_floor(pathLength / segSize)
  149. for i = 1, segmentCount do
  150. local waypoint = vector.add(pos1, vector.multiply(pathVec, i / (segmentCount + 1)))
  151. -- shift waypoint a few blocks in a random direction orthogonally to the pathVec, to make the path crooked.
  152. local crossProduct
  153. repeat
  154. crossProduct = vector.normalize(vector.cross(pathVecNorm, random_unit_vector()))
  155. until vector.length(crossProduct) > 0
  156. local deviation = vector.multiply(crossProduct, math.random(1, maxDeviation))
  157. waypoint = vector.add(waypoint, deviation)
  158. waypoint = {
  159. x = math_min(maxBound.x, math_max(minBound.x, waypoint.x)),
  160. y = math_min(maxBound.y, math_max(minBound.y, waypoint.y)),
  161. z = math_min(maxBound.z, math_max(minBound.z, waypoint.z))
  162. }
  163. result[#result + 1] = waypoint
  164. end
  165. result[#result + 1] = pos2
  166. return result
  167. end
  168. function excavate_pathway(data, area, nether_pos, center_pos, minp, maxp)
  169. local ystride = area.ystride
  170. local zstride = area.zstride
  171. math.randomseed(nether_pos.x + 10 * nether_pos.y + 100 * nether_pos.z) -- so each tunnel generates deterministically (this doesn't have to be a quality seed)
  172. local dist = math_floor(vector.distance(nether_pos, center_pos))
  173. local waypoints = generate_waypoints(nether_pos, center_pos, minp, maxp)
  174. -- First pass: record path details
  175. local linedata = {}
  176. local last_pos = {}
  177. local line_index = 1
  178. local first_filled_index, boundary_index, last_filled_index
  179. for i = 0, dist do
  180. -- Bresenham's line would be good here, but too much lua code
  181. local waypointProgress = (#waypoints - 1) * i / dist
  182. local segmentIndex = math_min(math_floor(waypointProgress) + 1, #waypoints - 1) -- from the integer portion of waypointProgress
  183. local segmentInterp = waypointProgress - (segmentIndex - 1) -- the remaining fractional portion
  184. local segmentStart = waypoints[segmentIndex]
  185. local segmentVector = vector.subtract(waypoints[segmentIndex + 1], segmentStart)
  186. local pos = vector.round(vector.add(segmentStart, vector.multiply(segmentVector, segmentInterp)))
  187. if not vector.equals(pos, last_pos) then
  188. local vi = area:indexp(pos)
  189. local node_id = data[vi]
  190. linedata[line_index] = {
  191. pos = pos,
  192. vi = vi,
  193. node_id = node_id
  194. }
  195. if boundary_index == nil and node_id == c_netherrack_deep then
  196. boundary_index = line_index
  197. end
  198. if node_id == c_air then
  199. if boundary_index ~= nil and last_filled_index == nil then
  200. last_filled_index = line_index
  201. end
  202. else
  203. if first_filled_index == nil then
  204. first_filled_index = line_index
  205. end
  206. end
  207. line_index = line_index + 1
  208. last_pos = pos
  209. end
  210. end
  211. first_filled_index = first_filled_index or 1
  212. last_filled_index = last_filled_index or #linedata
  213. boundary_index = boundary_index or last_filled_index
  214. -- limit tunnel radius to roughly the closest that startPos or stopPos comes to minp-maxp, so we
  215. -- don't end up exceeding minp-maxp and having excavation filled in when the next chunk is generated.
  216. local startPos, stopPos = linedata[first_filled_index].pos, linedata[last_filled_index].pos
  217. local radiusLimit = vector_min(vector.subtract(startPos, minp))
  218. radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(stopPos, minp)))
  219. radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(maxp, startPos)))
  220. radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(maxp, stopPos)))
  221. if radiusLimit < 4 then -- This is a logic check, ignore it. It could be commented out
  222. -- 4 is (79 - 75), and values less than 4 shouldn't be possible if sampling-skip was 10
  223. -- i.e. if sampling-skip was 10 then {5, 15, 25, 35, 45, 55, 65, 75} should be sampled from possible positions 0 to 79
  224. debugf("Error: radiusLimit %s is smaller then half the sampling distance. min %s, max %s, start %s, stop %s", radiusLimit, minp, maxp, startPos, stopPos)
  225. end
  226. radiusLimit = radiusLimit + 1 -- chunk walls wont be visibly flat if the radius only exceeds it a little ;)
  227. -- Second pass: excavate
  228. local start_index, stop_index = math_max(1, first_filled_index - 2), math_min(#linedata, last_filled_index + 3)
  229. for i = start_index, stop_index, 3 do
  230. -- Adjust radius so that tunnels start wide but thin out in the middle
  231. local distFromEnds = 1 - math_abs(((start_index + stop_index) / 2) - i) / ((stop_index - start_index) / 2) -- from 0 to 1, with 0 at ends and 1 in the middle
  232. -- Have it more flaired at the ends, rather than linear.
  233. -- i.e. sizeAdj approaches 1 quickly as distFromEnds increases
  234. local distFromMiddle = 1 - distFromEnds
  235. local sizeAdj = 1 - (distFromMiddle * distFromMiddle * distFromMiddle)
  236. local radius = math_min(radiusLimit, math.random(50 - (25 * sizeAdj), 80 - (45 * sizeAdj)) / 10)
  237. local radiusSquared = radius * radius
  238. local radiusCeil = math_floor(radius + 0.5)
  239. linedata[i].radius = radius -- Needed in third pass
  240. linedata[i].distFromEnds = distFromEnds -- Needed in third pass
  241. local vi = linedata[i].vi
  242. for z = -radiusCeil, radiusCeil do
  243. local vi_z = vi + z * zstride
  244. for y = -radiusCeil, radiusCeil do
  245. local vi_zy = vi_z + y * ystride
  246. local xSquaredLimit = radiusSquared - (z * z + y * y)
  247. for x = -radiusCeil, radiusCeil do
  248. if x * x < xSquaredLimit then
  249. data[vi_zy + x] = c_air
  250. end
  251. end
  252. end
  253. end
  254. end
  255. -- Third pass: decorate
  256. -- Add glowstones to make tunnels to the mantle easier to find
  257. -- https://i.imgur.com/sRA28x7.jpg
  258. for i = start_index, stop_index, 3 do
  259. if linedata[i].distFromEnds < 0.3 then
  260. local glowcount = 0
  261. local radius = linedata[i].radius
  262. for _ = 1, 20 do
  263. local testPos = vector.round(vector.add(linedata[i].pos, vector.multiply(random_unit_vector(), radius + 0.5)))
  264. local vi = area:indexp(testPos)
  265. if data[vi] ~= c_air then
  266. data[vi] = c_glowstone
  267. glowcount = glowcount + 1
  268. --else
  269. -- data[vi] = c_debug
  270. end
  271. if glowcount >= 2 then break end
  272. end
  273. end
  274. end
  275. end
  276. -- excavates a tunnel connecting the Primary or Secondary region with the mantle / central region
  277. -- if a suitable path is found.
  278. -- Returns true if successful
  279. mapgen.excavate_tunnel_to_center_of_the_nether = function(data, area, nvals_cave, minp, maxp)
  280. local result = false
  281. local extent = vector.subtract(maxp, minp)
  282. local skip = 10 -- sampling rate of 1 in 10
  283. local highest = -1000
  284. local lowest = 1000
  285. local lowest_vi
  286. local highest_vi
  287. local yCaveStride = maxp.x - minp.x + 1
  288. local zCaveStride = yCaveStride * yCaveStride
  289. local vi_offset = area:indexp(vector.add(minp, math_floor(skip / 2))) -- start half the sampling distance away from minp
  290. local vi, ni
  291. for y = 0, extent.y - 1, skip do
  292. local sealevel = mapgen.find_nearest_lava_sealevel(minp.y + y)
  293. if minp.y + y > sealevel then -- only create tunnels above sea level
  294. for z = 0, extent.z - 1, skip do
  295. vi = vi_offset + y * area.ystride + z * area.zstride
  296. ni = z * zCaveStride + y * yCaveStride + 1
  297. for x = 0, extent.x - 1, skip do
  298. local noise = math_abs(nvals_cave[ni])
  299. if noise < lowest then
  300. lowest = noise
  301. lowest_vi = vi
  302. end
  303. if noise > highest then
  304. highest = noise
  305. highest_vi = vi
  306. end
  307. ni = ni + skip
  308. vi = vi + skip
  309. end
  310. end
  311. end
  312. end
  313. if lowest < mapgen.CENTER_CAVERN_LIMIT and highest > mapgen.TCAVE + 0.03 then
  314. local mantle_y = area:position(lowest_vi).y
  315. local _, cavern_limit_distance = mapgen.find_nearest_lava_sealevel(mantle_y)
  316. local _, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(mantle_y)
  317. -- cavern_noise_adj gets added to noise value instead of added to the limit np_noise
  318. -- is compared against, so subtract centerRegionLimit_adj instead of adding
  319. local cavern_noise_adj =
  320. mapgen.CENTER_REGION_LIMIT * (cavern_limit_distance * cavern_limit_distance * cavern_limit_distance) -
  321. centerRegionLimit_adj
  322. if lowest + cavern_noise_adj < mapgen.CENTER_CAVERN_LIMIT then
  323. excavate_pathway(data, area, area:position(highest_vi), area:position(lowest_vi), minp, maxp)
  324. result = true
  325. end
  326. end
  327. return result
  328. end
  329. -- an enumerated list of the different regions in the nether
  330. mapgen.RegionEnum = {
  331. OVERWORLD = {name = "overworld", desc = S("The Overworld") }, -- Outside the Nether / none of the regions in the Nether
  332. POSITIVE = {name = "positive", desc = S("Positive nether") }, -- The classic nether caverns are here - where cavePerlin > 0.6
  333. POSITIVESHELL = {name = "positive shell", desc = S("Shell between positive nether and center region") }, -- the nether side of the wall/buffer area separating classic nether from the mantle
  334. CENTER = {name = "center", desc = S("Center/Mantle, inside cavern") },
  335. CENTERSHELL = {name = "center shell", desc = S("Center/Mantle, but outside the caverns") }, -- the mantle side of the wall/buffer area separating the positive and negative regions from the center region
  336. NEGATIVE = {name = "negative", desc = S("Negative nether") }, -- Secondary/spare region - where cavePerlin < -0.6
  337. NEGATIVESHELL = {name = "negative shell", desc = S("Shell between negative nether and center region") } -- the spare region side of the wall/buffer area separating the negative region from the mantle
  338. }
  339. -- Returns (region, noise) where region is a value from mapgen.RegionEnum
  340. -- and noise is the unadjusted cave perlin value
  341. mapgen.get_region = function(pos)
  342. if pos.y > nether.DEPTH_CEILING or pos.y < nether.DEPTH_FLOOR then
  343. return mapgen.RegionEnum.OVERWORLD, nil
  344. end
  345. local caveNoise = mapgen.get_cave_perlin_at(pos)
  346. local sealevel, cavern_limit_distance = mapgen.find_nearest_lava_sealevel(pos.y)
  347. local tcave_adj, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(pos.y)
  348. local tcave = mapgen.TCAVE + tcave_adj
  349. local tmantle = mapgen.CENTER_REGION_LIMIT + centerRegionLimit_adj
  350. -- cavern_noise_adj gets added to noise value instead of added to the limit np_noise
  351. -- is compared against, so subtract centerRegionLimit_adj instead of adding
  352. local cavern_noise_adj =
  353. mapgen.CENTER_REGION_LIMIT * (cavern_limit_distance * cavern_limit_distance * cavern_limit_distance) -
  354. centerRegionLimit_adj
  355. local region
  356. if caveNoise > tcave then
  357. region = mapgen.RegionEnum.POSITIVE
  358. elseif -caveNoise > tcave then
  359. region = mapgen.RegionEnum.NEGATIVE
  360. elseif math_abs(caveNoise) < tmantle then
  361. if math_abs(caveNoise) + cavern_noise_adj < mapgen.CENTER_CAVERN_LIMIT then
  362. region = mapgen.RegionEnum.CENTER
  363. else
  364. region = mapgen.RegionEnum.CENTERSHELL
  365. end
  366. elseif caveNoise > 0 then
  367. region = mapgen.RegionEnum.POSITIVESHELL
  368. else
  369. region = mapgen.RegionEnum.NEGATIVESHELL
  370. end
  371. return region, caveNoise
  372. end
  373. minetest.register_chatcommand("nether_whereami",
  374. {
  375. description = S("Describes which region of the nether the player is in"),
  376. privs = {debug = true},
  377. func = function(name, param)
  378. local player = minetest.get_player_by_name(name)
  379. if player == nil then return false, S("Unknown player position") end
  380. local playerPos = vector.round(player:get_pos())
  381. local region, caveNoise = mapgen.get_region(playerPos)
  382. local seaLevel, cavernLimitDistance = mapgen.find_nearest_lava_sealevel(playerPos.y)
  383. local tcave_adj, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(playerPos.y)
  384. local seaDesc = ""
  385. local boundaryDesc = ""
  386. local perlinDesc = ""
  387. if region ~= mapgen.RegionEnum.OVERWORLD then
  388. local seaPos = playerPos.y - seaLevel
  389. if seaPos > 0 then
  390. seaDesc = S(", @1m above lava-sea level", seaPos)
  391. else
  392. seaDesc = S(", @1m below lava-sea level", seaPos)
  393. end
  394. if tcave_adj > 0 then
  395. boundaryDesc = S(", approaching y boundary of Nether")
  396. end
  397. perlinDesc = S("[Perlin @1] ", (math_floor(caveNoise * 1000) / 1000))
  398. end
  399. return true, S("@1@2@3@4", perlinDesc, region.desc, seaDesc, boundaryDesc)
  400. end
  401. }
  402. )