Replace deprecated get/set_attribute
[minetest_schemedit.git] / init.lua
blob1a17a53d90edc25ede36fe266b634eaa1403d728
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 local pmeta = player:get_meta()
146 pmeta:set_string("schemedit:tab", tab)
148 else
149 minetest.close_formspec(pname, "schemedit:"..tab)
154 -- [event] On receive fields
155 minetest.register_on_player_receive_fields(function(player, formname, fields)
156 local formname = formname:split(":")
158 if formname[1] == "schemedit" and forms[formname[2]] then
159 local handle = forms[formname[2]].handle
160 local name = player:get_player_name()
161 if contexts[name] then
162 if not form_data[name] then
163 form_data[name] = {}
166 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
167 handle(form_data[name], contexts[name], name, fields)
171 end)
173 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
174 schemedit.scan_metadata = function(pos1, pos2)
175 local prob_list = {}
177 for x=pos1.x, pos2.x do
178 for y=pos1.y, pos2.y do
179 for z=pos1.z, pos2.z do
180 local scanpos = {x=x, y=y, z=z}
181 local node = minetest.get_node_or_nil(scanpos)
183 local prob, force_place
184 if node == nil or node.name == "schemedit:void" then
185 prob = 0
186 force_place = false
187 else
188 local meta = minetest.get_meta(scanpos)
190 prob = tonumber(meta:get_string("schemedit_prob")) or 255
191 local fp = meta:get_string("schemedit_force_place")
192 if fp == "true" then
193 force_place = true
194 else
195 force_place = false
199 local hashpos = minetest.hash_node_position(scanpos)
200 prob_list[hashpos] = {
201 pos = scanpos,
202 prob = prob,
203 force_place = force_place,
209 return prob_list
212 -- Sets probability and force_place metadata of an item.
213 -- Also updates item description.
214 -- The itemstack is updated in-place.
215 local function set_item_metadata(itemstack, prob, force_place)
216 local smeta = itemstack:get_meta()
217 local prob_desc = "\n"..S("Probability: @1", prob or
218 smeta:get_string("schemedit_prob") or S("Not Set"))
219 -- Update probability
220 if prob and prob >= 0 and prob < 255 then
221 smeta:set_string("schemedit_prob", tostring(prob))
222 elseif prob and prob == 255 then
223 -- Clear prob metadata for default probability
224 prob_desc = ""
225 smeta:set_string("schemedit_prob", nil)
226 else
227 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
228 S("Not Set"))
231 -- Update force place
232 if force_place == true then
233 smeta:set_string("schemedit_force_place", "true")
234 elseif force_place == false then
235 smeta:set_string("schemedit_force_place", nil)
238 -- Update description
239 local desc = minetest.registered_items[itemstack:get_name()].description
240 local meta_desc = smeta:get_string("description")
241 if meta_desc and meta_desc ~= "" then
242 desc = meta_desc
245 local original_desc = smeta:get_string("original_description")
246 if original_desc and original_desc ~= "" then
247 desc = original_desc
248 else
249 smeta:set_string("original_description", desc)
252 local force_desc = ""
253 if smeta:get_string("schemedit_force_place") == "true" then
254 force_desc = "\n"..S("Force placement")
257 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
259 smeta:set_string("description", desc)
261 return itemstack
265 --- Formspec Tabs
267 local import_btn = ""
268 if can_import then
269 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
271 schemedit.add_form("main", {
272 tab = true,
273 caption = S("Main"),
274 get = function(self, pos, name)
275 local meta = minetest.get_meta(pos):to_table().fields
276 local strpos = minetest.pos_to_string(pos)
277 local hashpos = minetest.hash_node_position(pos)
279 local border_button
280 if meta.schem_border == "true" and schemedit.markers[hashpos] then
281 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
282 else
283 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
286 local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
287 local size = {x=xs, y=ys, z=zs}
288 local schem_name = meta.schem_name or ""
290 local form = [[
291 size[7,8]
292 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
293 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
294 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name)))..[[]
295 label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
297 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name or "")..[[]
298 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
299 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
300 field_close_on_enter[name;false]
302 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
303 import_btn..[[
304 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
305 export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
306 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
307 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
308 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
309 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
310 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
311 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
312 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
313 field_close_on_enter[x;false]
314 field_close_on_enter[y;false]
315 field_close_on_enter[z;false]
316 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
317 ]]..
318 border_button
319 if minetest.get_modpath("doc") then
320 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
321 "tooltip[doc;"..F(S("Help")).."]"
323 return form
324 end,
325 handle = function(self, pos, name, fields)
326 if fields.doc then
327 doc.show_entry(name, "nodes", "schemedit:creator", true)
328 return
331 if not check_priv(name, fields.quit) then
332 return
335 local realmeta = minetest.get_meta(pos)
336 local meta = realmeta:to_table().fields
337 local hashpos = minetest.hash_node_position(pos)
339 -- Save size vector values
340 if (fields.x and fields.x ~= "") then
341 local x = tonumber(fields.x)
342 if x then
343 meta.x_size = math.max(x, 1)
346 if (fields.y and fields.y ~= "") then
347 local y = tonumber(fields.y)
348 if y then
349 meta.y_size = math.max(y, 1)
352 if (fields.z and fields.z ~= "") then
353 local z = tonumber(fields.z)
354 if z then
355 meta.z_size = math.max(z, 1)
359 -- Save schematic name
360 if fields.name then
361 meta.schem_name = fields.name
364 -- Node conversion
365 if (fields.air2void) then
366 local pos1, pos2 = schemedit.size(pos)
367 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
368 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"air"})
369 minetest.bulk_set_node(nodes, {name="schemedit:void"})
370 return
371 elseif (fields.void2air) then
372 local pos1, pos2 = schemedit.size(pos)
373 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
374 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"schemedit:void"})
375 minetest.bulk_set_node(nodes, {name="air"})
376 return
379 -- Toggle border
380 if fields.border then
381 if meta.schem_border == "true" and schemedit.markers[hashpos] then
382 schemedit.unmark(pos)
383 meta.schem_border = "false"
384 else
385 schemedit.mark(pos)
386 meta.schem_border = "true"
390 -- Export schematic
391 if fields.export and meta.schem_name and meta.schem_name ~= "" then
392 local pos1, pos2 = schemedit.size(pos)
393 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
394 local path = export_path_full .. DIR_DELIM
395 minetest.mkdir(path)
397 local plist = schemedit.scan_metadata(pos1, pos2)
398 local probability_list = {}
399 for hash, i in pairs(plist) do
400 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
401 if i.force_place == true then
402 prob = prob + 128
405 table.insert(probability_list, {
406 pos = minetest.get_position_from_hash(hash),
407 prob = prob,
411 local slist = minetest.deserialize(meta.slices)
412 local slice_list = {}
413 for _, i in pairs(slist) do
414 slice_list[#slice_list + 1] = {
415 ypos = pos.y + i.ypos,
416 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
420 local filepath = path..meta.schem_name..".mts"
421 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
423 if res then
424 minetest.chat_send_player(name, minetest.colorize("#00ff00",
425 S("Exported schematic to @1", filepath)))
426 -- Additional export to Lua file if MTS export was successful
427 local schematic = minetest.read_schematic(filepath, {})
428 if schematic and minetest.settings:get_bool("schemedit_export_lua") then
429 local filepath_lua = path..meta.schem_name..".lua"
430 res = export_schematic_to_lua(schematic, filepath_lua)
431 if res then
432 minetest.chat_send_player(name, minetest.colorize("#00ff00",
433 S("Exported schematic to @1", filepath_lua)))
436 else
437 minetest.chat_send_player(name, minetest.colorize("red",
438 S("Failed to export schematic to @1", filepath)))
442 -- Import schematic
443 if fields.import and meta.schem_name and meta.schem_name ~= "" then
444 if not can_import then
445 return
447 local pos1
448 local node = minetest.get_node(pos)
449 local path = export_path_full .. DIR_DELIM
451 local filepath = path..meta.schem_name..".mts"
452 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
453 local success = false
455 if schematic then
456 meta.x_size = schematic.size.x
457 meta.y_size = schematic.size.y
458 meta.z_size = schematic.size.z
459 meta.slices = minetest.serialize(schematic.yslice_prob)
461 if node.param2 == 1 then
462 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
463 elseif node.param2 == 2 then
464 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
465 elseif node.param2 == 3 then
466 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
467 else
468 pos1 = vector.add(pos, {x=0,y=0,z=1})
471 local schematic_for_meta = table.copy(schematic)
472 -- Strip probability data for placement
473 schematic.yslice_prob = {}
474 for d=1, #schematic.data do
475 schematic.data[d].prob = nil
478 -- Place schematic
479 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
481 -- Add special schematic data to nodes
482 if success then
483 local d = 1
484 for z=0, meta.z_size-1 do
485 for y=0, meta.y_size-1 do
486 for x=0, meta.x_size-1 do
487 local data = schematic_for_meta.data[d]
488 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
489 if data.prob == 0 then
490 minetest.set_node(pp, {name="schemedit:void"})
491 else
492 local meta = minetest.get_meta(pp)
493 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
494 meta:set_string("schemedit_prob", tostring(data.prob))
495 else
496 meta:set_string("schemedit_prob", "")
498 if data.force_place then
499 meta:set_string("schemedit_force_place", "true")
500 else
501 meta:set_string("schemedit_force_place", "")
504 d = d + 1
510 if success then
511 minetest.chat_send_player(name, minetest.colorize("#00ff00",
512 S("Imported schematic from @1", filepath)))
513 else
514 minetest.chat_send_player(name, minetest.colorize("red",
515 S("Failed to import schematic from @1", filepath)))
521 -- Save meta before updating visuals
522 local inv = realmeta:get_inventory():get_lists()
523 realmeta:from_table({fields = meta, inventory = inv})
525 -- Update border
526 if not fields.border and meta.schem_border == "true" then
527 schemedit.mark(pos)
530 -- Update formspec
531 if not fields.quit then
532 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
534 end,
537 schemedit.add_form("slice", {
538 caption = S("Y Slices"),
539 tab = true,
540 get = function(self, pos, name, visible_panel)
541 local meta = minetest.get_meta(pos):to_table().fields
543 self.selected = self.selected or 1
544 local selected = tostring(self.selected)
545 local slice_list = minetest.deserialize(meta.slices)
546 local slices = ""
547 for _, i in pairs(slice_list) do
548 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
549 slices = slices..insert..","
551 slices = slices:sub(1, -2) -- Remove final comma
553 local form = [[
554 size[7,8]
555 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
558 if self.panel_add or self.panel_edit then
559 local ypos_default, prob_default = "", ""
560 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
561 if self.panel_edit then
562 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
563 if slice_list[self.selected] then
564 ypos_default = slice_list[self.selected].ypos
565 prob_default = slice_list[self.selected].prob
569 form = form..[[
570 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
571 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
572 field_close_on_enter[ypos;false]
573 field_close_on_enter[prob;false]
574 ]]..done_button
577 if not self.panel_edit then
578 form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
581 if slices ~= "" and self.selected and not self.panel_add then
582 if not self.panel_edit then
583 form = form..[[
584 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
585 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
587 else
588 form = form..[[
589 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
590 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
595 return form
596 end,
597 handle = function(self, pos, name, fields)
598 if not check_priv(name, fields.quit) then
599 return
602 local meta = minetest.get_meta(pos)
603 local player = minetest.get_player_by_name(name)
605 if fields.slices then
606 local slices = fields.slices:split(":")
607 self.selected = tonumber(slices[2])
610 if fields.add then
611 if not self.panel_add then
612 self.panel_add = true
613 schemedit.show_formspec(pos, player, "slice")
614 else
615 self.panel_add = nil
616 schemedit.show_formspec(pos, player, "slice")
620 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
621 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
622 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
623 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
624 local slice_list = minetest.deserialize(meta:get_string("slices"))
625 local index = #slice_list + 1
626 if fields.done_edit then
627 index = self.selected
630 slice_list[index] = {ypos = ypos, prob = prob}
632 meta:set_string("slices", minetest.serialize(slice_list))
634 -- Update and show formspec
635 self.panel_add = nil
636 schemedit.show_formspec(pos, player, "slice")
639 if fields.remove and self.selected then
640 local slice_list = minetest.deserialize(meta:get_string("slices"))
641 slice_list[self.selected] = nil
642 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
644 -- Update formspec
645 self.selected = 1
646 self.panel_edit = nil
647 schemedit.show_formspec(pos, player, "slice")
650 if fields.edit then
651 if not self.panel_edit then
652 self.panel_edit = true
653 schemedit.show_formspec(pos, player, "slice")
654 else
655 self.panel_edit = nil
656 schemedit.show_formspec(pos, player, "slice")
659 end,
662 schemedit.add_form("probtool", {
663 cache_name = false,
664 caption = S("Schematic Node Probability Tool"),
665 get = function(self, pos, name)
666 local player = minetest.get_player_by_name(name)
667 if not player then
668 return
670 local probtool = player:get_wielded_item()
671 if probtool:get_name() ~= "schemedit:probtool" then
672 return
675 local meta = probtool:get_meta()
676 local prob = tonumber(meta:get_string("schemedit_prob"))
677 local force_place = meta:get_string("schemedit_force_place")
679 if not prob then
680 prob = 255
682 if force_place == nil or force_place == "" then
683 force_place = "false"
685 local form = "size[5,4]"..
686 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
687 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
688 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
689 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
690 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
691 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
692 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
693 "field_close_on_enter[prob;false]"
694 return form
695 end,
696 handle = function(self, pos, name, fields)
697 if not check_priv(name, fields.quit) then
698 return
701 if fields.submit then
702 local prob = tonumber(fields.prob)
703 if prob then
704 local player = minetest.get_player_by_name(name)
705 if not player then
706 return
708 local probtool = player:get_wielded_item()
709 if probtool:get_name() ~= "schemedit:probtool" then
710 return
713 local force_place = self.force_place == true
715 set_item_metadata(probtool, prob, force_place)
717 -- Repurpose the tool's wear bar to display the set probability
718 probtool:set_wear(math.floor(((255-prob)/255)*65535))
720 player:set_wielded_item(probtool)
723 if fields.force_place == "true" then
724 self.force_place = true
725 elseif fields.force_place == "false" then
726 self.force_place = false
728 end,
732 --- API
735 --- Copies and modifies positions `pos1` and `pos2` so that each component of
736 -- `pos1` is less than or equal to the corresponding component of `pos2`.
737 -- Returns the new positions.
738 function schemedit.sort_pos(pos1, pos2)
739 if not pos1 or not pos2 then
740 return
743 pos1, pos2 = table.copy(pos1), table.copy(pos2)
744 if pos1.x > pos2.x then
745 pos2.x, pos1.x = pos1.x, pos2.x
747 if pos1.y > pos2.y then
748 pos2.y, pos1.y = pos1.y, pos2.y
750 if pos1.z > pos2.z then
751 pos2.z, pos1.z = pos1.z, pos2.z
753 return pos1, pos2
756 -- [function] Prepare size
757 function schemedit.size(pos)
758 local pos1 = vector.new(pos)
759 local meta = minetest.get_meta(pos)
760 local node = minetest.get_node(pos)
761 local param2 = node.param2
762 local size = {
763 x = meta:get_int("x_size"),
764 y = math.max(meta:get_int("y_size") - 1, 0),
765 z = meta:get_int("z_size"),
768 if param2 == 1 then
769 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
770 pos1.x = pos1.x + 1
771 new_pos.z = new_pos.z + 1
772 return pos1, new_pos
773 elseif param2 == 2 then
774 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
775 pos1.z = pos1.z - 1
776 new_pos.x = new_pos.x + 1
777 return pos1, new_pos
778 elseif param2 == 3 then
779 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
780 pos1.x = pos1.x - 1
781 new_pos.z = new_pos.z - 1
782 return pos1, new_pos
783 else
784 local new_pos = vector.add(size, pos)
785 pos1.z = pos1.z + 1
786 new_pos.x = new_pos.x - 1
787 return pos1, new_pos
791 -- [function] Mark region
792 function schemedit.mark(pos)
793 schemedit.unmark(pos)
795 local id = minetest.hash_node_position(pos)
796 local owner = minetest.get_meta(pos):get_string("owner")
797 local pos1, pos2 = schemedit.size(pos)
798 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
800 local thickness = 0.2
801 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
802 local m = {}
803 local low = true
804 local offset
806 -- XY plane markers
807 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
808 if low then
809 offset = -0.01
810 else
811 offset = 0.01
813 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
814 if marker ~= nil then
815 marker:set_properties({
816 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
818 marker:get_luaentity().id = id
819 marker:get_luaentity().owner = owner
820 table.insert(m, marker)
822 low = false
825 low = true
826 -- YZ plane markers
827 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
828 if low then
829 offset = -0.01
830 else
831 offset = 0.01
834 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
835 if marker ~= nil then
836 marker:set_properties({
837 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
839 marker:set_rotation({x=0, y=math.pi / 2, z=0})
840 marker:get_luaentity().id = id
841 marker:get_luaentity().owner = owner
842 table.insert(m, marker)
844 low = false
847 low = true
848 -- XZ plane markers
849 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
850 if low then
851 offset = -0.01
852 else
853 offset = 0.01
856 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
857 if marker ~= nil then
858 marker:set_properties({
859 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
861 marker:set_rotation({x=math.pi/2, y=0, z=0})
862 marker:get_luaentity().id = id
863 marker:get_luaentity().owner = owner
864 table.insert(m, marker)
866 low = false
871 schemedit.markers[id] = m
872 return true
875 -- [function] Unmark region
876 function schemedit.unmark(pos)
877 local id = minetest.hash_node_position(pos)
878 if schemedit.markers[id] then
879 local retval
880 for _, entity in ipairs(schemedit.markers[id]) do
881 entity:remove()
882 retval = true
884 return retval
889 --- Mark node probability values near player
892 -- Show probability and force_place status of a particular position for player in HUD.
893 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
894 -- The distance to the node is also displayed below that. This can't be avoided and is
895 -- and artifact of the waypoint HUD element.
896 function schemedit.display_node_prob(player, pos, prob, force_place)
897 local wpstring
898 if prob and force_place == true then
899 wpstring = string.format("%s [F]", prob)
900 elseif prob and type(tonumber(prob)) == "number" then
901 wpstring = prob
902 elseif force_place == true then
903 wpstring = "[F]"
905 if wpstring then
906 return player:hud_add({
907 hud_elem_type = "waypoint",
908 name = wpstring,
909 precision = 0,
910 text = "m", -- For the distance artifact
911 number = text_color_number,
912 world_pos = pos,
917 -- Display the node probabilities and force_place status of the nodes in a region.
918 -- By default, this is done for nodes near the player (distance: 5).
919 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
920 function schemedit.display_node_probs_region(player, pos1, pos2)
921 local playername = player:get_player_name()
922 local pos = vector.round(player:get_pos())
924 local dist = 5
925 -- Default: 5 nodes away from player in any direction
926 if not pos1 then
927 pos1 = vector.subtract(pos, dist)
929 if not pos2 then
930 pos2 = vector.add(pos, dist)
932 for x=pos1.x, pos2.x do
933 for y=pos1.y, pos2.y do
934 for z=pos1.z, pos2.z do
935 local checkpos = {x=x, y=y, z=z}
936 local nodehash = minetest.hash_node_position(checkpos)
938 -- If node is already displayed, remove it so it can re replaced later
939 if displayed_waypoints[playername][nodehash] then
940 player:hud_remove(displayed_waypoints[playername][nodehash])
941 displayed_waypoints[playername][nodehash] = nil
944 local prob, force_place
945 local meta = minetest.get_meta(checkpos)
946 prob = meta:get_string("schemedit_prob")
947 force_place = meta:get_string("schemedit_force_place") == "true"
948 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
949 if hud_id then
950 displayed_waypoints[playername][nodehash] = hud_id
951 displayed_waypoints[playername].display_active = true
958 -- Remove all active displayed node statuses.
959 function schemedit.clear_displayed_node_probs(player)
960 local playername = player:get_player_name()
961 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
962 player:hud_remove(hud_id)
963 displayed_waypoints[playername][nodehash] = nil
964 displayed_waypoints[playername].display_active = false
968 minetest.register_on_joinplayer(function(player)
969 displayed_waypoints[player:get_player_name()] = {
970 display_active = false -- If true, there *might* be at least one active node prob HUD display
971 -- If false, no node probabilities are displayed for sure.
973 end)
975 minetest.register_on_leaveplayer(function(player)
976 displayed_waypoints[player:get_player_name()] = nil
977 end)
979 -- Regularily clear the displayed node probabilities and force_place
980 -- for all players who do not wield the probtool.
981 -- This makes sure the screen is not spammed with information when it
982 -- isn't needed.
983 local cleartimer = 0
984 minetest.register_globalstep(function(dtime)
985 cleartimer = cleartimer + dtime
986 if cleartimer > 2 then
987 local players = minetest.get_connected_players()
988 for p = 1, #players do
989 local player = players[p]
990 local pname = player:get_player_name()
991 if displayed_waypoints[pname].display_active then
992 local item = player:get_wielded_item()
993 if item:get_name() ~= "schemedit:probtool" then
994 schemedit.clear_displayed_node_probs(player)
998 cleartimer = 0
1000 end)
1003 --- Registrations
1006 -- [priv] schematic_override
1007 minetest.register_privilege("schematic_override", {
1008 description = S("Allows you to access schemedit nodes not owned by you"),
1009 give_to_singleplayer = false,
1012 local help_import = ""
1013 if can_import then
1014 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"
1017 -- [node] Schematic creator
1018 minetest.register_node("schemedit:creator", {
1019 description = S("Schematic Creator"),
1020 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1021 _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"..
1022 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"..
1023 help_import..
1024 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1025 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"..
1026 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."),
1027 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1028 "schemedit_creator_sides.png"},
1029 groups = { dig_immediate = 2},
1030 paramtype2 = "facedir",
1031 is_ground_content = false,
1033 after_place_node = function(pos, player)
1034 local name = player:get_player_name()
1035 local meta = minetest.get_meta(pos)
1037 meta:set_string("owner", name)
1038 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
1039 meta:set_string("prob_list", minetest.serialize({}))
1040 meta:set_string("slices", minetest.serialize({}))
1042 local node = minetest.get_node(pos)
1043 local dir = minetest.facedir_to_dir(node.param2)
1045 meta:set_int("x_size", 1)
1046 meta:set_int("y_size", 1)
1047 meta:set_int("z_size", 1)
1049 -- Don't take item from itemstack
1050 return true
1051 end,
1052 can_dig = function(pos, player)
1053 local name = player:get_player_name()
1054 local meta = minetest.get_meta(pos)
1055 if meta:get_string("owner") == name or
1056 minetest.check_player_privs(player, "schematic_override") == true then
1057 return true
1060 return false
1061 end,
1062 on_rightclick = function(pos, node, player)
1063 local meta = minetest.get_meta(pos)
1064 local name = player:get_player_name()
1065 if meta:get_string("owner") == name or
1066 minetest.check_player_privs(player, "schematic_override") == true then
1067 -- Get player attribute
1068 local pmeta = player:get_meta()
1069 local tab = pmeta:get_string("schemedit:tab")
1070 if not forms[tab] or not tab then
1071 tab = "main"
1074 schemedit.show_formspec(pos, player, tab, true)
1076 end,
1077 after_destruct = function(pos)
1078 schemedit.unmark(pos)
1079 end,
1081 -- No support for Minetest Game's screwdriver
1082 on_rotate = false,
1085 minetest.register_tool("schemedit:probtool", {
1086 description = S("Schematic Node Probability Tool"),
1087 _doc_items_longdesc =
1088 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"..
1089 S("It allows you to set two things:").."\n"..
1090 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1091 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1092 _doc_items_usagehelp = "\n"..
1093 S("BASIC USAGE:").."\n"..
1094 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"..
1095 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"..
1096 S("NODE HUD:").."\n"..
1097 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"..
1098 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1099 S("UPDATING THE NODE HUD:").."\n"..
1100 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."),
1101 wield_image = "schemedit_probtool.png",
1102 inventory_image = "schemedit_probtool.png",
1103 liquids_pointable = true,
1104 groups = { disable_repair = 1 },
1105 on_use = function(itemstack, user, pointed_thing)
1106 local uname = user:get_player_name()
1107 if uname and not check_priv(uname) then
1108 return
1111 local ctrl = user:get_player_control()
1112 -- Simple use
1113 if not ctrl.sneak then
1114 -- Open dialog to change the probability to apply to nodes
1115 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1117 -- Use + sneak
1118 else
1119 -- Display the probability and force_place values for nodes.
1121 -- If a schematic creator was punched, only enable display for all nodes
1122 -- within the creator's region.
1123 local use_creator_region = false
1124 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1125 local punchpos = pointed_thing.under
1126 local node = minetest.get_node(punchpos)
1127 if node.name == "schemedit:creator" then
1128 local pos1, pos2 = schemedit.size(punchpos)
1129 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1130 schemedit.display_node_probs_region(user, pos1, pos2)
1131 return
1135 -- Otherwise, just display the region close to the player
1136 schemedit.display_node_probs_region(user)
1138 end,
1139 on_secondary_use = function(itemstack, user, pointed_thing)
1140 local uname = user:get_player_name()
1141 if uname and not check_priv(uname) then
1142 return
1145 schemedit.clear_displayed_node_probs(user)
1146 end,
1147 -- Set note probability and force_place and enable node probability display
1148 on_place = function(itemstack, placer, pointed_thing)
1149 local pname = placer:get_player_name()
1150 if pname and not check_priv(pname) then
1151 return
1154 -- Use pointed node's on_rightclick function first, if present
1155 local node = minetest.get_node(pointed_thing.under)
1156 if placer and not placer:get_player_control().sneak then
1157 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1158 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1162 -- This sets the node probability of pointed node to the
1163 -- currently used probability stored in the tool.
1164 local pos = pointed_thing.under
1165 local node = minetest.get_node(pos)
1166 -- Schematic void are ignored, they always have probability 0
1167 if node.name == "schemedit:void" then
1168 return itemstack
1170 local nmeta = minetest.get_meta(pos)
1171 local imeta = itemstack:get_meta()
1172 local prob = tonumber(imeta:get_string("schemedit_prob"))
1173 local force_place = imeta:get_string("schemedit_force_place")
1175 if not prob or prob == 255 then
1176 nmeta:set_string("schemedit_prob", nil)
1177 else
1178 nmeta:set_string("schemedit_prob", prob)
1180 if force_place == "true" then
1181 nmeta:set_string("schemedit_force_place", "true")
1182 else
1183 nmeta:set_string("schemedit_force_place", nil)
1186 -- Enable node probablity display
1187 schemedit.display_node_probs_region(placer)
1189 return itemstack
1190 end,
1193 minetest.register_node("schemedit:void", {
1194 description = S("Schematic Void"),
1195 _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."),
1196 _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."),
1197 tiles = { "schemedit_void.png" },
1198 drawtype = "nodebox",
1199 is_ground_content = false,
1200 paramtype = "light",
1201 walkable = false,
1202 sunlight_propagates = true,
1203 node_box = {
1204 type = "fixed",
1205 fixed = {
1206 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1209 groups = { dig_immediate = 3},
1212 -- [entity] Visible schematic border
1213 minetest.register_entity("schemedit:display", {
1214 visual = "upright_sprite",
1215 textures = {"schemedit_border.png"},
1216 visual_size = {x=10, y=10},
1217 pointable = false,
1218 physical = false,
1219 static_save = false,
1220 glow = minetest.LIGHT_MAX,
1222 on_step = function(self, dtime)
1223 if not self.id then
1224 self.object:remove()
1225 elseif not schemedit.markers[self.id] then
1226 self.object:remove()
1228 end,
1229 on_activate = function(self)
1230 self.object:set_armor_groups({immortal = 1})
1231 end,
1234 minetest.register_lbm({
1235 label = "Reset schematic creator border entities",
1236 name = "schemedit:reset_border",
1237 nodenames = "schemedit:creator",
1238 run_at_every_load = true,
1239 action = function(pos, node)
1240 local meta = minetest.get_meta(pos)
1241 meta:set_string("schem_border", "false")
1242 end,
1245 local function add_suffix(schem)
1246 -- Automatically add file name suffix if omitted
1247 local schem_full, schem_lua
1248 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1249 schem_full = schem
1250 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1251 else
1252 schem_full = schem .. ".mts"
1253 schem_lua = schem .. ".lua"
1255 return schem_full, schem_lua
1258 -- [chatcommand] Place schematic
1259 minetest.register_chatcommand("placeschem", {
1260 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1261 privs = {server = true},
1262 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1263 func = function(name, param)
1264 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1265 local pos = minetest.string_to_pos(p)
1267 if not schem then
1268 return false, S("No schematic file specified.")
1271 if not pos then
1272 pos = minetest.get_player_by_name(name):get_pos()
1275 local schem_full, schem_lua = add_suffix(schem)
1276 local success = false
1277 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1278 if minetest.read_schematic then
1279 -- We don't call minetest.place_schematic with the path name directly because
1280 -- this would trigger the caching and we wouldn't get any updates to the schematic
1281 -- files when we reload. minetest.read_schematic circumvents that.
1282 local schematic = minetest.read_schematic(schem_path, {})
1283 if schematic then
1284 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1286 else
1287 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1288 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1291 if success == nil then
1292 return false, S("Schematic file could not be loaded!")
1293 else
1294 return true
1296 end,
1299 if can_import then
1300 -- [chatcommand] Convert MTS schematic file to .lua file
1301 minetest.register_chatcommand("mts2lua", {
1302 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1303 privs = {server = true},
1304 params = S("<schematic name>[.mts] [comments]"),
1305 func = function(name, param)
1306 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1308 if not schem then
1309 return false, S("No schematic file specified.")
1312 local comments = comments_str == "comments"
1314 -- Automatically add file name suffix if omitted
1315 local schem_full, schem_lua = add_suffix(schem)
1316 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1317 local schematic = minetest.read_schematic(schem_path, {})
1319 if schematic then
1320 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1321 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1322 local file = io.open(lua_path, "w")
1323 if file and str then
1324 file:write(str)
1325 file:flush()
1326 file:close()
1327 return true, S("Exported schematic to @1", lua_path)
1328 else
1329 return false, S("Failed!")
1332 end,