Minetest_Tutorial/mods/tutorial_mapgen/init.lua

412 lines
14 KiB
Lua

-- TUTORIAL MAP GENERATION
-- == DEBUG SETTINGS ==
-- If true, the generated tutorial map is in "map editing" mode, only generating
-- the raw castle, no grass layer or other random decorations will be generated.
-- Also, 2 commands to manage the schematic will be available:
-- /treset and /tsave commands will be available.
-- (/tsave only if tutorial is trusted mod)
local map_editing = minetest.settings:get_bool("tutorial_debug_map_editing")
-- == END OF DEBUG SETTINGS ==
local c_dirt = minetest.get_content_id("default:dirt")
local c_dirt_with_grass = minetest.get_content_id("default:dirt_with_grass")
local c_grass = minetest.get_content_id("default:grass_5")
-- Directory where the map data will be stored
tutorial.map_directory = minetest.get_modpath("tutorial_mapgen").."/mapdata/"
local insecure_environment = minetest.request_insecure_environment()
minetest.log("error", tostring(insecure_environment ~= nil))
minetest.log("error", tostring(map_editing ~= nil))
-- entity management functions
local function init_item_spawners(spawners)
local count = 0
for n=1, #spawners do
local timer = minetest.get_node_timer(spawners[n])
timer:start(3)
count = count + 1
end
minetest.log("action", "[tutorial_mapgen] " .. count .. " item spawners initialized")
end
---
-- Sectors of the map to save/load
-- Each element of the array will contain the coordinate where the sector starts
-- along with a "l" property indicating its length in each direction.
tutorial.map_sector = {}
-- Array with the minimum and the maximum positions of the cube that contains the
-- entire Tutorial World, it's best if the start matches the start of a mapchunk
tutorial.limits = {
{ x = -32, y = -32, z = -32 },
{ x = 224, y = 48, z = 144 },
}
-- size of the sectors to form divisions of the map.
-- This needs to be a multiple of 16, since it will also determine the
-- chunksize
tutorial.sector_size = 80
-- perform the divisions using the given sector size within the limits provided
for x = tutorial.limits[1].x, tutorial.limits[2].x, tutorial.sector_size do
for y = tutorial.limits[1].y, tutorial.limits[2].y, tutorial.sector_size do
for z = tutorial.limits[1].z, tutorial.limits[2].z, tutorial.sector_size do
table.insert(tutorial.map_sector, {x=x,y=y,z=z,l=(tutorial.sector_size - 1)})
end
end
end
--]]
-- Load the sector schematics from disc
tutorial.sector_data = {}
for k,sector in pairs(tutorial.map_sector) do
local filename = tutorial.map_directory .. "sector_"..k
local f, err = io.open(filename..".meta", "rb")
if f then
local data = minetest.deserialize(minetest.decompress(f:read("*a")))
tutorial.sector_data[filename] = data
f:close()
end
end
-- Saves schematic in the Minetest Schematic (and metadata) to disk.
-- Takes the same arguments as minetest.create_schematic
-- @param minp Lowest position (in all 3 coordinates) of the area to save
-- @param maxp Highest position (in all 3 coordinates) of the area to save
-- @param probability_list = {{pos={x=,y=,z=},prob=}, ...} list of probabilities for the nodes to be loaded (if nil, always load)
-- @param filename (without externsion) with the path to save the shcematic and metadata to
-- @param slice_prob_list = {{ypos=,prob=}, ...} list of probabilities for the slices to be loaded (if nil, always load)
-- @return The number of nodes with metadata.
local function save_region(minp, maxp, probability_list, filename, slice_prob_list)
local success = minetest.create_schematic(minp, maxp, probability_list, filename .. ".mts", slice_prob_list)
if not success then
minetest.log("error", "[tutorial_mapgen] problem creating schematic on ".. minetest.pos_to_string(minp) .. ": " .. filename)
return false
end
local manip = minetest.get_voxel_manip()
manip:read_from_map(minp, maxp)
local pos = {x=minp.x, y=0, z=0}
local count = 0
local nodes = {}
local get_node, get_meta = minetest.get_node, minetest.get_meta
while pos.x <= maxp.x do
pos.y = minp.y
while pos.y <= maxp.y do
pos.z = minp.z
while pos.z <= maxp.z do
local node = get_node(pos)
if node.name ~= "air" and node.name ~= "ignore" then
local meta = get_meta(pos):to_table()
local meta_empty = true
-- Convert metadata item stacks to item strings
for name, inventory in pairs(meta.inventory) do
for index, stack in ipairs(inventory) do
meta_empty = false
inventory[index] = stack.to_string and stack:to_string() or stack
end
end
if meta.fields and next(meta.fields) ~= nil then
meta_empty = false
end
if not meta_empty then
count = count + 1
nodes[count] = {
x = pos.x - minp.x,
y = pos.y - minp.y,
z = pos.z - minp.z,
meta = meta,
}
end
end
pos.z = pos.z + 1
end
pos.y = pos.y + 1
end
pos.x = pos.x + 1
end
if count > 0 then
local result = {
size = {
x = maxp.x - minp.x,
y = maxp.y - minp.y,
z = maxp.z - minp.z,
},
nodes = nodes,
}
-- Serialize entries
result = minetest.serialize(result)
local file, err = insecure_environment.io.open(filename..".meta", "wb")
if err ~= nil then
error("Couldn't write to \"" .. filename .. "\"")
end
file:write(minetest.compress(result))
file:flush()
file:close()
minetest.log("action", "[tutorial_mapgen] schematic + metadata saved: " .. filename)
else
minetest.log("action", "[tutorial_mapgen] schematic (no metadata) saved: " .. filename)
end
return success, count
end
-- Places the schematic specified in the given position.
-- @param minp Lowest position (in all 3 coordinates) of the area to load
-- @param filename without extension, but with path of the file to load
-- @param vmanip voxelmanip object to use to place the schematic in
-- @param rotation can be 0, 90, 180, 270, or "random".
-- @param replacements = {["old_name"] = "convert_to", ...}
-- @param force_placement is a boolean indicating whether nodes other than air and ignore are replaced by the schematic
-- @return boolean indicating success or failure
local function load_region(minp, filename, vmanip, rotation, replacements, force_placement)
if rotation == "random" then
rotation = {nil, 90, 180, 270}
rotation = rotation[math.random(1,4)]
end
local success
if vmanip and minetest.place_schematic_on_vmanip then
success = minetest.place_schematic_on_vmanip(vmanip, minp, filename .. ".mts", tostring(rotation), replacements, force_placement)
else
success = minetest.place_schematic(minp, filename .. ".mts", tostring(rotation), replacements, force_placement)
end
if success == false then
minetest.log("action", "[tutorial_mapgen] schematic partionally loaded on ".. minetest.pos_to_string(minp))
elseif not success then
minetest.log("error", "[tutorial_mapgen] problem placing schematic on ".. minetest.pos_to_string(minp) .. ": " .. filename)
return nil
end
local data = tutorial.sector_data[filename]
if not data then return true, {} end
local get_meta = minetest.get_meta
local spawners = {}
if not rotation or rotation == 0 then
for i, entry in ipairs(data.nodes) do
entry.x, entry.y, entry.z = minp.x + entry.x, minp.y + entry.y, minp.z + entry.z
if entry.meta then
get_meta(entry):from_table(entry.meta)
if entry.meta.fields.spawned then
table.insert(spawners, {x=entry.x, y=entry.y, z=entry.z})
end
end
end
else
local maxp_x, maxp_z = minp.x + data.size.x, minp.z + data.size.z
if rotation == 90 then
for i, entry in ipairs(data.nodes) do
entry.x, entry.y, entry.z = minp.x + entry.z, minp.y + entry.y, maxp_z - entry.x
if entry.meta then get_meta(entry):from_table(entry.meta) end
end
elseif rotation == 180 then
for i, entry in ipairs(data.nodes) do
entry.x, entry.y, entry.z = maxp_x - entry.x, minp.y + entry.y, maxp_z - entry.z
if entry.meta then get_meta(entry):from_table(entry.meta) end
end
elseif rotation == 270 then
for i, entry in ipairs(data.nodes) do
entry.x, entry.y, entry.z = maxp_x - entry.z, minp.y + entry.y, minp.z + entry.x
if entry.meta then get_meta(entry):from_table(entry.meta) end
end
else
minetest.log("error", "[tutorial_mapgen] unsupported rotation angle: " .. (rotation or "nil"))
return false
end
end
minetest.log("action", "[tutorial_mapgen] schematic + metadata loaded on ".. minetest.pos_to_string(minp))
return true, spawners
end
local function save_schematic()
local success = true
for k,sector in pairs(tutorial.map_sector) do
local filename = tutorial.map_directory .. "sector_"..k
local minp = sector
local maxp = {
x = sector.x + sector.l,
y = sector.y + sector.l,
z = sector.z + sector.l
}
if not save_region(minp, maxp, nil, filename) then
minetest.log("error", "[tutorial_mapgen] error loading Tutorial World sector " .. minetest.pos_to_string(sector))
success = false
end
end
return success
end
local function load_schematic()
local success = true
for k,sector in pairs(tutorial.map_sector) do
local filename = tutorial.map_directory .. "sector_"..k
minetest.log("action", "loading sector " .. minetest.pos_to_string(sector))
sector.maxp = vector.add(sector, {x=sector.l, y=sector.l, z=sector.l})
-- Load the area above the schematic to guarantee we have blue sky above
-- and prevent lighting glitches
--minetest.emerge_area(vector.add(sector, {x=0, y=sector.l, z=0}), vector.add(sector.maxp, {x=0,y=32,z=0}))
local vmanip = VoxelManip(sector, sector.maxp)
if not load_region(sector, filename, vmanip, nil, nil, true) then
minetest.log("error", "[tutorial_mapgen] error loading Tutorial World sector " .. minetest.pos_to_string(sector))
success = false
end
vmanip:calc_lighting()
vmanip:write_to_map()
vmanip:update_map()
end
return success
end
------ Commands
if map_editing then
minetest.log("action", "Map editing mode is enabled.")
minetest.register_on_joinplayer(function(player)
minetest.chat_send_player(player:get_player_name(), "Map editing mode is enabled.")
end)
minetest.register_privilege("tutorialmap", "Can use commands to manage the tutorial map")
minetest.register_chatcommand("treset", {
params = "",
description = "Resets the tutorial map",
privs = {tutorialmap=true},
func = function(name, param)
if load_schematic() then
minetest.chat_send_player(name, "Tutorial World schematic loaded")
else
minetest.chat_send_player(name, "An error occurred while loading Tutorial World schematic")
end
-- TODO: re-load entities?
end,
})
-- Add commands for saving map and entities, but only if tutorial mod is trusted
if insecure_environment then
minetest.register_chatcommand("tsave", {
params = "",
description = "Saves the tutorial map",
privs = {tutorialmap=true},
func = function(name, param)
if save_schematic() then
minetest.chat_send_player(name, "Tutorial World schematic saved")
else
minetest.chat_send_player(name, "An error occurred while saving Tutorial World schematic")
end
end,
})
else
minetest.log("warning", "Could not create insecure environment! /tsave command is disabled.")
minetest.register_on_joinplayer(function(player)
minetest.chat_send_player(player:get_player_name(), "Could not create insecure environment! /tsave command is not available.")
end)
end
end
------ Map Generation
local vbuffer = nil
tutorial.state = tutorial.state or {}
tutorial.state.loaded = tutorial.state.loaded or {}
minetest.register_on_generated(function(minp, maxp, seed)
local state_changed = false
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
for k,sector in pairs(tutorial.map_sector) do
if not tutorial.state.loaded[k] then
if sector.maxp == nil then
sector.maxp = {
x = sector.x + sector.l,
y = sector.y + sector.l,
z = sector.z + sector.l,
}
end
-- Only load it if not out of the generating range
if not ((maxp.x < sector.x) or (minp.x > sector.maxp.x)
or (maxp.y < sector.y) or (minp.y > sector.maxp.y)
or (maxp.z < sector.z) or (minp.z > sector.maxp.z))
then
local filename = tutorial.map_directory .. "sector_" .. k
local loaded, spawners = load_region(sector, filename, vm)
if loaded then
-- Initialize item spawners in the area as well, and mark it as loaded
init_item_spawners(spawners)
tutorial.state.loaded[k] = true
end
state_changed = true
end
end
end
-- Generate a flat grass land and a dirt-only underground for the rest of the map
if map_editing ~= true then
local grasslev = 0
if minp.y <= grasslev then
local vdata = vm:get_data(vbuffer)
local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
for x = minp.x, maxp.x do
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local p_pos = area:index(x, y, z)
local p_pos_above
if minp.y <= grasslev+1 and maxp.y >= maxp.y then
p_pos_above = area:index(x, y + 1, z)
end
local _, areas_count = areas:getAreasAtPos({x=x,y=y,z=z})
if areas_count == 0 and vdata[p_pos] == minetest.CONTENT_AIR then
if y == grasslev then
vdata[p_pos] = c_dirt_with_grass
if p_pos_above and vdata[p_pos_above] == minetest.CONTENT_AIR then
if math.random(0,50) == 0 then
vdata[p_pos_above] = c_grass
end
end
elseif y < grasslev then
vdata[p_pos] = c_dirt
end
end
end
end
end
vm:set_data(vdata)
state_changed = true
end
end
if(state_changed) then
vm:calc_lighting(nil, nil, false)
vm:write_to_map()
tutorial.save_state()
end
end)
minetest.set_mapgen_setting("mg_name", "singlenode")
minetest.set_mapgen_setting("water_level", "-31000")
minetest.set_mapgen_setting("chunksize", tostring(tutorial.sector_size/16))
-- coordinates for the first time the player spawns
tutorial.first_spawn = { pos={x=42,y=0.5,z=28}, yaw=(math.pi * 0.5) }