765 lines
19 KiB
Lua

boxes = {}
local modpath = minetest.get_modpath(minetest.get_current_modname())
-- Box allocation
dofile(modpath .. "/valloc.lua")
-- Handling the data that encodes boxes
dofile(modpath .. "/data.lua")
local function bytes_to_string(bytes)
local s = {}
for i = 1, #bytes do
s[i] = string.char(bytes[i])
end
return table.concat(s)
end
-- Set the region from minp to maxp to air
function boxes.cleanup(minp, maxp)
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
local cid = minetest.get_content_id("air")
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid
param2[index] = 0
index = index + 1
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
end
function boxes.read_box(box_data)
local data_index = 1
local function read_u8()
local c = string.byte(box_data, data_index)
data_index = data_index + 1
return c
end
local function read_u16()
local a = read_u8()
local b = read_u8()
return 256 * a + b
end
local function read_pos()
local x = read_u16()
local y = read_u16()
local z = read_u16()
return {x = x, y = y, z = z}
end
local size = read_pos()
local entry = read_pos()
local exit = read_pos()
return {
size = size,
entry = entry,
exit = exit,
data = string.sub(box_data, data_index)
}
end
function boxes.write_box(box)
local data_index = 1
local data = {}
local function write_u8(x)
data[data_index] = x
data_index = data_index + 1
end
local function write_u16(x)
write_u8(math.floor(x / 256))
write_u8(x % 256)
end
local function write_pos(p)
write_u16(p.x)
write_u16(p.y)
write_u16(p.z)
end
write_pos(box.size)
write_pos(box.entry)
write_pos(box.exit)
return bytes_to_string(data) .. box.data
end
function vector.min(a, b)
return {
x = math.min(a.x, b.x),
y = math.min(a.y, b.y),
z = math.min(a.z, b.z),
}
end
function vector.max(a, b)
return {
x = math.max(a.x, b.x),
y = math.max(a.y, b.y),
z = math.max(a.z, b.z),
}
end
local players_in_boxes = {}
local function read_file(filename)
local file = io.open(filename, "r")
if file ~= nil then
local file_content = file:read("*all")
io.close(file)
return file_content
end
return ""
end
local function write_file(filename, data)
local file, err = io.open(filename, "w")
if file then
file:write(data)
io.close(file)
else
error(err)
end
end
local BOX_TYPE = 0
local ENTRY_TYPE = 1
local EXIT_TYPE = 2
if db.box_get_meta(0) == nil then
local box_data = read_file(modpath .. "/entry.box")
local box = boxes.read_box(box_data)
db.box_set_data(0, box.data)
db.box_set_meta(0, {
type = ENTRY_TYPE,
meta = {
size = box.size,
entry = box.entry,
exit = box.exit,
}
})
end
if db.box_get_meta(1) == nil then
local box_data = read_file(modpath .. "/exit.box")
local box = boxes.read_box(box_data)
db.box_set_data(1, box.data)
db.box_set_meta(1, {
type = EXIT_TYPE,
meta = {
size = box.size,
entry = box.entry,
exit = box.exit,
}
})
end
if db.box_get_meta(2) == nil then
local box_data = read_file(modpath .. "/box.box")
local box = boxes.read_box(box_data)
db.box_set_data(2, box.data)
db.box_set_meta(2, {
type = BOX_TYPE,
meta = {
size = box.size,
entry = box.entry,
exit = box.exit,
}
})
end
local function pop_node(pos)
local node = minetest.get_node(pos)
local meta = minetest.get_meta(pos)
local r = {pos = pos, node = node, meta = meta:to_table()}
minetest.remove_node(pos)
return r
end
local function unpop_node(data)
minetest.set_node(data.pos, data.node)
local meta = minetest.get_meta(data.pos)
meta:from_table(data.meta)
end
local function open_door(player, pos1, pos2)
local minp = vector.min(pos1, pos2)
local maxp = vector.max(pos1, pos2)
local nds = {}
local i = 1
for x = minp.x, maxp.x do
for y = minp.y, maxp.y do
for z = minp.z, maxp.z do
nds[i] = pop_node({x = x, y = y, z = z})
i = i + 1
end
end
end
local od = players_in_boxes[player:get_player_name()].open_doors
od[#od + 1] = {minx = maxp.x + 0.5, nodes = nds}
end
function boxes.open_exit(player)
local name = player:get_player_name()
local exds = players_in_boxes[name].exit_doors
local n = #exds
if n == 0 then return end
local ex = exds[n]
exds[n] = nil
open_door(player, ex, vector.add(ex, {x = -1, y = 1, z = 0}))
end
-- Close doors behind players
minetest.register_globalstep(function(dtime)
for playername, info in pairs(players_in_boxes) do
local i = 1
local player = minetest.get_player_by_name(playername)
local pos = player:get_pos()
local doors = info.open_doors
local n = #doors
while doors[i] do
if pos.x >= doors[i].minx then
for _, data in ipairs(doors[i].nodes) do
unpop_node(data)
end
doors[i] = doors[n]
doors[n] = nil
n = n - 1
else
i = i + 1
end
end
end
end)
function boxes.open_box(player, box_id_list)
local name = player:get_player_name()
if players_in_boxes[name] ~= nil then
return
end
local offset = {x = 0, y = 0, z = 0}
local pmin = {x = 0, y = 0, z = 0}
local pmax = {x = 0, y = 0, z = 0}
local metas = {}
local offsets = {}
for i, box in ipairs(box_id_list) do
local meta = db.box_get_meta(box).meta
metas[i] = meta
offset = vector.subtract(offset, meta.entry)
pmin = vector.min(pmin, offset)
pmax = vector.max(pmax, vector.add(offset, meta.size))
offsets[i] = offset
offset = vector.add(offset, meta.exit)
end
for i, off in ipairs(offsets) do
offsets[i] = vector.subtract(off, pmin)
end
pmax = vector.subtract(pmax, pmin)
local size = math.max(pmax.x, pmax.z)
local minp = boxes.valloc(size)
local maxp = vector.add(minp, vector.subtract(pmax, 1))
for i, box in ipairs(box_id_list) do
local data = db.box_get_data(box)
boxes.load(vector.add(minp, offsets[i]), data)
end
local spawn_pos = vector.add(minp, vector.add(metas[1].entry, offsets[1]))
player:set_pos(spawn_pos)
local exit_doors = {}
local n = #box_id_list
for i = 1, n - 1 do
local exd = vector.add(minp, vector.add(metas[n - i].exit, offsets[n - i]))
exit_doors[i] = exd
end
players_in_boxes[name] = {
minp = minp,
maxp = maxp,
exit_doors = exit_doors,
exit = vector.add(minp, vector.add(metas[n].exit, offsets[n])),
open_doors = {},
}
boxes.open_exit(player)
end
function boxes.close_box(player)
local name = player:get_player_name()
if players_in_boxes[name] == nil then
return
end
local bx = players_in_boxes[name]
boxes.cleanup(bx.minp, bx.maxp)
boxes.vfree(bx.minp)
players_in_boxes[name] = nil
end
minetest.register_chatcommand("enter", {
params = "<boxid>",
description = "Enter box with this id",
privs = {server = true},
func = function(name, param)
local player = minetest.get_player_by_name(name)
if players_in_boxes[name] then
minetest.chat_send_player(name, "You are already in a box!")
return
end
local id = tonumber(param)
if not id or id ~= math.floor(id) or id < 0 then
minetest.chat_send_player(name, "The id you supplied is not a nonnegative interger.")
return
end
local meta = db.box_get_meta(id)
if not meta or meta.type ~= BOX_TYPE then
minetest.chat_send_player(name, "The id you supplied does not correspond to any box.")
return
end
boxes.open_box(player, {0, id, 1})
end,
})
minetest.register_chatcommand("leave", {
params = "",
description = "Leave the current box",
privs = {server = true},
func = function(name, param)
if not players_in_boxes[name] then
minetest.chat_send_player(name, "You are not in a box!")
return
end
local player = minetest.get_player_by_name(name)
boxes.close_box(player)
players.return_to_lobby(player)
end,
})
minetest.register_chatcommand("open", {
params = "",
description = "Open the current exit.",
privs = {server = true},
func = function(name, param)
if not players_in_boxes[name] then
minetest.chat_send_player(name, "You are not in a box!")
return
end
if players_in_boxes[name].exit_doors[1] == nil then
minetest.chat_send_player(name, "There are no more exits to open!")
return
end
local player = minetest.get_player_by_name(name)
boxes.open_exit(player)
end,
})
-- This is only still needed if we want to change the size, the entry, or the
-- exit of the lobbies; changing the contents can be done by /edite.
local lobby_updates = {}
minetest.register_chatcommand("update_lobby", {
params = "entry|exit",
description = "Set corresponding lobby. Use without parameter to start \
updating a lobby, then punch both corners of the lobby and the bottom node \
of the exit. In the case of the entry lobby, stand at its spawnpoint to run \
the command.",
privs = {server = true},
func = function(name, param)
if param ~= "entry" and param ~= "exit" and param ~= "" then
return
end
if param == "" then
lobby_updates[name] = {}
elseif not lobby_updates[name] then
minetest.chat_send_player(name, "Not all positions have been set.")
return
else
local pos1 = lobby_updates[name].pos1
local pos2 = lobby_updates[name].pos2
local pos3 = lobby_updates[name].pos3
if not pos1 or not pos2 or not pos3 then
minetest.chat_send_player(name, "Not all positions have been set.")
return
end
local minp = vector.min(pos1, pos2)
local maxp = vector.max(pos1, pos2)
local data = boxes.save(minp, maxp)
local p3 = vector.subtract(pos3, minp)
if param == "exit" then
local player = minetest.get_player_by_name(name)
local exit = vector.subtract(vector.round(player:get_pos()), minp)
-- Exit lobby is hardcoded as id 1 for now
db.box_set_data(1, data)
db.box_set_meta(1, {
type = EXIT_TYPE,
meta = {
size = vector.add(vector.subtract(maxp, minp), 1),
entry = p3,
exit = exit,
}
})
else
local player = minetest.get_player_by_name(name)
local entry = vector.subtract(vector.round(player:get_pos()), minp)
p3.x = p3.x + 1
-- Entry lobby is hardcoded as id 0 for now
db.box_set_data(0, data)
db.box_set_meta(0, {
type = ENTRY_TYPE,
meta = {
size = vector.add(vector.subtract(maxp, minp), 1),
entry = entry,
exit = p3,
}
})
end
minetest.chat_send_player(name, "Updated.")
lobby_updates[name] = nil
end
end,
})
minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
if not puncher then return end
local name = puncher:get_player_name()
if not lobby_updates[name] then return end
if not lobby_updates[name].pos1 then
lobby_updates[name].pos1 = pos
minetest.chat_send_player(name, "Position 1 set to " .. dump(pos) .. ".")
elseif not lobby_updates[name].pos2 then
lobby_updates[name].pos2 = pos
minetest.chat_send_player(name, "Position 2 set to " .. dump(pos) .. ".")
elseif not lobby_updates[name].pos3 then
lobby_updates[name].pos3 = pos
minetest.chat_send_player(name, "Position 3 set to " .. dump(pos) .. ".")
end
end)
local players_editing_boxes = {}
-- FIXME: get that from db each time, but how?
local min_free_id = 0
while db.box_get_meta(min_free_id) ~= nil do
min_free_id = min_free_id + 1
end
local digits = {[0] =
{ true, true, true,
true, false, true,
true, false, true,
true, false, true,
true, true, true,
},
{false, false, true,
false, false, true,
false, false, true,
false, false, true,
false, false, true,
},
{ true, true, true,
false, false, true,
true, true, true,
true, false, false,
true, true, true,
},
{ true, true, true,
false, false, true,
true, true, true,
false, false, true,
true, true, true,
},
{ true, false, true,
true, false, true,
true, true, true,
false, false, true,
false, false, true,
},
{ true, true, true,
true, false, false,
true, true, true,
false, false, true,
true, true, true,
},
{ true, true, true,
true, false, false,
true, true, true,
true, false, true,
true, true, true,
},
{ true, true, true,
false, false, true,
false, false, true,
false, false, true,
false, false, true,
},
{ true, true, true,
true, false, true,
true, true, true,
true, false, true,
true, true, true,
},
{ true, true, true,
true, false, true,
true, true, true,
false, false, true,
true, true, true,
},
}
function boxes.make_new(player, size)
local minp = boxes.valloc(size + 2)
local maxp = vector.add(minp, size + 1)
-- Create the box
local cid_air = minetest.get_content_id("air")
local cid_stone = minetest.get_content_id("nodes:stone")
local cid_marble = minetest.get_content_id("nodes:marble")
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
-- Set to air
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid_air
param2[index] = 0
index = index + 1
end
end
end
-- Add stone for walls
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, minp.z)
local index2 = va:index(minp.x, y, maxp.z)
for x = minp.x, maxp.x do
vmdata[index] = cid_stone
vmdata[index2] = cid_stone
index = index + 1
index2 = index2 + 1
end
end
for z = minp.z, maxp.z do
local index = va:index(minp.x, minp.y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid_stone
index = index + 1
end
end
local ystride = emax.x - emin.x + 1
for z = minp.z, maxp.z do
local index = va:index(minp.x, minp.y, z)
local index2 = va:index(maxp.x, minp.y, z)
for y = minp.y, maxp.y do
vmdata[index] = cid_stone
vmdata[index2] = cid_stone
index = index + ystride
index2 = index2 + ystride
end
end
-- Write the box id
local id_string = tostring(min_free_id)
local id_sz = 4 * string.len(id_string) - 1
if size < 6 or size < id_sz + 2 then
print("WARNING: could not write box id: size too small")
else
local xoff = minp.x + math.floor((size + 2 - id_sz) / 2)
local yoff = minp.y + math.floor((size + 2 - 5 + 1) / 2)
local n = string.len(id_string)
for i = 1, string.len(id_string) do
for dx = 0, 2 do
for dy = 0, 4 do
if digits[string.byte(id_string, n - i + 1) - 48][3-dx+3*(4-dy)] then
local index = va:index(xoff + dx, yoff + dy, minp.z)
vmdata[index] = cid_marble
end
end
end
xoff = xoff + 4
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
local s2 = math.floor(size / 2)
local entry = {x = 0, y = 1, z = s2 + 1}
local exit = {x = size + 2, y = 1, z = s2 + 1}
local sz = {x = size + 2, y = size + 2, z = size + 2}
local meta = {
type = BOX_TYPE,
meta = {
entry = entry,
exit = exit,
size = sz,
}
}
player:set_pos(vector.add(minp, {x = 1, y = 1, z = s2 + 1}))
db.box_set_meta(min_free_id, meta)
db.box_set_data(min_free_id, boxes.save(minp, maxp))
players_editing_boxes[player:get_player_name()] = {
box_id = min_free_id,
minp = minp,
maxp = maxp,
}
min_free_id = min_free_id + 1
end
minetest.register_chatcommand("edit", {
params = "<box_size>",
description = "Start editing a new box.",
privs = {server = true},
func = function(name, param)
if players_editing_boxes[name] then
minetest.chat_send_player(name, "You are already editing a box!")
return
end
local size = tonumber(param)
if not size or size ~= math.floor(size) or size <= 0 then
minetest.chat_send_player(name, "Please specify a size.")
return
end
boxes.make_new(minetest.get_player_by_name(name), size)
end,
})
minetest.register_chatcommand("edite", {
params = "<box_id>",
description = "Edit an existing box.",
privs = {server = true},
func = function(name, param)
if players_editing_boxes[name] then
minetest.chat_send_player(name, "You are already editing a box!")
return
end
local id = tonumber(param)
if not id or id ~= math.floor(id) or id < 0 then
minetest.chat_send_player(name, "The id you supplied is not a nonnegative integer.")
return
end
local meta = db.box_get_meta(id)
if not meta then
minetest.chat_send_player(name, "The id you supplied is not in the database.")
end
local data = db.box_get_data(id)
local minp = boxes.valloc(math.max(meta.meta.size.x, meta.meta.size.z))
local maxp = vector.add(minp, vector.subtract(meta.meta.size, 1))
boxes.load(minp, data)
players_editing_boxes[name] = {
box_id = id,
minp = minp,
maxp = maxp,
}
local spawnpoint = vector.add({x = 1, y = 0, z = 0}, vector.add(minp, meta.meta.entry))
minetest.get_player_by_name(name):set_pos(spawnpoint)
end,
})
function boxes.save_edit(player, id)
local name = player:get_player_name()
local box = players_editing_boxes[name]
if not box then return end
if id == nil then
id = box.box_id
end
db.box_set_data(id, boxes.save(box.minp, box.maxp))
end
function boxes.stop_edit(player)
local name = player:get_player_name()
local box = players_editing_boxes[name]
if not box then return end
boxes.cleanup(box.minp, box.maxp)
boxes.vfree(box.minp)
players_editing_boxes[name] = nil
end
minetest.register_chatcommand("save", {
params = "[<id>]",
description = "Save the box you are currently editing. If id is supplied, a copy is instead saved to the box numbered id.",
privs = {server = true},
func = function(name, param)
if not players_editing_boxes[name] then
minetest.chat_send_player(name, "You are not currently editing a box!")
return
end
local box_id = nil
if param ~= "" then
local id = tonumber(param)
if not id or id ~= math.floor(id) or id < 0 then
minetest.chat_send_player(name, "The id you supplied is not a non-negative number.")
return
end
box_id = id
end
boxes.save_edit(minetest.get_player_by_name(name), box_id)
end,
})
minetest.register_chatcommand("stopedit", {
params = "",
description = "Stop editing a box.",
privs = {server = true},
func = function(name, param)
if not players_editing_boxes[name] then
minetest.chat_send_player(name, "You are not currently editing a box!")
return
end
local player = minetest.get_player_by_name(name)
boxes.save_edit(player)
boxes.stop_edit(player)
players.return_to_lobby(player)
end,
})
local function on_leaveplayer(player)
print("leave", player:get_player_name())
boxes.close_box(player)
boxes.save_edit(player)
boxes.stop_edit(player)
end
minetest.register_on_leaveplayer(on_leaveplayer)
minetest.register_on_shutdown(function()
for _, player in ipairs(minetest.get_connected_players()) do
on_leaveplayer(player)
end
db.shutdown()
end)