File: mapgen_mantle.lua

package info (click to toggle)
minetest-mod-nether 3.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 932 kB
  • sloc: makefile: 2
file content (512 lines) | stat: -rw-r--r-- 19,788 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
--[[

  Nether mod for minetest

  This file contains helper functions for generating the Mantle
  (AKA center region), which are moved into a separate file to keep the
  size of mapgen.lua manageable.


  Copyright (C) 2021 Treer

  Permission to use, copy, modify, and/or distribute this software for
  any purpose with or without fee is hereby granted, provided that the
  above copyright notice and this permission notice appear in all copies.

  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
  WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
  WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
  BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
  OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  SOFTWARE.

]]--


local debugf = nether.debug
local mapgen = nether.mapgen
local S      = nether.get_translator

local BASALT_COLUMN_UPPER_LIMIT = mapgen.BASALT_COLUMN_UPPER_LIMIT
local BASALT_COLUMN_LOWER_LIMIT = mapgen.BASALT_COLUMN_LOWER_LIMIT


-- 2D noise for basalt formations
local np_basalt = {
	offset      =-0.85,
	scale       = 1,
	spread      = {x = 46, y = 46, z = 46},
	seed        = 1000,
	octaves     = 5,
	persistence = 0.5,
	lacunarity  = 2.6,
	flags = "eased"
}


-- Buffers and objects we shouldn't recreate every on_generate

local nobj_basalt = nil
local nbuf_basalt = {}

-- Content ids

local c_air              = minetest.get_content_id("air")
local c_netherrack_deep  = minetest.get_content_id("nether:rack_deep")
local c_glowstone        = minetest.get_content_id("nether:glowstone")
local c_lavasea_source   = minetest.get_content_id("nether:lava_source") -- same as lava but with staggered animation to look better as an ocean
local c_lava_crust       = minetest.get_content_id("nether:lava_crust")
local c_basalt           = minetest.get_content_id("nether:basalt")


-- Math funcs
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

function random_unit_vector()
	return vector.normalize({
		x = math.random() - 0.5,
		y = math.random() - 0.5,
		z = math.random() - 0.5
	})
end

-- returns the smallest component in the vector
function vector_min(v)
	return math_min(v.x, math_min(v.y, v.z))
end


-- Mantle mapgen functions (AKA Center region)

-- Returns (absolute height, fractional distance from ceiling or sea floor)
-- the fractional distance from ceiling or sea floor is a value between 0 and 1 (inclusive)
-- Note it may find the most relevent sea-level - not necesssarily the one you are closest
-- to, since the space above the sea reaches much higher than the depth below the sea.
mapgen.find_nearest_lava_sealevel = function(y)
	-- todo: put oceans near the bottom of chunks to improve ability to generate tunnels to the center
	-- todo: constrain y to be not near the bounds of the nether
	-- todo: add some random adj at each level, seeded only by the level height
	local sealevel = math.floor((y + 100) / 200) * 200
	--local sealevel = math.floor((y + 80) / 160) * 160
	--local sealevel = math.floor((y + 120) / 240) * 240

	local cavern_limits_fraction
	local height_above_sea = y - sealevel
	if height_above_sea >= 0 then
		cavern_limits_fraction = math_min(1, height_above_sea / 95)
	else
		-- approaches 1 much faster as the lava sea is shallower than the cavern above it
		cavern_limits_fraction = math_min(1, -height_above_sea / 40)
	end

	return sealevel, cavern_limits_fraction
end




