Update installation instructions - adopted from Wuzzy's notes.
[insidethebox/insidethebox_wuzzy.git] / mods / boxes / init.lua
blob02bbd24ee00b16cf1181014eda426e4b58b315c7
2 --[[
4 ITB (insidethebox) minetest game - Copyright (C) 2017-2018 sofar & nore
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public License
8 as published by the Free Software Foundation; either version 2.1
9 of the License, or (at your option) any later version.
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free
18 Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
19 MA 02111-1307 USA
21 ]]--
23 boxes = {}
24 boxes.teleport_to_tutorial_exit = {}
26 local modpath = minetest.get_modpath(minetest.get_current_modname())
28 -- Box allocation
29 dofile(modpath .. "/valloc.lua")
31 -- Handling the data that encodes boxes
32 dofile(modpath .. "/data.lua")
34 -- Import/export boxes to files
35 dofile(modpath .. "/io.lua")
37 -- Handle inventory-related callbacks (i.e. whether the itemstacks
38 -- should be decreased when placing nodes)
39 dofile(modpath .. "/inv.lua")
41 -- Nodes that have a box-related effect
42 dofile(modpath .. "/nodes.lua")
44 -- Box score recording code
45 dofile(modpath .. "/score.lua")
47 local areas = AreaStore("insidethebox")
49 function boxes.find_box(pos)
50 local count = 0
51 local name = nil
52 for k, v in pairs(areas:get_areas_for_pos(pos, false, true)) do
53 name = v.data
54 count = count + 1
55 end
56 if count == 1 then
57 if boxes.players_in_boxes[name] then
58 return boxes.players_in_boxes[name]
59 elseif boxes.players_editing_boxes[name] then
60 return boxes.players_editing_boxes[name]
61 end
62 end
63 return false
64 end
66 -- Set the region from minp to maxp to air
67 function boxes.cleanup(minp, maxp)
68 -- Clear any leftover entities
69 local center = vector.divide(vector.add(minp, maxp), 2)
70 local radius = vector.length(vector.subtract(vector.add(maxp, 1), minp)) / 2
71 for _, obj in ipairs(minetest.get_objects_inside_radius(center, radius)) do
72 if not obj:is_player() then
73 local pos = obj:get_pos()
74 if minp.x - 0.5 <= pos.x and pos.x <= maxp.x + 0.5 and
75 minp.y - 0.5 <= pos.y and pos.y <= maxp.y + 0.5 and
76 minp.z - 0.5 <= pos.z and pos.z <= maxp.z + 0.5
77 then
78 obj:remove()
79 end
80 end
81 end
83 local vm = minetest.get_voxel_manip(minp, maxp)
84 local emin, emax = vm:get_emerged_area()
85 local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
86 local vmdata = vm:get_data()
87 local param2 = vm:get_param2_data()
88 local cid = minetest.get_content_id("air")
90 for z = minp.z, maxp.z do
91 for y = minp.y, maxp.y do
92 local index = va:index(minp.x, y, z)
93 for x = minp.x, maxp.x do
94 vmdata[index] = cid
95 param2[index] = 0
96 index = index + 1
97 end
98 end
99 end
101 vm:set_data(vmdata)
102 vm:set_param2_data(param2)
103 vm:update_liquids()
104 vm:write_to_map()
105 vm:update_map()
106 minetest.after(0.1, minetest.fix_light, minp, maxp)
109 function vector.min(a, b)
110 return {
111 x = math.min(a.x, b.x),
112 y = math.min(a.y, b.y),
113 z = math.min(a.z, b.z),
117 function vector.max(a, b)
118 return {
119 x = math.max(a.x, b.x),
120 y = math.max(a.y, b.y),
121 z = math.max(a.z, b.z),
125 boxes.players_in_boxes = {}
127 local function open_door(player, pos1, pos2, bminp, bmaxp)
128 local minp = vector.min(pos1, pos2)
129 local maxp = vector.max(pos1, pos2)
130 local drs = {}
131 local i = 1
132 for x = minp.x, maxp.x do
133 for y = minp.y, maxp.y do
134 for z = minp.z, maxp.z do
135 local door = doors.get({x = x, y = y, z = z})
136 if door then
137 door:open()
138 drs[i] = {x = x, y = y, z = z}
139 i = i + 1
144 local od = boxes.players_in_boxes[player:get_player_name()].open_doors
145 od[#od + 1] = {minp = bminp, maxp = bmaxp, doors = drs, respawn =
146 {x = maxp.x + 1, y = minp.y, z = (minp.z + maxp.z) / 2}}
149 function boxes.player_success(player)
150 local name = player:get_player_name()
151 local box = boxes.players_in_boxes[name]
152 local id = box.box_id
153 local time_taken = minetest.get_gametime() - box.start_time
154 local deaths = box.deaths
155 local damage = box.damage
156 local bmeta = db.box_get_meta(id)
158 boxes.score(name, id, "time", {time_taken})
159 boxes.score(name, id, "damage", {damage})
160 boxes.score(name, id, "deaths", {deaths})
162 bmeta.meta.num_completed_players = db.box_get_num_completed_players(id)
164 db.box_set_meta(id, bmeta)
165 for _, p in ipairs(box.signs_to_update) do
166 local node = minetest.get_node(p)
167 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_exit_update then
168 minetest.registered_nodes[node.name].on_exit_update(p, player, {
169 time_taken = {format = "time", data = time_taken},
170 damage_taken = {format = "text", data = tostring(damage)},
171 num_deaths = {format = "text", data = tostring(deaths)},
176 local sid = box.in_series
177 if sid then
178 local pmeta = db.player_get_meta(name)
179 local index = pmeta.series_progress[sid] or 1
180 pmeta.series_progress[sid] = index + 1
181 db.player_set_meta(name, pmeta)
183 -- check if player completed tutorial series
184 local bxs = db.series_get_boxes(sid)
185 if bxs[index + 1] then
186 return
188 -- omit check if tutorial is actually required here
189 if sid == conf.tutorial.series then
190 player:set_attribute("tutorial_completed", 1)
191 minetest.log("action", name .. " completed the tutorial")
192 minetest.chat_send_all(name .. " completed the tutorial!")
193 if irc then
194 irc.say(name .. " has completed the tutorial!")
196 -- reward: create priv
197 local privs = minetest.get_player_privs(name)
198 privs.create = true
199 minetest.set_player_privs(name, privs)
200 boxes.teleport_to_tutorial_exit[name] = true
205 function boxes.open_exit(player)
206 local name = player:get_player_name()
207 local box = boxes.players_in_boxes[name]
208 local exds = box.exit_doors
209 local n = #exds
210 if n == 0 then return end
211 local ex = exds[n]
212 exds[n] = nil
213 open_door(player, ex.door_pos, vector.add(ex.door_pos, {x = -1, y = 0, z = 1}), ex.box_minp, ex.box_maxp)
216 function boxes.increase_items(player)
217 local name = player:get_player_name()
218 if not boxes.players_in_boxes[name] then
219 return
221 local exds = boxes.players_in_boxes[name].exit_doors
222 local n = #exds
223 if n == 0 then return end
224 local ex = exds[n]
225 ex.door_items = ex.door_items - 1
226 if ex.door_items == 0 then
227 boxes.open_exit(player)
231 -- Close doors behind players
232 minetest.register_globalstep(function(dtime)
233 for playername, info in pairs(boxes.players_in_boxes) do
234 local i = 1
235 local player = minetest.get_player_by_name(playername)
236 local pos = player:get_pos()
237 local odoors = info.open_doors
238 local n = #odoors
239 while odoors[i] do
240 local off = 0.7
241 if odoors[i].minp.x + off <= pos.x and pos.x <= odoors[i].maxp.x - off and
242 odoors[i].minp.y <= pos.y and pos.y <= odoors[i].maxp.y and
243 odoors[i].minp.z + off <= pos.z and pos.z <= odoors[i].maxp.z - off
244 then
245 for _, dpos in ipairs(odoors[i].doors) do
246 local door = doors.get(dpos)
247 if door then
248 door:close()
251 info.respawn = odoors[i].respawn
252 odoors[i] = odoors[n]
253 odoors[n] = nil
254 n = n - 1
255 if not info.start_time then
256 info.start_time = minetest.get_gametime()
257 info.deaths = 0
258 info.damage = 0
259 music.start(player, info, "box")
260 else
261 -- We closed the last door: the player got out!
262 boxes.player_success(player)
263 minetest.log("action", playername .. " completed box " .. info.box_id)
264 minetest.chat_send_all(playername .. " completed box " .. info.box_id .. "!")
265 music.start(player, info, "exit")
267 else
268 i = i + 1
272 end)
274 local function translate_all(l, offset)
275 local result = {}
276 for i, p in ipairs(l) do
277 result[i] = vector.add(p, offset)
279 return result
282 -- Although this functions takes a list as an argument,
283 -- several parts of the code rely on it having length exactly
284 -- 3 and being entry lobby, box, exit
285 function boxes.open_box(player, box_id_list)
286 local name = player:get_player_name()
287 if boxes.players_in_boxes[name] ~= nil or boxes.players_editing_boxes[name] ~= nil then
288 return
291 minetest.log("action", name .. " entered box " .. box_id_list[2])
293 local offset = {x = 0, y = 0, z = 0}
294 local pmin = {x = 0, y = 0, z = 0}
295 local pmax = {x = 0, y = 0, z = 0}
296 local metas = {}
297 local offsets = {}
298 for i, box in ipairs(box_id_list) do
299 local meta = db.box_get_meta(box).meta
300 metas[i] = meta
301 offset = vector.subtract(offset, meta.entry)
302 pmin = vector.min(pmin, offset)
303 pmax = vector.max(pmax, vector.add(offset, meta.size))
304 offsets[i] = offset
305 offset = vector.add(offset, meta.exit)
307 for i, off in ipairs(offsets) do
308 offsets[i] = vector.subtract(off, pmin)
310 pmax = vector.subtract(pmax, pmin)
312 local size = math.max(pmax.x, pmax.z)
313 local minp = boxes.valloc(size)
314 local maxp = vector.add(minp, vector.subtract(pmax, 1))
316 local exit_doors = {}
317 local n = #box_id_list
318 for i = 1, n - 1 do
319 local exd = vector.add(minp, vector.add(metas[n - i].exit, offsets[n - i]))
320 exit_doors[i] = {
321 door_items = metas[n - i].num_items,
322 door_pos = exd,
323 box_minp = vector.add(minp, offsets[n - i + 1]),
324 box_maxp = vector.add(minp, vector.add(offsets[n - i + 1], vector.subtract(metas[n - i + 1].size, 1))),
328 local spawn_pos = vector.add(minp, vector.add(metas[1].entry, offsets[1]))
329 spawn_pos.y = spawn_pos.y - 0.5
331 local base_exit = vector.add(offsets[3], minp)
332 local signs_to_update = translate_all(metas[3].signs_to_update, base_exit)
333 local star_positions = translate_all(metas[3].star_positions, base_exit)
334 local category_positions = translate_all(metas[3].category_positions, base_exit)
336 local areas_id = areas:insert_area(vector.add(minp, 1), vector.subtract(maxp, 1), name)
337 boxes.players_in_boxes[name] = {
338 name = name,
339 box_id = box_id_list[2],
340 minp = minp,
341 maxp = maxp,
342 areas_id = areas_id,
343 exit_doors = exit_doors,
344 exit = vector.add(minp, vector.add(metas[n].exit, offsets[n])),
345 open_doors = {},
346 respawn = spawn_pos,
347 signs_to_update = signs_to_update,
348 star_positions = star_positions,
349 category_positions = category_positions,
353 for i, box in ipairs(box_id_list) do
354 boxes.load(vector.add(minp, offsets[i]), box, player)
357 player:set_physics_override({gravity = 0, jump = 0})
358 spawn_pos.y = spawn_pos.y + 0.5
360 -- the `send_mapblock` method is not yet merged, requiring this compat check
361 if player.send_mapblock then
362 local tv = vector.floor((vector.divide(spawn_pos, 16)))
363 local ret = player:send_mapblock(tv)
364 if not ret then
365 minetest.log("error", "send_mapblock failed for " .. player:get_player_name() .. " to " .. minetest.pos_to_string(tv))
369 player:set_pos(spawn_pos)
370 minetest.after(0.5, function()
371 player:set_physics_override({gravity = 1, jump = 1})
372 end)
373 player:set_look_horizontal(3 * math.pi / 2)
374 player:set_look_vertical(0)
375 players.give_box_inventory(player)
376 skybox.set(player, metas[n - 1].skybox)
378 boxes.open_exit(player)
380 minetest.after(1.0, function()
381 if not player then
382 return
385 local p = player:get_pos()
386 if p and p.y < spawn_pos.y - 0.95 and boxes.players_in_boxes[name] then
387 minetest.log("error", name .. " fell from an entrance lobby")
388 if (math.abs(spawn_pos.x - p.x) < 2) and (math.abs(spawn_pos.z - p.z) < 2) then
389 player:set_pos({x = p.x, y = spawn_pos.y + 0.5, z = p.z})
390 else
391 player:set_pos(spawn_pos)
394 end)
397 function boxes.close_box(player)
398 local name = player:get_player_name()
399 if boxes.players_in_boxes[name] == nil then
400 return
403 local bx = boxes.players_in_boxes[name]
405 minetest.log("action", name .. " left box " .. bx.box_id)
407 boxes.cleanup(bx.minp, bx.maxp)
408 boxes.vfree(bx.minp)
409 areas:remove_area(boxes.players_in_boxes[name].areas_id)
410 boxes.players_in_boxes[name] = nil
413 function boxes.next_series(player, sid, is_entering)
414 local name = player:get_player_name()
415 local pmeta = db.player_get_meta(name)
416 local index = pmeta.series_progress[sid] or 1
417 local bxs = db.series_get_boxes(sid)
418 if not bxs[index] then
419 if not is_entering then
420 players.return_to_lobby(player)
422 return false
423 else
424 if sid == conf.tutorial.series and
425 conf.tutorial.required and
426 player:get_attribute("tutorial_completed") ~= "1" then
427 boxes.open_box(player, {conf.tutorial.entry_lobby, bxs[index], conf.tutorial.exit_lobby})
428 else
429 boxes.open_box(player, {0, bxs[index], 1})
431 boxes.players_in_boxes[name].in_series = sid
432 return true
436 function boxes.validate_pos(player, pos, withborder)
437 local name = player:get_player_name()
438 local box = boxes.players_in_boxes[name] or boxes.players_editing_boxes[name]
439 if not box then
440 if not minetest.check_player_privs(player, "server") then
441 return false
443 box = {
444 minp = { x = -32768, y = -32768, z = -32768 },
445 maxp = { x = 32768, y = 32768, z = 32768 },
448 if withborder then
449 if pos.x < box.minp.x or pos.x > box.maxp.x or
450 pos.y < box.minp.y or pos.y > box.maxp.y or
451 pos.z < box.minp.z or pos.z > box.maxp.z then
452 return false
454 else
455 if pos.x < box.minp.x + 1 or pos.x > box.maxp.x - 1 or
456 pos.y < box.minp.y + 1 or pos.y > box.maxp.y - 1 or
457 pos.z < box.minp.z + 1 or pos.z > box.maxp.z - 1 then
458 return false
462 return true
465 boxes.players_editing_boxes = {}
467 minetest.register_chatcommand("enter", {
468 params = "<boxid>",
469 description = "Enter box with this id",
470 -- privs = {server = true},
471 func = function(name, param)
472 local player = minetest.get_player_by_name(name)
473 if boxes.players_in_boxes[name] or boxes.players_editing_boxes[name] then
474 minetest.chat_send_player(name, "You are already in a box!")
475 return
477 local id = tonumber(param)
478 if not id or id ~= math.floor(id) or id < 0 then
479 minetest.chat_send_player(name, "The id you supplied is not a nonnegative interger.")
480 return
482 local meta = db.box_get_meta(id)
483 if not meta or meta.type ~= db.BOX_TYPE then
484 minetest.chat_send_player(name, "The id you supplied does not correspond to any box.")
485 return
487 -- only server admins may enter any box
488 -- normal players may not enter anything but published boxes or their own boxes
489 if minetest.check_player_privs(player, "server") or
490 meta.meta.status == db.STATUS_ACCEPTED or
491 meta.meta.builder == name then
492 boxes.open_box(player, {0, id, 1})
493 else
494 minetest.chat_send_player(name, "You do not have permissions to enter that box.")
496 end,
499 minetest.register_chatcommand("series_create", {
500 params = "<name>",
501 description = "Create a new series",
502 privs = {server = true},
503 func = function(name, param)
504 if param == "" then
505 minetest.chat_send_player(name, "Please privide a name for the series")
506 return
508 local id = db.series_create(param)
509 if id then
510 minetest.chat_send_player(name, "Series successfully created with id " .. id)
511 else
512 minetest.chat_send_player(name, "Series failed to create")
517 minetest.register_chatcommand("series_destroy", {
518 params = "<series_id>",
519 description = "Destroy a series",
520 privs = {server = true},
521 func = function(name, param)
522 if param == "" then
523 minetest.chat_send_player(name, "Please privide a series id to destroy")
524 return
526 local id = tonumber(param)
527 if not db.series_destroy(id) then
528 minetest.chat_send_player(name, "Failed to destroy series " .. id)
529 else
530 minetest.chat_send_player(name, "Destroyed series " .. id)
535 minetest.register_chatcommand("series_add", {
536 params = "<series_id> <box_id>",
537 description = "Add a box a series",
538 privs = {server = true},
539 func = function(name, param)
540 local sid, bid = string.match(param, "^([^ ]+) +(.+)$")
541 sid = tonumber(sid)
542 bid = tonumber(bid)
543 if not sid or not bid or not db.series_get_meta(sid) or not db.box_get_meta(bid) then
544 minetest.chat_send_player(name, "Box or series doesn't exist.")
545 return
547 db.series_add_at_end(sid, bid)
548 minetest.chat_send_player(name, "Done")
552 minetest.register_chatcommand("series_list", {
553 params = "",
554 description = "List defined series",
555 privs = {server = true},
556 func = function(name, param)
557 if param ~= "" then
558 -- list boxes in this series.
559 local sid = tonumber(param)
560 if not sid or not db.series_get_meta(sid) then
561 minetest.chat_send_player(name, "Series doesn't exist.")
562 return
564 for _, v in ipairs(db.series_get_boxes(sid)) do
565 minetest.chat_send_player(name, v)
567 else
568 -- list all series
569 for k, v in ipairs(db.series_get_series()) do
570 minetest.chat_send_player(name, v.id .. " - " .. v.name)
576 minetest.register_chatcommand("series_remove", {
577 params = "<series_id> <box_id>",
578 description = "Remove a box from specified series",
579 privs = {server = true},
580 func = function(name, param)
581 local sid, bid = string.match(param, "^([^ ]+) +(.+)$")
582 sid = tonumber(sid)
583 bid = tonumber(bid)
584 if not sid or not bid or not db.series_get_meta(sid) or not db.box_get_meta(bid) then
585 minetest.chat_send_player(name, "Box or series doesn't exist.")
586 return
588 db.series_delete_box(sid, bid)
589 minetest.chat_send_player(name, "Done")
594 minetest.register_chatcommand("leave", {
595 params = "",
596 description = "Leave the current box",
597 privs = {server = true},
598 func = function(name, param)
599 if not boxes.players_in_boxes[name] then
600 minetest.chat_send_player(name, "You are not in a box!")
601 return
603 local player = minetest.get_player_by_name(name)
604 boxes.close_box(player)
605 music.stop(player)
606 players.return_to_lobby(player)
607 end,
610 minetest.register_chatcommand("open", {
611 params = "",
612 description = "Open the current exit.",
613 privs = {server = true},
614 func = function(name, param)
615 local box = boxes.players_in_boxes[name]
616 if not box then
617 minetest.chat_send_player(name, "You are not in a box!")
618 return
620 if box.exit_doors[1] == nil then
621 minetest.chat_send_player(name, "There are no more exits to open!")
622 return
624 if box.open_doors[1] ~= nil then
625 minetest.chat_send_player(name, "This box already has an open door!")
626 return
628 local player = minetest.get_player_by_name(name)
629 boxes.open_exit(player)
630 end,
633 -- This is only still needed if we want to change the size, the entry, or the
634 -- exit of the lobbies; changing the contents can be done by /edite.
636 local lobby_updates = {}
637 minetest.register_chatcommand("update_lobby", {
638 params = "entry|exit <box id>",
639 description = "Set corresponding lobby. Use without parameter to start \
640 updating a lobby, then punch both corners of the lobby and the bottom node \
641 of the exit. In the case of the entry lobby, stand at its spawnpoint to run \
642 the command.",
643 privs = {server = true},
644 func = function(name, param)
645 local update_type, box_id = string.match(param, "^([^ ]+) +(.+)$")
647 if update_type ~= "entry" and update_type ~= "exit" and param ~= "" then
648 return
651 if param == "" then
652 lobby_updates[name] = {}
653 elseif not lobby_updates[name] then
654 minetest.chat_send_player(name, "Not all positions have been set.")
655 return
656 else
657 box_id = tonumber(box_id)
658 local pos1 = lobby_updates[name].pos1
659 local pos2 = lobby_updates[name].pos2
660 local pos3 = lobby_updates[name].pos3
661 if not pos1 or not pos2 or not pos3 then
662 minetest.chat_send_player(name, "Not all positions have been set.")
663 return
665 local minp = vector.min(pos1, pos2)
666 local maxp = vector.max(pos1, pos2)
667 local data = boxes.save(minp, maxp)
668 local p3 = vector.subtract(pos3, minp)
669 if update_type == "exit" then
670 box_id = box_id or 1
671 local player = minetest.get_player_by_name(name)
672 local exit = vector.subtract(vector.round(player:get_pos()), minp)
673 db.box_set_data(box_id, data)
674 db.box_set_meta(box_id, {
675 type = db.EXIT_TYPE,
676 meta = {
677 size = vector.add(vector.subtract(maxp, minp), 1),
678 entry = p3,
679 exit = exit,
682 else
683 box_id = box_id or 0
684 local player = minetest.get_player_by_name(name)
685 local entry = vector.subtract(vector.round(player:get_pos()), minp)
686 p3.x = p3.x + 1
687 db.box_set_data(box_id, data)
688 db.box_set_meta(box_id, {
689 type = db.ENTRY_TYPE,
690 meta = {
691 size = vector.add(vector.subtract(maxp, minp), 1),
692 entry = entry,
693 exit = p3,
697 minetest.chat_send_player(name, "Updated.")
698 lobby_updates[name] = nil
700 end,
703 minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
704 if not puncher then return end
705 local name = puncher:get_player_name()
706 if not lobby_updates[name] then return end
707 if not lobby_updates[name].pos1 then
708 lobby_updates[name].pos1 = pos
709 minetest.chat_send_player(name, "Position 1 set to " .. dump(pos) .. ".")
710 elseif not lobby_updates[name].pos2 then
711 lobby_updates[name].pos2 = pos
712 minetest.chat_send_player(name, "Position 2 set to " .. dump(pos) .. ".")
713 elseif not lobby_updates[name].pos3 then
714 lobby_updates[name].pos3 = pos
715 minetest.chat_send_player(name, "Position 3 set to " .. dump(pos) .. ".")
717 end)
720 local digits = {[0] =
721 { true, true, true,
722 true, false, true,
723 true, false, true,
724 true, false, true,
725 true, true, true,
727 {false, false, true,
728 false, false, true,
729 false, false, true,
730 false, false, true,
731 false, false, true,
733 { true, true, true,
734 false, false, true,
735 true, true, true,
736 true, false, false,
737 true, true, true,
739 { true, true, true,
740 false, false, true,
741 true, true, true,
742 false, false, true,
743 true, true, true,
745 { true, false, true,
746 true, false, true,
747 true, true, true,
748 false, false, true,
749 false, false, true,
751 { true, true, true,
752 true, false, false,
753 true, true, true,
754 false, false, true,
755 true, true, true,
757 { true, true, true,
758 true, false, false,
759 true, true, true,
760 true, false, true,
761 true, true, true,
763 { true, true, true,
764 false, false, true,
765 false, false, true,
766 false, false, true,
767 false, false, true,
769 { true, true, true,
770 true, false, true,
771 true, true, true,
772 true, false, true,
773 true, true, true,
775 { true, true, true,
776 true, false, true,
777 true, true, true,
778 false, false, true,
779 true, true, true,
783 function boxes.make_new(player, size)
784 local minp = boxes.valloc(size + 2)
785 local maxp = vector.add(minp, size + 1)
787 -- Clear existing metadata
788 local meta_positions = minetest.find_nodes_with_meta(minp, maxp)
789 for _, pos in ipairs(meta_positions) do
790 minetest.get_meta(pos):from_table()
793 -- Create the box
794 local cid_air = minetest.get_content_id("air")
795 local cid_wall = minetest.get_content_id("nodes:marbleb")
796 local cid_digit = minetest.get_content_id("nodes:bronzeb")
797 local cid_barrier = minetest.get_content_id("nodes:barrier")
799 local vm = minetest.get_voxel_manip(minp, maxp)
800 local emin, emax = vm:get_emerged_area()
801 local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
802 local vmdata = vm:get_data()
803 local param2 = vm:get_param2_data()
805 -- Set to air
806 for z = minp.z, maxp.z do
807 for y = minp.y, maxp.y do
808 local index = va:index(minp.x, y, z)
809 for x = minp.x, maxp.x do
810 vmdata[index] = cid_air
811 param2[index] = 0
812 index = index + 1
817 -- Add stone for walls and barrier at the top
818 for z = minp.z, maxp.z do
819 local index = va:index(minp.x, minp.y, z)
820 local index2 = va:index(minp.x, maxp.y, z)
821 for x = minp.x, maxp.x do
822 vmdata[index] = cid_wall
823 vmdata[index2] = cid_barrier
824 index = index + 1
825 index2 = index2 + 1
828 for y = minp.y, maxp.y - 1 do
829 local index = va:index(minp.x, y, minp.z)
830 local index2 = va:index(minp.x, y, maxp.z)
831 for x = minp.x, maxp.x do
832 vmdata[index] = cid_wall
833 vmdata[index2] = cid_wall
834 index = index + 1
835 index2 = index2 + 1
838 local ystride = emax.x - emin.x + 1
839 for z = minp.z, maxp.z do
840 local index = va:index(minp.x, minp.y, z)
841 local index2 = va:index(maxp.x, minp.y, z)
842 for y = minp.y, maxp.y - 1 do
843 vmdata[index] = cid_wall
844 vmdata[index2] = cid_wall
845 index = index + ystride
846 index2 = index2 + ystride
850 local box_id = db.get_last_box_id() + 1
852 -- Write the box id
853 local id_string = tostring(box_id)
854 local id_sz = 4 * string.len(id_string) - 1
855 if size < 6 or size < id_sz + 2 then
856 minetest.log("error", "boxes.make_new(" .. size .. "): box size too small")
857 else
858 local xoff = minp.x + math.floor((size + 2 - id_sz) / 2)
859 local yoff = minp.y + math.floor((size + 2 - 5 + 1) / 2)
860 local n = string.len(id_string)
861 for i = 1, string.len(id_string) do
862 for dx = 0, 2 do
863 for dy = 0, 4 do
864 if digits[string.byte(id_string, n - i + 1) - 48][3-dx+3*(4-dy)] then
865 local index = va:index(xoff + dx, yoff + dy, minp.z)
866 vmdata[index] = cid_digit
870 xoff = xoff + 4
873 local digit_pos = {
874 x = math.floor((size + 2 - id_sz) / 2),
875 y = math.floor((size + 2 - 5 + 1) / 2),
876 z = 0
879 vm:set_data(vmdata)
880 vm:set_param2_data(param2)
881 vm:update_liquids()
882 vm:write_to_map()
883 vm:update_map()
884 minetest.after(0.1, minetest.fix_light, minp, maxp)
886 local s2 = math.floor(size / 2)
887 local entry = {x = 0, y = 1, z = s2 + 1}
888 local exit = {x = size + 2, y = 1, z = s2 + 1}
889 local sz = {x = size + 2, y = size + 2, z = size + 2}
890 local meta = {
891 type = db.BOX_TYPE,
892 meta = {
893 entry = entry,
894 exit = exit,
895 size = sz,
896 num_items = 0,
897 box_name = "(No name)",
898 builder = player:get_player_name(),
899 build_time = 0,
900 skybox = 0,
901 digit_pos = digit_pos,
905 player:set_pos(vector.add(minp, {x = 1, y = 1, z = s2 + 1}))
906 players.give_edit_inventory(player)
907 db.box_set_meta(box_id, meta)
908 db.box_set_data(box_id, boxes.save(minp, maxp))
910 local name = player:get_player_name()
911 local areas_id = areas:insert_area(vector.add(minp, 1), vector.subtract(maxp, 1), name)
912 boxes.players_editing_boxes[name] = {
913 name = name,
914 box_id = box_id,
915 minp = minp,
916 maxp = maxp,
917 areas_id = areas_id,
918 num_items = 0,
919 entry = vector.add(minp, entry),
920 exit = vector.add(minp, exit),
921 start_edit_time = minetest.get_gametime(),
922 box_name = "",
923 skybox = 0,
927 local function get_digit_pos(player)
928 local name = player:get_player_name()
929 local box = boxes.players_editing_boxes[name]
930 if not box then
931 return
933 local id = box.box_id
934 local meta = db.box_get_meta(id)
935 if meta.meta.digit_pos then
936 return meta.meta.digit_pos
939 -- Digit position is not specified in meta (old boxes)
940 -- We have to infer it.
941 local minp = box.minp
942 local maxp = {x = box.maxp.x, y = box.maxp.y, z = box.minp.z}
943 local vm = minetest.get_voxel_manip(minp, maxp)
944 local emin, emax = vm:get_emerged_area()
945 local va = VoxelArea:new{MinEdge=emin, MaxEdge=emax}
946 local vmdata = vm:get_data()
947 local cid_digit = minetest.get_content_id("nodes:bronzeb")
948 local maxx = minp.x
949 local maxy = minp.y
950 for y = minp.y, maxp.y do
951 local index = va:index(minp.x, y, minp.z)
952 for x = minp.x, maxp.x do
953 if vmdata[index] == cid_digit then
954 if x > maxx then
955 maxx = x
957 if y > maxy then
958 maxy = y
961 index = index + 1
964 local id_string = tostring(id)
965 local id_sz = 4 * string.len(id_string) - 1
966 local digit_pos = {x = maxx - minp.x - id_sz + 1, y = maxy - minp.y - 4, z = 0}
967 meta.meta.digit_pos = digit_pos
968 db.box_set_meta(id, meta)
969 return digit_pos
972 local function paint_digits(pos, node, id)
973 local id_string = tostring(id)
974 local n = string.len(id_string)
975 for i = 1, n do
976 for dx = 0, 2 do
977 for dy = 0, 4 do
978 if digits[string.byte(id_string, n - i + 1) - 48][3-dx+3*(4-dy)] then
979 local p = {x = pos.x + 4 * i - 4 + dx, y = pos.y + dy, z = pos.z}
980 minetest.set_node(p, node)
987 function boxes.move_digits(player, pos)
988 local name = player:get_player_name()
989 local box = boxes.players_editing_boxes[name]
990 if not box then
991 return
993 local id = box.box_id
994 local old_pos = get_digit_pos(player)
995 if not old_pos then
996 minetest.log("boxes.move_digits: error: old digit pos is nil")
997 return
999 paint_digits(vector.add(box.minp, old_pos), {name = "nodes:marbleb"}, id)
1000 paint_digits(pos, {name = "nodes:bronzeb"}, id)
1001 local meta = db.box_get_meta(id)
1002 meta.meta.digit_pos = vector.subtract(pos, box.minp)
1003 db.box_set_meta(id, meta)
1006 minetest.register_privilege("create", "Can create new boxes")
1008 minetest.register_chatcommand("create", {
1009 params = "<box_size>",
1010 description = "Start editing a new box.",
1011 privs = {server = true},
1012 func = function(name, param)
1013 if boxes.players_editing_boxes[name] or boxes.players_in_boxes[name] then
1014 minetest.chat_send_player(name, "You are already editing a box!")
1015 return
1018 local size = tonumber(param)
1019 if not size or size ~= math.floor(size) or size <= 0 then
1020 minetest.chat_send_player(name, "Please specify a size.")
1021 return
1024 if size < 20 or size > 40 then
1025 minetest.chat_send_player(name, "Size is not in the allowed range [20, 40]")
1026 return
1029 boxes.make_new(minetest.get_player_by_name(name), size)
1030 end,
1033 minetest.register_chatcommand("edit", {
1034 params = "",
1035 description = "DEPRECATED",
1036 privs = {server = true},
1037 func = function(name, param)
1038 minetest.chat_send_player(name, "Did you mean /create or /edite?")
1039 end,
1043 minetest.register_chatcommand("edite", {
1044 params = "<box_id>",
1045 description = "Edit an existing box.",
1046 privs = {create = true},
1047 func = function(name, param)
1048 if boxes.players_editing_boxes[name] or boxes.players_in_boxes[name] then
1049 minetest.chat_send_player(name, "You are already editing a box!")
1050 return
1053 local id = tonumber(param)
1054 if not id or id ~= math.floor(id) or id < 0 then
1055 minetest.chat_send_player(name, "The id you supplied is not a nonnegative integer.")
1056 return
1059 local meta = db.box_get_meta(id)
1060 if not meta then
1061 minetest.chat_send_player(name, "The id you supplied is not in the database.")
1062 return
1065 if not minetest.check_player_privs(name, "server") then
1066 if meta.meta.builder ~= name or meta.meta.status ~= db.STATUS_EDITING then
1067 minetest.chat_send_player(name, "You are not allowed to edit this box.")
1068 return
1072 local player = minetest.get_player_by_name(name)
1073 local minp = boxes.valloc(math.max(meta.meta.size.x, meta.meta.size.z))
1074 local maxp = vector.add(minp, vector.subtract(meta.meta.size, 1))
1076 local areas_id = areas:insert_area(vector.add(minp, 1), vector.subtract(maxp, 1), name)
1077 boxes.players_editing_boxes[name] = {
1078 name = name,
1079 box_id = id,
1080 minp = minp,
1081 maxp = maxp,
1082 areas_id = areas_id,
1083 num_items = meta.meta.num_items,
1084 entry = vector.add(minp, meta.meta.entry),
1085 exit = vector.add(minp, meta.meta.exit),
1086 start_edit_time = minetest.get_gametime(),
1087 box_name = meta.meta.box_name,
1088 skybox = meta.meta.skybox,
1090 boxes.load(minp, id, player)
1091 local spawnpoint = vector.add({x = 1, y = 0, z = 0}, vector.add(minp, meta.meta.entry))
1092 player:set_pos(spawnpoint)
1093 players.give_edit_inventory(player)
1094 music.start(player, nil, "create")
1095 -- skybox may not exist for lobbies
1096 if meta.meta.skybox then
1097 skybox.set(player, meta.meta.skybox)
1099 end,
1102 function boxes.save_edit(player, id)
1103 local name = player:get_player_name()
1104 local box = boxes.players_editing_boxes[name]
1105 if not box then return end
1107 local bmeta = db.box_get_meta(box.box_id)
1108 if id == nil then
1109 id = box.box_id
1112 db.box_set_data(id, boxes.save(box.minp, box.maxp))
1113 bmeta.meta.num_items = box.num_items
1114 bmeta.meta.entry = vector.subtract(box.entry, box.minp)
1115 bmeta.meta.exit = vector.subtract(box.exit, box.minp)
1116 bmeta.meta.box_name = box.box_name
1117 if box.box_name == "" then
1118 bmeta.meta.box_name = "(No name)"
1120 bmeta.meta.skybox = box.skybox
1121 local t = minetest.get_gametime()
1122 if name == bmeta.meta.builder then -- Do not count admin edit times
1123 bmeta.meta.build_time = bmeta.meta.build_time + t - box.start_edit_time
1124 box.start_edit_time = t
1126 db.box_set_meta(id, bmeta)
1128 minetest.log("action", name .. " saved box " .. id)
1129 minetest.chat_send_player(name, "Box " .. id .. " saved successfully")
1132 function boxes.stop_edit(player)
1133 local name = player:get_player_name()
1134 local box = boxes.players_editing_boxes[name]
1135 if not box then return end
1137 boxes.cleanup(box.minp, box.maxp)
1138 boxes.vfree(box.minp)
1139 areas:remove_area(boxes.players_editing_boxes[name].areas_id)
1140 boxes.players_editing_boxes[name] = nil
1143 minetest.register_chatcommand("setbuilder", {
1144 params = "<id> <name>",
1145 description = "Set the builder of the box <id> to <name>.",
1146 privs = {server = true},
1147 func = function(name, param)
1148 local box_id, nname = string.match(param, "^([^ ]+) +(.+)$")
1149 box_id = tonumber(box_id)
1150 if not box_id or not nname then
1151 minetest.chat_send_player(name, "Supplied box id/name missing or incorrect format!")
1152 return
1154 local bmeta = db.box_get_meta(box_id)
1155 if not bmeta then
1156 minetest.chat_send_player(name, "Supplied box id does not exist!")
1157 return
1159 if bmeta.type ~= db.BOX_TYPE then
1160 minetest.chat_send_player(name, "This id does not correspond to a box.")
1161 return
1163 bmeta.meta.builder = nname
1164 db.box_set_meta(box_id, bmeta)
1165 minetest.chat_send_player(name, "Done.")
1166 end,
1169 minetest.register_chatcommand("save", {
1170 params = "[<id>]",
1171 description = "Save the box you are currently editing. If id is supplied, a copy is" ..
1172 "instead saved to the box numbered id.",
1173 privs = {server = true},
1174 func = function(name, param)
1175 if not boxes.players_editing_boxes[name] then
1176 minetest.chat_send_player(name, "You are not currently editing a box!")
1177 return
1180 local box_id = nil
1181 if param ~= "" then
1182 local id = tonumber(param)
1183 if not id or id ~= math.floor(id) or id < 0 then
1184 minetest.chat_send_player(name, "The id you supplied is not a non-negative number.")
1185 return
1187 box_id = id
1190 boxes.save_edit(minetest.get_player_by_name(name), box_id)
1191 end,
1194 minetest.register_chatcommand("stopedit", {
1195 params = "",
1196 description = "Stop editing a box.",
1197 privs = {server = true},
1198 func = function(name, param)
1199 if not boxes.players_editing_boxes[name] then
1200 minetest.chat_send_player(name, "You are not currently editing a box!")
1201 return
1204 local player = minetest.get_player_by_name(name)
1205 if param ~= "false" then
1206 boxes.save_edit(player)
1208 boxes.stop_edit(player)
1209 music.stop(player)
1210 players.return_to_lobby(player)
1211 end,
1214 local function on_leaveplayer(player)
1215 minetest.log("action", player:get_player_name() .. " left the game")
1216 music.stop(player)
1217 boxes.close_box(player)
1218 boxes.save_edit(player)
1219 boxes.stop_edit(player)
1222 minetest.register_on_leaveplayer(on_leaveplayer)
1223 minetest.register_on_shutdown(function()
1224 for _, player in ipairs(minetest.get_connected_players()) do
1225 on_leaveplayer(player)
1227 db.shutdown()
1228 end)
1230 minetest.register_on_respawnplayer(function(player)
1231 local name = player:get_player_name()
1232 if boxes.players_in_boxes[name] then
1233 local box = boxes.players_in_boxes[name]
1234 player:setpos(box.respawn)
1235 elseif boxes.players_editing_boxes[name] then
1236 local box = boxes.players_editing_boxes[name]
1237 player:setpos(vector.add(box.entry, {x = 1, y = 0, z = 0}))
1238 else
1239 players.return_to_lobby(player)
1241 return true
1242 end)
1244 minetest.register_on_dieplayer(function(player)
1245 local name = player:get_player_name()
1246 local box = boxes.players_in_boxes[name]
1247 if box and box.deaths then
1248 box.deaths = box.deaths + 1
1250 end)
1252 minetest.register_on_player_hpchange(function(player, hp_change)
1253 local name = player:get_player_name()
1254 local box = boxes.players_in_boxes[name]
1255 if box and box.damage and hp_change < 0 then
1256 box.damage = box.damage - hp_change
1258 end, false)
1260 function boxes.can_edit(player)
1261 local name = player:get_player_name()
1262 if boxes.players_editing_boxes[name] then
1263 return true
1265 if boxes.players_in_boxes[name] then
1266 return false
1268 return minetest.check_player_privs(name, "server")
1272 local metadata_updates = {}
1273 minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
1274 if not puncher then return end
1275 local name = puncher:get_player_name()
1276 if not metadata_updates[name] then return end
1277 if not boxes.players_editing_boxes[name] then return end
1278 local box = boxes.players_editing_boxes[name]
1279 local p = vector.subtract(pos, box.minp)
1280 local upd = metadata_updates[name]
1281 local key = upd.update_key
1282 local bmeta = db.box_get_meta(box.box_id)
1283 if upd.update_type == "set" then
1284 bmeta.meta[key] = p
1285 db.box_set_meta(box.box_id, bmeta)
1286 metadata_updates[name] = nil
1287 sfinv.set_player_inventory_formspec(puncher)
1288 elseif upd.update_type == "add" then
1289 bmeta.meta[key][#bmeta.meta[key] + 1] = p
1290 db.box_set_meta(box.box_id, bmeta)
1291 sfinv.set_player_inventory_formspec(puncher)
1293 end)
1295 local meta_types = {
1296 [db.BOX_TYPE] = {
1297 {key_name = "builder", type = "text"},
1298 {key_name = "status", type = "number"},
1300 [db.ENTRY_TYPE] = {
1301 {key_name = "status", type = "number"},
1303 [db.EXIT_TYPE] = {
1304 {key_name = "signs_to_update", type = "poslist"},
1305 {key_name = "star_positions", type = "poslist"},
1306 {key_name = "category_positions", type = "poslist"},
1307 {key_name = "status", type = "number"},
1311 sfinv.register_page("boxes:admin", {
1312 title = "Admin",
1313 is_in_nav = function(self, player, context)
1314 return minetest.check_player_privs(player, "server") and
1315 boxes.players_editing_boxes[player:get_player_name()]
1316 end,
1317 get = function(self, player, context)
1318 local name = player:get_player_name()
1319 local box = boxes.players_editing_boxes[name]
1320 local upd = metadata_updates[name]
1321 local bmeta = db.box_get_meta(box.box_id)
1322 local form = ""
1323 local y = 0
1324 for i, setting in ipairs(meta_types[bmeta.type]) do
1325 local current_value = bmeta.meta[setting.key_name]
1326 if setting.type == "text" then
1327 form = form .. "field[0.3," .. (y + 0.8) .. ";5,1;" ..
1328 tostring(i) .. ";" .. setting.key_name .. ";" ..
1329 minetest.formspec_escape(current_value) .. "]" ..
1330 "field_close_on_enter[" .. tostring(i) .. ";false]"
1331 y = y + 1.2
1332 elseif setting.type == "number" then
1333 form = form .. "field[0.3," .. (y + 0.8) .. ";5,1;" ..
1334 tostring(i) .. ";" .. setting.key_name .. ";" ..
1335 tostring(current_value) .. "]" ..
1336 "field_close_on_enter[" .. tostring(i) .. ";false]"
1337 y = y + 1.2
1338 elseif setting.type == "pos" then
1339 local blab = "Edit"
1340 if upd and upd.update_key == setting.key_name then
1341 blab = "Cancel edit"
1343 form = form .. "label[0," .. (y + 0.3) .. ";" ..
1344 minetest.formspec_escape(setting.key_name .. ": " .. minetest.pos_to_string(current_value)) .. "]"
1345 form = form .. "button[0," .. (y + 0.7) .. ";3,1;" ..
1346 tostring(i) .. ";" .. blab .. "]"
1347 y = y + 1.6
1348 elseif setting.type == "poslist" then
1349 local blab = "Edit"
1350 if upd and upd.update_key == setting.key_name then
1351 blab = "Stop edit"
1353 local cvs = "{"
1354 for j, pos in ipairs(current_value) do
1355 if j > 1 then
1356 cvs = cvs .. ", "
1358 cvs = cvs .. minetest.pos_to_string(pos)
1360 cvs = cvs .. "}"
1361 form = form .. "label[0," .. (y + 0.3) .. ";" ..
1362 minetest.formspec_escape(setting.key_name .. ": " .. cvs) .. "]"
1363 form = form .. "button[0," .. (y + 0.7) .. ";3,1;" ..
1364 tostring(i) .. ";" .. blab .. "]"
1365 y = y + 1.6
1368 return sfinv.make_formspec(player, context, form, false)
1369 end,
1370 on_player_receive_fields = function(self, player, context, fields)
1371 if not minetest.check_player_privs(player, "server") then
1372 return
1375 local update = false
1377 local name = player:get_player_name()
1378 local box = boxes.players_editing_boxes[name]
1379 local bmeta = db.box_get_meta(box.box_id)
1380 local settings = meta_types[bmeta.type]
1381 for index, value in pairs(fields) do
1382 local i = tonumber(index)
1383 if i ~= nil then
1384 local setting = settings[i]
1385 if setting.type == "number" then
1386 value = tonumber(value)
1388 --FIXME log changes
1389 if value ~= nil and setting.type ~= "pos" and setting.type ~= "poslist" then
1390 bmeta.meta[setting.key_name] = value
1391 elseif setting.type == "pos" or setting.type == "poslist" then
1392 if metadata_updates[name] and metadata_updates[name].update_key == setting.key_name then
1393 metadata_updates[name] = nil
1394 else
1395 if setting.type == "poslist" then
1396 bmeta.meta[setting.key_name] = {}
1398 metadata_updates[name] = {
1399 update_type = (setting.type == "pos" and "set" or "add"),
1400 update_key = setting.key_name
1403 update = true
1407 db.box_set_meta(box.box_id, bmeta)
1409 if update then
1410 sfinv.set_page(player, "boxes:admin")
1412 end,
1416 --[[
1418 -- Handle box expositions
1419 -- These are boxes whose inside can be seen by the players before entering them
1420 -- Proof of concept for now, so only one such box.
1422 local box_expo_minp = {x = 5, y = 0, z = 5}
1423 local box_expo_size = {x = 22, y = 22, z = 22}
1426 minetest.register_chatcommand("expo", {
1427 params = "<box_id>",
1428 description = "Select the chosen box for exposition",
1429 privs = {server = true},
1430 func = function(name, param)
1431 local id = tonumber(param)
1432 if not id or id ~= math.floor(id) or id < 0 then
1433 minetest.chat_send_player(name, "The id you supplied is not a nonnegative integer.")
1434 return
1437 local meta = db.box_get_meta(id)
1438 if not meta then
1439 minetest.chat_send_player(name, "The id you supplied is not in the database.")
1440 return
1442 if meta.type ~= db.BOX_TYPE then
1443 minetest.chat_send_player(name, "The id you supplied does not correspond to a box.")
1444 return
1447 local size = meta.meta.size
1448 if not vector.equals(size, box_expo_size) then
1449 minetest.chat_send_player(name, "The box you chose does not have the correct size.")
1450 return
1453 boxes.load(box_expo_minp, id, nil)
1454 -- Open up a side
1456 local minp = vector.add(box_expo_minp, {x = 1, y = 1, z = size.z - 1})
1457 local maxp = vector.add(box_expo_minp, {x = size.x - 2, y = size.y - 1, z = size.z - 1})
1459 local cid_barrier = minetest.get_content_id("nodes:barrier")
1461 local vm = minetest.get_voxel_manip(minp, maxp)
1462 local emin, emax = vm:get_emerged_area()
1463 local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
1464 local vmdata = vm:get_data()
1466 for y = minp.y, maxp.y do
1467 local index = va:index(minp.x, y, maxp.z)
1468 for x = minp.x, maxp.x do
1469 vmdata[index] = cid_barrier
1470 index = index + 1
1474 vm:set_data(vmdata)
1475 vm:write_to_map()
1476 vm:update_map()
1478 end,