Version 1.4.1
[minetest_schemedit.git] / init.lua
blob0ea799fc3fca0b0c8971ca1f0ff85e1be248dcd0
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(renumber(schematic.yslice_prob))
460 local special_x_size = meta.x_size
461 local special_y_size = meta.y_size
462 local special_z_size = meta.z_size
464 if node.param2 == 1 then
465 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
466 meta.x_size, meta.z_size = meta.z_size, meta.x_size
467 elseif node.param2 == 2 then
468 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
469 elseif node.param2 == 3 then
470 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
471 meta.x_size, meta.z_size = meta.z_size, meta.x_size
472 else
473 pos1 = vector.add(pos, {x=0,y=0,z=1})
476 local schematic_for_meta = table.copy(schematic)
477 -- Strip probability data for placement
478 schematic.yslice_prob = {}
479 for d=1, #schematic.data do
480 schematic.data[d].prob = nil
483 -- Place schematic
484 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
486 -- Add special schematic data to nodes
487 if success then
488 local d = 1
489 for z=0, special_z_size-1 do
490 for y=0, special_y_size-1 do
491 for x=0, special_x_size-1 do
492 local data = schematic_for_meta.data[d]
493 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
494 if data.prob == 0 then
495 minetest.set_node(pp, {name="schemedit:void"})
496 else
497 local meta = minetest.get_meta(pp)
498 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
499 meta:set_string("schemedit_prob", tostring(data.prob))
500 else
501 meta:set_string("schemedit_prob", "")
503 if data.force_place then
504 meta:set_string("schemedit_force_place", "true")
505 else
506 meta:set_string("schemedit_force_place", "")
509 d = d + 1
515 if success then
516 minetest.chat_send_player(name, minetest.colorize("#00ff00",
517 S("Imported schematic from @1", filepath)))
518 else
519 minetest.chat_send_player(name, minetest.colorize("red",
520 S("Failed to import schematic from @1", filepath)))
526 -- Save meta before updating visuals
527 local inv = realmeta:get_inventory():get_lists()
528 realmeta:from_table({fields = meta, inventory = inv})
530 -- Update border
531 if not fields.border and meta.schem_border == "true" then
532 schemedit.mark(pos)
535 -- Update formspec
536 if not fields.quit then
537 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
539 end,
542 schemedit.add_form("slice", {
543 caption = S("Y Slices"),
544 tab = true,
545 get = function(self, pos, name, visible_panel)
546 local meta = minetest.get_meta(pos):to_table().fields
548 self.selected = self.selected or 1
549 local selected = tostring(self.selected)
550 local slice_list = minetest.deserialize(meta.slices)
551 local slices = ""
552 for _, i in pairs(slice_list) do
553 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
554 slices = slices..insert..","
556 slices = slices:sub(1, -2) -- Remove final comma
558 local form = [[
559 size[7,8]
560 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
563 if self.panel_add or self.panel_edit then
564 local ypos_default, prob_default = "", ""
565 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Add")).."]"
566 if self.panel_edit then
567 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Apply")).."]"
568 if slice_list[self.selected] then
569 ypos_default = slice_list[self.selected].ypos
570 prob_default = slice_list[self.selected].prob
574 local field_ypos = ""
575 if self.panel_add then
576 field_ypos = "field[0.3,7.5;2.5,1;ypos;"..F(S("Y position (max. @1):", (meta.y_size - 1)))..";"..ypos_default.."]"
579 form = form..[[
580 ]]..field_ypos..[[
581 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
582 field_close_on_enter[ypos;false]
583 field_close_on_enter[prob;false]
584 ]]..done_button
587 if not self.panel_edit then
588 if self.panel_add then
589 form = form.."button[0,6;2.4,1;add;"..F(S("Cancel")).."]"
590 else
591 form = form.."button[0,6;2.4,1;add;"..F(S("Add slice")).."]"
595 if slices ~= "" and self.selected and not self.panel_add then
596 if not self.panel_edit then
597 form = form..[[
598 button[2.4,6;2.4,1;remove;]]..F(S("Remove slice"))..[[]
599 button[4.8,6;2.4,1;edit;]]..F(S("Edit slice"))..[[]
601 else
602 form = form..[[
603 button[4.8,6;2.4,1;edit;]]..F(S("Back"))..[[]
608 return form
609 end,
610 handle = function(self, pos, name, fields)
611 if not check_priv(name, fields.quit) then
612 return
615 local meta = minetest.get_meta(pos)
616 local player = minetest.get_player_by_name(name)
618 if fields.slices then
619 local slices = fields.slices:split(":")
620 self.selected = tonumber(slices[2])
623 if fields.add then
624 if not self.panel_add then
625 self.panel_add = true
626 schemedit.show_formspec(pos, player, "slice")
627 else
628 self.panel_add = nil
629 schemedit.show_formspec(pos, player, "slice")
633 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
634 if fields.done_edit then
635 ypos = 0
637 if (fields.done_add or fields.done_edit) and ypos and prob and
638 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
639 local slice_list = minetest.deserialize(meta:get_string("slices"))
640 local index = #slice_list + 1
641 if fields.done_edit then
642 index = self.selected
645 local dupe = false
646 if fields.done_add then
647 for k,v in pairs(slice_list) do
648 if v.ypos == ypos then
649 v.prob = prob
650 dupe = true
654 if fields.done_edit and slice_list[index] then
655 ypos = slice_list[index].ypos
657 if not dupe then
658 slice_list[index] = {ypos = ypos, prob = prob}
661 meta:set_string("slices", minetest.serialize(slice_list))
663 -- Update and show formspec
664 self.panel_add = nil
665 schemedit.show_formspec(pos, player, "slice")
668 if fields.remove and self.selected then
669 local slice_list = minetest.deserialize(meta:get_string("slices"))
670 slice_list[self.selected] = nil
671 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
673 -- Update formspec
674 self.selected = math.max(1, self.selected-1)
675 self.panel_edit = nil
676 schemedit.show_formspec(pos, player, "slice")
679 if fields.edit then
680 if not self.panel_edit then
681 self.panel_edit = true
682 schemedit.show_formspec(pos, player, "slice")
683 else
684 self.panel_edit = nil
685 schemedit.show_formspec(pos, player, "slice")
688 end,
691 schemedit.add_form("probtool", {
692 cache_name = false,
693 caption = S("Schematic Node Probability Tool"),
694 get = function(self, pos, name)
695 local player = minetest.get_player_by_name(name)
696 if not player then
697 return
699 local probtool = player:get_wielded_item()
700 if probtool:get_name() ~= "schemedit:probtool" then
701 return
704 local meta = probtool:get_meta()
705 local prob = tonumber(meta:get_string("schemedit_prob"))
706 local force_place = meta:get_string("schemedit_force_place")
708 if not prob then
709 prob = 255
711 if force_place == nil or force_place == "" then
712 force_place = "false"
714 local form = "size[5,4]"..
715 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
716 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
717 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
718 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
719 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
720 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
721 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
722 "field_close_on_enter[prob;false]"
723 return form
724 end,
725 handle = function(self, pos, name, fields)
726 if not check_priv(name, fields.quit) then
727 return
730 if fields.submit then
731 local prob = tonumber(fields.prob)
732 if prob then
733 local player = minetest.get_player_by_name(name)
734 if not player then
735 return
737 local probtool = player:get_wielded_item()
738 if probtool:get_name() ~= "schemedit:probtool" then
739 return
742 local force_place = self.force_place == true
744 set_item_metadata(probtool, prob, force_place)
746 -- Repurpose the tool's wear bar to display the set probability
747 probtool:set_wear(math.floor(((255-prob)/255)*65535))
749 player:set_wielded_item(probtool)
752 if fields.force_place == "true" then
753 self.force_place = true
754 elseif fields.force_place == "false" then
755 self.force_place = false
757 end,
761 --- API
764 --- Copies and modifies positions `pos1` and `pos2` so that each component of
765 -- `pos1` is less than or equal to the corresponding component of `pos2`.
766 -- Returns the new positions.
767 function schemedit.sort_pos(pos1, pos2)
768 if not pos1 or not pos2 then
769 return
772 pos1, pos2 = table.copy(pos1), table.copy(pos2)
773 if pos1.x > pos2.x then
774 pos2.x, pos1.x = pos1.x, pos2.x
776 if pos1.y > pos2.y then
777 pos2.y, pos1.y = pos1.y, pos2.y
779 if pos1.z > pos2.z then
780 pos2.z, pos1.z = pos1.z, pos2.z
782 return pos1, pos2
785 -- [function] Prepare size
786 function schemedit.size(pos)
787 local pos1 = vector.new(pos)
788 local meta = minetest.get_meta(pos)
789 local node = minetest.get_node(pos)
790 local param2 = node.param2
791 local size = {
792 x = meta:get_int("x_size"),
793 y = math.max(meta:get_int("y_size") - 1, 0),
794 z = meta:get_int("z_size"),
797 if param2 == 1 then
798 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
799 pos1.x = pos1.x + 1
800 new_pos.z = new_pos.z + 1
801 return pos1, new_pos
802 elseif param2 == 2 then
803 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
804 pos1.z = pos1.z - 1
805 new_pos.x = new_pos.x + 1
806 return pos1, new_pos
807 elseif param2 == 3 then
808 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
809 pos1.x = pos1.x - 1
810 new_pos.z = new_pos.z - 1
811 return pos1, new_pos
812 else
813 local new_pos = vector.add(size, pos)
814 pos1.z = pos1.z + 1
815 new_pos.x = new_pos.x - 1
816 return pos1, new_pos
820 -- [function] Mark region
821 function schemedit.mark(pos)
822 schemedit.unmark(pos)
824 local id = minetest.hash_node_position(pos)
825 local owner = minetest.get_meta(pos):get_string("owner")
826 local pos1, pos2 = schemedit.size(pos)
827 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
829 local thickness = 0.2
830 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
831 local m = {}
832 local low = true
833 local offset
835 -- XY plane markers
836 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
837 if low then
838 offset = -0.01
839 else
840 offset = 0.01
842 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
843 if marker ~= nil then
844 marker:set_properties({
845 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
847 marker:get_luaentity().id = id
848 marker:get_luaentity().owner = owner
849 table.insert(m, marker)
851 low = false
854 low = true
855 -- YZ plane markers
856 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
857 if low then
858 offset = -0.01
859 else
860 offset = 0.01
863 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
864 if marker ~= nil then
865 marker:set_properties({
866 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
868 marker:set_rotation({x=0, y=math.pi / 2, z=0})
869 marker:get_luaentity().id = id
870 marker:get_luaentity().owner = owner
871 table.insert(m, marker)
873 low = false
876 low = true
877 -- XZ plane markers
878 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
879 if low then
880 offset = -0.01
881 else
882 offset = 0.01
885 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
886 if marker ~= nil then
887 marker:set_properties({
888 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
890 marker:set_rotation({x=math.pi/2, y=0, z=0})
891 marker:get_luaentity().id = id
892 marker:get_luaentity().owner = owner
893 table.insert(m, marker)
895 low = false
900 schemedit.markers[id] = m
901 return true
904 -- [function] Unmark region
905 function schemedit.unmark(pos)
906 local id = minetest.hash_node_position(pos)
907 if schemedit.markers[id] then
908 local retval
909 for _, entity in ipairs(schemedit.markers[id]) do
910 entity:remove()
911 retval = true
913 return retval
918 --- Mark node probability values near player
921 -- Show probability and force_place status of a particular position for player in HUD.
922 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
923 -- The distance to the node is also displayed below that. This can't be avoided and is
924 -- and artifact of the waypoint HUD element.
925 function schemedit.display_node_prob(player, pos, prob, force_place)
926 local wpstring
927 if prob and force_place == true then
928 wpstring = string.format("%s [F]", prob)
929 elseif prob and type(tonumber(prob)) == "number" then
930 wpstring = prob
931 elseif force_place == true then
932 wpstring = "[F]"
934 if wpstring then
935 return player:hud_add({
936 hud_elem_type = "waypoint",
937 name = wpstring,
938 precision = 0,
939 text = "m", -- For the distance artifact
940 number = text_color_number,
941 world_pos = pos,
946 -- Display the node probabilities and force_place status of the nodes in a region.
947 -- By default, this is done for nodes near the player (distance: 5).
948 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
949 function schemedit.display_node_probs_region(player, pos1, pos2)
950 local playername = player:get_player_name()
951 local pos = vector.round(player:get_pos())
953 local dist = 5
954 -- Default: 5 nodes away from player in any direction
955 if not pos1 then
956 pos1 = vector.subtract(pos, dist)
958 if not pos2 then
959 pos2 = vector.add(pos, dist)
961 for x=pos1.x, pos2.x do
962 for y=pos1.y, pos2.y do
963 for z=pos1.z, pos2.z do
964 local checkpos = {x=x, y=y, z=z}
965 local nodehash = minetest.hash_node_position(checkpos)
967 -- If node is already displayed, remove it so it can re replaced later
968 if displayed_waypoints[playername][nodehash] then
969 player:hud_remove(displayed_waypoints[playername][nodehash])
970 displayed_waypoints[playername][nodehash] = nil
973 local prob, force_place
974 local meta = minetest.get_meta(checkpos)
975 prob = meta:get_string("schemedit_prob")
976 force_place = meta:get_string("schemedit_force_place") == "true"
977 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
978 if hud_id then
979 displayed_waypoints[playername][nodehash] = hud_id
980 displayed_waypoints[playername].display_active = true
987 -- Remove all active displayed node statuses.
988 function schemedit.clear_displayed_node_probs(player)
989 local playername = player:get_player_name()
990 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
991 player:hud_remove(hud_id)
992 displayed_waypoints[playername][nodehash] = nil
993 displayed_waypoints[playername].display_active = false
997 minetest.register_on_joinplayer(function(player)
998 displayed_waypoints[player:get_player_name()] = {
999 display_active = false -- If true, there *might* be at least one active node prob HUD display
1000 -- If false, no node probabilities are displayed for sure.
1002 end)
1004 minetest.register_on_leaveplayer(function(player)
1005 displayed_waypoints[player:get_player_name()] = nil
1006 end)
1008 -- Regularily clear the displayed node probabilities and force_place
1009 -- for all players who do not wield the probtool.
1010 -- This makes sure the screen is not spammed with information when it
1011 -- isn't needed.
1012 local cleartimer = 0
1013 minetest.register_globalstep(function(dtime)
1014 cleartimer = cleartimer + dtime
1015 if cleartimer > 2 then
1016 local players = minetest.get_connected_players()
1017 for p = 1, #players do
1018 local player = players[p]
1019 local pname = player:get_player_name()
1020 if displayed_waypoints[pname].display_active then
1021 local item = player:get_wielded_item()
1022 if item:get_name() ~= "schemedit:probtool" then
1023 schemedit.clear_displayed_node_probs(player)
1027 cleartimer = 0
1029 end)
1032 --- Registrations
1035 -- [priv] schematic_override
1036 minetest.register_privilege("schematic_override", {
1037 description = S("Allows you to access schemedit nodes not owned by you"),
1038 give_to_singleplayer = false,
1041 local help_import = ""
1042 if can_import then
1043 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"
1046 -- [node] Schematic creator
1047 minetest.register_node("schemedit:creator", {
1048 description = S("Schematic Creator"),
1049 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1050 _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"..
1051 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"..
1052 help_import..
1053 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1054 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"..
1055 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."),
1056 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1057 "schemedit_creator_sides.png"},
1058 groups = { dig_immediate = 2},
1059 paramtype2 = "facedir",
1060 is_ground_content = false,
1062 after_place_node = function(pos, player)
1063 local name = player:get_player_name()
1064 local meta = minetest.get_meta(pos)
1066 meta:set_string("owner", name)
1067 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
1068 meta:set_string("prob_list", minetest.serialize({}))
1069 meta:set_string("slices", minetest.serialize({}))
1071 local node = minetest.get_node(pos)
1072 local dir = minetest.facedir_to_dir(node.param2)
1074 meta:set_int("x_size", 1)
1075 meta:set_int("y_size", 1)
1076 meta:set_int("z_size", 1)
1078 -- Don't take item from itemstack
1079 return true
1080 end,
1081 can_dig = function(pos, player)
1082 local name = player:get_player_name()
1083 local meta = minetest.get_meta(pos)
1084 if meta:get_string("owner") == name or
1085 minetest.check_player_privs(player, "schematic_override") == true then
1086 return true
1089 return false
1090 end,
1091 on_rightclick = function(pos, node, player)
1092 local meta = minetest.get_meta(pos)
1093 local name = player:get_player_name()
1094 if meta:get_string("owner") == name or
1095 minetest.check_player_privs(player, "schematic_override") == true then
1096 -- Get player attribute
1097 local pmeta = player:get_meta()
1098 local tab = pmeta:get_string("schemedit:tab")
1099 if not forms[tab] or not tab then
1100 tab = "main"
1103 schemedit.show_formspec(pos, player, tab, true)
1105 end,
1106 after_destruct = function(pos)
1107 schemedit.unmark(pos)
1108 end,
1110 -- No support for Minetest Game's screwdriver
1111 on_rotate = false,
1114 minetest.register_tool("schemedit:probtool", {
1115 description = S("Schematic Node Probability Tool"),
1116 _doc_items_longdesc =
1117 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"..
1118 S("It allows you to set two things:").."\n"..
1119 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1120 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1121 _doc_items_usagehelp = "\n"..
1122 S("BASIC USAGE:").."\n"..
1123 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"..
1124 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"..
1125 S("NODE HUD:").."\n"..
1126 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"..
1127 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1128 S("UPDATING THE NODE HUD:").."\n"..
1129 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."),
1130 wield_image = "schemedit_probtool.png",
1131 inventory_image = "schemedit_probtool.png",
1132 liquids_pointable = true,
1133 groups = { disable_repair = 1 },
1134 on_use = function(itemstack, user, pointed_thing)
1135 local uname = user:get_player_name()
1136 if uname and not check_priv(uname) then
1137 return
1140 local ctrl = user:get_player_control()
1141 -- Simple use
1142 if not ctrl.sneak then
1143 -- Open dialog to change the probability to apply to nodes
1144 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1146 -- Use + sneak
1147 else
1148 -- Display the probability and force_place values for nodes.
1150 -- If a schematic creator was punched, only enable display for all nodes
1151 -- within the creator's region.
1152 local use_creator_region = false
1153 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1154 local punchpos = pointed_thing.under
1155 local node = minetest.get_node(punchpos)
1156 if node.name == "schemedit:creator" then
1157 local pos1, pos2 = schemedit.size(punchpos)
1158 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1159 schemedit.display_node_probs_region(user, pos1, pos2)
1160 return
1164 -- Otherwise, just display the region close to the player
1165 schemedit.display_node_probs_region(user)
1167 end,
1168 on_secondary_use = function(itemstack, user, pointed_thing)
1169 local uname = user:get_player_name()
1170 if uname and not check_priv(uname) then
1171 return
1174 schemedit.clear_displayed_node_probs(user)
1175 end,
1176 -- Set note probability and force_place and enable node probability display
1177 on_place = function(itemstack, placer, pointed_thing)
1178 local pname = placer:get_player_name()
1179 if pname and not check_priv(pname) then
1180 return
1183 -- Use pointed node's on_rightclick function first, if present
1184 local node = minetest.get_node(pointed_thing.under)
1185 if placer and not placer:get_player_control().sneak then
1186 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1187 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1191 -- This sets the node probability of pointed node to the
1192 -- currently used probability stored in the tool.
1193 local pos = pointed_thing.under
1194 local node = minetest.get_node(pos)
1195 -- Schematic void are ignored, they always have probability 0
1196 if node.name == "schemedit:void" then
1197 return itemstack
1199 local nmeta = minetest.get_meta(pos)
1200 local imeta = itemstack:get_meta()
1201 local prob = tonumber(imeta:get_string("schemedit_prob"))
1202 local force_place = imeta:get_string("schemedit_force_place")
1204 if not prob or prob == 255 then
1205 nmeta:set_string("schemedit_prob", nil)
1206 else
1207 nmeta:set_string("schemedit_prob", prob)
1209 if force_place == "true" then
1210 nmeta:set_string("schemedit_force_place", "true")
1211 else
1212 nmeta:set_string("schemedit_force_place", nil)
1215 -- Enable node probablity display
1216 schemedit.display_node_probs_region(placer)
1218 return itemstack
1219 end,
1222 local use_texture_alpha_void
1223 if minetest.features.use_texture_alpha_string_modes then
1224 use_texture_alpha_void = "clip"
1225 else
1226 use_texture_alpha_void = true
1229 minetest.register_node("schemedit:void", {
1230 description = S("Schematic Void"),
1231 _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."),
1232 _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."),
1233 tiles = { "schemedit_void.png" },
1234 use_texture_alpha = use_texture_alpha_void,
1235 drawtype = "nodebox",
1236 is_ground_content = false,
1237 paramtype = "light",
1238 walkable = false,
1239 sunlight_propagates = true,
1240 node_box = {
1241 type = "fixed",
1242 fixed = {
1243 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1246 groups = { dig_immediate = 3},
1249 -- [entity] Visible schematic border
1250 minetest.register_entity("schemedit:display", {
1251 visual = "upright_sprite",
1252 textures = {"schemedit_border.png"},
1253 visual_size = {x=10, y=10},
1254 pointable = false,
1255 physical = false,
1256 static_save = false,
1257 glow = minetest.LIGHT_MAX,
1259 on_step = function(self, dtime)
1260 if not self.id then
1261 self.object:remove()
1262 elseif not schemedit.markers[self.id] then
1263 self.object:remove()
1265 end,
1266 on_activate = function(self)
1267 self.object:set_armor_groups({immortal = 1})
1268 end,
1271 minetest.register_lbm({
1272 label = "Reset schematic creator border entities",
1273 name = "schemedit:reset_border",
1274 nodenames = "schemedit:creator",
1275 run_at_every_load = true,
1276 action = function(pos, node)
1277 local meta = minetest.get_meta(pos)
1278 meta:set_string("schem_border", "false")
1279 end,
1282 local function add_suffix(schem)
1283 -- Automatically add file name suffix if omitted
1284 local schem_full, schem_lua
1285 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1286 schem_full = schem
1287 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1288 else
1289 schem_full = schem .. ".mts"
1290 schem_lua = schem .. ".lua"
1292 return schem_full, schem_lua
1295 -- [chatcommand] Place schematic
1296 minetest.register_chatcommand("placeschem", {
1297 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1298 privs = {server = true},
1299 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1300 func = function(name, param)
1301 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1302 local pos = minetest.string_to_pos(p)
1304 if not schem then
1305 return false, S("No schematic file specified.")
1308 if not pos then
1309 pos = minetest.get_player_by_name(name):get_pos()
1312 local schem_full, schem_lua = add_suffix(schem)
1313 local success = false
1314 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1315 if minetest.read_schematic then
1316 -- We don't call minetest.place_schematic with the path name directly because
1317 -- this would trigger the caching and we wouldn't get any updates to the schematic
1318 -- files when we reload. minetest.read_schematic circumvents that.
1319 local schematic = minetest.read_schematic(schem_path, {})
1320 if schematic then
1321 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1323 else
1324 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1325 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1328 if success == nil then
1329 return false, S("Schematic file could not be loaded!")
1330 else
1331 return true
1333 end,
1336 if can_import then
1337 -- [chatcommand] Convert MTS schematic file to .lua file
1338 minetest.register_chatcommand("mts2lua", {
1339 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1340 privs = {server = true},
1341 params = S("<schematic name>[.mts] [comments]"),
1342 func = function(name, param)
1343 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1345 if not schem then
1346 return false, S("No schematic file specified.")
1349 local comments = comments_str == "comments"
1351 -- Automatically add file name suffix if omitted
1352 local schem_full, schem_lua = add_suffix(schem)
1353 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1354 local schematic = minetest.read_schematic(schem_path, {})
1356 if schematic then
1357 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1358 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1359 local file = io.open(lua_path, "w")
1360 if file and str then
1361 file:write(str)
1362 file:flush()
1363 file:close()
1364 return true, S("Exported schematic to @1", lua_path)
1365 else
1366 return false, S("Failed!")
1369 end,