mapgen.add_basalt_columns = function(data, area, minp, maxp)
	-- Basalt columns are structures found in lava oceans, and the only way to obtain
	-- nether basalt.
	-- Their x, z position is determined by a 2d noise map and a 2d slice of the cave
	-- noise (taken at lava-sealevel).

	local x0, y0, z0 = minp.x, math_max(minp.y, nether.DEPTH_FLOOR),   minp.z
	local x1, y1, z1 = maxp.x, math_min(maxp.y, nether.DEPTH_CEILING), maxp.z

	local yStride = area.ystride
	local yCaveStride = x1 - x0 + 1

	local cavePerlin = mapgen.get_cave_point_perlin()
	nobj_basalt = nobj_basalt or minetest.get_perlin_map(np_basalt, {x = yCaveStride, y = yCaveStride})
	local nvals_basalt = nobj_basalt:get_2d_map_flat({x=minp.x, y=minp.z}, {x=yCaveStride, y=yCaveStride}, nbuf_basalt)

	local nearest_sea_level, _ = mapgen.find_nearest_lava_sealevel(math_floor((y0 + y1) / 2))

	local leeway = mapgen.CENTER_CAVERN_LIMIT * 0.18

	for z = z0, z1 do
		local noise2di = 1 + (z - z0) * yCaveStride

		for x = x0, x1 do

			local basaltNoise = nvals_basalt[noise2di]
			if basaltNoise > 0 then
				-- a basalt column is here

				local abs_sealevel_cave_noise = math_abs(cavePerlin:get_3d({x = x, y = nearest_sea_level, z = z}))

				-- Add Some quick deterministic noise to the column heights
				-- This is probably not good noise, but it doesn't have to be.
				local fastNoise = 17
				fastNoise = 37 * fastNoise + y0
				fastNoise = 37 * fastNoise + z
				fastNoise = 37 * fastNoise + x
				fastNoise = 37 * fastNoise + math_floor(basaltNoise * 32)

				local columnHeight = basaltNoise * 18 + ((fastNoise % 3) - 1)

				-- columns should drop below sealevel where lava rivers are flowing
				-- i.e. anywhere abs_sealevel_cave_noise < BASALT_COLUMN_LOWER_LIMIT
				-- And we'll also have it drop off near the edges of the lava ocean so that
				-- basalt columns can only be found by the player reaching a lava ocean.
				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
				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
				local columnHeightAdj = lowerClip * -upperClip -- all are values between 1 and -1

				columnHeight = columnHeight + math_floor(columnHeightAdj * 12 - 12)

				local vi = area:index(x, y0, z) -- Initial voxelmanip index

				for y = y0, y1 do -- Y loop first to minimise tcave & lava-sea calculations

					if y < nearest_sea_level + columnHeight  then

						local id = data[vi] -- Existing node
						if id == c_lava_crust or id == c_lavasea_source or (id == c_air and y > nearest_sea_level) then
							-- Avoid letting columns extend beyond the central region.
							-- (checking node ids saves having to calculate abs_cave_noise_adjusted here
							-- to test it against CENTER_CAVERN_LIMIT)
							data[vi] = c_basalt
						end
					end

					vi = vi + yStride
				end
			end

			noise2di = noise2di + 1
		end
	end
end


