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
)
24 local timer
= minetest
.get_node_timer(spawners
[n
])
28 minetest
.log("action", "[tutorial] " .. count
.. " item spawners initialized")
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
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
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)})
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")
66 local data
= minetest
.deserialize(minetest
.decompress(f
:read("*a")))
67 tutorial
.sector_data
[filename
] = data
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
)
84 minetest
.log("error", "[tutorial] problem creating schematic on ".. minetest
.pos_to_string(minp
) .. ": " .. filename
)
88 local manip
= minetest
.get_voxel_manip()
89 manip
:read_from_map(minp
, maxp
)
90 local pos
= {x
=minp
.x
, y
=0, z
=0}
93 local get_node
, get_meta
= minetest
.get_node
, minetest
.get_meta
94 while pos
.x
<= maxp
.x
do
96 while pos
.y
<= maxp
.y
do
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
108 inventory
[index
] = stack
.to_string
and stack
:to_string() or stack
111 if meta
.fields
and next(meta
.fields
) ~= nil then
115 if not meta_empty
then
143 result
= minetest
.serialize(result
)
145 local file
, err
= insecure_environment
.io
.open(filename
..".meta", "wb")
147 error("Couldn't write to \"" .. filename
.. "\"")
149 file
:write(minetest
.compress(result
))
152 minetest
.log("action", "[tutorial] schematic + metadata saved: " .. filename
)
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)]
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
)
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
)
190 local data
= tutorial
.sector_data
[filename
]
191 if not data
then return true, {} end
193 local get_meta
= minetest
.get_meta
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
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
})
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
224 minetest
.log("error", "[tutorial] unsupported rotation angle: " .. (rotation
or "nil"))
228 minetest
.log("action", "[tutorial] schematic + metadata loaded on ".. minetest
.pos_to_string(minp
))
229 return true, spawners
232 local function save_schematic()
234 for k
,sector
in pairs(tutorial
.map_sector
) do
235 local filename
= tutorial
.map_directory
.. "sector_"..k
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
))
250 local function load_schematic()
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
))
266 vmanip
:calc_lighting()
267 vmanip
:write_to_map()
276 minetest
.register_privilege("tutorialmap", "Can use commands to manage the tutorial map")
277 minetest
.register_chatcommand("treset", {
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")
285 minetest
.chat_send_player(name
, "An error occurred while loading Tutorial World schematic")
288 -- TODO: re-load entities?
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", {
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")
302 minetest
.chat_send_player(name
, "An error occurred while saving Tutorial World schematic")
308 ------ Map Generation
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
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
))
335 local filename
= tutorial
.map_directory
.. "sector_" .. k
336 local loaded
, spawners
= load_region(sector
, filename
, vm
)
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
347 -- Generate a flat grass land and a dirt-only underground for the rest of the map
348 if map_editing
~= true then
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
)
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
382 if(state_changed
) then
383 vm
:calc_lighting(nil, nil, false)
385 tutorial
.save_state()
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) }