449 lines
14 KiB
Lua
449 lines
14 KiB
Lua
|
|
|
|
-- Directory where the map data will be stored
|
|
tutorial.map_directory = minetest.get_modpath("tutorial").."/mapdata/"
|
|
|
|
-- entity management functions
|
|
|
|
function save_entities()
|
|
local entities = {}
|
|
local count = 0;
|
|
|
|
for id,entity in pairs(minetest.luaentities) do
|
|
local entry = {
|
|
pos = entity.object:getpos(),
|
|
name = entity.name,
|
|
staticdata = entity.object:get_luaentity().get_staticdata(entity.object)
|
|
}
|
|
if entry.name == "__builtin:item" then
|
|
entry.itemstring = entity.itemstring
|
|
end
|
|
table.insert(entities, entry)
|
|
count = count+1
|
|
minetest.log("action", "[tutorial] entity FOUND to be saved: "..(entry.itemstring or entry.name).." at " ..entry.pos.x..","..entry.pos.y..","..entry.pos.z)
|
|
end
|
|
|
|
-- Because the entities can easily be unloaded, we won't override the
|
|
-- entities save file. Instead, we will try to deduce as best as we can to try
|
|
-- to include as well the already saved entities without creating duplicates.
|
|
local saved_entities = get_saved_entities()
|
|
|
|
for k,ent in pairs(saved_entities) do
|
|
local already_added=false
|
|
|
|
for k,e in pairs(entities) do
|
|
if math.abs(ent.pos.x-e.pos.x) + math.abs(ent.pos.y - e.pos.y) + math.abs(ent.pos.z -e.pos.z) < 1 then
|
|
already_added=true
|
|
break
|
|
end
|
|
end
|
|
if not already_added then
|
|
table.insert(entities,ent)
|
|
count = count + 1
|
|
minetest.log("action", "[tutorial] entity to CONTINUE saved: "..(ent.itemstring or ent.name).." at " ..ent.pos.x..","..ent.pos.y..","..ent.pos.z)
|
|
end
|
|
end
|
|
|
|
local str = minetest.serialize(entities)
|
|
|
|
local filename = tutorial.map_directory .. "entities"
|
|
local file, err = io.open(filename, "wb")
|
|
if err ~= nil then
|
|
error("Couldn't write to \"" .. filename .. "\"")
|
|
end
|
|
file:write(minetest.compress(str))
|
|
file:flush()
|
|
file:close()
|
|
minetest.log("action","[tutorial] " .. filename .. ": " .. count .. " entities saved")
|
|
return count
|
|
end
|
|
|
|
function get_saved_entities()
|
|
local filename = tutorial.map_directory .. "entities"
|
|
local f, err = io.open(filename, "rb")
|
|
if not f then
|
|
minetest.log("action", "[tutorial] Could not open file '" .. filename .. "': " .. err)
|
|
return {}
|
|
end
|
|
local entities = minetest.deserialize(minetest.decompress(f:read("*a")))
|
|
f:close()
|
|
return entities
|
|
end
|
|
|
|
function load_entities()
|
|
local entities = get_saved_entities()
|
|
|
|
local count = 0
|
|
for k,entity in pairs(entities) do
|
|
if entity.name == "__builtin:item" then
|
|
minetest.add_item(entity.pos, entity.itemstring)
|
|
else
|
|
local luaentity = minetest.add_entity(entity.pos, entity.name)
|
|
luaentity.on_activate(luaentity, entity.staticdata)
|
|
end
|
|
count = count + 1
|
|
end
|
|
minetest.log("action", "[tutorial] " .. count .. " entities loaded")
|
|
end
|
|
|
|
function load_entities_area(minp, maxp)
|
|
|
|
if not tutorial.entities_cache then
|
|
tutorial.entities_cache = get_saved_entities()
|
|
end
|
|
|
|
local count = 0
|
|
for k,entity in pairs(tutorial.entities_cache) do
|
|
|
|
-- Only load it if not out of the generating range
|
|
if not ((maxp.x < entity.pos.x) or (minp.x > entity.pos.x)
|
|
or (maxp.y < entity.pos.y) or (minp.y > entity.pos.y)
|
|
or (maxp.z < entity.pos.z) or (minp.z > entity.pos.z))
|
|
then
|
|
if entity.name == "__builtin:item" then
|
|
minetest.add_item(entity.pos, entity.itemstring)
|
|
else
|
|
local luaentity = minetest.add_entity(entity.pos, entity.name)
|
|
luaentity.on_activate(luaentity, entity.staticdata)
|
|
end
|
|
count = count + 1
|
|
end
|
|
end
|
|
minetest.log("action", "[tutorial] " .. count .. " entities loaded")
|
|
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
|
|
--]]
|
|
|
|
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] error loading Tutorial World sector " .. minetest.pos_to_string(sector))
|
|
success = false
|
|
end
|
|
end
|
|
return success
|
|
end
|
|
|
|
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] 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
|
|
|
|
|
|
-- 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.
|
|
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] 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 = 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] schematic + metadata saved: " .. filename)
|
|
else
|
|
minetest.log("action", "[tutorial] 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
|
|
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] schematic partionally loaded on ".. minetest.pos_to_string(minp))
|
|
elseif not success then
|
|
minetest.log("error", "[tutorial] problem placing schematic on ".. minetest.pos_to_string(minp) .. ": " .. filename)
|
|
return nil
|
|
end
|
|
|
|
local f, err = io.open(filename..".meta", "rb")
|
|
if not f then
|
|
minetest.log("action", "[tutorial] schematic loaded on ".. minetest.pos_to_string(minp))
|
|
return true
|
|
end
|
|
local data = minetest.deserialize(minetest.decompress(f:read("*a")))
|
|
f:close()
|
|
if not data then return end
|
|
|
|
local get_meta = minetest.get_meta
|
|
|
|
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) 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] unsupported rotation angle: " .. (rotation or "nil"))
|
|
return false
|
|
end
|
|
end
|
|
minetest.log("action", "[tutorial] schematic + metadata loaded on ".. minetest.pos_to_string(minp))
|
|
return true
|
|
end
|
|
|
|
|
|
------ Commands
|
|
|
|
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: right now there's no clear way we can properly remove all entities
|
|
--remove_entities()
|
|
--load_entities()
|
|
end,
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
minetest.register_chatcommand("tsave_entities", {
|
|
params = "",
|
|
description = "Saves the tutorial map",
|
|
privs = {tutorialmap=true},
|
|
func = function(name, param)
|
|
for k,s in pairs(tutorial.map_sector) do
|
|
minetest.forceload_block(s)
|
|
end
|
|
local count = save_entities()
|
|
minetest.chat_send_player(name, count .. " entities saved")
|
|
end,
|
|
})
|
|
|
|
------ Map Generation
|
|
|
|
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 = load_region(sector, filename, vm)
|
|
if loaded then
|
|
-- Load entities in the area as well, and mark it as loaded
|
|
load_entities_area(sector, sector.maxp)
|
|
tutorial.state.loaded[k] = true
|
|
end
|
|
state_changed = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if(state_changed) then
|
|
vm:calc_lighting(nil, nil, false)
|
|
vm:write_to_map()
|
|
tutorial.save_state()
|
|
-- Update the lgihting of the sector below as well
|
|
minp.y = minp.y - tutorial.sector_size
|
|
local vm = minetest.get_voxel_manip(minp, maxp)
|
|
vm:calc_lighting(nil, nil, false)
|
|
vm:write_to_map()
|
|
vm:update_map()
|
|
end
|
|
end)
|
|
|
|
minetest.register_on_mapgen_init(function(mgparams)
|
|
minetest.set_mapgen_params({mgname="singlenode", water_level=-31000, chunksize=(tutorial.sector_size/16)})
|
|
end)
|
|
|
|
-- coordinates for the first time the player spawns
|
|
tutorial.first_spawn = { pos={x=42,y=0.5,z=28}, yaw=(math.pi * 0.5) }
|