Add buttons to convert air to void to air
[minetest_schemedit.git] / init.lua
blob162f12c8d3f05bed7bce010bdc3ace37cbc6304c
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 local NEEDED_PRIV = "server"
30 local function check_priv(player_name, quit)
31 local privs = minetest.get_player_privs(player_name)
32 if privs[NEEDED_PRIV] then
33 return true
34 else
35 if not quit then
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 end
39 return false
40 end
41 end
43 -- Lua export
44 local export_schematic_to_lua
45 if can_import then
46 export_schematic_to_lua = function(schematic, filepath, options)
47 if not options then options = {} end
48 local str = minetest.serialize_schematic(schematic, "lua", options)
49 local file = io.open(filepath, "w")
50 if file and str then
51 file:write(str)
52 file:flush()
53 file:close()
54 return true
55 else
56 return false
57 end
58 end
59 end
61 ---
62 --- Formspec API
63 ---
65 local contexts = {}
66 local form_data = {}
67 local tabs = {}
68 local forms = {}
69 local displayed_waypoints = {}
71 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
72 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
73 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
74 -- on an actual export to a schematic.
76 function schemedit.lua_prob_to_schematic_prob(lua_prob)
77 return math.floor(lua_prob / 2)
78 end
80 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
81 return schematic_prob * 2
83 end
85 -- [function] Add form
86 function schemedit.add_form(name, def)
87 def.name = name
88 forms[name] = def
90 if def.tab then
91 tabs[#tabs + 1] = name
92 end
93 end
95 -- [function] Generate tabs
96 function schemedit.generate_tabs(current)
97 local retval = "tabheader[0,0;tabs;"
98 for _, t in pairs(tabs) do
99 local f = forms[t]
100 if f.tab ~= false and f.caption then
101 retval = retval..f.caption..","
103 if type(current) ~= "number" and current == f.name then
104 current = _
108 retval = retval:sub(1, -2) -- Strip last comma
109 retval = retval..";"..current.."]" -- Close tabheader
110 return retval
113 -- [function] Handle tabs
114 function schemedit.handle_tabs(pos, name, fields)
115 local tab = tonumber(fields.tabs)
116 if tab and tabs[tab] and forms[tabs[tab]] then
117 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
118 return true
122 -- [function] Show formspec
123 function schemedit.show_formspec(pos, player, tab, show, ...)
124 if forms[tab] then
125 if type(player) == "string" then
126 player = minetest.get_player_by_name(player)
128 local name = player:get_player_name()
130 if show ~= false then
131 if not form_data[name] then
132 form_data[name] = {}
135 local form = forms[tab].get(form_data[name], pos, name, ...)
136 if forms[tab].tab then
137 form = form..schemedit.generate_tabs(tab)
140 minetest.show_formspec(name, "schemedit:"..tab, form)
141 contexts[name] = pos
143 -- Update player attribute
144 if forms[tab].cache_name ~= false then
145 player:set_attribute("schemedit:tab", tab)
147 else
148 minetest.close_formspec(pname, "schemedit:"..tab)
153 -- [event] On receive fields
154 minetest.register_on_player_receive_fields(function(player, formname, fields)
155 local formname = formname:split(":")
157 if formname[1] == "schemedit" and forms[formname[2]] then
158 local handle = forms[formname[2]].handle
159 local name = player:get_player_name()
160 if contexts[name] then
161 if not form_data[name] then
162 form_data[name] = {}
165 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
166 handle(form_data[name], contexts[name], name, fields)
170 end)
172 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
173 schemedit.scan_metadata = function(pos1, pos2)
174 local prob_list = {}
176 for x=pos1.x, pos2.x do
177 for y=pos1.y, pos2.y do
178 for z=pos1.z, pos2.z do
179 local scanpos = {x=x, y=y, z=z}
180 local node = minetest.get_node_or_nil(scanpos)
182 local prob, force_place
183 if node == nil or node.name == "schemedit:void" then
184 prob = 0
185 force_place = false
186 else
187 local meta = minetest.get_meta(scanpos)
189 prob = tonumber(meta:get_string("schemedit_prob")) or 255
190 local fp = meta:get_string("schemedit_force_place")
191 if fp == "true" then
192 force_place = true
193 else
194 force_place = false
198 local hashpos = minetest.hash_node_position(scanpos)
199 prob_list[hashpos] = {
200 pos = scanpos,
201 prob = prob,
202 force_place = force_place,
208 return prob_list
211 -- Sets probability and force_place metadata of an item.
212 -- Also updates item description.
213 -- The itemstack is updated in-place.
214 local function set_item_metadata(itemstack, prob, force_place)
215 local smeta = itemstack:get_meta()
216 local prob_desc = "\n"..S("Probability: @1", prob or
217 smeta:get_string("schemedit_prob") or S("Not Set"))
218 -- Update probability
219 if prob and prob >= 0 and prob < 255 then
220 smeta:set_string("schemedit_prob", tostring(prob))
221 elseif prob and prob == 255 then
222 -- Clear prob metadata for default probability
223 prob_desc = ""
224 smeta:set_string("schemedit_prob", nil)
225 else
226 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
227 S("Not Set"))
230 -- Update force place
231 if force_place == true then
232 smeta:set_string("schemedit_force_place", "true")
233 elseif force_place == false then
234 smeta:set_string("schemedit_force_place", nil)
237 -- Update description
238 local desc = minetest.registered_items[itemstack:get_name()].description
239 local meta_desc = smeta:get_string("description")
240 if meta_desc and meta_desc ~= "" then
241 desc = meta_desc
244 local original_desc = smeta:get_string("original_description")
245 if original_desc and original_desc ~= "" then
246 desc = original_desc
247 else
248 smeta:set_string("original_description", desc)
251 local force_desc = ""
252 if smeta:get_string("schemedit_force_place") == "true" then
253 force_desc = "\n"..S("Force placement")
256 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
258 smeta:set_string("description", desc)
260 return itemstack
264 --- Formspec Tabs
266 local import_btn = ""
267 if can_import then
268 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
270 schemedit.add_form("main", {
271 tab = true,
272 caption = S("Main"),
273 get = function(self, pos, name)
274 local meta = minetest.get_meta(pos):to_table().fields
275 local strpos = minetest.pos_to_string(pos)
276 local hashpos = minetest.hash_node_position(pos)
278 local border_button
279 if meta.schem_border == "true" and schemedit.markers[hashpos] then
280 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
281 else
282 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
285 local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
286 local size = {x=xs, y=ys, z=zs}
287 local schem_name = meta.schem_name or ""
289 local form = [[
290 size[7,8]
291 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
292 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
293 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name)))..[[]
294 label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
296 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name or "")..[[]
297 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
298 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
299 field_close_on_enter[name;false]
301 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
302 import_btn..[[
303 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
304 export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
305 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
306 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
307 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
308 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
309 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
310 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
311 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
312 field_close_on_enter[x;false]
313 field_close_on_enter[y;false]
314 field_close_on_enter[z;false]
315 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
316 ]]..
317 border_button
318 if minetest.get_modpath("doc") then
319 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
320 "tooltip[doc;"..F(S("Help")).."]"
322 return form
323 end,
324 handle = function(self, pos, name, fields)
325 if fields.doc then
326 doc.show_entry(name, "nodes", "schemedit:creator", true)
327 return
330 if not check_priv(name, fields.quit) then
331 return
334 local realmeta = minetest.get_meta(pos)
335 local meta = realmeta:to_table().fields
336 local hashpos = minetest.hash_node_position(pos)
338 -- Save size vector values
339 if (fields.x and fields.x ~= "") then
340 local x = tonumber(fields.x)
341 if x then
342 meta.x_size = math.max(x, 1)
345 if (fields.y and fields.y ~= "") then
346 local y = tonumber(fields.y)
347 if y then
348 meta.y_size = math.max(y, 1)
351 if (fields.z and fields.z ~= "") then
352 local z = tonumber(fields.z)
353 if z then
354 meta.z_size = math.max(z, 1)
358 -- Save schematic name
359 if fields.name then
360 meta.schem_name = fields.name
363 -- Node conversion
364 if (fields.air2void) then
365 local pos1, pos2 = schemedit.size(pos)
366 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
367 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"air"})
368 minetest.bulk_set_node(nodes, {name="schemedit:void"})
369 return
370 elseif (fields.void2air) then
371 local pos1, pos2 = schemedit.size(pos)
372 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
373 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"schemedit:void"})
374 minetest.bulk_set_node(nodes, {name="air"})
375 return
378 -- Toggle border
379 if fields.border then
380 if meta.schem_border == "true" and schemedit.markers[hashpos] then
381 schemedit.unmark(pos)
382 meta.schem_border = "false"
383 else
384 schemedit.mark(pos)
385 meta.schem_border = "true"
389 -- Export schematic
390 if fields.export and meta.schem_name and meta.schem_name ~= "" then
391 local pos1, pos2 = schemedit.size(pos)
392 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
393 local path = export_path_full .. DIR_DELIM
394 minetest.mkdir(path)
396 local plist = schemedit.scan_metadata(pos1, pos2)
397 local probability_list = {}
398 for hash, i in pairs(plist) do
399 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
400 if i.force_place == true then
401 prob = prob + 128
404 table.insert(probability_list, {
405 pos = minetest.get_position_from_hash(hash),
406 prob = prob,
410 local slist = minetest.deserialize(meta.slices)
411 local slice_list = {}
412 for _, i in pairs(slist) do
413 slice_list[#slice_list + 1] = {
414 ypos = pos.y + i.ypos,
415 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
419 local filepath = path..meta.schem_name..".mts"
420 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
422 if res then
423 minetest.chat_send_player(name, minetest.colorize("#00ff00",
424 S("Exported schematic to @1", filepath)))
425 -- Additional export to Lua file if MTS export was successful
426 local schematic = minetest.read_schematic(filepath, {})
427 if schematic and minetest.settings:get_bool("schemedit_export_lua") then
428 local filepath_lua = path..meta.schem_name..".lua"
429 res = export_schematic_to_lua(schematic, filepath_lua)
430 if res then
431 minetest.chat_send_player(name, minetest.colorize("#00ff00",
432 S("Exported schematic to @1", filepath_lua)))
435 else
436 minetest.chat_send_player(name, minetest.colorize("red",
437 S("Failed to export schematic to @1", filepath)))
441 -- Import schematic
442 if fields.import and meta.schem_name and meta.schem_name ~= "" then
443 if not can_import then
444 return
446 local pos1
447 local node = minetest.get_node(pos)
448 local path = export_path_full .. DIR_DELIM
450 local filepath = path..meta.schem_name..".mts"
451 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
452 local success = false
454 if schematic then
455 meta.x_size = schematic.size.x
456 meta.y_size = schematic.size.y
457 meta.z_size = schematic.size.z
458 meta.slices = minetest.serialize(schematic.yslice_prob)
460 if node.param2 == 1 then
461 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
462 elseif node.param2 == 2 then
463 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
464 elseif node.param2 == 3 then
465 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
466 else
467 pos1 = vector.add(pos, {x=0,y=0,z=1})
470 local schematic_for_meta = table.copy(schematic)
471 -- Strip probability data for placement
472 schematic.yslice_prob = {}
473 for d=1, #schematic.data do
474 schematic.data[d].prob = nil
477 -- Place schematic
478 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
480 -- Add special schematic data to nodes
481 if success then
482 local d = 1
483 for z=0, meta.z_size-1 do
484 for y=0, meta.y_size-1 do
485 for x=0, meta.x_size-1 do
486 local data = schematic_for_meta.data[d]
487 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
488 if data.prob == 0 then
489 minetest.set_node(pp, {name="schemedit:void"})
490 else
491 local meta = minetest.get_meta(pp)
492 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
493 meta:set_string("schemedit_prob", tostring(data.prob))
494 else
495 meta:set_string("schemedit_prob", "")
497 if data.force_place then
498 meta:set_string("schemedit_force_place", "true")
499 else
500 meta:set_string("schemedit_force_place", "")
503 d = d + 1
509 if success then
510 minetest.chat_send_player(name, minetest.colorize("#00ff00",
511 S("Imported schematic from @1", filepath)))
512 else
513 minetest.chat_send_player(name, minetest.colorize("red",
514 S("Failed to import schematic from @1", filepath)))
520 -- Save meta before updating visuals
521 local inv = realmeta:get_inventory():get_lists()
522 realmeta:from_table({fields = meta, inventory = inv})
524 -- Update border
525 if not fields.border and meta.schem_border == "true" then
526 schemedit.mark(pos)
529 -- Update formspec
530 if not fields.quit then
531 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
533 end,
536 schemedit.add_form("slice", {
537 caption = S("Y Slices"),
538 tab = true,
539 get = function(self, pos, name, visible_panel)
540 local meta = minetest.get_meta(pos):to_table().fields
542 self.selected = self.selected or 1
543 local selected = tostring(self.selected)
544 local slice_list = minetest.deserialize(meta.slices)
545 local slices = ""
546 for _, i in pairs(slice_list) do
547 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
548 slices = slices..insert..","
550 slices = slices:sub(1, -2) -- Remove final comma
552 local form = [[
553 size[7,8]
554 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
557 if self.panel_add or self.panel_edit then
558 local ypos_default, prob_default = "", ""
559 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
560 if self.panel_edit then
561 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
562 if slice_list[self.selected] then
563 ypos_default = slice_list[self.selected].ypos
564 prob_default = slice_list[self.selected].prob
568 form = form..[[
569 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
570 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
571 field_close_on_enter[ypos;false]
572 field_close_on_enter[prob;false]
573 ]]..done_button
576 if not self.panel_edit then
577 form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
580 if slices ~= "" and self.selected and not self.panel_add then
581 if not self.panel_edit then
582 form = form..[[
583 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
584 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
586 else
587 form = form..[[
588 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
589 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
594 return form
595 end,
596 handle = function(self, pos, name, fields)
597 if not check_priv(name, fields.quit) then
598 return
601 local meta = minetest.get_meta(pos)
602 local player = minetest.get_player_by_name(name)
604 if fields.slices then
605 local slices = fields.slices:split(":")
606 self.selected = tonumber(slices[2])
609 if fields.add then
610 if not self.panel_add then
611 self.panel_add = true
612 schemedit.show_formspec(pos, player, "slice")
613 else
614 self.panel_add = nil
615 schemedit.show_formspec(pos, player, "slice")
619 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
620 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
621 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
622 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
623 local slice_list = minetest.deserialize(meta:get_string("slices"))
624 local index = #slice_list + 1
625 if fields.done_edit then
626 index = self.selected
629 slice_list[index] = {ypos = ypos, prob = prob}
631 meta:set_string("slices", minetest.serialize(slice_list))
633 -- Update and show formspec
634 self.panel_add = nil
635 schemedit.show_formspec(pos, player, "slice")
638 if fields.remove and self.selected then
639 local slice_list = minetest.deserialize(meta:get_string("slices"))
640 slice_list[self.selected] = nil
641 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
643 -- Update formspec
644 self.selected = 1
645 self.panel_edit = nil
646 schemedit.show_formspec(pos, player, "slice")
649 if fields.edit then
650 if not self.panel_edit then
651 self.panel_edit = true
652 schemedit.show_formspec(pos, player, "slice")
653 else
654 self.panel_edit = nil
655 schemedit.show_formspec(pos, player, "slice")
658 end,
661 schemedit.add_form("probtool", {
662 cache_name = false,
663 caption = S("Schematic Node Probability Tool"),
664 get = function(self, pos, name)
665 local player = minetest.get_player_by_name(name)
666 if not player then
667 return
669 local probtool = player:get_wielded_item()
670 if probtool:get_name() ~= "schemedit:probtool" then
671 return
674 local meta = probtool:get_meta()
675 local prob = tonumber(meta:get_string("schemedit_prob"))
676 local force_place = meta:get_string("schemedit_force_place")
678 if not prob then
679 prob = 255
681 if force_place == nil or force_place == "" then
682 force_place = "false"
684 local form = "size[5,4]"..
685 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
686 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
687 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
688 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
689 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
690 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
691 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
692 "field_close_on_enter[prob;false]"
693 return form
694 end,
695 handle = function(self, pos, name, fields)
696 if not check_priv(name, fields.quit) then
697 return
700 if fields.submit then
701 local prob = tonumber(fields.prob)
702 if prob then
703 local player = minetest.get_player_by_name(name)
704 if not player then
705 return
707 local probtool = player:get_wielded_item()
708 if probtool:get_name() ~= "schemedit:probtool" then
709 return
712 local force_place = self.force_place == true
714 set_item_metadata(probtool, prob, force_place)
716 -- Repurpose the tool's wear bar to display the set probability
717 probtool:set_wear(math.floor(((255-prob)/255)*65535))
719 player:set_wielded_item(probtool)
722 if fields.force_place == "true" then
723 self.force_place = true
724 elseif fields.force_place == "false" then
725 self.force_place = false
727 end,
731 --- API
734 --- Copies and modifies positions `pos1` and `pos2` so that each component of
735 -- `pos1` is less than or equal to the corresponding component of `pos2`.
736 -- Returns the new positions.
737 function schemedit.sort_pos(pos1, pos2)
738 if not pos1 or not pos2 then
739 return
742 pos1, pos2 = table.copy(pos1), table.copy(pos2)
743 if pos1.x > pos2.x then
744 pos2.x, pos1.x = pos1.x, pos2.x
746 if pos1.y > pos2.y then
747 pos2.y, pos1.y = pos1.y, pos2.y
749 if pos1.z > pos2.z then
750 pos2.z, pos1.z = pos1.z, pos2.z
752 return pos1, pos2
755 -- [function] Prepare size
756 function schemedit.size(pos)
757 local pos1 = vector.new(pos)
758 local meta = minetest.get_meta(pos)
759 local node = minetest.get_node(pos)
760 local param2 = node.param2
761 local size = {
762 x = meta:get_int("x_size"),
763 y = math.max(meta:get_int("y_size") - 1, 0),
764 z = meta:get_int("z_size"),
767 if param2 == 1 then
768 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
769 pos1.x = pos1.x + 1
770 new_pos.z = new_pos.z + 1
771 return pos1, new_pos
772 elseif param2 == 2 then
773 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
774 pos1.z = pos1.z - 1
775 new_pos.x = new_pos.x + 1
776 return pos1, new_pos
777 elseif param2 == 3 then
778 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
779 pos1.x = pos1.x - 1
780 new_pos.z = new_pos.z - 1
781 return pos1, new_pos
782 else
783 local new_pos = vector.add(size, pos)
784 pos1.z = pos1.z + 1
785 new_pos.x = new_pos.x - 1
786 return pos1, new_pos
790 -- [function] Mark region
791 function schemedit.mark(pos)
792 schemedit.unmark(pos)
794 local id = minetest.hash_node_position(pos)
795 local owner = minetest.get_meta(pos):get_string("owner")
796 local pos1, pos2 = schemedit.size(pos)
797 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
799 local thickness = 0.2
800 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
801 local m = {}
802 local low = true
803 local offset
805 -- XY plane markers
806 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
807 if low then
808 offset = -0.01
809 else
810 offset = 0.01
812 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
813 if marker ~= nil then
814 marker:set_properties({
815 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
817 marker:get_luaentity().id = id
818 marker:get_luaentity().owner = owner
819 table.insert(m, marker)
821 low = false
824 low = true
825 -- YZ plane markers
826 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
827 if low then
828 offset = -0.01
829 else
830 offset = 0.01
833 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
834 if marker ~= nil then
835 marker:set_properties({
836 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
838 marker:set_rotation({x=0, y=math.pi / 2, z=0})
839 marker:get_luaentity().id = id
840 marker:get_luaentity().owner = owner
841 table.insert(m, marker)
843 low = false
846 low = true
847 -- XZ plane markers
848 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
849 if low then
850 offset = -0.01
851 else
852 offset = 0.01
855 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
856 if marker ~= nil then
857 marker:set_properties({
858 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
860 marker:set_rotation({x=math.pi/2, y=0, z=0})
861 marker:get_luaentity().id = id
862 marker:get_luaentity().owner = owner
863 table.insert(m, marker)
865 low = false
870 schemedit.markers[id] = m
871 return true
874 -- [function] Unmark region
875 function schemedit.unmark(pos)
876 local id = minetest.hash_node_position(pos)
877 if schemedit.markers[id] then
878 local retval
879 for _, entity in ipairs(schemedit.markers[id]) do
880 entity:remove()
881 retval = true
883 return retval
888 --- Mark node probability values near player
891 -- Show probability and force_place status of a particular position for player in HUD.
892 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
893 -- The distance to the node is also displayed below that. This can't be avoided and is
894 -- and artifact of the waypoint HUD element.
895 function schemedit.display_node_prob(player, pos, prob, force_place)
896 local wpstring
897 if prob and force_place == true then
898 wpstring = string.format("%s [F]", prob)
899 elseif prob and type(tonumber(prob)) == "number" then
900 wpstring = prob
901 elseif force_place == true then
902 wpstring = "[F]"
904 if wpstring then
905 return player:hud_add({
906 hud_elem_type = "waypoint",
907 name = wpstring,
908 precision = 0,
909 text = "m", -- For the distance artifact
910 number = text_color_number,
911 world_pos = pos,
916 -- Display the node probabilities and force_place status of the nodes in a region.
917 -- By default, this is done for nodes near the player (distance: 5).
918 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
919 function schemedit.display_node_probs_region(player, pos1, pos2)
920 local playername = player:get_player_name()
921 local pos = vector.round(player:get_pos())
923 local dist = 5
924 -- Default: 5 nodes away from player in any direction
925 if not pos1 then
926 pos1 = vector.subtract(pos, dist)
928 if not pos2 then
929 pos2 = vector.add(pos, dist)
931 for x=pos1.x, pos2.x do
932 for y=pos1.y, pos2.y do
933 for z=pos1.z, pos2.z do
934 local checkpos = {x=x, y=y, z=z}
935 local nodehash = minetest.hash_node_position(checkpos)
937 -- If node is already displayed, remove it so it can re replaced later
938 if displayed_waypoints[playername][nodehash] then
939 player:hud_remove(displayed_waypoints[playername][nodehash])
940 displayed_waypoints[playername][nodehash] = nil
943 local prob, force_place
944 local meta = minetest.get_meta(checkpos)
945 prob = meta:get_string("schemedit_prob")
946 force_place = meta:get_string("schemedit_force_place") == "true"
947 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
948 if hud_id then
949 displayed_waypoints[playername][nodehash] = hud_id
950 displayed_waypoints[playername].display_active = true
957 -- Remove all active displayed node statuses.
958 function schemedit.clear_displayed_node_probs(player)
959 local playername = player:get_player_name()
960 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
961 player:hud_remove(hud_id)
962 displayed_waypoints[playername][nodehash] = nil
963 displayed_waypoints[playername].display_active = false
967 minetest.register_on_joinplayer(function(player)
968 displayed_waypoints[player:get_player_name()] = {
969 display_active = false -- If true, there *might* be at least one active node prob HUD display
970 -- If false, no node probabilities are displayed for sure.
972 end)
974 minetest.register_on_leaveplayer(function(player)
975 displayed_waypoints[player:get_player_name()] = nil
976 end)
978 -- Regularily clear the displayed node probabilities and force_place
979 -- for all players who do not wield the probtool.
980 -- This makes sure the screen is not spammed with information when it
981 -- isn't needed.
982 local cleartimer = 0
983 minetest.register_globalstep(function(dtime)
984 cleartimer = cleartimer + dtime
985 if cleartimer > 2 then
986 local players = minetest.get_connected_players()
987 for p = 1, #players do
988 local player = players[p]
989 local pname = player:get_player_name()
990 if displayed_waypoints[pname].display_active then
991 local item = player:get_wielded_item()
992 if item:get_name() ~= "schemedit:probtool" then
993 schemedit.clear_displayed_node_probs(player)
997 cleartimer = 0
999 end)
1002 --- Registrations
1005 -- [priv] schematic_override
1006 minetest.register_privilege("schematic_override", {
1007 description = S("Allows you to access schemedit nodes not owned by you"),
1008 give_to_singleplayer = false,
1011 local help_import = ""
1012 if can_import then
1013 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"
1016 -- [node] Schematic creator
1017 minetest.register_node("schemedit:creator", {
1018 description = S("Schematic Creator"),
1019 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1020 _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"..
1021 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"..
1022 help_import..
1023 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1024 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"..
1025 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."),
1026 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1027 "schemedit_creator_sides.png"},
1028 groups = { dig_immediate = 2},
1029 paramtype2 = "facedir",
1030 is_ground_content = false,
1032 after_place_node = function(pos, player)
1033 local name = player:get_player_name()
1034 local meta = minetest.get_meta(pos)
1036 meta:set_string("owner", name)
1037 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
1038 meta:set_string("prob_list", minetest.serialize({}))
1039 meta:set_string("slices", minetest.serialize({}))
1041 local node = minetest.get_node(pos)
1042 local dir = minetest.facedir_to_dir(node.param2)
1044 meta:set_int("x_size", 1)
1045 meta:set_int("y_size", 1)
1046 meta:set_int("z_size", 1)
1048 -- Don't take item from itemstack
1049 return true
1050 end,
1051 can_dig = function(pos, player)
1052 local name = player:get_player_name()
1053 local meta = minetest.get_meta(pos)
1054 if meta:get_string("owner") == name or
1055 minetest.check_player_privs(player, "schematic_override") == true then
1056 return true
1059 return false
1060 end,
1061 on_rightclick = function(pos, node, player)
1062 local meta = minetest.get_meta(pos)
1063 local name = player:get_player_name()
1064 if meta:get_string("owner") == name or
1065 minetest.check_player_privs(player, "schematic_override") == true then
1066 -- Get player attribute
1067 local tab = player:get_attribute("schemedit:tab")
1068 if not forms[tab] or not tab then
1069 tab = "main"
1072 schemedit.show_formspec(pos, player, tab, true)
1074 end,
1075 after_destruct = function(pos)
1076 schemedit.unmark(pos)
1077 end,
1079 -- No support for Minetest Game's screwdriver
1080 on_rotate = false,
1083 minetest.register_tool("schemedit:probtool", {
1084 description = S("Schematic Node Probability Tool"),
1085 _doc_items_longdesc =
1086 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"..
1087 S("It allows you to set two things:").."\n"..
1088 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1089 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1090 _doc_items_usagehelp = "\n"..
1091 S("BASIC USAGE:").."\n"..
1092 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"..
1093 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"..
1094 S("NODE HUD:").."\n"..
1095 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"..
1096 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1097 S("UPDATING THE NODE HUD:").."\n"..
1098 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."),
1099 wield_image = "schemedit_probtool.png",
1100 inventory_image = "schemedit_probtool.png",
1101 liquids_pointable = true,
1102 groups = { disable_repair = 1 },
1103 on_use = function(itemstack, user, pointed_thing)
1104 local uname = user:get_player_name()
1105 if uname and not check_priv(uname) then
1106 return
1109 local ctrl = user:get_player_control()
1110 -- Simple use
1111 if not ctrl.sneak then
1112 -- Open dialog to change the probability to apply to nodes
1113 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1115 -- Use + sneak
1116 else
1117 -- Display the probability and force_place values for nodes.
1119 -- If a schematic creator was punched, only enable display for all nodes
1120 -- within the creator's region.
1121 local use_creator_region = false
1122 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1123 local punchpos = pointed_thing.under
1124 local node = minetest.get_node(punchpos)
1125 if node.name == "schemedit:creator" then
1126 local pos1, pos2 = schemedit.size(punchpos)
1127 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1128 schemedit.display_node_probs_region(user, pos1, pos2)
1129 return
1133 -- Otherwise, just display the region close to the player
1134 schemedit.display_node_probs_region(user)
1136 end,
1137 on_secondary_use = function(itemstack, user, pointed_thing)
1138 local uname = user:get_player_name()
1139 if uname and not check_priv(uname) then
1140 return
1143 schemedit.clear_displayed_node_probs(user)
1144 end,
1145 -- Set note probability and force_place and enable node probability display
1146 on_place = function(itemstack, placer, pointed_thing)
1147 local pname = placer:get_player_name()
1148 if pname and not check_priv(pname) then
1149 return
1152 -- Use pointed node's on_rightclick function first, if present
1153 local node = minetest.get_node(pointed_thing.under)
1154 if placer and not placer:get_player_control().sneak then
1155 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1156 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1160 -- This sets the node probability of pointed node to the
1161 -- currently used probability stored in the tool.
1162 local pos = pointed_thing.under
1163 local node = minetest.get_node(pos)
1164 -- Schematic void are ignored, they always have probability 0
1165 if node.name == "schemedit:void" then
1166 return itemstack
1168 local nmeta = minetest.get_meta(pos)
1169 local imeta = itemstack:get_meta()
1170 local prob = tonumber(imeta:get_string("schemedit_prob"))
1171 local force_place = imeta:get_string("schemedit_force_place")
1173 if not prob or prob == 255 then
1174 nmeta:set_string("schemedit_prob", nil)
1175 else
1176 nmeta:set_string("schemedit_prob", prob)
1178 if force_place == "true" then
1179 nmeta:set_string("schemedit_force_place", "true")
1180 else
1181 nmeta:set_string("schemedit_force_place", nil)
1184 -- Enable node probablity display
1185 schemedit.display_node_probs_region(placer)
1187 return itemstack
1188 end,
1191 minetest.register_node("schemedit:void", {
1192 description = S("Schematic Void"),
1193 _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."),
1194 _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."),
1195 tiles = { "schemedit_void.png" },
1196 drawtype = "nodebox",
1197 is_ground_content = false,
1198 paramtype = "light",
1199 walkable = false,
1200 sunlight_propagates = true,
1201 node_box = {
1202 type = "fixed",
1203 fixed = {
1204 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1207 groups = { dig_immediate = 3},
1210 -- [entity] Visible schematic border
1211 minetest.register_entity("schemedit:display", {
1212 visual = "upright_sprite",
1213 textures = {"schemedit_border.png"},
1214 visual_size = {x=10, y=10},
1215 pointable = false,
1216 physical = false,
1217 static_save = false,
1218 glow = minetest.LIGHT_MAX,
1220 on_step = function(self, dtime)
1221 if not self.id then
1222 self.object:remove()
1223 elseif not schemedit.markers[self.id] then
1224 self.object:remove()
1226 end,
1227 on_activate = function(self)
1228 self.object:set_armor_groups({immortal = 1})
1229 end,
1232 minetest.register_lbm({
1233 label = "Reset schematic creator border entities",
1234 name = "schemedit:reset_border",
1235 nodenames = "schemedit:creator",
1236 run_at_every_load = true,
1237 action = function(pos, node)
1238 local meta = minetest.get_meta(pos)
1239 meta:set_string("schem_border", "false")
1240 end,
1243 local function add_suffix(schem)
1244 -- Automatically add file name suffix if omitted
1245 local schem_full, schem_lua
1246 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1247 schem_full = schem
1248 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1249 else
1250 schem_full = schem .. ".mts"
1251 schem_lua = schem .. ".lua"
1253 return schem_full, schem_lua
1256 -- [chatcommand] Place schematic
1257 minetest.register_chatcommand("placeschem", {
1258 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1259 privs = {server = true},
1260 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1261 func = function(name, param)
1262 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1263 local pos = minetest.string_to_pos(p)
1265 if not schem then
1266 return false, S("No schematic file specified.")
1269 if not pos then
1270 pos = minetest.get_player_by_name(name):get_pos()
1273 local schem_full, schem_lua = add_suffix(schem)
1274 local success = false
1275 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1276 if minetest.read_schematic then
1277 -- We don't call minetest.place_schematic with the path name directly because
1278 -- this would trigger the caching and we wouldn't get any updates to the schematic
1279 -- files when we reload. minetest.read_schematic circumvents that.
1280 local schematic = minetest.read_schematic(schem_path, {})
1281 if schematic then
1282 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1284 else
1285 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1286 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1289 if success == nil then
1290 return false, S("Schematic file could not be loaded!")
1291 else
1292 return true
1294 end,
1297 if can_import then
1298 -- [chatcommand] Convert MTS schematic file to .lua file
1299 minetest.register_chatcommand("mts2lua", {
1300 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1301 privs = {server = true},
1302 params = S("<schematic name>[.mts] [comments]"),
1303 func = function(name, param)
1304 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1306 if not schem then
1307 return false, S("No schematic file specified.")
1310 local comments = comments_str == "comments"
1312 -- Automatically add file name suffix if omitted
1313 local schem_full, schem_lua = add_suffix(schem)
1314 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1315 local schematic = minetest.read_schematic(schem_path, {})
1317 if schematic then
1318 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1319 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1320 local file = io.open(lua_path, "w")
1321 if file and str then
1322 file:write(str)
1323 file:flush()
1324 file:close()
1325 return true, S("Exported schematic to @1", lua_path)
1326 else
1327 return false, S("Failed!")
1330 end,