-- returns an array of points from pos1 and pos2 which deviate from a straight line
-- but which don't venture too close to a chunk boundary
function generate_waypoints(pos1, pos2, minp, maxp)

	local segSize = 10
	local maxDeviation = 7
	local minDistanceFromChunkWall = 5

	local pathVec     = vector.subtract(pos2, pos1)
	local pathVecNorm = vector.normalize(pathVec)
	local pathLength  = vector.distance(pos1, pos2)
	local minBound    = vector.add(minp, minDistanceFromChunkWall)
	local maxBound    = vector.subtract(maxp, minDistanceFromChunkWall)

	local result = {}
	result[1] = pos1

	local segmentCount = math_floor(pathLength / segSize)
	for i = 1, segmentCount do
		local waypoint = vector.add(pos1, vector.multiply(pathVec, i / (segmentCount + 1)))

		-- shift waypoint a few blocks in a random direction orthogonally to the pathVec, to make the path crooked.
		local crossProduct
		repeat
			crossProduct = vector.normalize(vector.cross(pathVecNorm, random_unit_vector()))
		until vector.length(crossProduct) > 0
		local deviation = vector.multiply(crossProduct, math.random(1, maxDeviation))
		waypoint = vector.add(waypoint, deviation)
		waypoint = {
			x = math_min(maxBound.x, math_max(minBound.x, waypoint.x)),
			y = math_min(maxBound.y, math_max(minBound.y, waypoint.y)),
			z = math_min(maxBound.z, math_max(minBound.z, waypoint.z))
		}

		result[#result + 1] = waypoint
	end

	result[#result + 1] = pos2
	return result
end


function excavate_pathway(data, area, nether_pos, center_pos, minp, maxp)

	local ystride = area.ystride
	local zstride = area.zstride

	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)
	local dist = math_floor(vector.distance(nether_pos, center_pos))
	local waypoints = generate_waypoints(nether_pos, center_pos, minp, maxp)

	-- First pass: record path details
	local linedata = {}
	local last_pos = {}
	local line_index = 1
	local first_filled_index, boundary_index, last_filled_index
	for i = 0, dist do
		-- Bresenham's line would be good here, but too much lua code
		local waypointProgress = (#waypoints - 1) * i / dist
		local segmentIndex = math_min(math_floor(waypointProgress) + 1, #waypoints - 1) -- from the integer portion of waypointProgress
		local segmentInterp = waypointProgress - (segmentIndex - 1)                     -- the remaining fractional portion
		local segmentStart = waypoints[segmentIndex]
		local segmentVector = vector.subtract(waypoints[segmentIndex + 1], segmentStart)
		local pos = vector.round(vector.add(segmentStart, vector.multiply(segmentVector, segmentInterp)))

		if not vector.equals(pos, last_pos) then
			local vi = area:indexp(pos)
			local node_id = data[vi]
			linedata[line_index] = {
				pos = pos,
				vi = vi,
				node_id = node_id
			}
			if boundary_index == nil and node_id == c_netherrack_deep then
				boundary_index = line_index
			end
			if node_id == c_air then
				if boundary_index ~= nil and last_filled_index == nil then
					last_filled_index = line_index
				end
			else
				if first_filled_index == nil then
					first_filled_index = line_index
				end
			end
			line_index = line_index + 1
			last_pos = pos
		end
	end
	first_filled_index = first_filled_index or 1
	last_filled_index  = last_filled_index  or #linedata
	boundary_index     = boundary_index     or last_filled_index


	-- limit tunnel radius to roughly the closest that startPos or stopPos comes to minp-maxp, so we
	-- don't end up exceeding minp-maxp and having excavation filled in when the next chunk is generated.
	local startPos, stopPos = linedata[first_filled_index].pos, linedata[last_filled_index].pos
	local radiusLimit = vector_min(vector.subtract(startPos, minp))
	radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(stopPos, minp)))
	radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(maxp, startPos)))
	radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(maxp, stopPos)))

	if radiusLimit < 4 then -- This is a logic check, ignore it. It could be commented out
		-- 4 is (79 - 75), and values less than 4 shouldn't be possible if sampling-skip was 10
		-- 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
		debugf("Error: radiusLimit %s is smaller then half the sampling distance. min %s, max %s, start %s, stop %s", radiusLimit, minp, maxp, startPos, stopPos)
	end
	radiusLimit = radiusLimit + 1 -- chunk walls wont be visibly flat if the radius only exceeds it a little ;)

	-- Second pass: excavate
	local start_index, stop_index = math_max(1, first_filled_index - 2), math_min(#linedata, last_filled_index + 3)
	for i = start_index, stop_index, 3 do

		-- Adjust radius so that tunnels start wide but thin out in the middle
		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
		-- Have it more flaired at the ends, rather than linear.
		-- i.e. sizeAdj approaches 1 quickly as distFromEnds increases
		local distFromMiddle = 1 - distFromEnds
		local sizeAdj = 1 - (distFromMiddle * distFromMiddle * distFromMiddle)

		local radius = math_min(radiusLimit, math.random(50 - (25 * sizeAdj), 80 - (45 * sizeAdj)) / 10)
		local radiusSquared = radius * radius
		local radiusCeil = math_floor(radius + 0.5)

		linedata[i].radius = radius             -- Needed in third pass
		linedata[i].distFromEnds = distFromEnds -- Needed in third pass

		local vi = linedata[i].vi
		for z = -radiusCeil, radiusCeil do
			local vi_z = vi + z * zstride
			for y = -radiusCeil, radiusCeil do
				local vi_zy = vi_z + y * ystride
				local xSquaredLimit = radiusSquared - (z * z + y * y)
				for x = -radiusCeil, radiusCeil do
					if x * x < xSquaredLimit then
						data[vi_zy + x] = c_air
					end
				end
			end
		end

	end

	-- Third pass: decorate
	-- Add glowstones to make tunnels to the mantle easier to find
	-- https://i.imgur.com/sRA28x7.jpg
	for i = start_index, stop_index, 3 do
		if linedata[i].distFromEnds < 0.3 then
			local glowcount = 0
			local radius = linedata[i].radius
			for _ = 1, 20 do
				local testPos = vector.round(vector.add(linedata[i].pos, vector.multiply(random_unit_vector(), radius + 0.5)))
				local vi = area:indexp(testPos)
				if data[vi] ~= c_air then
					data[vi] = c_glowstone
					glowcount = glowcount + 1
				--else
				--	data[vi] = c_debug
				end
				if glowcount >= 2 then break end
			end
		end
	end

end


-- excavates a tunnel connecting the Primary or Secondary region with the mantle / central region
-- if a suitable path is found.
-- Returns true if successful
mapgen.excavate_tunnel_to_center_of_the_nether = function(data, area, nvals_cave, minp, maxp)

	local result = false
	local extent = vector.subtract(maxp, minp)
	local skip = 10 -- sampling rate of 1 in 10

	local highest = -1000
	local lowest  =  1000
	local lowest_vi
	local highest_vi

	local yCaveStride = maxp.x - minp.x + 1
	local zCaveStride = yCaveStride * yCaveStride

	local vi_offset = area:indexp(vector.add(minp, math_floor(skip / 2))) -- start half the sampling distance away from minp
	local vi, ni

	for y = 0, extent.y - 1, skip do
		local sealevel = mapgen.find_nearest_lava_sealevel(minp.y + y)

		if minp.y + y > sealevel then -- only create tunnels above sea level
			for z = 0, extent.z - 1, skip do

				vi = vi_offset + y * area.ystride + z * area.zstride
				ni = z * zCaveStride + y * yCaveStride + 1
				for x = 0, extent.x - 1, skip do

					local noise = math_abs(nvals_cave[ni])
					if noise < lowest then
						lowest = noise
						lowest_vi = vi
					end
					if noise > highest then
						highest = noise
						highest_vi = vi
					end
					ni = ni + skip
					vi = vi + skip
				end
			end
		end
	end

	if lowest < mapgen.CENTER_CAVERN_LIMIT and highest > mapgen.TCAVE + 0.03 then

		local mantle_y = area:position(lowest_vi).y
		local _, cavern_limit_distance = mapgen.find_nearest_lava_sealevel(mantle_y)
		local _, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(mantle_y)

		 -- cavern_noise_adj gets added to noise value instead of added to the limit np_noise
		 -- is compared against, so subtract centerRegionLimit_adj instead of adding
		local cavern_noise_adj =
			mapgen.CENTER_REGION_LIMIT * (cavern_limit_distance * cavern_limit_distance * cavern_limit_distance) -
			centerRegionLimit_adj

		if lowest + cavern_noise_adj < mapgen.CENTER_CAVERN_LIMIT then
			excavate_pathway(data, area, area:position(highest_vi), area:position(lowest_vi), minp, maxp)
			result = true
		end
	end
	return result
end


-- an enumerated list of the different regions in the nether
mapgen.RegionEnum = {
	OVERWORLD     = {name = "overworld",      desc = S("The Overworld") },   -- Outside the Nether / none of the regions in the Nether
	POSITIVE      = {name = "positive",       desc = S("Positive nether") }, -- The classic nether caverns are here - where cavePerlin > 0.6
	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
	CENTER        = {name = "center",         desc = S("Center/Mantle, inside cavern") },
	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
	NEGATIVE      = {name = "negative",       desc = S("Negative nether") }, -- Secondary/spare region - where cavePerlin < -0.6
	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
}


-- Returns (region, noise) where region is a value from mapgen.RegionEnum
-- and noise is the unadjusted cave perlin value
mapgen.get_region = function(pos)

	if pos.y > nether.DEPTH_CEILING or pos.y < nether.DEPTH_FLOOR then
		return mapgen.RegionEnum.OVERWORLD, nil
	end

	local caveNoise = mapgen.get_cave_perlin_at(pos)
	local sealevel, cavern_limit_distance = mapgen.find_nearest_lava_sealevel(pos.y)
	local tcave_adj, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(pos.y)
	local tcave   = mapgen.TCAVE + tcave_adj
	local tmantle = mapgen.CENTER_REGION_LIMIT + centerRegionLimit_adj

	-- cavern_noise_adj gets added to noise value instead of added to the limit np_noise
	-- is compared against, so subtract centerRegionLimit_adj instead of adding
	local cavern_noise_adj =
		mapgen.CENTER_REGION_LIMIT * (cavern_limit_distance * cavern_limit_distance * cavern_limit_distance) -
		centerRegionLimit_adj

	local region
	if caveNoise > tcave then
		region = mapgen.RegionEnum.POSITIVE
	elseif -caveNoise > tcave then
		region = mapgen.RegionEnum.NEGATIVE
	elseif math_abs(caveNoise) < tmantle then

		if math_abs(caveNoise) + cavern_noise_adj < mapgen.CENTER_CAVERN_LIMIT then
			region = mapgen.RegionEnum.CENTER
		else
			region = mapgen.RegionEnum.CENTERSHELL
		end

	elseif caveNoise > 0 then
		region = mapgen.RegionEnum.POSITIVESHELL
	else
		region = mapgen.RegionEnum.NEGATIVESHELL
	end

	return region, caveNoise
end


minetest.register_chatcommand("nether_whereami",
	{
		description = S("Describes which region of the nether the player is in"),
		privs = {debug = true},
		func = function(name, param)

			local player = minetest.get_player_by_name(name)
			if player == nil then return false, S("Unknown player position") end
			local playerPos = vector.round(player:get_pos())

			local region, caveNoise                = mapgen.get_region(playerPos)
			local seaLevel, cavernLimitDistance    = mapgen.find_nearest_lava_sealevel(playerPos.y)
			local tcave_adj, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(playerPos.y)

			local seaDesc = ""
			local boundaryDesc = ""
			local perlinDesc = ""

			if region ~= mapgen.RegionEnum.OVERWORLD then

				local seaPos = playerPos.y - seaLevel
				if seaPos > 0 then
					seaDesc = S(", @1m above lava-sea level", seaPos)
				else
					seaDesc = S(", @1m below lava-sea level", seaPos)
				end

				if tcave_adj > 0 then
					boundaryDesc = S(", approaching y boundary of Nether")
				end

				perlinDesc = S("[Perlin @1] ", (math_floor(caveNoise * 1000) / 1000))
			end

			return true, S("@1@2@3@4", perlinDesc, region.desc, seaDesc, boundaryDesc)
		end
	}
)