many worldgen mods and gonna try technic now
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,41 @@
|
||||
# Compiled Lua sources
|
||||
luac.out
|
||||
|
||||
# luarocks build files
|
||||
*.src.rock
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.os
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
*.def
|
||||
*.exp
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
NOTE: several files in this mod were derived from Minetest code that is licensed under the GNU Lesser General Public License version 2.1 or later. These files have headers indicating the specific licensing of that file's code.
|
||||
|
||||
The rest of this mod is licensed under the MIT license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 FaceDeer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,177 @@
|
||||
Mapgen Helper
|
||||
=============
|
||||
|
||||
This mod provides a library of methods intended for use with other Minetest mapgen mods, automating routine tasks and giving tools to more easily perform mapgen operations. It's a bit of a random grab-bag, I put methods in here when I find that I use them frequently in other mods or when I just did a bunch of work solving a specific problem that could potentially be reused in future mods.
|
||||
|
||||
Part of the purpose of this mod is to make sure these methods are as efficient as possible, allowing them to be used in mapgens or ABMs or other such contexts where speed is of the essence or where the code will be executed many millions of times. Several of these methods may seem trivial to accomplish already by using a combination of existing methods (for example `VoxelArea:get_y(vi)` can already be accomplished with `VoxelArea:position(i).y`) but in this mod they've been written to eliminate unnecessary intermediate steps and unused calculations.
|
||||
|
||||
## Voxel Manipulator methods
|
||||
|
||||
* `mapgen_helper.mapgen_vm_data()`
|
||||
* returns vm, data, area
|
||||
* Almost every mapgen mod does this operation first. This method retrieves the Voxel Manipulator for the generated map block, the voxel data array (automatically using a buffer internal to mapgen_helper to store it), and a VoxelArea for indexing the data array.
|
||||
* Note that you still need to write the data array to the voxel manipulator at the end of map generation.
|
||||
|
||||
* `mapgen_helper.mapgen_vm_data_param2()`
|
||||
* returns vm, data, param2_data, area
|
||||
* Almost identical to the previous method, this one also returns the param2 data array.
|
||||
|
||||
# VoxelArea methods
|
||||
|
||||
These methods are added to the existing VoxelArea LUA class.
|
||||
|
||||
* `VoxelArea:iterp_xyz(minp, maxp)`
|
||||
* Returns an iterator that gives not just the voxel index (vi), but also the x, y and z coordinates it represents. This lets you perform actions in your mapgen loop based on x, y and z coordinates without having to look them up from the voxel index. For example:
|
||||
`for vi, x, y, z in area:iterp_xyz(minp, maxp) do`
|
||||
|
||||
* `VoxelArea:iter_xyz(minx, miny, minz, maxx, maxy, maxz)`
|
||||
* The same as above, but takes individual coordinate arguments instead of points.
|
||||
|
||||
* `VoxelArea:iterp_yxz(minp, maxp)`
|
||||
* Similar to the above, but the order of iteration is changed. Using this iterator, indices are visited in the +y direction along "columns". This is handy for mapgens that want to build stuff based on the nodes that are "below" the current node, for example if you're scanning through the map block looking for a walkable surface to place something on.
|
||||
|
||||
* `VoxelArea:iter_yxz(minx, miny, minz, maxx, maxy, maxz)`
|
||||
* Same as above, but takes individual coordinate arguments instead of points.
|
||||
|
||||
* `VoxelArea:get_y(vi)`
|
||||
* Returns the absolute y coordinate of the index provided. Useful for mapgens that want to do something at a specific elevation.
|
||||
|
||||
* `VoxelArea:transform(area, vi)`
|
||||
* Takes a VoxelArea and an index into that VoxelArea as parameters returns an index into its own VoxelArea, or nil if it's not in the voxelarea. This is useful when you've got two 3d data arrays that cover different volumes and would like to "map" values from one into the other. For example, if you've generated a 3d noise array using the mapgen's minp and maxp and want to map it onto the voxelmanipulator's emin and emax data array.
|
||||
|
||||
## Biomes
|
||||
|
||||
* `mapgen_helper.get_biome_def(biome_id)`
|
||||
* Given an id from the biome map, returns a biome definition. Normally translating a biome_id into a biome definition requires several steps and lookups, this method caches the intermediate results internal to mapgen_helper to speed up the operation.
|
||||
|
||||
* `mapgen_helper.get_biome_def_i(biomemap, minp, maxp, area, vi)`
|
||||
* When iterating through a voxelmanip's data array it is common to want to check what biome a given index belongs to. This method automates that lookup. Give it the biomemap that was provided by get_mapgen_object("biomemap"), the minp and maxp parameters from register_on_generated, the VoxelArea representing the generated map chunk, and the voxel index, and this method returns the biome_def that that voxel index lies inside.
|
||||
|
||||
## Node definitions
|
||||
|
||||
* `mapgen_helper.buildable_to(c_node)`
|
||||
*Given a node id, returns whether that node's definition is "buildable_to"
|
||||
|
||||
* `mapgen_helper.is_ground_content(c_node)`
|
||||
* Given a node id, returns whether that node's definition is "ground_content"
|
||||
|
||||
## Noise manager
|
||||
|
||||
* `mapgen_helper.perlin3d(name, minp, maxp, perlin_params)`
|
||||
* Returns nvals_perlin, area
|
||||
* Efficient generation and use of 3d perlin noise requires a bunch of steps that this method automates. It uses the "name" parameter to allocate buffers and generator objects in a table stored internally by mapgen_helper, if you're using multiple different noise fields in the same scope make sure to give them different names.
|
||||
* The perlin_params argument is actually optional if you've already made a call to this method (or used the 'mapgen_helper.register_perlin' method below), the perlin parameters are stored when provided and can be looked back up again by mapgen_helper.
|
||||
* The second return value is a VoxelArea for minp, maxp.
|
||||
|
||||
* `mapgen_helper.perlin2d(name, minp, maxp, perlin_params)`
|
||||
* Similar to the above method but for generating 2d noise. This method only returns the data array, it does not return a VoxelArea.
|
||||
|
||||
* `mapgen_helper.register_perlin(name, perlin_params)`
|
||||
* Pre-registering a perlin_params table is optional, any call to one of the above perlin methods with perlin_params provided will override the parameters registered here.
|
||||
|
||||
## 2D array indexing
|
||||
|
||||
* `mapgen_helper.index2d(minp, maxp, x, z)`
|
||||
* Returns an index for getting values out of 2d arrays (such as heightmap, biomemap, heatmap, arrays generated by perlin2d, etc) given x and z coordinates.
|
||||
* minp and maxp must be provided, these are the extents that were used when generating the 2d array.
|
||||
|
||||
* `mapgen_helper.index2dp(minp, maxp, pos)`
|
||||
* Version of the above that takes a pos instead of separate coordinates. pos.y is ignored.
|
||||
|
||||
* `mapgen_helper.index2di(minp, maxp, area, vi)`
|
||||
* Version of the above that indexes a 2d array based on the index in a 3d array.
|
||||
|
||||
## Schematics
|
||||
|
||||
*NOTE:* These methods operate on the LUA table format of schematic. To convert a .mts file into its lua representation, use `minetest.deserialize(minetest.serialize_schematic(schematic, "lua"))` to convert an .mts into a lua table.
|
||||
|
||||
* `mapgen_helper.get_schematic_bounding_box(pos, schematic, rotation, flags)`
|
||||
* Returns the minpos and maxpos of the bounding box that this schematic will be placed in given the rotation and flag parameters. Useful for testing whether a schematic will fit in a place before actually writing it to the data.
|
||||
|
||||
* `mapgen_helper.place_schematic_on_data(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)`
|
||||
* Takes a lua-format schematic and applies it to the data and param2_data arrays produced by vmanip instead of being applied to the vmanip directly. Useful in a mapgen loop that's doing other things with the data before and after applying schematics. A VoxelArea for the data also needs to be provided.
|
||||
* *NOTE:* As of the writing of this documentation, this method is still somewhat of a work-in-progress. It only supports the table version of the `flags` parameter (eg, `{["place_center_x"] = true}`), and doesn't support rotating nodes of paramtype2 `wallmounted`, `colorfacedir`, or `colorwallmounted`. Support is planned in the near future.
|
||||
|
||||
* This method has some additional features that the existing minetest schematic-placement API doesn't support:
|
||||
* node defs can have a "`place_on_condition(c_node)`" property defined. This is a function that is given the node content ID of the node the schematic is about to replace and returns true to indicate the schematic should replace it or false to indicate it should not. Useful, for example, in a schematic that should replace water but not earth (placing decorations on the bottom of the ocean), or a schematic that replaces all buildable_to nodes (to prevent tufts of grass from knocking holes in foundations).
|
||||
* The schematic can have a "`center_pos`" position defined relative to the schematic's minpos that will be treated as the rotation and placement origin, unless overridden by the flags parameter.
|
||||
* Returns true if the schematic was entirely contained within the given voxelarea, false otherwise.
|
||||
|
||||
## Bounding boxes
|
||||
|
||||
* `mapgen_helper.is_within_distance_box(pos1, pos2, distance)`
|
||||
* A very cheap nearness test using Manhattan distance, useful as a fast first-pass check to see whether a more math-heavy test is needed.
|
||||
|
||||
* `mapgen_helper.intersect(minpos1, maxpos1, minpos2, maxpos2)`
|
||||
* Given two bounding boxes, returns a pair of points representing the minpos and maxpos of the intersection between them. If no intersection exists it returns nil.
|
||||
|
||||
* `mapgen_helper.intersect_exists(minpos1, maxpos1, minpos2, maxpos2)`
|
||||
* A cheaper version of the above method that only returns a true/false indication that an intersection exists.
|
||||
|
||||
* `mapgen_helper.intersect_exists_xz(minpos1, maxpos1, minpos2, maxpos2)`
|
||||
* An _even cheaper_ version of the above method that returns true/false based on whether the two bounding boxes overlap on the x-z plane.
|
||||
|
||||
## Lines
|
||||
|
||||
* `mapgen_helper.distance_to_segment(pos, start, end)`
|
||||
* Returns the minimum distance from the point "pos" to any point along the line segment defined by the points "start" and "end"
|
||||
|
||||
* `mapgen_helper.distance_to_path(pos, path)`
|
||||
* Similar to the above, but "path" is a list of points forming a continuous series of line segments. Distance returned is the shortest distance to any segment in the list.
|
||||
|
||||
## Random values
|
||||
|
||||
* `mapgen_helper.get_random_points(minp, maxp, min_output_size, max_output_size)`
|
||||
* Returns a consistent list of random points within a volume. Each call to this method will give the same set of points if the same parameters are provided, making this useful for placing things that extend outside of the currently-generated map chunk - for example, minp and maxp could be kilometers apart and the points designate the locations where mountains are to be placed.
|
||||
|
||||
* `mapgen_helper.xz_consistent_randomp(pos)'
|
||||
* Returns a random value based on the x and z coordinates of pos. The random value is always the same for the same x and z.
|
||||
|
||||
* `mapgen_helper.xz_consistent_randomi(area, vi)`
|
||||
* Same as above, but takes a VoxelArea and a voxel index as parameters.
|
||||
|
||||
## Region tiling
|
||||
|
||||
These methods are useful with things like `mapgen_helper.get_random_points`, allowing you to seamlessly place items that cross mapchunk boundaries by generating sets of random points for all adjacent regions.
|
||||
|
||||
* `mapgen_helper.get_region_minp_xz(pos_xz, mapblocks)`
|
||||
* Returns a consistent large region that doesn't cross mapgen block boundaries. Regions are `mapblocks * chunksize` nodes in width and depth. Ignores the y coordinate.
|
||||
|
||||
* `mapgen_helper.get_nearest_regions(pos_xz, gridscale)`
|
||||
|
||||
* Given a position and a region scale, returns the minp corners of the four regions that the player is closest to.
|
||||
* For example, if we have a region scale of 6 and the parameter position is at "*", the four points marked "R" would be returned:
|
||||
|
||||
|-----|-----|-----| |-----|-----|-----| |-----|-----|-----|
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
|-----|-----|-----| |-----|-----|-----| |-----R-----R-----|
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | *| |
|
||||
| | | | | | | | | | | |
|
||||
| | * | | | | *| | | | | |
|
||||
| | | | | | | | | | | |
|
||||
R-----R-----|-----| |-----R-----R-----| |-----R-----R-----|
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
| | | | | | | | | | | |
|
||||
R-----R-----|-----| |-----R-----R-----| |-----|-----|-----|
|
||||
|
||||
* `mapgen_helper.get_adjacent_regions(pos_xz, gridscale)`
|
||||
* Similar to the above, but this method returns all adjacent regions and not just the nearest ones:
|
||||
|
||||
|---|---|---|
|
||||
| | | |
|
||||
| | | |
|
||||
R---R---R---|
|
||||
| | | |
|
||||
| | *| |
|
||||
R---R---R---|
|
||||
| | | |
|
||||
| | | |
|
||||
R---R---R---|
|
||||
@@ -0,0 +1,54 @@
|
||||
-- A cheap nearness test, using Manhattan distance.
|
||||
mapgen_helper.is_within_distance_box = function(pos1, pos2, distance)
|
||||
return math.abs(pos1.x-pos2.x) <= distance and
|
||||
math.abs(pos1.y-pos2.y) <= distance and
|
||||
math.abs(pos1.z-pos2.z) <= distance
|
||||
end
|
||||
|
||||
-- Finds an intersection between two axis-aligned bounding boxes (AABB)s, or nil if there's no overlap
|
||||
mapgen_helper.intersect = function(minpos1, maxpos1, minpos2, maxpos2)
|
||||
--checking x and z first since they're more likely to fail to overlap
|
||||
if minpos1.x <= maxpos2.x and maxpos1.x >= minpos2.x and
|
||||
minpos1.z <= maxpos2.z and maxpos1.z >= minpos2.z and
|
||||
minpos1.y <= maxpos2.y and maxpos1.y >= minpos2.y then
|
||||
|
||||
return {
|
||||
x = math.max(minpos1.x, minpos2.x),
|
||||
y = math.max(minpos1.y, minpos2.y),
|
||||
z = math.max(minpos1.z, minpos2.z)
|
||||
},
|
||||
{
|
||||
x = math.min(maxpos1.x, maxpos2.x),
|
||||
y = math.min(maxpos1.y, maxpos2.y),
|
||||
z = math.min(maxpos1.z, maxpos2.z)
|
||||
}
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- a simpler version of the above if all you care about is whether an intersection exists
|
||||
mapgen_helper.intersect_exists = function(minpos1, maxpos1, minpos2, maxpos2)
|
||||
--checking x and z first since they're more likely to fail to overlap
|
||||
return (minpos1.x <= maxpos2.x and maxpos1.x >= minpos2.x and
|
||||
minpos1.z <= maxpos2.z and maxpos1.z >= minpos2.z and
|
||||
minpos1.y <= maxpos2.y and maxpos1.y >= minpos2.y)
|
||||
end
|
||||
|
||||
-- A version of the above that only cares about the x and z parameters, useful for example to lay out non-overlapping trees or buildings
|
||||
mapgen_helper.intersect_exists_xz = function(minpos1, maxpos1, minpos2, maxpos2)
|
||||
return (minpos1.x <= maxpos2.x and maxpos1.x >= minpos2.x and
|
||||
minpos1.z <= maxpos2.z and maxpos1.z >= minpos2.z)
|
||||
end
|
||||
|
||||
-- Simply tests whether pos is within the bounding box defined by minpos and maxpos
|
||||
local intersect_exists = mapgen_helper.intersect_exists
|
||||
mapgen_helper.is_pos_within_box = function(pos, minpos, maxpos)
|
||||
return intersect_exists(pos, pos, minpos, maxpos)
|
||||
end
|
||||
|
||||
-- Tests whether box 1 is entirely contained within box 2
|
||||
mapgen_helper.is_box_within_box = function(minpos1, maxpos1, minpos2, maxpos2)
|
||||
return (minpos1.x >= minpos2.x and maxpos1.x <= maxpos2.x and
|
||||
minpos1.z >= minpos2.z and maxpos1.z <= maxpos2.z and
|
||||
minpos1.y >= minpos2.y and maxpos1.y <= maxpos2.y)
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
mapgen_helper = {}
|
||||
|
||||
mapgen_helper.mapgen_seed = tonumber(minetest.get_mapgen_setting("seed"))
|
||||
|
||||
-- The "sidelen" used in almost every mapgen
|
||||
mapgen_helper.block_size = tonumber(minetest.get_mapgen_setting("chunksize")) * 16
|
||||
|
||||
local MP = minetest.get_modpath(minetest.get_current_modname())
|
||||
dofile(MP.."/bounding_boxes.lua")
|
||||
dofile(MP.."/random.lua")
|
||||
dofile(MP.."/voxelarea_iterator.lua")
|
||||
dofile(MP.."/voxeldata.lua")
|
||||
dofile(MP.."/region_functions.lua")
|
||||
dofile(MP.."/lines.lua")
|
||||
dofile(MP.."/place_schematic.lua")
|
||||
dofile(MP.."/noise_manager.lua")
|
||||
dofile(MP.."/metrics.lua")
|
||||
dofile(MP.."/log_location.lua")
|
||||
|
||||
local map_data = {}
|
||||
local map_param2_data = {}
|
||||
|
||||
mapgen_helper.mapgen_vm_data = function()
|
||||
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
|
||||
vm:get_data(map_data)
|
||||
return vm, map_data, VoxelArea:new{MinEdge=emin, MaxEdge=emax}
|
||||
end
|
||||
|
||||
mapgen_helper.mapgen_vm_data_param2 = function()
|
||||
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
|
||||
vm:get_data(map_data)
|
||||
vm:get_param2_data(map_param2_data)
|
||||
return vm, map_data, map_param2_data, VoxelArea:new{MinEdge=emin, MaxEdge=emax}
|
||||
end
|
||||
|
||||
mapgen_helper.biome_defs = nil
|
||||
mapgen_helper.get_biome_def = function(biome_id) -- given an id from the biome map, returns a biome definition.
|
||||
if mapgen_helper.biome_defs == nil then
|
||||
-- First time this was asked for, populate the table.
|
||||
-- Biome registration is only done at load time so we don't have to worry about new biomes invalidating this table
|
||||
mapgen_helper.biome_defs = {}
|
||||
for name, desc in pairs(minetest.registered_biomes) do
|
||||
local i = minetest.get_biome_id(desc.name)
|
||||
mapgen_helper.biome_defs[i] = desc
|
||||
end
|
||||
end
|
||||
return mapgen_helper.biome_defs[biome_id]
|
||||
end
|
||||
|
||||
mapgen_helper.get_biome_def_i = function(biomemap, minp, maxp, area, vi)
|
||||
if biomemap == nil then
|
||||
return nil
|
||||
end
|
||||
local index2d = mapgen_helper.index2di(minp, maxp, area, vi)
|
||||
return mapgen_helper.get_biome_def(biomemap[index2d])
|
||||
end
|
||||
|
||||
|
||||
-- returns whether a content ID is "buildable to"
|
||||
local buildable_to
|
||||
mapgen_helper.buildable_to = function(c_node)
|
||||
if buildable_to then return buildable_to[c_node] end
|
||||
buildable_to = {}
|
||||
for k, v in pairs(minetest.registered_nodes) do
|
||||
if v.buildable_to then
|
||||
buildable_to[minetest.get_content_id(k)] = true
|
||||
end
|
||||
end
|
||||
return buildable_to[c_node]
|
||||
end
|
||||
|
||||
local is_ground_content = nil
|
||||
mapgen_helper.is_ground_content = function(c_node) -- If false, the cave generator will not carve through this node
|
||||
if is_ground_content ~= nil then
|
||||
return is_ground_content[c_node]
|
||||
end
|
||||
is_ground_content = {}
|
||||
for k, v in pairs(minetest.registered_nodes) do
|
||||
if v.is_ground_content == true then
|
||||
is_ground_content[minetest.get_content_id(k)] = true
|
||||
end
|
||||
end
|
||||
return is_ground_content[c_node]
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
-- algorithms taken from https://gist.github.com/reciprocum/4e3599a9563ec83ba2a63f5a6cdd39eb
|
||||
|
||||
local dot = function(a, b)
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
end
|
||||
|
||||
local modulus = function(v)
|
||||
return math.sqrt(dot(v,v))
|
||||
end
|
||||
|
||||
local cross = function(a, b)
|
||||
return {x= a.y * b.z - a.z * b.y,
|
||||
y= a.z * b.x - a.x * b.z,
|
||||
z= a.x * b.y - a.y * b.x}
|
||||
end
|
||||
|
||||
-- TODO: add bounding box tests to early-out on distant segments
|
||||
|
||||
-- Calculates the distance from a point to a line segment.
|
||||
-- v - the point
|
||||
-- a - start of line segment
|
||||
-- b - end of line segment
|
||||
|
||||
mapgen_helper.distance_to_segment = function(v, a, b)
|
||||
local ab = vector.subtract(b, a)
|
||||
local av = vector.subtract(v, a)
|
||||
|
||||
if dot(av, ab) <= 0.0 then -- Point is lagging behind start of the segment, so perpendicular distance is not viable.
|
||||
return modulus(av) -- Use distance to start of segment instead.
|
||||
end
|
||||
|
||||
local bv = vector.subtract(v, b)
|
||||
|
||||
if dot(bv, ab) >= 0.0 then -- Point is advanced past the end of the segment, so perpendicular distance is not viable.
|
||||
return modulus(bv) -- Use distance to end of the segment instead.
|
||||
end
|
||||
|
||||
return modulus( cross(ab, av )) / modulus(ab) -- Perpendicular distance of point to segment.
|
||||
end
|
||||
|
||||
-- Calculates the euclidean distance from a point to a line segmented path.
|
||||
-- v - the point from with the distance is measured
|
||||
-- path - the array of points which, when sequentialy joined by line segments, form a path
|
||||
-- returns the distance from v to the closest of the path forming line segments
|
||||
|
||||
mapgen_helper.distance_to_path = function(v, path)
|
||||
local minimum_distance = 2^32 -- Should be big enough for any practical minetest needs
|
||||
for i = 2, table.getn(path) do
|
||||
local distance = mapgen_helper.distance_to_segment(path[i-1], path[i])
|
||||
if (distance < minimum_distance) then
|
||||
minimum_distance = distance
|
||||
end
|
||||
end
|
||||
return minimum_distance
|
||||
end
|
||||
|
||||
|
||||
-- For digging out straight lines
|
||||
|
||||
local dist_sq = function(x1, y1, z1, x2, y2, z2)
|
||||
return (x2-x1)^2 + (y2-y1)^2 + (z2-z1)^2
|
||||
end
|
||||
local constrain = function(n, min_n, max_n)
|
||||
if n < min_n then return min_n
|
||||
elseif n > max_n then return max_n
|
||||
else return n end
|
||||
end
|
||||
local dist_to_segment_squared = function(px, py, pz, lx1, ly1, lz1, lx2, ly2, lz2)
|
||||
local line_dist = dist_sq(lx1, ly1, lz1, lx2, ly2, lz2)
|
||||
if (line_dist == 0) then
|
||||
return dist_sq(px, py, pz, lx1, ly1, lz1)
|
||||
end
|
||||
local t = ((px - lx1) * (lx2 - lx1) + (py - ly1) * (ly2 - ly1) + (pz - lz1) * (lz2 - lz1)) / line_dist
|
||||
t = constrain(t, 0, 1)
|
||||
return dist_sq(px, py, pz, lx1 + t * (lx2 - lx1), ly1 + t * (ly2 - ly1), lz1 + t * (lz2 - lz1))
|
||||
end
|
||||
@@ -0,0 +1,123 @@
|
||||
if not minetest.settings:get_bool("mapgen_helper_log_locations", false) then
|
||||
mapgen_helper.log_first_location = function()
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
mapgen_helper.log_location_enabled = true
|
||||
|
||||
local worldpath = minetest.get_worldpath()
|
||||
local locations = {}
|
||||
|
||||
local save_data = function()
|
||||
local file = io.open(worldpath.."/mapgen_helper_test_locations.json", "w")
|
||||
if file then
|
||||
local data = {}
|
||||
data.locations = locations
|
||||
file:write(minetest.serialize(data))
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
local read_data = function()
|
||||
local file = io.open(worldpath.."/mapgen_helper_test_locations.json", "r")
|
||||
if file then
|
||||
local data = minetest.deserialize(file:read("*all"))
|
||||
locations = data.locations or {}
|
||||
-- to upgrade legacy data that didn't have counts recorded
|
||||
for _, location in pairs(locations) do
|
||||
location.count = location.count or 1
|
||||
end
|
||||
file:close()
|
||||
else
|
||||
locations = {}
|
||||
end
|
||||
end
|
||||
read_data()
|
||||
|
||||
-- logs only the first location of a particular type, ignores any subsequent ones
|
||||
mapgen_helper.log_first_location = function(name, pos, desc, notes)
|
||||
if not locations[name] then
|
||||
locations[name] = {pos=pos, desc=desc, notes=notes, count = 1}
|
||||
minetest.log("info", "[mapgen_helper] recorded location " .. name .. " at " .. minetest.pos_to_string(pos) .. " with desc '" .. (desc or "") .. "'")
|
||||
save_data()
|
||||
end
|
||||
end
|
||||
|
||||
mapgen_helper.log_location = function(name, pos, desc, notes)
|
||||
local location = locations[name] or {pos=pos, desc=desc, notes=notes, count = 0}
|
||||
locations[name] = location
|
||||
location.pos = pos
|
||||
location.desc = desc
|
||||
location.notes = notes
|
||||
location.count = location.count + 1
|
||||
end
|
||||
|
||||
|
||||
minetest.register_chatcommand("mapgen_helper_loc", {
|
||||
params = "[location name]",
|
||||
description = "sends the player to the named test location, or lists recorded locations if no parameter is given",
|
||||
privs = {server=true},
|
||||
func = function(name, param)
|
||||
if not param or param == "" then
|
||||
minetest.chat_send_player(name,"test locations available:")
|
||||
local names = {}
|
||||
for loc_name, loc in pairs(locations) do
|
||||
table.insert(names, loc_name)
|
||||
end
|
||||
table.sort(names)
|
||||
for number, loc_name in ipairs(names) do
|
||||
minetest.chat_send_player(name, "\t"..loc_name .. " - " .. (locations[loc_name].desc or "") .. " - " .. tostring(locations[loc_name].count))
|
||||
end
|
||||
return
|
||||
end
|
||||
local loc = locations[param]
|
||||
if not loc then
|
||||
minetest.chat_send_all("test location " .. param .. " not found")
|
||||
return
|
||||
end
|
||||
local player = minetest.get_player_by_name(name)
|
||||
player:set_pos(loc.pos)
|
||||
if loc.notes then
|
||||
minetest.chat_send_player(name, loc.notes)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
local tour_index = 0
|
||||
minetest.register_chatcommand("mapgen_helper_tour", {
|
||||
params = "<next|prev>",
|
||||
description = "cycles through the various recorded mapgen test locations, forward or backward",
|
||||
privs = {server=true},
|
||||
func = function(name, param)
|
||||
local names = {}
|
||||
for name, loc in pairs(locations) do
|
||||
table.insert(names, name)
|
||||
end
|
||||
table.sort(names)
|
||||
if param == "prev" then
|
||||
tour_index = tour_index - 1
|
||||
if tour_index < 1 then
|
||||
tour_index = #names
|
||||
end
|
||||
else
|
||||
tour_index = tour_index + 1
|
||||
if tour_index > #names then
|
||||
tour_index = 1
|
||||
end
|
||||
end
|
||||
|
||||
local loc = locations[names[tour_index]]
|
||||
local player = minetest.get_player_by_name(name)
|
||||
player:set_pos(loc.pos)
|
||||
minetest.chat_send_player(name, "Teleport to test location " .. names[tour_index])
|
||||
if loc.notes then
|
||||
minetest.chat_send_player(name, loc.notes)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
minetest.register_on_shutdown(function()
|
||||
save_data()
|
||||
end)
|
||||
@@ -0,0 +1,113 @@
|
||||
if not minetest.settings:get_bool("mapgen_helper_record_time", false) then
|
||||
mapgen_helper.record_time = function()
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local worldpath = minetest.get_worldpath()
|
||||
|
||||
local persist = minetest.settings:get_bool("mapgen_helper_persist_recorded_time", false)
|
||||
local filename = "mapgen_helper_metrics.lua"
|
||||
local filename_old = "mapgen_helper_metrics_old.lua"
|
||||
|
||||
local resolution = minetest.settings:get("mapgen_helper_record_time_resolution") or 0.1
|
||||
|
||||
local data = {}
|
||||
data.times = {}
|
||||
|
||||
if persist then
|
||||
local read_data = loadfile(worldpath.."/"..filename)
|
||||
if read_data then
|
||||
data = read_data()
|
||||
end
|
||||
end
|
||||
|
||||
if data.resolution ~= nil and data.resolution ~= resolution then
|
||||
minetest.log("error", "[mapgen_helper] resolution of recorded metrics doesn't match saved metrics. Moving the old saved metrics to " .. filename_old)
|
||||
local file, e = io.open(worldpath.."/"..filename_old, "w");
|
||||
if not file then
|
||||
return error(e);
|
||||
end
|
||||
file:write(minetest.serialize(data))
|
||||
file:close()
|
||||
data = {}
|
||||
data.times = {}
|
||||
end
|
||||
|
||||
data.resolution = resolution
|
||||
local times = data.times
|
||||
|
||||
mapgen_helper.record_time = function(label, time)
|
||||
local time_slot = math.ceil(time/resolution)
|
||||
local label_data = times[label] or {}
|
||||
local counts = label_data.counts or {}
|
||||
local existing_count = counts[time_slot] or 0
|
||||
local total_time = label_data.total_time or 0
|
||||
|
||||
counts[time_slot] = existing_count + 1
|
||||
total_time = total_time + time
|
||||
label_data.total_time = total_time
|
||||
label_data.counts = counts
|
||||
times[label] = label_data
|
||||
end
|
||||
|
||||
minetest.register_on_shutdown(function()
|
||||
if next(times) == nil then
|
||||
return
|
||||
end
|
||||
minetest.log("warning", "[mapgen_helper] Average chunk generation times:")
|
||||
for name, label_data in pairs(times) do
|
||||
minetest.log("warning", string.rep("-", 40))
|
||||
|
||||
local min_slot
|
||||
local max_slot
|
||||
local highest_slot_count = 0
|
||||
local total_count = 0
|
||||
local max_digits
|
||||
|
||||
for slot, count in pairs(label_data.counts) do
|
||||
if (not min_slot) or (slot < min_slot) then
|
||||
min_slot = slot
|
||||
end
|
||||
if (not max_slot) or (slot > max_slot) then
|
||||
max_slot = slot
|
||||
end
|
||||
if (not max_digits) or (#tostring(slot*resolution) > max_digits) then
|
||||
max_digits = #tostring(slot*resolution)
|
||||
end
|
||||
total_count = total_count + count
|
||||
highest_slot_count = math.max(highest_slot_count, count)
|
||||
end
|
||||
|
||||
local average = label_data.total_time / total_count
|
||||
|
||||
minetest.log("warning", "For " .. name .. ":")
|
||||
if min_slot == max_slot then
|
||||
minetest.log("warning", "All chunks took between " .. (min_slot-1)*resolution .. " and " .. min_slot*resolution .. " seconds.")
|
||||
else
|
||||
for i = min_slot, max_slot do
|
||||
local count = label_data.counts[i] or 0
|
||||
local spacing = max_digits - #tostring(i*resolution)
|
||||
local count_string = ""
|
||||
if count > 0 then
|
||||
count_string = " " .. count
|
||||
end
|
||||
minetest.log("warning", i*resolution .. "s" .. string.rep(" ", spacing) .. ": " ..
|
||||
string.rep("=", math.ceil((count / highest_slot_count) * 40)) .. count_string)
|
||||
end
|
||||
end
|
||||
|
||||
minetest.log("warning", "Total number of chunks: " .. total_count .. ". Average time per chunk: " .. average .. " seconds.")
|
||||
end
|
||||
minetest.log("warning", string.rep("-", 40))
|
||||
|
||||
if persist then
|
||||
local file, e = io.open(worldpath.."/"..filename, "w");
|
||||
if not file then
|
||||
return error(e);
|
||||
end
|
||||
file:write(minetest.serialize(data))
|
||||
file:close()
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,2 @@
|
||||
name = mapgen_helper
|
||||
description = A library of mapgen-related functions and tools.
|
||||
@@ -0,0 +1,114 @@
|
||||
local perlin_buffers = {}
|
||||
|
||||
mapgen_helper.register_perlin = function(name, perlin_params)
|
||||
perlin_buffers[name] = perlin_buffers[name] or {}
|
||||
if perlin_buffers[name].perlin_params then
|
||||
minetest.log("error", "mapgen_helper.register_perlin3d called for " .. name .. " when perlin parameters were already registered.")
|
||||
return
|
||||
end
|
||||
perlin_buffers[name].perlin_params = perlin_params
|
||||
end
|
||||
|
||||
local get_buffer = function(name, sidelen, perlin_params)
|
||||
perlin_buffers[name] = perlin_buffers[name] or {}
|
||||
local buffer = perlin_buffers[name]
|
||||
|
||||
if buffer.sidelen ~= nil and buffer.sidelen ~= sidelen then
|
||||
buffer.nobj_perlin = nil -- parameter changed, force regenerate of object
|
||||
end
|
||||
buffer.sidelen = sidelen
|
||||
|
||||
if perlin_params then
|
||||
if buffer.perlin_params then
|
||||
for k, v in pairs(buffer.perlin_params) do
|
||||
if perlin_params[k] ~= v then
|
||||
buffer.nobj_perlin = nil -- parameter changed, force regenerate of object
|
||||
end
|
||||
end
|
||||
end
|
||||
buffer.perlin_params = perlin_params -- record provided parameters
|
||||
elseif buffer.perlin_params == nil then
|
||||
minetest.log("error", "mapgen_helper.register_perlin3d called for " .. name .. " with no registered or provided perlin parameters.")
|
||||
return
|
||||
else
|
||||
perlin_params = buffer.perlin_params -- retrieve recorded parameters
|
||||
end
|
||||
|
||||
buffer.last_used = minetest.get_gametime()
|
||||
return buffer, perlin_params
|
||||
end
|
||||
|
||||
mapgen_helper.perlin3d = function(name, minp, maxp, perlin_params)
|
||||
local minx = minp.x
|
||||
local minz = minp.z
|
||||
local sidelen = maxp.x - minp.x + 1 --length of a mapblock
|
||||
local chunk_lengths = {x = sidelen, y = sidelen, z = sidelen} --table of chunk edges
|
||||
|
||||
local buffer, perlin_params = get_buffer(name, sidelen, perlin_params)
|
||||
|
||||
buffer.nvals_perlin_buffer = buffer.nvals_perlin_buffer or {}
|
||||
buffer.nobj_perlin = buffer.nobj_perlin or minetest.get_perlin_map(perlin_params, chunk_lengths)
|
||||
if buffer.nobj_perlin.get_3d_map_flat then
|
||||
return buffer.nobj_perlin:get_3d_map_flat(minp, buffer.nvals_perlin_buffer), VoxelArea:new{MinEdge=minp, MaxEdge=maxp}
|
||||
else
|
||||
return buffer.nobj_perlin:get3dMap_flat(minp, buffer.nvals_perlin_buffer), VoxelArea:new{MinEdge=minp, MaxEdge=maxp}
|
||||
end
|
||||
end
|
||||
|
||||
mapgen_helper.perlin2d = function(name, minp, maxp, perlin_params)
|
||||
local minx = minp.x
|
||||
local minz = minp.z
|
||||
local sidelen = maxp.x - minp.x + 1 --length of a mapblock
|
||||
local chunk_lengths = {x = sidelen, y = sidelen, z = sidelen} --table of chunk edges
|
||||
|
||||
local buffer, perlin_params = get_buffer(name, sidelen, perlin_params)
|
||||
|
||||
buffer.nvals_perlin_buffer = perlin_buffers[name].nvals_perlin_buffer or {}
|
||||
buffer.nobj_perlin = perlin_buffers[name].nobj_perlin or minetest.get_perlin_map(perlin_params, chunk_lengths)
|
||||
if buffer.nobj_perlin.get_2d_map_flat then
|
||||
return buffer.nobj_perlin:get_2d_map_flat({x=minp.x, y=minp.z}, perlin_buffers[name].nvals_perlin_buffer)
|
||||
else
|
||||
return buffer.nobj_perlin:get2dMap_flat({x=minp.x, y=minp.z}, perlin_buffers[name].nvals_perlin_buffer)
|
||||
end
|
||||
end
|
||||
|
||||
mapgen_helper.index2d = function(minp, maxp, x, z)
|
||||
return x - minp.x +
|
||||
(maxp.x - minp.x + 1) -- sidelen
|
||||
*(z - minp.z)
|
||||
+ 1
|
||||
end
|
||||
|
||||
mapgen_helper.index2dp = function(minp, maxp, pos)
|
||||
return mapgen_helper.index2d(minp, maxp, pos.x, pos.z)
|
||||
end
|
||||
|
||||
-- Takes an index into a 3D area and returns the corresponding 2D index
|
||||
-- assumes equal edge lengths
|
||||
mapgen_helper.index2di = function(minp, maxp, area, vi)
|
||||
local MinEdge = area.MinEdge
|
||||
local zstride = area.zstride
|
||||
local minpx = minp.x
|
||||
local i = vi - 1
|
||||
local z = math.floor(i / zstride) + MinEdge.z
|
||||
local x = math.floor((i % zstride) % area.ystride) + MinEdge.x
|
||||
return x - minpx +
|
||||
(maxp.x - minpx + 1) -- sidelen
|
||||
*(z - minp.z)
|
||||
+ 1
|
||||
end
|
||||
|
||||
-- If a noise buffer hasn't been used in 60 seconds, let garbage collection take it.
|
||||
local time_elapsed = 0
|
||||
minetest.register_globalstep(function(dtime)
|
||||
time_elapsed = time_elapsed + dtime
|
||||
if time_elapsed > 60 then
|
||||
local current_time = minetest.get_gametime()
|
||||
time_elapsed = 0
|
||||
for _, buffer in pairs(perlin_buffers) do
|
||||
if current_time - buffer.last_used > 60 then
|
||||
buffer.nvals_perlin_buffer = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,319 @@
|
||||
-- These functions are a modification of the schematic placement code from src/mapgen/mg_schematic.cpp.
|
||||
-- As such, this file is separately licened under the LGPL as follows:
|
||||
|
||||
-- License of Minetest source code
|
||||
-------------------------------
|
||||
|
||||
--Minetest
|
||||
--Copyright (C) 2010-2018 celeron55, Perttu Ahola <celeron55@gmail.com>
|
||||
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
local c_air = minetest.get_content_id("air")
|
||||
local c_ignore = minetest.get_content_id("ignore")
|
||||
|
||||
-- Table value = rotated facedir
|
||||
-- Columns: 90, 180, 270 degrees rotation around vertical axis
|
||||
-- Rotation is anticlockwise as seen from above (+Y)
|
||||
local rotate_facedir_y =
|
||||
{
|
||||
[0] = {1, 2, 3} ,
|
||||
[1] = {2, 3, 0} ,
|
||||
[2] = {3, 0, 1} ,
|
||||
[3] = {0, 1, 2} ,
|
||||
[4] = {13, 10, 19} ,
|
||||
[5] = {14, 11, 16} ,
|
||||
[6] = {15, 8, 17} ,
|
||||
[7] = {12, 9, 18} ,
|
||||
[8] = {17, 6, 15} ,
|
||||
[9] = {18, 7, 12} ,
|
||||
[10] = {19, 4, 13} ,
|
||||
[11] = {16, 5, 14} ,
|
||||
[12] = {9, 18, 7} ,
|
||||
[13] = {10, 19, 4} ,
|
||||
[14] = {11, 16, 5} ,
|
||||
[15] = {8, 17, 6} ,
|
||||
[16] = {5, 14, 11} ,
|
||||
[17] = {6, 15, 8} ,
|
||||
[18] = {7, 12, 9} ,
|
||||
[19] = {4, 13, 10} ,
|
||||
[20] = {23, 22, 21} ,
|
||||
[21] = {20, 23, 22} ,
|
||||
[22] = {21, 20, 23} ,
|
||||
[23] = {22, 21, 20} ,
|
||||
}
|
||||
|
||||
local random_rotations = {0, 90, 180, 270}
|
||||
|
||||
local rotate_param2 = function(param2, paramtype2, rotation)
|
||||
param2 = param2 or 0
|
||||
if paramtype2 == "facedir" then
|
||||
if rotation == 90 then
|
||||
param2 = rotate_facedir_y[param2][1]
|
||||
elseif rotation == 180 then
|
||||
param2 = rotate_facedir_y[param2][2]
|
||||
elseif rotation == 270 then
|
||||
param2 = rotate_facedir_y[param2][3]
|
||||
end
|
||||
elseif paramtype2 == "wallmounted" then
|
||||
--TODO
|
||||
elseif paramtype2 == "colorfacedir" then
|
||||
--TODO
|
||||
elseif paramtype2 == "colorwallmounted" then
|
||||
--TODO
|
||||
end
|
||||
|
||||
return param2
|
||||
end
|
||||
|
||||
|
||||
local swap = function(size_x, size_z)
|
||||
return size_z, size_x
|
||||
end
|
||||
|
||||
-- Returns the minpos and maxpos of the bounding box that this schematic will be placed in given
|
||||
-- the rotation and flag parameters. Useful for testing whether a schematic will fit in a place before actually
|
||||
-- writing it to the data, so that you can abort and try something else instead.
|
||||
mapgen_helper.get_schematic_bounding_box = function(pos, schematic, rotation, flags)
|
||||
flags = flags or {}
|
||||
local size = schematic.size
|
||||
local size_x = size.x
|
||||
local size_y = size.y
|
||||
local size_z = size.z
|
||||
local center_pos = schematic.center_pos
|
||||
|
||||
if center_pos and rotation ~= nil and rotation ~= 0 then
|
||||
center_pos = vector.new(center_pos) -- make a copy so we can mess with it without damaging the schematic
|
||||
if rotation == 90 then
|
||||
center_pos.x, center_pos.z = swap(size_x - center_pos.x - 1, center_pos.z)
|
||||
elseif rotation == 180 then
|
||||
center_pos.z = size_z - center_pos.z - 1
|
||||
center_pos.x = size_x - center_pos.x - 1
|
||||
elseif rotation == 270 then
|
||||
center_pos.x, center_pos.z = swap(center_pos.x, size_z - center_pos.z - 1)
|
||||
end
|
||||
end
|
||||
|
||||
if rotation == 90 or rotation == 270 then
|
||||
size_x, size_z = swap(size_x, size_z)
|
||||
end
|
||||
|
||||
local minpos = vector.new(pos)
|
||||
|
||||
if center_pos then
|
||||
if not flags.place_center_x then
|
||||
minpos.x = minpos.x - center_pos.x
|
||||
end
|
||||
if not flags.place_center_y then
|
||||
minpos.y = minpos.y - center_pos.y
|
||||
end
|
||||
if not flags.place_center_z then
|
||||
minpos.z = minpos.z - center_pos.z
|
||||
end
|
||||
end
|
||||
if flags.place_center_x then
|
||||
minpos.x = math.floor(minpos.x - (size_x - 1) / 2)
|
||||
end
|
||||
if flags.place_center_y then
|
||||
minpos.y = math.floor(minpos.y - (size_y - 1) / 2)
|
||||
end
|
||||
if flags.place_center_z then
|
||||
minpos.z = math.floor(minpos.z - (size_z - 1) / 2)
|
||||
end
|
||||
|
||||
local maxpos = vector.add(minpos, {x=size_x-1, y=size_y-1, z=size_z-1})
|
||||
|
||||
return minpos, maxpos
|
||||
end
|
||||
|
||||
-- Takes a lua-format schematic and applies it to the data and param2_data arrays produced by vmanip instead of being applied to the vmanip directly. Useful in a mapgen loop that's doing other things with the data before and after applying schematics. A VoxelArea for the data also needs to be provided.
|
||||
|
||||
-- Schematic enhancements beyond the basic Minetest API that work with this:
|
||||
-- * node defs can have a "place_on_condition" property defined, which is a function that takes a node content ID parameter and returns true to indicate the schematic should replace it or false to indicate it should not. Useful for, for example, a schematic that should replace water but not stone (placing decorations on the bottom of the ocean), or a schematic that replaces all buildable_to nodes (to prevent tufts of grass from knocking holes in foundations). "data, area, vi" parameters are also provided if you want to get fancy and base the condition on surrounding nodes, but bear in mind that the schematic is in the process of being written already so some neighbors will already have been replaced with schematic nodes and some neighbors will be replaced in the future.
|
||||
-- * schematic can have a "center_pos" position defined relative to the placement pos that will be treated as the rotation and placement origin, unless overridden by the flags parameter
|
||||
|
||||
-- returns true if the schematic was entirely contained within the voxelarea, false otherwise.
|
||||
|
||||
local empty_table = {}
|
||||
|
||||
mapgen_helper.place_schematic_on_data = function(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
|
||||
pos = vector.new(pos)
|
||||
replacements = replacements or empty_table
|
||||
flags = flags or empty_table -- TODO: support all flags formats
|
||||
if flags.force_placement ~= nil then
|
||||
force_placement = flags.force_placement -- TODO: unclear which force_placement parameter should have prededence here
|
||||
end
|
||||
local center_pos = schematic.center_pos
|
||||
if rotation == "random" then rotation = random_rotations[math.random(1,4)] end
|
||||
|
||||
local schemdata = schematic.data
|
||||
local slice_probs = schematic.yslice_prob or {}
|
||||
|
||||
local size = schematic.size
|
||||
local size_x = size.x
|
||||
local size_y = size.y
|
||||
local size_z = size.z
|
||||
|
||||
local xstride = 1
|
||||
local ystride = size_x
|
||||
local zstride = size_x * size_y
|
||||
|
||||
if center_pos and rotation ~= nil and rotation ~= 0 then
|
||||
center_pos = vector.new(center_pos) -- make a copy so we can mess with it without damaging the schematic
|
||||
if rotation == 90 then
|
||||
center_pos.x, center_pos.z = swap(size_x - center_pos.x - 1, center_pos.z)
|
||||
elseif rotation == 180 then
|
||||
center_pos.z = size_z - center_pos.z - 1
|
||||
center_pos.x = size_x - center_pos.x - 1
|
||||
elseif rotation == 270 then
|
||||
center_pos.x, center_pos.z = swap(center_pos.x, size_z - center_pos.z - 1)
|
||||
end
|
||||
end
|
||||
|
||||
local i_start, i_step_x, i_step_z
|
||||
if rotation == 90 then
|
||||
i_start = size_x
|
||||
i_step_x = zstride
|
||||
i_step_z = -xstride
|
||||
size_x, size_z = swap(size_x, size_z)
|
||||
elseif rotation == 180 then
|
||||
i_start = zstride * (size_z - 1) + size_x
|
||||
i_step_x = -xstride
|
||||
i_step_z = -zstride
|
||||
elseif rotation == 270 then
|
||||
i_start = zstride * (size_z - 1) + 1
|
||||
i_step_x = -zstride
|
||||
i_step_z = xstride
|
||||
size_x, size_z = swap(size_x, size_z)
|
||||
else
|
||||
i_start = 1
|
||||
i_step_x = xstride
|
||||
i_step_z = zstride
|
||||
end
|
||||
|
||||
-- Adjust placement position if necessary
|
||||
if center_pos then
|
||||
if not flags.place_center_x then
|
||||
pos.x = pos.x - center_pos.x
|
||||
end
|
||||
if not flags.place_center_y then
|
||||
pos.y = pos.y - center_pos.y
|
||||
end
|
||||
if not flags.place_center_z then
|
||||
pos.z = pos.z - center_pos.z
|
||||
end
|
||||
end
|
||||
if flags.place_center_x then
|
||||
pos.x = math.floor(pos.x - (size_x - 1) / 2)
|
||||
end
|
||||
if flags.place_center_y then
|
||||
pos.y = math.floor(pos.y - (size_y - 1) / 2)
|
||||
end
|
||||
if flags.place_center_z then
|
||||
pos.z = math.floor(pos.z - (size_z - 1) / 2)
|
||||
end
|
||||
|
||||
local maxpos = vector.add(pos, {x=size_x-1, y=size_y-1, z=size_z-1})
|
||||
local minEdge = area.MinEdge
|
||||
local maxEdge = area.MaxEdge
|
||||
if not (pos.x <= maxEdge.x and maxpos.x >= minEdge.x and
|
||||
pos.z <= maxEdge.z and maxpos.z >= minEdge.z and
|
||||
pos.y <= maxEdge.y and maxpos.y >= minEdge.y) then
|
||||
return false -- the bounding boxes of the area and the schematic don't overlap at all
|
||||
end
|
||||
|
||||
local contained_in_area = true
|
||||
local on_place_callbacks = {}
|
||||
|
||||
local y_map = pos.y
|
||||
for y = 0, size_y-1 do
|
||||
if slice_probs[y] == nil or slice_probs[y] == 255 or slice_probs[y] <= math.random(1, 255) then
|
||||
for z = 0, size_z-1 do
|
||||
local i = z * i_step_z + y * ystride + i_start
|
||||
for x = 0, size_x-1 do
|
||||
local vi = area:index(pos.x + x, y_map, pos.z + z)
|
||||
if area:containsi(vi) then
|
||||
local node_def = schemdata[i]
|
||||
local node_name = replacements[node_def.name] or node_def.name
|
||||
if node_name ~= "ignore" then
|
||||
local placement_prob = node_def.prob or 255
|
||||
if placement_prob ~= 0 then
|
||||
|
||||
local force_place_node = node_def.force_place
|
||||
local place_on_condition = node_def.place_on_condition
|
||||
local on_place = node_def.on_place
|
||||
local old_node_id = data[vi]
|
||||
|
||||
if (force_placement or force_place_node
|
||||
or (place_on_condition and place_on_condition(old_node_id, data, area, vi))
|
||||
or (not place_on_condition and (old_node_id == c_air or old_node_id == c_ignore)))
|
||||
and (placement_prob == 255 or math.random(1,255) <= placement_prob)
|
||||
then
|
||||
local registered_def = minetest.registered_nodes[node_name]
|
||||
if registered_def ~= nil then
|
||||
local paramtype2 = registered_def.paramtype2
|
||||
data[vi] = minetest.get_content_id(node_name)
|
||||
data_param2[vi] = rotate_param2(node_def.param2, paramtype2, rotation)
|
||||
|
||||
if on_place then
|
||||
table.insert(on_place_callbacks, {on_place, vi})
|
||||
end
|
||||
else
|
||||
minetest.log("error", "mapgen_helper.place_schematic was given a schematic with unregistered node " .. tostring(node_name) .. " in it.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
contained_in_area = false -- schematic spilled over the edge of the area
|
||||
end
|
||||
i = i + i_step_x
|
||||
end
|
||||
end
|
||||
end
|
||||
y_map = y_map + 1
|
||||
end
|
||||
|
||||
for k, callback in pairs(on_place_callbacks) do
|
||||
callback[1](callback[2], data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
|
||||
end
|
||||
|
||||
return contained_in_area
|
||||
end
|
||||
|
||||
-- aborts schematic placement if it won't fit into the provided data
|
||||
mapgen_helper.place_schematic_on_data_if_it_fits = function(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
|
||||
local minbound, maxbound = mapgen_helper.get_schematic_bounding_box(pos, schematic, rotation, flags)
|
||||
if mapgen_helper.is_box_within_box(minbound, maxbound, area.MinEdge, area.MaxEdge) then
|
||||
return mapgen_helper.place_schematic_on_data(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
-- wraps the above for convenience, so you can use this style of schematic in non-mapgen contexts as well
|
||||
mapgen_helper.place_schematic = function(pos, schematic, rotation, replacements, force_placement, flags)
|
||||
local minpos, maxpos = mapgen_helper.get_schematic_bounding_box(pos, schematic, rotation, flags)
|
||||
local vmanip = minetest.get_voxel_manip(minpos, maxpos)
|
||||
local data = vmanip:get_data()
|
||||
local data_param2 = vmanip:get_param2_data()
|
||||
local emin, emax = vmanip:get_emerged_area()
|
||||
local area = VoxelArea:new{MinEdge=emin, MaxEdge=emax}
|
||||
local ret = mapgen_helper.place_schematic_on_data(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
|
||||
vmanip:set_data(data)
|
||||
vmanip:set_param2_data(data_param2)
|
||||
vmanip:write_to_map()
|
||||
return ret -- should always be true since we created the voxelarea to fit the schematic
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
-- Returns a consistent list of random points within a volume.
|
||||
-- Each call to this method will give the same set of points if the same parameters are provided
|
||||
mapgen_helper.get_random_points = function(minp, maxp, min_output_size, max_output_size)
|
||||
local next_seed = math.random(1, 2^21) -- should be 2^31, but I've had a report that this causes a crash in the Lua interpreter on some systems.
|
||||
math.randomseed(minetest.hash_node_position(minp) + mapgen_helper.mapgen_seed)
|
||||
|
||||
local count = math.random(min_output_size, max_output_size)
|
||||
local result = {}
|
||||
while count > 0 do
|
||||
local point = {}
|
||||
point.x = math.random(minp.x, maxp.x)
|
||||
point.y = math.random(minp.y, maxp.y)
|
||||
point.z = math.random(minp.z, maxp.z)
|
||||
table.insert(result, point)
|
||||
count = count - 1
|
||||
end
|
||||
|
||||
math.randomseed(next_seed)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Returns a random value based on the x and z coordinates of pos, always the same for the same x and z
|
||||
mapgen_helper.xz_consistent_randomp = function(pos)
|
||||
local next_seed = math.random(1, 2^21) -- should be 2^31, but I've had a report that this causes a crash in the Lua interpreter on some systems.
|
||||
math.randomseed(pos.x + pos.z * 2 ^ 8)
|
||||
local output = math.random()
|
||||
math.randomseed(next_seed)
|
||||
return output
|
||||
end
|
||||
|
||||
mapgen_helper.xz_consistent_randomi = function(area, vi)
|
||||
local pos = area:position(vi)
|
||||
return mapgen_helper.xz_consistent_randomp(pos)
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
local mapgen_chunksize = tonumber(minetest.get_mapgen_setting("chunksize"))
|
||||
|
||||
-- For getting large regions that don't cross mapgen block boundaries
|
||||
-- For some reason, map chunks generate with -32, -32, -32 as the "origin" minp. To make the large-scale grid align with map chunks it needs to be offset like this.
|
||||
mapgen_helper.get_region_minp_xz = function(pos_xz, mapblocks)
|
||||
local region_size = mapblocks * mapgen_chunksize * 16
|
||||
return {x = math.floor((pos.x+32) / region_size) * region_size - 32, y = 0, z = math.floor((pos.z+32) / region_size) * region_size - 32}
|
||||
end
|
||||
|
||||
-- Given a position and a region scale, returns the minp corners of the four regions that the player is closest to.
|
||||
-- For example, if we have a region scale of 6 and the parameter position is at "*", the four points marked "R" would be returned.
|
||||
|
||||
-- |-----|-----|-----| |-----|-----|-----| |-----|-----|-----|
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- |-----|-----|-----| |-----|-----|-----| |-----R-----R-----|
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | *| |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | * | | | | *| | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- R-----R-----|-----| |-----R-----R-----| |-----R-----R-----|
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- | | | | | | | | | | | |
|
||||
-- R-----R-----|-----| |-----R-----R-----| |-----|-----|-----|
|
||||
|
||||
-- In this way you can be sure you aren't near an area you haven't tested.
|
||||
|
||||
mapgen_helper.get_nearest_regions = function(pos_xz, gridscale)
|
||||
local half_scale = gridscale / 2
|
||||
local grid_cell = {x = math.floor(pos_xz.x / gridscale) * gridscale, z = math.floor(pos_xz.z / gridscale) * gridscale}
|
||||
local pos_internal = {x = pos_xz.x % gridscale, z = pos_xz.z % gridscale}
|
||||
local result = {grid_cell}
|
||||
local shift_x = gridscale
|
||||
local shift_z = gridscale
|
||||
if (pos_internal.x < half_scale) then
|
||||
shift_x = -gridscale
|
||||
end
|
||||
if (pos_internal.z < half_scale) then
|
||||
shift_z = -gridscale
|
||||
end
|
||||
|
||||
table.insert(result, {x = grid_cell.x + shift_x, z = grid_cell.z + shift_z})
|
||||
table.insert(result, {x = grid_cell.x + shift_x, z = grid_cell.z})
|
||||
table.insert(result, {x = grid_cell.x, z = grid_cell.z + shift_z})
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
-- |---|---|---|
|
||||
-- | | | |
|
||||
-- | | | |
|
||||
-- R---R---R---|
|
||||
-- | | | |
|
||||
-- | | *| |
|
||||
-- R---R---R---|
|
||||
-- | | | |
|
||||
-- | | | |
|
||||
-- R---R---R---|
|
||||
|
||||
mapgen_helper.get_adjacent_regions = function(pos_xz, gridscale)
|
||||
local grid_cell = {x = math.floor(pos_xz.x / gridscale) * gridscale, z = math.floor(pos_xz.z / gridscale) * gridscale}
|
||||
local result = {grid_cell}
|
||||
table.insert(result, {x = grid_cell.x + gridscale, z = grid_cell.y + gridscale})
|
||||
table.insert(result, {x = grid_cell.x + gridscale, z = grid_cell.y})
|
||||
table.insert(result, {x = grid_cell.x, z = grid_cell.y + gridscale})
|
||||
table.insert(result, {x = grid_cell.x - gridscale, z = grid_cell.y - gridscale})
|
||||
table.insert(result, {x = grid_cell.x - gridscale, z = grid_cell.y})
|
||||
table.insert(result, {x = grid_cell.x, z = grid_cell.y - gridscale})
|
||||
return result
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
mapgen_helper_record_time (Enable time-taken logging) bool false
|
||||
mapgen_helper_persist_recorded_time (Persist time-taken logging between server sessions) bool false
|
||||
mapgen_helper_record_time_resolution (Resolution of time-taken histogram) float 0.1
|
||||
mapgen_helper_log_locations (Enable location logging) bool false
|
||||
@@ -0,0 +1,75 @@
|
||||
-- These functions are a minor modification of the voxel area iterator defined in the built in voxelarea.lua lua code.
|
||||
-- As such, this file is separately licened under the LGPL as follows:
|
||||
|
||||
-- License of Minetest source code
|
||||
-------------------------------
|
||||
|
||||
--Minetest
|
||||
--Copyright (C) 2010-2018 celeron55, Perttu Ahola <celeron55@gmail.com>
|
||||
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
function VoxelArea:iter_xyz(minx, miny, minz, maxx, maxy, maxz)
|
||||
local i = self:index(minx, miny, minz) - 1
|
||||
|
||||
local x = minx - 1 -- subtracting one because x gets incremented before it gets returned the first time.
|
||||
local xrange = maxx - minx + 1
|
||||
local nextaction = i + 1 + xrange
|
||||
|
||||
local y = 0
|
||||
local yrange = maxy - miny + 1
|
||||
local yreqstride = self.ystride - xrange
|
||||
|
||||
local z = 0
|
||||
local zrange = maxz - minz + 1
|
||||
local multistride = self.zstride - ((yrange - 1) * self.ystride + xrange)
|
||||
|
||||
return function()
|
||||
-- continue i until it needs to jump
|
||||
i = i + 1
|
||||
x = x + 1
|
||||
if i ~= nextaction then
|
||||
return i, x, y+miny, z+minz
|
||||
end
|
||||
|
||||
-- continue y until maxy is exceeded
|
||||
y = y + 1
|
||||
x = minx -- new line
|
||||
if y ~= yrange then
|
||||
-- set i to index(minx, miny + y, minz + z) - 1
|
||||
i = i + yreqstride
|
||||
nextaction = i + xrange
|
||||
return i, x, y+miny, z+minz
|
||||
end
|
||||
|
||||
-- continue z until maxz is exceeded
|
||||
z = z + 1
|
||||
if z == zrange then
|
||||
-- cuboid finished, return nil
|
||||
return
|
||||
end
|
||||
|
||||
-- set i to index(minx, miny, minz + z) - 1
|
||||
i = i + multistride
|
||||
|
||||
y = 0
|
||||
nextaction = i + xrange
|
||||
return i, x, y+miny, z+minz
|
||||
end
|
||||
end
|
||||
|
||||
function VoxelArea:iterp_xyz(minp, maxp)
|
||||
return self:iter_xyz(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z)
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
-- similar to iter_xyz, but iterates first along the y axis. Useful in mapgens that want to detect a vertical transition (eg, finding ground level)
|
||||
function VoxelArea:iter_yxz(minx, miny, minz, maxx, maxy, maxz)
|
||||
local i = self:index(minx, miny, minz) - self.ystride
|
||||
|
||||
local x = minx
|
||||
local y = miny - 1
|
||||
local z = minz
|
||||
|
||||
return function()
|
||||
y = y + 1
|
||||
|
||||
if y <= maxy then
|
||||
i = i + self.ystride
|
||||
return i, x, y, z
|
||||
end
|
||||
|
||||
y = miny
|
||||
x = x + 1
|
||||
|
||||
if x <= maxx then
|
||||
i = self:index(x, y, z)
|
||||
return i, x, y, z
|
||||
end
|
||||
|
||||
x = minx
|
||||
z = z + 1
|
||||
|
||||
if z <= maxz then
|
||||
i = self:index(x, y, z)
|
||||
return i, x, y, z
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function VoxelArea:iterp_yxz(minp, maxp)
|
||||
return self:iter_yxz(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z)
|
||||
end
|
||||
|
||||
function VoxelArea:get_y(i)
|
||||
return math.floor(((i - 1) % self.zstride) / self.ystride) + self.MinEdge.y
|
||||
end
|
||||
|
||||
-- Used to make transform more efficient by skipping a table creation
|
||||
function VoxelArea:position_xyz(vi)
|
||||
local MinEdge = self.MinEdge
|
||||
local Zstride = self.zstride
|
||||
local Ystride = self.ystride
|
||||
vi = vi - 1
|
||||
local z = math.floor(vi / Zstride) + MinEdge.z
|
||||
vi = vi % Zstride
|
||||
local y = math.floor(vi / Ystride) + MinEdge.y
|
||||
vi = vi % Ystride
|
||||
local x = vi + MinEdge.x
|
||||
return x, y, z
|
||||
end
|
||||
|
||||
-- Takes another voxelarea and an index in it and transforms it into an index into its own
|
||||
-- voxelarea, or nil if it's not in the voxelarea. This is useful when you've got, for example,
|
||||
-- a mapgen's voxelmanipulator and a perlin noise array covering the map block but not the entire
|
||||
-- emerged volume.
|
||||
function VoxelArea:transform(area, vi)
|
||||
local x,y,z = area:position_xyz(vi)
|
||||
if self:contains(x,y,z) then
|
||||
return self:index(x,y,z)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
Reference in New Issue
Block a user