Auke Kok 80cbd6d34a Add license headers to all lua files.
Some of these are copies from the respective origins from mtg,
to make sure we have headers everywhere listing the proper code.

I've relicensed spectator_mode from WT*PL to LGPL-2.1. No other
licenses were changed.
2018-06-21 22:56:48 -07:00

1468 lines
41 KiB
Lua

--[[
ITB (insidethebox) minetest game - Copyright (C) 2017-2018 sofar & nore
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation; either version 2.1
of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
MA 02111-1307 USA
]]--
boxes = {}
boxes.teleport_to_tutorial_exit = {}
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")
-- Import/export boxes to files
dofile(modpath .. "/io.lua")
-- Handle inventory-related callbacks (i.e. whether the itemstacks
-- should be decreased when placing nodes)
dofile(modpath .. "/inv.lua")
-- Nodes that have a box-related effect
dofile(modpath .. "/nodes.lua")
-- Box score recording code
dofile(modpath .. "/score.lua")
local areas = AreaStore("insidethebox")
function boxes.find_box(pos)
local count = 0
local name = nil
for k, v in pairs(areas:get_areas_for_pos(pos, false, true)) do
name = v.data
count = count + 1
end
if count == 1 then
if boxes.players_in_boxes[name] then
return boxes.players_in_boxes[name]
elseif boxes.players_editing_boxes[name] then
return boxes.players_editing_boxes[name]
end
end
return false
end
-- Set the region from minp to maxp to air
function boxes.cleanup(minp, maxp)
-- Clear any leftover entities
local center = vector.divide(vector.add(minp, maxp), 2)
local radius = vector.length(vector.subtract(vector.add(maxp, 1), minp)) / 2
for _, obj in ipairs(minetest.get_objects_inside_radius(center, radius)) do
if not obj:is_player() then
local pos = obj:get_pos()
if minp.x - 0.5 <= pos.x and pos.x <= maxp.x + 0.5 and
minp.y - 0.5 <= pos.y and pos.y <= maxp.y + 0.5 and
minp.z - 0.5 <= pos.z and pos.z <= maxp.z + 0.5
then
obj:remove()
end
end
end
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()
minetest.after(0.1, minetest.fix_light, minp, maxp)
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
boxes.players_in_boxes = {}
local function open_door(player, pos1, pos2, bminp, bmaxp)
local minp = vector.min(pos1, pos2)
local maxp = vector.max(pos1, pos2)
local drs = {}
local i = 1
for x = minp.x, maxp.x do
for y = minp.y, maxp.y do
for z = minp.z, maxp.z do
local door = doors.get({x = x, y = y, z = z})
if door then
door:open()
drs[i] = {x = x, y = y, z = z}
i = i + 1
end
end
end
end
local od = boxes.players_in_boxes[player:get_player_name()].open_doors
od[#od + 1] = {minp = bminp, maxp = bmaxp, doors = drs, respawn =
{x = maxp.x + 1, y = minp.y, z = (minp.z + maxp.z) / 2}}
end
function boxes.player_success(player)
local name = player:get_player_name()
local box = boxes.players_in_boxes[name]
local id = box.box_id
local time_taken = minetest.get_gametime() - box.start_time
local deaths = box.deaths
local damage = box.damage
local bmeta = db.box_get_meta(id)
boxes.score(name, id, "time", {time_taken})
boxes.score(name, id, "damage", {damage})
boxes.score(name, id, "deaths", {deaths})
bmeta.meta.num_completed_players = db.box_get_num_completed_players(id)
db.box_set_meta(id, bmeta)
for _, p in ipairs(box.signs_to_update) do
local node = minetest.get_node(p)
if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_exit_update then
minetest.registered_nodes[node.name].on_exit_update(p, player, {
time_taken = {format = "time", data = time_taken},
damage_taken = {format = "text", data = tostring(damage)},
num_deaths = {format = "text", data = tostring(deaths)},
})
end
end
local sid = box.in_series
if sid then
local pmeta = db.player_get_meta(name)
local index = pmeta.series_progress[sid] or 1
pmeta.series_progress[sid] = index + 1
db.player_set_meta(name, pmeta)
-- check if player completed tutorial series
local bxs = db.series_get_boxes(sid)
if bxs[index + 1] then
return
end
-- omit check if tutorial is actually required here
if sid == conf.tutorial.series then
player:set_attribute("tutorial_completed", 1)
minetest.log("action", name .. " completed the tutorial")
minetest.chat_send_all(name .. " completed the tutorial!")
if irc then
irc.say(name .. " has completed the tutorial!")
end
-- reward: create priv
local privs = minetest.get_player_privs(name)
privs.create = true
minetest.set_player_privs(name, privs)
boxes.teleport_to_tutorial_exit[name] = true
end
end
end
function boxes.open_exit(player)
local name = player:get_player_name()
local box = boxes.players_in_boxes[name]
local exds = box.exit_doors
local n = #exds
if n == 0 then return end
local ex = exds[n]
exds[n] = nil
open_door(player, ex.door_pos, vector.add(ex.door_pos, {x = -1, y = 0, z = 1}), ex.box_minp, ex.box_maxp)
end
function boxes.increase_items(player)
local name = player:get_player_name()
if not boxes.players_in_boxes[name] then
return
end
local exds = boxes.players_in_boxes[name].exit_doors
local n = #exds
if n == 0 then return end
local ex = exds[n]
ex.door_items = ex.door_items - 1
if ex.door_items == 0 then
boxes.open_exit(player)
end
end
-- Close doors behind players
minetest.register_globalstep(function(dtime)
for playername, info in pairs(boxes.players_in_boxes) do
local i = 1
local player = minetest.get_player_by_name(playername)
local pos = player:get_pos()
local odoors = info.open_doors
local n = #odoors
while odoors[i] do
local off = 0.7
if odoors[i].minp.x + off <= pos.x and pos.x <= odoors[i].maxp.x - off and
odoors[i].minp.y <= pos.y and pos.y <= odoors[i].maxp.y and
odoors[i].minp.z + off <= pos.z and pos.z <= odoors[i].maxp.z - off
then
for _, dpos in ipairs(odoors[i].doors) do
local door = doors.get(dpos)
if door then
door:close()
end
end
info.respawn = odoors[i].respawn
odoors[i] = odoors[n]
odoors[n] = nil
n = n - 1
if not info.start_time then
info.start_time = minetest.get_gametime()
info.deaths = 0
info.damage = 0
music.start(player, info, "box")
else
-- We closed the last door: the player got out!
boxes.player_success(player)
minetest.log("action", playername .. " completed box " .. info.box_id)
minetest.chat_send_all(playername .. " completed box " .. info.box_id .. "!")
music.start(player, info, "exit")
end
else
i = i + 1
end
end
end
end)
local function translate_all(l, offset)
local result = {}
for i, p in ipairs(l) do
result[i] = vector.add(p, offset)
end
return result
end
-- Although this functions takes a list as an argument,
-- several parts of the code rely on it having length exactly
-- 3 and being entry lobby, box, exit
function boxes.open_box(player, box_id_list)
local name = player:get_player_name()
if boxes.players_in_boxes[name] ~= nil or boxes.players_editing_boxes[name] ~= nil then
return
end
minetest.log("action", name .. " entered box " .. box_id_list[2])
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))
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] = {
door_items = metas[n - i].num_items,
door_pos = exd,
box_minp = vector.add(minp, offsets[n - i + 1]),
box_maxp = vector.add(minp, vector.add(offsets[n - i + 1], vector.subtract(metas[n - i + 1].size, 1))),
}
end
local spawn_pos = vector.add(minp, vector.add(metas[1].entry, offsets[1]))
spawn_pos.y = spawn_pos.y - 0.5
local base_exit = vector.add(offsets[3], minp)
local signs_to_update = translate_all(metas[3].signs_to_update, base_exit)
local star_positions = translate_all(metas[3].star_positions, base_exit)
local category_positions = translate_all(metas[3].category_positions, base_exit)
local areas_id = areas:insert_area(vector.add(minp, 1), vector.subtract(maxp, 1), name)
boxes.players_in_boxes[name] = {
name = name,
box_id = box_id_list[2],
minp = minp,
maxp = maxp,
areas_id = areas_id,
exit_doors = exit_doors,
exit = vector.add(minp, vector.add(metas[n].exit, offsets[n])),
open_doors = {},
respawn = spawn_pos,
signs_to_update = signs_to_update,
star_positions = star_positions,
category_positions = category_positions,
}
for i, box in ipairs(box_id_list) do
boxes.load(vector.add(minp, offsets[i]), box, player)
end
player:set_physics_override({gravity = 0, jump = 0})
spawn_pos.y = spawn_pos.y + 0.5
player:set_pos(spawn_pos)
minetest.after(0.5, function()
player:set_physics_override({gravity = 1, jump = 1})
end)
player:set_look_horizontal(3 * math.pi / 2)
player:set_look_vertical(0)
players.give_box_inventory(player)
skybox.set(player, metas[n - 1].skybox)
boxes.open_exit(player)
minetest.after(2.0, function()
if not player then
return
end
local p = player:get_pos()
if p and p.y < spawn_pos.y - 0.25 and boxes.players_in_boxes[name] then
minetest.log("error", name .. " fell from an entrance lobby")
player:set_pos(spawn_pos)
end
end)
end
function boxes.close_box(player)
local name = player:get_player_name()
if boxes.players_in_boxes[name] == nil then
return
end
local bx = boxes.players_in_boxes[name]
minetest.log("action", name .. " left box " .. bx.box_id)
boxes.cleanup(bx.minp, bx.maxp)
boxes.vfree(bx.minp)
areas:remove_area(boxes.players_in_boxes[name].areas_id)
boxes.players_in_boxes[name] = nil
end
function boxes.next_series(player, sid, is_entering)
local name = player:get_player_name()
local pmeta = db.player_get_meta(name)
local index = pmeta.series_progress[sid] or 1
local bxs = db.series_get_boxes(sid)
if not bxs[index] then
if not is_entering then
players.return_to_lobby(player)
end
return false
else
if sid == conf.tutorial.series and
conf.tutorial.required and
player:get_attribute("tutorial_completed") ~= "1" then
boxes.open_box(player, {conf.tutorial.entry_lobby, bxs[index], conf.tutorial.exit_lobby})
else
boxes.open_box(player, {0, bxs[index], 1})
end
boxes.players_in_boxes[name].in_series = sid
return true
end
end
function boxes.validate_pos(player, pos, withborder)
local name = player:get_player_name()
local box = boxes.players_in_boxes[name] or boxes.players_editing_boxes[name]
if not box then
if not minetest.check_player_privs(player, "server") then
return false
end
box = {
minp = { x = -32768, y = -32768, z = -32768 },
maxp = { x = 32768, y = 32768, z = 32768 },
}
end
if withborder then
if pos.x < box.minp.x or pos.x > box.maxp.x or
pos.y < box.minp.y or pos.y > box.maxp.y or
pos.z < box.minp.z or pos.z > box.maxp.z then
return false
end
else
if pos.x < box.minp.x + 1 or pos.x > box.maxp.x - 1 or
pos.y < box.minp.y + 1 or pos.y > box.maxp.y - 1 or
pos.z < box.minp.z + 1 or pos.z > box.maxp.z - 1 then
return false
end
end
return true
end
boxes.players_editing_boxes = {}
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 boxes.players_in_boxes[name] or boxes.players_editing_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 ~= db.BOX_TYPE then
minetest.chat_send_player(name, "The id you supplied does not correspond to any box.")
return
end
-- only server admins may enter any box
-- normal players may not enter anything but published boxes or their own boxes
if minetest.check_player_privs(player, "server") or
meta.meta.status == db.STATUS_ACCEPTED or
meta.meta.builder == name then
boxes.open_box(player, {0, id, 1})
else
minetest.chat_send_player(name, "You do not have permissions to enter that box.")
end
end,
})
minetest.register_chatcommand("series_create", {
params = "<name>",
description = "Create a new series",
privs = {server = true},
func = function(name, param)
if param == "" then
minetest.chat_send_player(name, "Please privide a name for the series")
return
end
local id = db.series_create(param)
if id then
minetest.chat_send_player(name, "Series successfully created with id " .. id)
else
minetest.chat_send_player(name, "Series failed to create")
end
end
})
minetest.register_chatcommand("series_destroy", {
params = "<series_id>",
description = "Destroy a series",
privs = {server = true},
func = function(name, param)
if param == "" then
minetest.chat_send_player(name, "Please privide a series id to destroy")
return
end
local id = tonumber(param)
if not db.series_destroy(id) then
minetest.chat_send_player(name, "Failed to destroy series " .. id)
else
minetest.chat_send_player(name, "Destroyed series " .. id)
end
end
})
minetest.register_chatcommand("series_add", {
params = "<series_id> <box_id>",
description = "Add a box a series",
privs = {server = true},
func = function(name, param)
local sid, bid = string.match(param, "^([^ ]+) +(.+)$")
sid = tonumber(sid)
bid = tonumber(bid)
if not sid or not bid or not db.series_get_meta(sid) or not db.box_get_meta(bid) then
minetest.chat_send_player(name, "Box or series doesn't exist.")
return
end
db.series_add_at_end(sid, bid)
minetest.chat_send_player(name, "Done")
end
})
minetest.register_chatcommand("series_list", {
params = "",
description = "List defined series",
privs = {server = true},
func = function(name, param)
if param ~= "" then
-- list boxes in this series.
local sid = tonumber(param)
if not sid or not db.series_get_meta(sid) then
minetest.chat_send_player(name, "Series doesn't exist.")
return
end
for _, v in ipairs(db.series_get_boxes(sid)) do
minetest.chat_send_player(name, v)
end
else
-- list all series
for k, v in ipairs(db.series_get_series()) do
minetest.chat_send_player(name, v.id .. " - " .. v.name)
end
end
end
})
minetest.register_chatcommand("series_remove", {
params = "<series_id> <box_id>",
description = "Remove a box from specified series",
privs = {server = true},
func = function(name, param)
local sid, bid = string.match(param, "^([^ ]+) +(.+)$")
sid = tonumber(sid)
bid = tonumber(bid)
if not sid or not bid or not db.series_get_meta(sid) or not db.box_get_meta(bid) then
minetest.chat_send_player(name, "Box or series doesn't exist.")
return
end
db.series_delete_box(sid, bid)
minetest.chat_send_player(name, "Done")
end
})
minetest.register_chatcommand("leave", {
params = "",
description = "Leave the current box",
privs = {server = true},
func = function(name, param)
if not boxes.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)
music.stop(player)
players.return_to_lobby(player)
end,
})
minetest.register_chatcommand("open", {
params = "",
description = "Open the current exit.",
privs = {server = true},
func = function(name, param)
local box = boxes.players_in_boxes[name]
if not box then
minetest.chat_send_player(name, "You are not in a box!")
return
end
if box.exit_doors[1] == nil then
minetest.chat_send_player(name, "There are no more exits to open!")
return
end
if box.open_doors[1] ~= nil then
minetest.chat_send_player(name, "This box already has an open door!")
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 <box id>",
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)
local update_type, box_id = string.match(param, "^([^ ]+) +(.+)$")
if update_type ~= "entry" and update_type ~= "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
box_id = tonumber(box_id)
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 update_type == "exit" then
box_id = box_id or 1
local player = minetest.get_player_by_name(name)
local exit = vector.subtract(vector.round(player:get_pos()), minp)
db.box_set_data(box_id, data)
db.box_set_meta(box_id, {
type = db.EXIT_TYPE,
meta = {
size = vector.add(vector.subtract(maxp, minp), 1),
entry = p3,
exit = exit,
}
})
else
box_id = box_id or 0
local player = minetest.get_player_by_name(name)
local entry = vector.subtract(vector.round(player:get_pos()), minp)
p3.x = p3.x + 1
db.box_set_data(box_id, data)
db.box_set_meta(box_id, {
type = db.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 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)
-- Clear existing metadata
local meta_positions = minetest.find_nodes_with_meta(minp, maxp)
for _, pos in ipairs(meta_positions) do
minetest.get_meta(pos):from_table()
end
-- Create the box
local cid_air = minetest.get_content_id("air")
local cid_wall = minetest.get_content_id("nodes:marbleb")
local cid_digit = minetest.get_content_id("nodes:bronzeb")
local cid_barrier = minetest.get_content_id("nodes:barrier")
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 and barrier at the top
for z = minp.z, maxp.z do
local index = va:index(minp.x, minp.y, z)
local index2 = va:index(minp.x, maxp.y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid_wall
vmdata[index2] = cid_barrier
index = index + 1
index2 = index2 + 1
end
end
for y = minp.y, maxp.y - 1 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_wall
vmdata[index2] = cid_wall
index = index + 1
index2 = index2 + 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 - 1 do
vmdata[index] = cid_wall
vmdata[index2] = cid_wall
index = index + ystride
index2 = index2 + ystride
end
end
local box_id = db.get_last_box_id() + 1
-- Write the box id
local id_string = tostring(box_id)
local id_sz = 4 * string.len(id_string) - 1
if size < 6 or size < id_sz + 2 then
minetest.log("error", "boxes.make_new(" .. size .. "): box 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_digit
end
end
end
xoff = xoff + 4
end
end
local digit_pos = {
x = math.floor((size + 2 - id_sz) / 2),
y = math.floor((size + 2 - 5 + 1) / 2),
z = 0
}
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
minetest.after(0.1, minetest.fix_light, minp, maxp)
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 = db.BOX_TYPE,
meta = {
entry = entry,
exit = exit,
size = sz,
num_items = 0,
box_name = "(No name)",
builder = player:get_player_name(),
build_time = 0,
skybox = 0,
digit_pos = digit_pos,
}
}
player:set_pos(vector.add(minp, {x = 1, y = 1, z = s2 + 1}))
players.give_edit_inventory(player)
db.box_set_meta(box_id, meta)
db.box_set_data(box_id, boxes.save(minp, maxp))
local name = player:get_player_name()
local areas_id = areas:insert_area(vector.add(minp, 1), vector.subtract(maxp, 1), name)
boxes.players_editing_boxes[name] = {
name = name,
box_id = box_id,
minp = minp,
maxp = maxp,
areas_id = areas_id,
num_items = 0,
entry = vector.add(minp, entry),
exit = vector.add(minp, exit),
start_edit_time = minetest.get_gametime(),
box_name = "",
skybox = 0,
}
end
local function get_digit_pos(player)
local name = player:get_player_name()
local box = boxes.players_editing_boxes[name]
if not box then
return
end
local id = box.box_id
local meta = db.box_get_meta(id)
if meta.meta.digit_pos then
return meta.meta.digit_pos
end
-- Digit position is not specified in meta (old boxes)
-- We have to infer it.
local minp = box.minp
local maxp = {x = box.maxp.x, y = box.maxp.y, z = box.minp.z}
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 cid_digit = minetest.get_content_id("nodes:bronzeb")
local maxx = minp.x
local maxy = minp.y
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, minp.z)
for x = minp.x, maxp.x do
if vmdata[index] == cid_digit then
if x > maxx then
maxx = x
end
if y > maxy then
maxy = y
end
end
index = index + 1
end
end
local id_string = tostring(id)
local id_sz = 4 * string.len(id_string) - 1
local digit_pos = {x = maxx - minp.x - id_sz + 1, y = maxy - minp.y - 4, z = 0}
meta.meta.digit_pos = digit_pos
db.box_set_meta(id, meta)
return digit_pos
end
local function paint_digits(pos, node, id)
local id_string = tostring(id)
local n = string.len(id_string)
for i = 1, n 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 p = {x = pos.x + 4 * i - 4 + dx, y = pos.y + dy, z = pos.z}
minetest.set_node(p, node)
end
end
end
end
end
function boxes.move_digits(player, pos)
local name = player:get_player_name()
local box = boxes.players_editing_boxes[name]
if not box then
return
end
local id = box.box_id
local old_pos = get_digit_pos(player)
if not old_pos then
minetest.log("boxes.move_digits: error: old digit pos is nil")
return
end
paint_digits(vector.add(box.minp, old_pos), {name = "nodes:marbleb"}, id)
paint_digits(pos, {name = "nodes:bronzeb"}, id)
local meta = db.box_get_meta(id)
meta.meta.digit_pos = vector.subtract(pos, box.minp)
db.box_set_meta(id, meta)
end
minetest.register_privilege("create", "Can create new boxes")
minetest.register_chatcommand("create", {
params = "<box_size>",
description = "Start editing a new box.",
privs = {server = true},
func = function(name, param)
if boxes.players_editing_boxes[name] or boxes.players_in_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
if size < 20 or size > 40 then
minetest.chat_send_player(name, "Size is not in the allowed range [20, 40]")
return
end
boxes.make_new(minetest.get_player_by_name(name), size)
end,
})
minetest.register_chatcommand("edit", {
params = "",
description = "DEPRECATED",
privs = {server = true},
func = function(name, param)
minetest.chat_send_player(name, "Did you mean /create or /edite?")
end,
})
minetest.register_chatcommand("edite", {
params = "<box_id>",
description = "Edit an existing box.",
privs = {create = true},
func = function(name, param)
if boxes.players_editing_boxes[name] or boxes.players_in_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.")
return
end
if not minetest.check_player_privs(name, "server") then
if meta.meta.builder ~= name or meta.meta.status ~= db.STATUS_EDITING then
minetest.chat_send_player(name, "You are not allowed to edit this box.")
return
end
end
local player = minetest.get_player_by_name(name)
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))
local areas_id = areas:insert_area(vector.add(minp, 1), vector.subtract(maxp, 1), name)
boxes.players_editing_boxes[name] = {
name = name,
box_id = id,
minp = minp,
maxp = maxp,
areas_id = areas_id,
num_items = meta.meta.num_items,
entry = vector.add(minp, meta.meta.entry),
exit = vector.add(minp, meta.meta.exit),
start_edit_time = minetest.get_gametime(),
box_name = meta.meta.box_name,
skybox = meta.meta.skybox,
}
boxes.load(minp, id, player)
local spawnpoint = vector.add({x = 1, y = 0, z = 0}, vector.add(minp, meta.meta.entry))
player:set_pos(spawnpoint)
players.give_edit_inventory(player)
music.start(player, nil, "create")
-- skybox may not exist for lobbies
if meta.meta.skybox then
skybox.set(player, meta.meta.skybox)
end
end,
})
function boxes.save_edit(player, id)
local name = player:get_player_name()
local box = boxes.players_editing_boxes[name]
if not box then return end
local bmeta = db.box_get_meta(box.box_id)
if id == nil then
id = box.box_id
end
db.box_set_data(id, boxes.save(box.minp, box.maxp))
bmeta.meta.num_items = box.num_items
bmeta.meta.entry = vector.subtract(box.entry, box.minp)
bmeta.meta.exit = vector.subtract(box.exit, box.minp)
bmeta.meta.box_name = box.box_name
if box.box_name == "" then
bmeta.meta.box_name = "(No name)"
end
bmeta.meta.skybox = box.skybox
local t = minetest.get_gametime()
if name == bmeta.meta.builder then -- Do not count admin edit times
bmeta.meta.build_time = bmeta.meta.build_time + t - box.start_edit_time
box.start_edit_time = t
end
db.box_set_meta(id, bmeta)
minetest.log("action", name .. " saved box " .. id)
minetest.chat_send_player(name, "Box " .. id .. " saved successfully")
end
function boxes.stop_edit(player)
local name = player:get_player_name()
local box = boxes.players_editing_boxes[name]
if not box then return end
boxes.cleanup(box.minp, box.maxp)
boxes.vfree(box.minp)
areas:remove_area(boxes.players_editing_boxes[name].areas_id)
boxes.players_editing_boxes[name] = nil
end
minetest.register_chatcommand("setbuilder", {
params = "<id> <name>",
description = "Set the builder of the box <id> to <name>.",
privs = {server = true},
func = function(name, param)
local box_id, nname = string.match(param, "^([^ ]+) +(.+)$")
box_id = tonumber(box_id)
if not box_id or not nname then
minetest.chat_send_player(name, "Supplied box id/name missing or incorrect format!")
return
end
local bmeta = db.box_get_meta(box_id)
if not bmeta then
minetest.chat_send_player(name, "Supplied box id does not exist!")
return
end
if bmeta.type ~= db.BOX_TYPE then
minetest.chat_send_player(name, "This id does not correspond to a box.")
return
end
bmeta.meta.builder = nname
db.box_set_meta(box_id, bmeta)
minetest.chat_send_player(name, "Done.")
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 boxes.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 boxes.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)
if param ~= "false" then
boxes.save_edit(player)
end
boxes.stop_edit(player)
music.stop(player)
players.return_to_lobby(player)
end,
})
local function on_leaveplayer(player)
minetest.log("action", player:get_player_name() .. " left the game")
music.stop(player)
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)
minetest.register_on_respawnplayer(function(player)
local name = player:get_player_name()
if boxes.players_in_boxes[name] then
local box = boxes.players_in_boxes[name]
player:setpos(box.respawn)
elseif boxes.players_editing_boxes[name] then
local box = boxes.players_editing_boxes[name]
player:setpos(vector.add(box.entry, {x = 1, y = 0, z = 0}))
else
players.return_to_lobby(player)
end
return true
end)
minetest.register_on_dieplayer(function(player)
local name = player:get_player_name()
local box = boxes.players_in_boxes[name]
if box and box.deaths then
box.deaths = box.deaths + 1
end
end)
minetest.register_on_player_hpchange(function(player, hp_change)
local name = player:get_player_name()
local box = boxes.players_in_boxes[name]
if box and box.damage and hp_change < 0 then
box.damage = box.damage - hp_change
end
end, false)
function boxes.can_edit(player)
local name = player:get_player_name()
if boxes.players_editing_boxes[name] then
return true
end
if boxes.players_in_boxes[name] then
return false
end
return minetest.check_player_privs(name, "server")
end
local metadata_updates = {}
minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
if not puncher then return end
local name = puncher:get_player_name()
if not metadata_updates[name] then return end
if not boxes.players_editing_boxes[name] then return end
local box = boxes.players_editing_boxes[name]
local p = vector.subtract(pos, box.minp)
local upd = metadata_updates[name]
local key = upd.update_key
local bmeta = db.box_get_meta(box.box_id)
if upd.update_type == "set" then
bmeta.meta[key] = p
db.box_set_meta(box.box_id, bmeta)
metadata_updates[name] = nil
sfinv.set_player_inventory_formspec(puncher)
elseif upd.update_type == "add" then
bmeta.meta[key][#bmeta.meta[key] + 1] = p
db.box_set_meta(box.box_id, bmeta)
sfinv.set_player_inventory_formspec(puncher)
end
end)
local meta_types = {
[db.BOX_TYPE] = {
{key_name = "builder", type = "text"},
{key_name = "status", type = "number"},
},
[db.ENTRY_TYPE] = {
{key_name = "status", type = "number"},
},
[db.EXIT_TYPE] = {
{key_name = "signs_to_update", type = "poslist"},
{key_name = "star_positions", type = "poslist"},
{key_name = "category_positions", type = "poslist"},
{key_name = "status", type = "number"},
}
}
sfinv.register_page("boxes:admin", {
title = "Admin",
is_in_nav = function(self, player, context)
return minetest.check_player_privs(player, "server") and
boxes.players_editing_boxes[player:get_player_name()]
end,
get = function(self, player, context)
local name = player:get_player_name()
local box = boxes.players_editing_boxes[name]
local upd = metadata_updates[name]
local bmeta = db.box_get_meta(box.box_id)
local form = ""
local y = 0
for i, setting in ipairs(meta_types[bmeta.type]) do
local current_value = bmeta.meta[setting.key_name]
if setting.type == "text" then
form = form .. "field[0.3," .. (y + 0.8) .. ";5,1;" ..
tostring(i) .. ";" .. setting.key_name .. ";" ..
minetest.formspec_escape(current_value) .. "]" ..
"field_close_on_enter[" .. tostring(i) .. ";false]"
y = y + 1.2
elseif setting.type == "number" then
form = form .. "field[0.3," .. (y + 0.8) .. ";5,1;" ..
tostring(i) .. ";" .. setting.key_name .. ";" ..
tostring(current_value) .. "]" ..
"field_close_on_enter[" .. tostring(i) .. ";false]"
y = y + 1.2
elseif setting.type == "pos" then
local blab = "Edit"
if upd and upd.update_key == setting.key_name then
blab = "Cancel edit"
end
form = form .. "label[0," .. (y + 0.3) .. ";" ..
minetest.formspec_escape(setting.key_name .. ": " .. minetest.pos_to_string(current_value)) .. "]"
form = form .. "button[0," .. (y + 0.7) .. ";3,1;" ..
tostring(i) .. ";" .. blab .. "]"
y = y + 1.6
elseif setting.type == "poslist" then
local blab = "Edit"
if upd and upd.update_key == setting.key_name then
blab = "Stop edit"
end
local cvs = "{"
for j, pos in ipairs(current_value) do
if j > 1 then
cvs = cvs .. ", "
end
cvs = cvs .. minetest.pos_to_string(pos)
end
cvs = cvs .. "}"
form = form .. "label[0," .. (y + 0.3) .. ";" ..
minetest.formspec_escape(setting.key_name .. ": " .. cvs) .. "]"
form = form .. "button[0," .. (y + 0.7) .. ";3,1;" ..
tostring(i) .. ";" .. blab .. "]"
y = y + 1.6
end
end
return sfinv.make_formspec(player, context, form, false)
end,
on_player_receive_fields = function(self, player, context, fields)
if not minetest.check_player_privs(player, "server") then
return
end
local update = false
local name = player:get_player_name()
local box = boxes.players_editing_boxes[name]
local bmeta = db.box_get_meta(box.box_id)
local settings = meta_types[bmeta.type]
for index, value in pairs(fields) do
local i = tonumber(index)
if i ~= nil then
local setting = settings[i]
if setting.type == "number" then
value = tonumber(value)
end
--FIXME log changes
if value ~= nil and setting.type ~= "pos" and setting.type ~= "poslist" then
bmeta.meta[setting.key_name] = value
elseif setting.type == "pos" or setting.type == "poslist" then
if metadata_updates[name] and metadata_updates[name].update_key == setting.key_name then
metadata_updates[name] = nil
else
if setting.type == "poslist" then
bmeta.meta[setting.key_name] = {}
end
metadata_updates[name] = {
update_type = (setting.type == "pos" and "set" or "add"),
update_key = setting.key_name
}
end
update = true
end
end
end
db.box_set_meta(box.box_id, bmeta)
if update then
sfinv.set_page(player, "boxes:admin")
end
end,
})
--[[
-- Handle box expositions
-- These are boxes whose inside can be seen by the players before entering them
-- Proof of concept for now, so only one such box.
local box_expo_minp = {x = 5, y = 0, z = 5}
local box_expo_size = {x = 22, y = 22, z = 22}
minetest.register_chatcommand("expo", {
params = "<box_id>",
description = "Select the chosen box for exposition",
privs = {server = true},
func = function(name, param)
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.")
return
end
if meta.type ~= db.BOX_TYPE then
minetest.chat_send_player(name, "The id you supplied does not correspond to a box.")
return
end
local size = meta.meta.size
if not vector.equals(size, box_expo_size) then
minetest.chat_send_player(name, "The box you chose does not have the correct size.")
return
end
boxes.load(box_expo_minp, id, nil)
-- Open up a side
local minp = vector.add(box_expo_minp, {x = 1, y = 1, z = size.z - 1})
local maxp = vector.add(box_expo_minp, {x = size.x - 2, y = size.y - 1, z = size.z - 1})
local cid_barrier = minetest.get_content_id("nodes:barrier")
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()
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, maxp.z)
for x = minp.x, maxp.x do
vmdata[index] = cid_barrier
index = index + 1
end
end
vm:set_data(vmdata)
vm:write_to_map()
vm:update_map()
end,
})
]]