Require new privilege to do stuff
[minetest_schemedit.git] / init.lua
blobcfcc3f40306d7a62a799b4829f0ea2d857757ed9
1 local S = minetest.get_translator("schemedit")
2 local F = minetest.formspec_escape
4 local schemedit = {}
6 local DIR_DELIM = "/"
8 local export_path_full = table.concat({minetest.get_worldpath(), "schems"}, DIR_DELIM)
10 -- truncated export path so the server directory structure is not exposed publicly
11 local export_path_trunc = table.concat({S("<world path>"), "schems"}, DIR_DELIM)
13 local text_color = "#D79E9E"
14 local text_color_number = 0xD79E9E
16 local can_import = minetest.read_schematic ~= nil
18 schemedit.markers = {}
20 -- [local function] Renumber table
21 local function renumber(t)
22 local res = {}
23 for _, i in pairs(t) do
24 res[#res + 1] = i
25 end
26 return res
27 end
29 minetest.register_privilege("schemedit")
30 local NEEDED_PRIV = "schemedit"
31 local function check_priv(player_name)
32 local privs = minetest.get_player_privs(player_name)
33 if privs[NEEDED_PRIV] then
34 return true
35 else
36 minetest.chat_send_player(player_name, minetest.colorize("red",
37 S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV)))
38 return false
39 end
40 end
42 -- Lua export
43 local export_schematic_to_lua
44 if can_import then
45 export_schematic_to_lua = function(schematic, filepath, options)
46 if not options then options = {} end
47 local str = minetest.serialize_schematic(schematic, "lua", options)
48 local file = io.open(filepath, "w")
49 if file and str then
50 file:write(str)
51 file:flush()
52 file:close()
53 return true
54 else
55 return false
56 end
57 end
58 end
60 ---
61 --- Formspec API
62 ---
64 local contexts = {}
65 local form_data = {}
66 local tabs = {}
67 local forms = {}
68 local displayed_waypoints = {}
70 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
71 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
72 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
73 -- on an actual export to a schematic.
75 function schemedit.lua_prob_to_schematic_prob(lua_prob)
76 return math.floor(lua_prob / 2)
77 end
79 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
80 return schematic_prob * 2
82 end
84 -- [function] Add form
85 function schemedit.add_form(name, def)
86 def.name = name
87 forms[name] = def
89 if def.tab then
90 tabs[#tabs + 1] = name
91 end
92 end
94 -- [function] Generate tabs
95 function schemedit.generate_tabs(current)
96 local retval = "tabheader[0,0;tabs;"
97 for _, t in pairs(tabs) do
98 local f = forms[t]
99 if f.tab ~= false and f.caption then
100 retval = retval..f.caption..","
102 if type(current) ~= "number" and current == f.name then
103 current = _
107 retval = retval:sub(1, -2) -- Strip last comma
108 retval = retval..";"..current.."]" -- Close tabheader
109 return retval
112 -- [function] Handle tabs
113 function schemedit.handle_tabs(pos, name, fields)
114 local tab = tonumber(fields.tabs)
115 if tab and tabs[tab] and forms[tabs[tab]] then
116 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
117 return true
121 -- [function] Show formspec
122 function schemedit.show_formspec(pos, player, tab, show, ...)
123 if forms[tab] then
124 if type(player) == "string" then
125 player = minetest.get_player_by_name(player)
127 local name = player:get_player_name()
129 if show ~= false then
130 if not form_data[name] then
131 form_data[name] = {}
134 local form = forms[tab].get(form_data[name], pos, name, ...)
135 if forms[tab].tab then
136 form = form..schemedit.generate_tabs(tab)
139 minetest.show_formspec(name, "schemedit:"..tab, form)
140 contexts[name] = pos
142 -- Update player attribute
143 if forms[tab].cache_name ~= false then
144 player:set_attribute("schemedit:tab", tab)
146 else
147 minetest.close_formspec(pname, "schemedit:"..tab)
152 -- [event] On receive fields
153 minetest.register_on_player_receive_fields(function(player, formname, fields)
154 local formname = formname:split(":")
156 if formname[1] == "schemedit" and forms[formname[2]] then
157 local handle = forms[formname[2]].handle
158 local name = player:get_player_name()
159 if contexts[name] then
160 if not form_data[name] then
161 form_data[name] = {}
164 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
165 handle(form_data[name], contexts[name], name, fields)
169 end)
171 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
172 schemedit.scan_metadata = function(pos1, pos2)
173 local prob_list = {}
175 for x=pos1.x, pos2.x do
176 for y=pos1.y, pos2.y do
177 for z=pos1.z, pos2.z do
178 local scanpos = {x=x, y=y, z=z}
179 local node = minetest.get_node_or_nil(scanpos)
181 local prob, force_place
182 if node == nil or node.name == "schemedit:void" then
183 prob = 0
184 force_place = false
185 else
186 local meta = minetest.get_meta(scanpos)
188 prob = tonumber(meta:get_string("schemedit_prob")) or 255
189 local fp = meta:get_string("schemedit_force_place")
190 if fp == "true" then
191 force_place = true
192 else
193 force_place = false
197 local hashpos = minetest.hash_node_position(scanpos)
198 prob_list[hashpos] = {
199 pos = scanpos,
200 prob = prob,
201 force_place = force_place,
207 return prob_list
210 -- Sets probability and force_place metadata of an item.
211 -- Also updates item description.
212 -- The itemstack is updated in-place.
213 local function set_item_metadata(itemstack, prob, force_place)
214 local smeta = itemstack:get_meta()
215 local prob_desc = "\n"..S("Probability: @1", prob or
216 smeta:get_string("schemedit_prob") or S("Not Set"))
217 -- Update probability
218 if prob and prob >= 0 and prob < 255 then
219 smeta:set_string("schemedit_prob", tostring(prob))
220 elseif prob and prob == 255 then
221 -- Clear prob metadata for default probability
222 prob_desc = ""
223 smeta:set_string("schemedit_prob", nil)
224 else
225 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
226 S("Not Set"))
229 -- Update force place
230 if force_place == true then
231 smeta:set_string("schemedit_force_place", "true")
232 elseif force_place == false then
233 smeta:set_string("schemedit_force_place", nil)
236 -- Update description
237 local desc = minetest.registered_items[itemstack:get_name()].description
238 local meta_desc = smeta:get_string("description")
239 if meta_desc and meta_desc ~= "" then
240 desc = meta_desc
243 local original_desc = smeta:get_string("original_description")
244 if original_desc and original_desc ~= "" then
245 desc = original_desc
246 else
247 smeta:set_string("original_description", desc)
250 local force_desc = ""
251 if smeta:get_string("schemedit_force_place") == "true" then
252 force_desc = "\n"..S("Force placement")
255 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
257 smeta:set_string("description", desc)
259 return itemstack
263 --- Formspec Tabs
265 local import_btn = ""
266 if can_import then
267 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
269 schemedit.add_form("main", {
270 tab = true,
271 caption = S("Main"),
272 get = function(self, pos, name)
273 local meta = minetest.get_meta(pos):to_table().fields
274 local strpos = minetest.pos_to_string(pos)
275 local hashpos = minetest.hash_node_position(pos)
277 local border_button
278 if meta.schem_border == "true" and schemedit.markers[hashpos] then
279 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
280 else
281 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
284 local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
285 local size = {x=xs, y=ys, z=zs}
287 local form = [[
288 size[7,8]
289 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
290 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
291 label[0.5,0.4;]]..F(S("Schematic name: @1", meta.schem_name))..[[]
292 label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
294 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(meta.schem_name or "")..[[]
295 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
296 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
297 field_close_on_enter[name;false]
299 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
300 import_btn..[[
301 textarea[0.8,4.5;6.2,5;;]]..F(S("Export/import path:\n@1",
302 export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
303 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
304 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
305 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
306 field_close_on_enter[x;false]
307 field_close_on_enter[y;false]
308 field_close_on_enter[z;false]
310 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
311 ]]..
312 border_button
313 if minetest.get_modpath("doc") then
314 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
315 "tooltip[doc;"..F(S("Help")).."]"
317 return form
318 end,
319 handle = function(self, pos, name, fields)
320 if not check_priv(name) then
321 return
324 local realmeta = minetest.get_meta(pos)
325 local meta = realmeta:to_table().fields
326 local hashpos = minetest.hash_node_position(pos)
328 -- Save size vector values
329 if (fields.x and fields.x ~= "") then
330 local x = tonumber(fields.x)
331 if x then
332 meta.x_size = math.max(x, 1)
335 if (fields.y and fields.y ~= "") then
336 local y = tonumber(fields.y)
337 if y then
338 meta.y_size = math.max(y, 1)
341 if (fields.z and fields.z ~= "") then
342 local z = tonumber(fields.z)
343 if z then
344 meta.z_size = math.max(z, 1)
348 -- Save schematic name
349 if fields.name then
350 meta.schem_name = fields.name
353 if fields.doc then
354 doc.show_entry(name, "nodes", "schemedit:creator", true)
355 return
358 -- Toggle border
359 if fields.border then
360 if meta.schem_border == "true" and schemedit.markers[hashpos] then
361 schemedit.unmark(pos)
362 meta.schem_border = "false"
363 else
364 schemedit.mark(pos)
365 meta.schem_border = "true"
369 -- Export schematic
370 if fields.export and meta.schem_name and meta.schem_name ~= "" then
371 local pos1, pos2 = schemedit.size(pos)
372 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
373 local path = export_path_full .. DIR_DELIM
374 minetest.mkdir(path)
376 local plist = schemedit.scan_metadata(pos1, pos2)
377 local probability_list = {}
378 for hash, i in pairs(plist) do
379 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
380 if i.force_place == true then
381 prob = prob + 128
384 table.insert(probability_list, {
385 pos = minetest.get_position_from_hash(hash),
386 prob = prob,
390 local slist = minetest.deserialize(meta.slices)
391 local slice_list = {}
392 for _, i in pairs(slist) do
393 slice_list[#slice_list + 1] = {
394 ypos = pos.y + i.ypos,
395 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
399 local filepath = path..meta.schem_name..".mts"
400 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
402 if res then
403 minetest.chat_send_player(name, minetest.colorize("#00ff00",
404 S("Exported schematic to @1", filepath)))
405 -- Additional export to Lua file if MTS export was successful
406 local schematic = minetest.read_schematic(filepath, {})
407 if schematic and minetest.settings:get_bool("schemedit_export_lua") then
408 local filepath_lua = path..meta.schem_name..".lua"
409 res = export_schematic_to_lua(schematic, filepath_lua)
410 if res then
411 minetest.chat_send_player(name, minetest.colorize("#00ff00",
412 S("Exported schematic to @1", filepath_lua)))
415 else
416 minetest.chat_send_player(name, minetest.colorize("red",
417 S("Failed to export schematic to @1", filepath)))
421 -- Import schematic
422 if fields.import and meta.schem_name and meta.schem_name ~= "" then
423 if not can_import then
424 return
426 local pos1
427 local node = minetest.get_node(pos)
428 local path = export_path_full .. DIR_DELIM
430 local filepath = path..meta.schem_name..".mts"
431 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
432 local success = false
434 if schematic then
435 meta.x_size = schematic.size.x
436 meta.y_size = schematic.size.y
437 meta.z_size = schematic.size.z
438 meta.slices = minetest.serialize(schematic.yslice_prob)
440 if node.param2 == 1 then
441 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
442 elseif node.param2 == 2 then
443 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
444 elseif node.param2 == 3 then
445 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
446 else
447 pos1 = vector.add(pos, {x=0,y=0,z=1})
450 local schematic_for_meta = table.copy(schematic)
451 -- Strip probability data for placement
452 schematic.yslice_prob = {}
453 for d=1, #schematic.data do
454 schematic.data[d].prob = nil
457 -- Place schematic
458 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
460 -- Add special schematic data to nodes
461 if success then
462 local d = 1
463 for z=0, meta.z_size-1 do
464 for y=0, meta.y_size-1 do
465 for x=0, meta.x_size-1 do
466 local data = schematic_for_meta.data[d]
467 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
468 if data.prob == 0 then
469 minetest.set_node(pp, {name="schemedit:void"})
470 else
471 local meta = minetest.get_meta(pp)
472 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
473 meta:set_string("schemedit_prob", tostring(data.prob))
474 else
475 meta:set_string("schemedit_prob", "")
477 if data.force_place then
478 meta:set_string("schemedit_force_place", "true")
479 else
480 meta:set_string("schemedit_force_place", "")
483 d = d + 1
489 if success then
490 minetest.chat_send_player(name, minetest.colorize("#00ff00",
491 S("Imported schematic from @1", filepath)))
492 else
493 minetest.chat_send_player(name, minetest.colorize("red",
494 S("Failed to import schematic from @1", filepath)))
500 -- Save meta before updating visuals
501 local inv = realmeta:get_inventory():get_lists()
502 realmeta:from_table({fields = meta, inventory = inv})
504 -- Update border
505 if not fields.border and meta.schem_border == "true" then
506 schemedit.mark(pos)
509 -- Update formspec
510 if not fields.quit then
511 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
513 end,
516 schemedit.add_form("slice", {
517 caption = S("Y Slices"),
518 tab = true,
519 get = function(self, pos, name, visible_panel)
520 local meta = minetest.get_meta(pos):to_table().fields
522 self.selected = self.selected or 1
523 local selected = tostring(self.selected)
524 local slice_list = minetest.deserialize(meta.slices)
525 local slices = ""
526 for _, i in pairs(slice_list) do
527 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
528 slices = slices..insert..","
530 slices = slices:sub(1, -2) -- Remove final comma
532 local form = [[
533 size[7,8]
534 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
537 if self.panel_add or self.panel_edit then
538 local ypos_default, prob_default = "", ""
539 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
540 if self.panel_edit then
541 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
542 if slice_list[self.selected] then
543 ypos_default = slice_list[self.selected].ypos
544 prob_default = slice_list[self.selected].prob
548 form = form..[[
549 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
550 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
551 field_close_on_enter[ypos;false]
552 field_close_on_enter[prob;false]
553 ]]..done_button
556 if not self.panel_edit then
557 form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
560 if slices ~= "" and self.selected and not self.panel_add then
561 if not self.panel_edit then
562 form = form..[[
563 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
564 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
566 else
567 form = form..[[
568 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
569 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
574 return form
575 end,
576 handle = function(self, pos, name, fields)
577 if not check_priv(name) then
578 return
581 local meta = minetest.get_meta(pos)
582 local player = minetest.get_player_by_name(name)
584 if fields.slices then
585 local slices = fields.slices:split(":")
586 self.selected = tonumber(slices[2])
589 if fields.add then
590 if not self.panel_add then
591 self.panel_add = true
592 schemedit.show_formspec(pos, player, "slice")
593 else
594 self.panel_add = nil
595 schemedit.show_formspec(pos, player, "slice")
599 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
600 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
601 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
602 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
603 local slice_list = minetest.deserialize(meta:get_string("slices"))
604 local index = #slice_list + 1
605 if fields.done_edit then
606 index = self.selected
609 slice_list[index] = {ypos = ypos, prob = prob}
611 meta:set_string("slices", minetest.serialize(slice_list))
613 -- Update and show formspec
614 self.panel_add = nil
615 schemedit.show_formspec(pos, player, "slice")
618 if fields.remove and self.selected then
619 local slice_list = minetest.deserialize(meta:get_string("slices"))
620 slice_list[self.selected] = nil
621 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
623 -- Update formspec
624 self.selected = 1
625 self.panel_edit = nil
626 schemedit.show_formspec(pos, player, "slice")
629 if fields.edit then
630 if not self.panel_edit then
631 self.panel_edit = true
632 schemedit.show_formspec(pos, player, "slice")
633 else
634 self.panel_edit = nil
635 schemedit.show_formspec(pos, player, "slice")
638 end,
641 schemedit.add_form("probtool", {
642 cache_name = false,
643 caption = S("Schematic Node Probability Tool"),
644 get = function(self, pos, name)
645 local player = minetest.get_player_by_name(name)
646 if not player then
647 return
649 local probtool = player:get_wielded_item()
650 if probtool:get_name() ~= "schemedit:probtool" then
651 return
654 local meta = probtool:get_meta()
655 local prob = tonumber(meta:get_string("schemedit_prob"))
656 local force_place = meta:get_string("schemedit_force_place")
658 if not prob then
659 prob = 255
661 if force_place == nil or force_place == "" then
662 force_place = "false"
664 local form = "size[5,4]"..
665 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
666 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
667 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
668 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
669 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
670 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
671 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
672 "field_close_on_enter[prob;false]"
673 return form
674 end,
675 handle = function(self, pos, name, fields)
676 if not check_priv(name) then
677 return
680 if fields.submit then
681 local prob = tonumber(fields.prob)
682 if prob then
683 local player = minetest.get_player_by_name(name)
684 if not player then
685 return
687 local probtool = player:get_wielded_item()
688 if probtool:get_name() ~= "schemedit:probtool" then
689 return
692 local force_place = self.force_place == true
694 set_item_metadata(probtool, prob, force_place)
696 -- Repurpose the tool's wear bar to display the set probability
697 probtool:set_wear(math.floor(((255-prob)/255)*65535))
699 player:set_wielded_item(probtool)
702 if fields.force_place == "true" then
703 self.force_place = true
704 elseif fields.force_place == "false" then
705 self.force_place = false
707 end,
711 --- API
714 --- Copies and modifies positions `pos1` and `pos2` so that each component of
715 -- `pos1` is less than or equal to the corresponding component of `pos2`.
716 -- Returns the new positions.
717 function schemedit.sort_pos(pos1, pos2)
718 if not pos1 or not pos2 then
719 return
722 pos1, pos2 = table.copy(pos1), table.copy(pos2)
723 if pos1.x > pos2.x then
724 pos2.x, pos1.x = pos1.x, pos2.x
726 if pos1.y > pos2.y then
727 pos2.y, pos1.y = pos1.y, pos2.y
729 if pos1.z > pos2.z then
730 pos2.z, pos1.z = pos1.z, pos2.z
732 return pos1, pos2
735 -- [function] Prepare size
736 function schemedit.size(pos)
737 local pos1 = vector.new(pos)
738 local meta = minetest.get_meta(pos)
739 local node = minetest.get_node(pos)
740 local param2 = node.param2
741 local size = {
742 x = meta:get_int("x_size"),
743 y = math.max(meta:get_int("y_size") - 1, 0),
744 z = meta:get_int("z_size"),
747 if param2 == 1 then
748 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
749 pos1.x = pos1.x + 1
750 new_pos.z = new_pos.z + 1
751 return pos1, new_pos
752 elseif param2 == 2 then
753 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
754 pos1.z = pos1.z - 1
755 new_pos.x = new_pos.x + 1
756 return pos1, new_pos
757 elseif param2 == 3 then
758 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
759 pos1.x = pos1.x - 1
760 new_pos.z = new_pos.z - 1
761 return pos1, new_pos
762 else
763 local new_pos = vector.add(size, pos)
764 pos1.z = pos1.z + 1
765 new_pos.x = new_pos.x - 1
766 return pos1, new_pos
770 -- [function] Mark region
771 function schemedit.mark(pos)
772 schemedit.unmark(pos)
774 local id = minetest.hash_node_position(pos)
775 local owner = minetest.get_meta(pos):get_string("owner")
776 local pos1, pos2 = schemedit.size(pos)
777 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
779 local thickness = 0.2
780 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
781 local m = {}
782 local low = true
783 local offset
785 -- XY plane markers
786 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
787 if low then
788 offset = -0.01
789 else
790 offset = 0.01
792 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
793 if marker ~= nil then
794 marker:set_properties({
795 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
797 marker:get_luaentity().id = id
798 marker:get_luaentity().owner = owner
799 table.insert(m, marker)
801 low = false
804 low = true
805 -- YZ plane markers
806 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
807 if low then
808 offset = -0.01
809 else
810 offset = 0.01
813 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
814 if marker ~= nil then
815 marker:set_properties({
816 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
818 marker:set_rotation({x=0, y=math.pi / 2, z=0})
819 marker:get_luaentity().id = id
820 marker:get_luaentity().owner = owner
821 table.insert(m, marker)
823 low = false
826 low = true
827 -- XZ plane markers
828 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
829 if low then
830 offset = -0.01
831 else
832 offset = 0.01
835 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
836 if marker ~= nil then
837 marker:set_properties({
838 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
840 marker:set_rotation({x=math.pi/2, y=0, z=0})
841 marker:get_luaentity().id = id
842 marker:get_luaentity().owner = owner
843 table.insert(m, marker)
845 low = false
850 schemedit.markers[id] = m
851 return true
854 -- [function] Unmark region
855 function schemedit.unmark(pos)
856 local id = minetest.hash_node_position(pos)
857 if schemedit.markers[id] then
858 local retval
859 for _, entity in ipairs(schemedit.markers[id]) do
860 entity:remove()
861 retval = true
863 return retval
868 --- Mark node probability values near player
871 -- Show probability and force_place status of a particular position for player in HUD.
872 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
873 -- The distance to the node is also displayed below that. This can't be avoided and is
874 -- and artifact of the waypoint HUD element.
875 function schemedit.display_node_prob(player, pos, prob, force_place)
876 local wpstring
877 if prob and force_place == true then
878 wpstring = string.format("%s [F]", prob)
879 elseif prob and type(tonumber(prob)) == "number" then
880 wpstring = prob
881 elseif force_place == true then
882 wpstring = "[F]"
884 if wpstring then
885 return player:hud_add({
886 hud_elem_type = "waypoint",
887 name = wpstring,
888 precision = 0,
889 text = "m", -- For the distance artifact
890 number = text_color_number,
891 world_pos = pos,
896 -- Display the node probabilities and force_place status of the nodes in a region.
897 -- By default, this is done for nodes near the player (distance: 5).
898 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
899 function schemedit.display_node_probs_region(player, pos1, pos2)
900 local playername = player:get_player_name()
901 local pos = vector.round(player:get_pos())
903 local dist = 5
904 -- Default: 5 nodes away from player in any direction
905 if not pos1 then
906 pos1 = vector.subtract(pos, dist)
908 if not pos2 then
909 pos2 = vector.add(pos, dist)
911 for x=pos1.x, pos2.x do
912 for y=pos1.y, pos2.y do
913 for z=pos1.z, pos2.z do
914 local checkpos = {x=x, y=y, z=z}
915 local nodehash = minetest.hash_node_position(checkpos)
917 -- If node is already displayed, remove it so it can re replaced later
918 if displayed_waypoints[playername][nodehash] then
919 player:hud_remove(displayed_waypoints[playername][nodehash])
920 displayed_waypoints[playername][nodehash] = nil
923 local prob, force_place
924 local meta = minetest.get_meta(checkpos)
925 prob = meta:get_string("schemedit_prob")
926 force_place = meta:get_string("schemedit_force_place") == "true"
927 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
928 if hud_id then
929 displayed_waypoints[playername][nodehash] = hud_id
930 displayed_waypoints[playername].display_active = true
937 -- Remove all active displayed node statuses.
938 function schemedit.clear_displayed_node_probs(player)
939 local playername = player:get_player_name()
940 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
941 player:hud_remove(hud_id)
942 displayed_waypoints[playername][nodehash] = nil
943 displayed_waypoints[playername].display_active = false
947 minetest.register_on_joinplayer(function(player)
948 displayed_waypoints[player:get_player_name()] = {
949 display_active = false -- If true, there *might* be at least one active node prob HUD display
950 -- If false, no node probabilities are displayed for sure.
952 end)
954 minetest.register_on_leaveplayer(function(player)
955 displayed_waypoints[player:get_player_name()] = nil
956 end)
958 -- Regularily clear the displayed node probabilities and force_place
959 -- for all players who do not wield the probtool.
960 -- This makes sure the screen is not spammed with information when it
961 -- isn't needed.
962 local cleartimer = 0
963 minetest.register_globalstep(function(dtime)
964 cleartimer = cleartimer + dtime
965 if cleartimer > 2 then
966 local players = minetest.get_connected_players()
967 for p = 1, #players do
968 local player = players[p]
969 local pname = player:get_player_name()
970 if displayed_waypoints[pname].display_active then
971 local item = player:get_wielded_item()
972 if item:get_name() ~= "schemedit:probtool" then
973 schemedit.clear_displayed_node_probs(player)
977 cleartimer = 0
979 end)
982 --- Registrations
985 -- [priv] schematic_override
986 minetest.register_privilege("schematic_override", {
987 description = S("Allows you to access schemedit nodes not owned by you"),
988 give_to_singleplayer = false,
991 local help_import = ""
992 if can_import then
993 help_import = S("Importing a schematic will load a schematic from the world directory, place it in front of the schematic creator and sets probability and force-place data accordingly.").."\n"
996 -- [node] Schematic creator
997 minetest.register_node("schemedit:creator", {
998 description = S("Schematic Creator"),
999 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1000 _doc_items_usagehelp = S("To get started, place the block facing directly in front of any bottom left corner of the structure you want to save. This block can only be accessed by the placer or by anyone with the “schematic_override” privilege.").."\n"..
1001 S("To save a region, use the block, enter the size and a schematic name and hit “Export schematic”. The file will always be saved in the world directory. Note you can use this name in the /placeschem command to place the schematic again.").."\n\n"..
1002 help_import..
1003 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1004 S("Y slices are used to remove entire slices based on chance. For each slice of the schematic region along the Y axis, you can specify that it occurs only with a certain chance. In the Y slice tab, you have to specify the Y slice height (0 = bottom) and a probability from 0 to 255 (255 is for 100%). By default, all Y slices occur always.").."\n\n"..
1005 S("With a schematic node probability tool, you can set a probability for each node and enable them to overwrite all nodes when placed as schematic. This tool must be used prior to the file export."),
1006 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1007 "schemedit_creator_sides.png"},
1008 groups = { dig_immediate = 2},
1009 paramtype2 = "facedir",
1010 is_ground_content = false,
1012 after_place_node = function(pos, player)
1013 local name = player:get_player_name()
1014 local meta = minetest.get_meta(pos)
1016 meta:set_string("owner", name)
1017 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
1018 meta:set_string("prob_list", minetest.serialize({}))
1019 meta:set_string("slices", minetest.serialize({}))
1021 local node = minetest.get_node(pos)
1022 local dir = minetest.facedir_to_dir(node.param2)
1024 meta:set_int("x_size", 1)
1025 meta:set_int("y_size", 1)
1026 meta:set_int("z_size", 1)
1028 -- Don't take item from itemstack
1029 return true
1030 end,
1031 can_dig = function(pos, player)
1032 local name = player:get_player_name()
1033 local meta = minetest.get_meta(pos)
1034 if meta:get_string("owner") == name or
1035 minetest.check_player_privs(player, "schematic_override") == true then
1036 return true
1039 return false
1040 end,
1041 on_rightclick = function(pos, node, player)
1042 local meta = minetest.get_meta(pos)
1043 local name = player:get_player_name()
1044 if meta:get_string("owner") == name or
1045 minetest.check_player_privs(player, "schematic_override") == true then
1046 -- Get player attribute
1047 local tab = player:get_attribute("schemedit:tab")
1048 if not forms[tab] or not tab then
1049 tab = "main"
1052 schemedit.show_formspec(pos, player, tab, true)
1054 end,
1055 after_destruct = function(pos)
1056 schemedit.unmark(pos)
1057 end,
1059 -- No support for Minetest Game's screwdriver
1060 on_rotate = false,
1063 minetest.register_tool("schemedit:probtool", {
1064 description = S("Schematic Node Probability Tool"),
1065 _doc_items_longdesc =
1066 S("This is an advanced tool which only makes sense when used together with a schematic creator. It is used to finetune the way how nodes from a schematic are placed.").."\n"..
1067 S("It allows you to set two things:").."\n"..
1068 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1069 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1070 _doc_items_usagehelp = "\n"..
1071 S("BASIC USAGE:").."\n"..
1072 S("Punch to configure the tool. Select a probability (0-255; 255 is for 100%) and enable or disable force placement. Now place the tool on any node to apply these values to the node. This information is preserved in the node until it is destroyed or changed by the tool again. This tool has no effect on schematic voids.").."\n"..
1073 S("Now you can use a schematic creator to save a region as usual, the nodes will now be saved with the special node settings applied.").."\n\n"..
1074 S("NODE HUD:").."\n"..
1075 S("To help you remember the node values, the nodes with special values are labelled in the HUD. The first line shows probability and force placement (with “[F]”). The second line is the current distance to the node. Nodes with default settings and schematic voids are not labelled.").."\n"..
1076 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1077 S("UPDATING THE NODE HUD:").."\n"..
1078 S("The node HUD is not updated automatically and may be outdated. The node HUD only updates the HUD for nodes close to you whenever you place the tool or press the punch and sneak keys simutanously. If you sneak-punch a schematic creator, then the node HUD is updated for all nodes within the schematic creator's region, even if this region is very big."),
1079 wield_image = "schemedit_probtool.png",
1080 inventory_image = "schemedit_probtool.png",
1081 liquids_pointable = true,
1082 groups = { disable_repair = 1 },
1083 on_use = function(itemstack, user, pointed_thing)
1084 local uname = user:get_player_name()
1085 if uname and not check_priv(uname) then
1086 return
1089 local ctrl = user:get_player_control()
1090 -- Simple use
1091 if not ctrl.sneak then
1092 -- Open dialog to change the probability to apply to nodes
1093 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1095 -- Use + sneak
1096 else
1097 -- Display the probability and force_place values for nodes.
1099 -- If a schematic creator was punched, only enable display for all nodes
1100 -- within the creator's region.
1101 local use_creator_region = false
1102 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1103 local punchpos = pointed_thing.under
1104 local node = minetest.get_node(punchpos)
1105 if node.name == "schemedit:creator" then
1106 local pos1, pos2 = schemedit.size(punchpos)
1107 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1108 schemedit.display_node_probs_region(user, pos1, pos2)
1109 return
1113 -- Otherwise, just display the region close to the player
1114 schemedit.display_node_probs_region(user)
1116 end,
1117 on_secondary_use = function(itemstack, user, pointed_thing)
1118 schemedit.clear_displayed_node_probs(user)
1119 end,
1120 -- Set note probability and force_place and enable node probability display
1121 on_place = function(itemstack, placer, pointed_thing)
1122 -- Use pointed node's on_rightclick function first, if present
1123 local node = minetest.get_node(pointed_thing.under)
1124 if placer and not placer:get_player_control().sneak then
1125 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1126 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1130 -- This sets the node probability of pointed node to the
1131 -- currently used probability stored in the tool.
1132 local pos = pointed_thing.under
1133 local node = minetest.get_node(pos)
1134 -- Schematic void are ignored, they always have probability 0
1135 if node.name == "schemedit:void" then
1136 return itemstack
1138 local nmeta = minetest.get_meta(pos)
1139 local imeta = itemstack:get_meta()
1140 local prob = tonumber(imeta:get_string("schemedit_prob"))
1141 local force_place = imeta:get_string("schemedit_force_place")
1143 if not prob or prob == 255 then
1144 nmeta:set_string("schemedit_prob", nil)
1145 else
1146 nmeta:set_string("schemedit_prob", prob)
1148 if force_place == "true" then
1149 nmeta:set_string("schemedit_force_place", "true")
1150 else
1151 nmeta:set_string("schemedit_force_place", nil)
1154 -- Enable node probablity display
1155 schemedit.display_node_probs_region(placer)
1157 return itemstack
1158 end,
1161 minetest.register_node("schemedit:void", {
1162 description = S("Schematic Void"),
1163 _doc_items_longdesc = S("This is an utility block used in the creation of schematic files. It should be used together with a schematic creator. When saving a schematic, all nodes with a schematic void will be left unchanged when the schematic is placed again. Technically, this is equivalent to a block with the node probability set to 0."),
1164 _doc_items_usagehelp = S("Just place the schematic void like any other block and use the schematic creator to save a portion of the world."),
1165 tiles = { "schemedit_void.png" },
1166 drawtype = "nodebox",
1167 is_ground_content = false,
1168 paramtype = "light",
1169 walkable = false,
1170 sunlight_propagates = true,
1171 node_box = {
1172 type = "fixed",
1173 fixed = {
1174 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1177 groups = { dig_immediate = 3},
1180 -- [entity] Visible schematic border
1181 minetest.register_entity("schemedit:display", {
1182 visual = "upright_sprite",
1183 textures = {"schemedit_border.png"},
1184 visual_size = {x=10, y=10},
1185 pointable = false,
1186 physical = false,
1187 static_save = false,
1188 glow = minetest.LIGHT_MAX,
1190 on_step = function(self, dtime)
1191 if not self.id then
1192 self.object:remove()
1193 elseif not schemedit.markers[self.id] then
1194 self.object:remove()
1196 end,
1197 on_activate = function(self)
1198 self.object:set_armor_groups({immortal = 1})
1199 end,
1202 minetest.register_lbm({
1203 label = "Reset schematic creator border entities",
1204 name = "schemedit:reset_border",
1205 nodenames = "schemedit:creator",
1206 run_at_every_load = true,
1207 action = function(pos, node)
1208 local meta = minetest.get_meta(pos)
1209 meta:set_string("schem_border", "false")
1210 end,
1213 local function add_suffix(schem)
1214 -- Automatically add file name suffix if omitted
1215 local schem_full, schem_lua
1216 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1217 schem_full = schem
1218 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1219 else
1220 schem_full = schem .. ".mts"
1221 schem_lua = schem .. ".lua"
1223 return schem_full, schem_lua
1226 -- [chatcommand] Place schematic
1227 minetest.register_chatcommand("placeschem", {
1228 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1229 privs = {server = true},
1230 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1231 func = function(name, param)
1232 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1233 local pos = minetest.string_to_pos(p)
1235 if not schem then
1236 return false, S("No schematic file specified.")
1239 if not pos then
1240 pos = minetest.get_player_by_name(name):get_pos()
1243 local schem_full, schem_lua = add_suffix(schem)
1244 local success = false
1245 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1246 if minetest.read_schematic then
1247 -- We don't call minetest.place_schematic with the path name directly because
1248 -- this would trigger the caching and we wouldn't get any updates to the schematic
1249 -- files when we reload. minetest.read_schematic circumvents that.
1250 local schematic = minetest.read_schematic(schem_path, {})
1251 if schematic then
1252 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1254 else
1255 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1256 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1259 if success == nil then
1260 return false, S("Schematic file could not be loaded!")
1261 else
1262 return true
1264 end,
1267 if can_import then
1268 -- [chatcommand] Convert MTS schematic file to .lua file
1269 minetest.register_chatcommand("mts2lua", {
1270 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1271 privs = {server = true},
1272 params = S("<schematic name>[.mts] [comments]"),
1273 func = function(name, param)
1274 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1276 if not schem then
1277 return false, S("No schematic file specified.")
1280 local comments = comments_str == "comments"
1282 -- Automatically add file name suffix if omitted
1283 local schem_full, schem_lua = add_suffix(schem)
1284 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1285 local schematic = minetest.read_schematic(schem_path, {})
1287 if schematic then
1288 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1289 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1290 local file = io.open(lua_path, "w")
1291 if file and str then
1292 file:write(str)
1293 file:flush()
1294 file:close()
1295 return true, S("Exported schematic to @1", lua_path)
1296 else
1297 return false, S("Failed!")
1300 end,