REALLY generate dirt all the way down
[minetest_tutorial_subgame.git] / mods / tutorial / mapgen.lua
blob83b364bfaa1cdc7a87cc8136561879cd31094a4d
1 -- TUTORIAL MAP GENERATION
3 -- == DEBUG SETTINGS ==
4 -- If true, the generated tutorial map is in "map editing" mode, only generating
5 -- the raw castle, no grass layer or other random decorations will be generated
6 local map_editing = minetest.settings:get_bool("tutorial_debug_map_editing")
8 -- == END OF DEBUG SETTINGS ==
10 local c_dirt = minetest.get_content_id("default:dirt")
11 local c_dirt_with_grass = minetest.get_content_id("default:dirt_with_grass")
12 local c_grass = minetest.get_content_id("default:grass_5")
14 -- Directory where the map data will be stored
15 tutorial.map_directory = minetest.get_modpath("tutorial").."/mapdata/"
17 local insecure_environment = minetest.request_insecure_environment()
19 -- entity management functions
21 local function init_item_spawners(spawners)
22 local count = 0
23 for n=1, #spawners do
24 local timer = minetest.get_node_timer(spawners[n])
25 timer:start(3)
26 count = count + 1
27 end
28 minetest.log("action", "[tutorial] " .. count .. " item spawners initialized")
29 end
31 ---
33 -- Sectors of the map to save/load
34 -- Each element of the array will contain the coordinate where the sector starts
35 -- along with a "l" property indicating its length in each direction.
36 tutorial.map_sector = {}
38 -- Array with the minimum and the maximum positions of the cube that contains the
39 -- entire Tutorial World, it's best if the start matches the start of a mapchunk
40 tutorial.limits = {
41 { x = -32, y = -32, z = -32 },
42 { x = 224, y = 48, z = 144 },
45 -- size of the sectors to form divisions of the map.
46 -- This needs to be a multiple of 16, since it will also determine the
47 -- chunksize
48 tutorial.sector_size = 80
50 -- perform the divisions using the given sector size within the limits provided
51 for x = tutorial.limits[1].x, tutorial.limits[2].x, tutorial.sector_size do
52 for y = tutorial.limits[1].y, tutorial.limits[2].y, tutorial.sector_size do
53 for z = tutorial.limits[1].z, tutorial.limits[2].z, tutorial.sector_size do
54 table.insert(tutorial.map_sector, {x=x,y=y,z=z,l=(tutorial.sector_size - 1)})
55 end
56 end
57 end
58 --]]
60 -- Load the sector schematics from disc
61 tutorial.sector_data = {}
62 for k,sector in pairs(tutorial.map_sector) do
63 local filename = tutorial.map_directory .. "sector_"..k
64 local f, err = io.open(filename..".meta", "rb")
65 if f then
66 local data = minetest.deserialize(minetest.decompress(f:read("*a")))
67 tutorial.sector_data[filename] = data
68 f:close()
69 end
70 end
72 -- Saves schematic in the Minetest Schematic (and metadata) to disk.
73 -- Takes the same arguments as minetest.create_schematic
74 -- @param minp Lowest position (in all 3 coordinates) of the area to save
75 -- @param maxp Highest position (in all 3 coordinates) of the area to save
76 -- @param probability_list = {{pos={x=,y=,z=},prob=}, ...} list of probabilities for the nodes to be loaded (if nil, always load)
77 -- @param filename (without externsion) with the path to save the shcematic and metadata to
78 -- @param slice_prob_list = {{ypos=,prob=}, ...} list of probabilities for the slices to be loaded (if nil, always load)
79 -- @return The number of nodes with metadata.
80 local function save_region(minp, maxp, probability_list, filename, slice_prob_list)
82 local success = minetest.create_schematic(minp, maxp, probability_list, filename .. ".mts", slice_prob_list)
83 if not success then
84 minetest.log("error", "[tutorial] problem creating schematic on ".. minetest.pos_to_string(minp) .. ": " .. filename)
85 return false
86 end
88 local manip = minetest.get_voxel_manip()
89 manip:read_from_map(minp, maxp)
90 local pos = {x=minp.x, y=0, z=0}
91 local count = 0
92 local nodes = {}
93 local get_node, get_meta = minetest.get_node, minetest.get_meta
94 while pos.x <= maxp.x do
95 pos.y = minp.y
96 while pos.y <= maxp.y do
97 pos.z = minp.z
98 while pos.z <= maxp.z do
99 local node = get_node(pos)
100 if node.name ~= "air" and node.name ~= "ignore" then
101 local meta = get_meta(pos):to_table()
103 local meta_empty = true
104 -- Convert metadata item stacks to item strings
105 for name, inventory in pairs(meta.inventory) do
106 for index, stack in ipairs(inventory) do
107 meta_empty = false
108 inventory[index] = stack.to_string and stack:to_string() or stack
111 if meta.fields and next(meta.fields) ~= nil then
112 meta_empty = false
115 if not meta_empty then
116 count = count + 1
117 nodes[count] = {
118 x = pos.x - minp.x,
119 y = pos.y - minp.y,
120 z = pos.z - minp.z,
121 meta = meta,
125 pos.z = pos.z + 1
127 pos.y = pos.y + 1
129 pos.x = pos.x + 1
131 if count > 0 then
133 local result = {
134 size = {
135 x = maxp.x - minp.x,
136 y = maxp.y - minp.y,
137 z = maxp.z - minp.z,
139 nodes = nodes,
142 -- Serialize entries
143 result = minetest.serialize(result)
145 local file, err = insecure_environment.io.open(filename..".meta", "wb")
146 if err ~= nil then
147 error("Couldn't write to \"" .. filename .. "\"")
149 file:write(minetest.compress(result))
150 file:flush()
151 file:close()
152 minetest.log("action", "[tutorial] schematic + metadata saved: " .. filename)
153 else
154 minetest.log("action", "[tutorial] schematic (no metadata) saved: " .. filename)
156 return success, count
161 -- Places the schematic specified in the given position.
162 -- @param minp Lowest position (in all 3 coordinates) of the area to load
163 -- @param filename without extension, but with path of the file to load
164 -- @param vmanip voxelmanip object to use to place the schematic in
165 -- @param rotation can be 0, 90, 180, 270, or "random".
166 -- @param replacements = {["old_name"] = "convert_to", ...}
167 -- @param force_placement is a boolean indicating whether nodes other than air and ignore are replaced by the schematic
168 -- @return boolean indicating success or failure
169 local function load_region(minp, filename, vmanip, rotation, replacements, force_placement)
171 if rotation == "random" then
172 rotation = {nil, 90, 180, 270}
173 rotation = rotation[math.random(1,4)]
176 local success
177 if vmanip and minetest.place_schematic_on_vmanip then
178 success = minetest.place_schematic_on_vmanip(vmanip, minp, filename .. ".mts", tostring(rotation), replacements, force_placement)
179 else
180 success = minetest.place_schematic(minp, filename .. ".mts", tostring(rotation), replacements, force_placement)
183 if success == false then
184 minetest.log("action", "[tutorial] schematic partionally loaded on ".. minetest.pos_to_string(minp))
185 elseif not success then
186 minetest.log("error", "[tutorial] problem placing schematic on ".. minetest.pos_to_string(minp) .. ": " .. filename)
187 return nil
190 local data = tutorial.sector_data[filename]
191 if not data then return true, {} end
193 local get_meta = minetest.get_meta
195 local spawners = {}
196 if not rotation or rotation == 0 then
197 for i, entry in ipairs(data.nodes) do
198 entry.x, entry.y, entry.z = minp.x + entry.x, minp.y + entry.y, minp.z + entry.z
199 if entry.meta then
200 get_meta(entry):from_table(entry.meta)
201 if entry.meta.fields.spawned then
202 table.insert(spawners, {x=entry.x, y=entry.y, z=entry.z})
206 else
207 local maxp_x, maxp_z = minp.x + data.size.x, minp.z + data.size.z
208 if rotation == 90 then
209 for i, entry in ipairs(data.nodes) do
210 entry.x, entry.y, entry.z = minp.x + entry.z, minp.y + entry.y, maxp_z - entry.x
211 if entry.meta then get_meta(entry):from_table(entry.meta) end
213 elseif rotation == 180 then
214 for i, entry in ipairs(data.nodes) do
215 entry.x, entry.y, entry.z = maxp_x - entry.x, minp.y + entry.y, maxp_z - entry.z
216 if entry.meta then get_meta(entry):from_table(entry.meta) end
218 elseif rotation == 270 then
219 for i, entry in ipairs(data.nodes) do
220 entry.x, entry.y, entry.z = maxp_x - entry.z, minp.y + entry.y, minp.z + entry.x
221 if entry.meta then get_meta(entry):from_table(entry.meta) end
223 else
224 minetest.log("error", "[tutorial] unsupported rotation angle: " .. (rotation or "nil"))
225 return false
228 minetest.log("action", "[tutorial] schematic + metadata loaded on ".. minetest.pos_to_string(minp))
229 return true, spawners
232 local function save_schematic()
233 local success = true
234 for k,sector in pairs(tutorial.map_sector) do
235 local filename = tutorial.map_directory .. "sector_"..k
236 local minp = sector
237 local maxp = {
238 x = sector.x + sector.l,
239 y = sector.y + sector.l,
240 z = sector.z + sector.l
242 if not save_region(minp, maxp, nil, filename) then
243 minetest.log("error", "[tutorial] error loading Tutorial World sector " .. minetest.pos_to_string(sector))
244 success = false
247 return success
250 local function load_schematic()
251 local success = true
252 for k,sector in pairs(tutorial.map_sector) do
253 local filename = tutorial.map_directory .. "sector_"..k
254 minetest.log("action", "loading sector " .. minetest.pos_to_string(sector))
255 sector.maxp = vector.add(sector, {x=sector.l, y=sector.l, z=sector.l})
257 -- Load the area above the schematic to guarantee we have blue sky above
258 -- and prevent lighting glitches
259 --minetest.emerge_area(vector.add(sector, {x=0, y=sector.l, z=0}), vector.add(sector.maxp, {x=0,y=32,z=0}))
261 local vmanip = VoxelManip(sector, sector.maxp)
262 if not load_region(sector, filename, vmanip, nil, nil, true) then
263 minetest.log("error", "[tutorial] error loading Tutorial World sector " .. minetest.pos_to_string(sector))
264 success = false
266 vmanip:calc_lighting()
267 vmanip:write_to_map()
268 vmanip:update_map()
270 return success
274 ------ Commands
276 minetest.register_privilege("tutorialmap", "Can use commands to manage the tutorial map")
277 minetest.register_chatcommand("treset", {
278 params = "",
279 description = "Resets the tutorial map",
280 privs = {tutorialmap=true},
281 func = function(name, param)
282 if load_schematic() then
283 minetest.chat_send_player(name, "Tutorial World schematic loaded")
284 else
285 minetest.chat_send_player(name, "An error occurred while loading Tutorial World schematic")
288 -- TODO: re-load entities?
289 end,
292 -- Add commands for saving map and entities, but only if tutorial mod is trusted
293 if insecure_environment then
294 minetest.register_chatcommand("tsave", {
295 params = "",
296 description = "Saves the tutorial map",
297 privs = {tutorialmap=true},
298 func = function(name, param)
299 if save_schematic() then
300 minetest.chat_send_player(name, "Tutorial World schematic saved")
301 else
302 minetest.chat_send_player(name, "An error occurred while saving Tutorial World schematic")
304 end,
308 ------ Map Generation
310 local vbuffer = nil
312 tutorial.state = tutorial.state or {}
313 tutorial.state.loaded = tutorial.state.loaded or {}
314 minetest.register_on_generated(function(minp, maxp, seed)
315 local state_changed = false
316 local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
318 for k,sector in pairs(tutorial.map_sector) do
319 if not tutorial.state.loaded[k] then
321 if sector.maxp == nil then
322 sector.maxp = {
323 x = sector.x + sector.l,
324 y = sector.y + sector.l,
325 z = sector.z + sector.l,
329 -- Only load it if not out of the generating range
330 if not ((maxp.x < sector.x) or (minp.x > sector.maxp.x)
331 or (maxp.y < sector.y) or (minp.y > sector.maxp.y)
332 or (maxp.z < sector.z) or (minp.z > sector.maxp.z))
333 then
335 local filename = tutorial.map_directory .. "sector_" .. k
336 local loaded, spawners = load_region(sector, filename, vm)
337 if loaded then
338 -- Initialize item spawners in the area as well, and mark it as loaded
339 init_item_spawners(spawners)
340 tutorial.state.loaded[k] = true
342 state_changed = true
347 -- Generate a flat grass land and a dirt-only underground for the rest of the map
348 if map_editing ~= true then
349 local grasslev = 0
350 if minp.y <= grasslev then
351 local vdata = vm:get_data(vbuffer)
352 local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
353 for x = minp.x, maxp.x do
354 for z = minp.z, maxp.z do
355 for y = minp.y, maxp.y do
356 local p_pos = area:index(x, y, z)
357 local p_pos_above
358 if minp.y <= grasslev+1 and maxp.y >= maxp.y then
359 p_pos_above = area:index(x, y + 1, z)
361 local _, areas_count = areas:getAreasAtPos({x=x,y=y,z=z})
362 if areas_count == 0 and vdata[p_pos] == minetest.CONTENT_AIR then
363 if y == grasslev then
364 vdata[p_pos] = c_dirt_with_grass
365 if p_pos_above and vdata[p_pos_above] == minetest.CONTENT_AIR then
366 if math.random(0,50) == 0 then
367 vdata[p_pos_above] = c_grass
370 elseif y < grasslev then
371 vdata[p_pos] = c_dirt
377 vm:set_data(vdata)
378 state_changed = true
382 if(state_changed) then
383 vm:calc_lighting(nil, nil, false)
384 vm:write_to_map()
385 tutorial.save_state()
387 end)
389 minetest.set_mapgen_setting("mg_name", "singlenode")
390 minetest.set_mapgen_setting("water_level", "-31000")
391 minetest.set_mapgen_setting("chunksize", tostring(tutorial.sector_size/16))
393 -- coordinates for the first time the player spawns
394 tutorial.first_spawn = { pos={x=42,y=0.5,z=28}, yaw=(math.pi * 0.5) }