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) -- 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.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.25 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 = "", 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 = "", 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 = "", 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 = " ", 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 = " ", 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 ", 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) print(dump(meta.meta.digit_pos)) meta.meta.digit_pos = vector.subtract(pos, box.minp) print(dump(meta.meta.digit_pos)) db.box_set_meta(id, meta) end minetest.register_privilege("create", "Can create new boxes") minetest.register_chatcommand("create", { params = "", 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 = "", 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 = " ", description = "Set the builder of the box to .", 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 = "[]", 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 = "", 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, }) ]]