Tweak parsing of /placeschem
[minetest_schemedit.git] / init.lua
blob0d39f6ee5125e6d1fcf795e9664de1e7dc69d0db
1 local S = minetest.get_translator("schemedit")
2 local F = minetest.formspec_escape
4 local schemedit = {}
6 local DIR_DELIM = "/"
8 -- Set to true to enable `make_schemedit_readme` command
9 local MAKE_README = false
11 local export_path_full = table.concat({minetest.get_worldpath(), "schems"}, DIR_DELIM)
13 -- truncated export path so the server directory structure is not exposed publicly
14 local export_path_trunc = table.concat({S("<world path>"), "schems"}, DIR_DELIM)
16 local text_color = "#D79E9E"
17 local text_color_number = 0xD79E9E
19 local can_import = minetest.read_schematic ~= nil
21 schemedit.markers = {}
23 -- [local function] Renumber table
24 local function renumber(t)
25 local res = {}
26 for _, i in pairs(t) do
27 res[#res + 1] = i
28 end
29 return res
30 end
32 local NEEDED_PRIV = "server"
33 local function check_priv(player_name, quit)
34 local privs = minetest.get_player_privs(player_name)
35 if privs[NEEDED_PRIV] then
36 return true
37 else
38 if not quit then
39 minetest.chat_send_player(player_name, minetest.colorize("red",
40 S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV)))
41 end
42 return false
43 end
44 end
46 -- Lua export
47 local export_schematic_to_lua
48 if can_import then
49 export_schematic_to_lua = function(schematic, filepath, options)
50 if not options then options = {} end
51 local str = minetest.serialize_schematic(schematic, "lua", options)
52 local file = io.open(filepath, "w")
53 if file and str then
54 file:write(str)
55 file:flush()
56 file:close()
57 return true
58 else
59 return false
60 end
61 end
62 end
64 ---
65 --- Formspec API
66 ---
68 local contexts = {}
69 local form_data = {}
70 local tabs = {}
71 local forms = {}
72 local displayed_waypoints = {}
74 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
75 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
76 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
77 -- on an actual export to a schematic.
79 function schemedit.lua_prob_to_schematic_prob(lua_prob)
80 return math.floor(lua_prob / 2)
81 end
83 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
84 return schematic_prob * 2
86 end
88 -- [function] Add form
89 function schemedit.add_form(name, def)
90 def.name = name
91 forms[name] = def
93 if def.tab then
94 tabs[#tabs + 1] = name
95 end
96 end
98 -- [function] Generate tabs
99 function schemedit.generate_tabs(current)
100 local retval = "tabheader[0,0;tabs;"
101 for _, t in pairs(tabs) do
102 local f = forms[t]
103 if f.tab ~= false and f.caption then
104 retval = retval..f.caption..","
106 if type(current) ~= "number" and current == f.name then
107 current = _
111 retval = retval:sub(1, -2) -- Strip last comma
112 retval = retval..";"..current.."]" -- Close tabheader
113 return retval
116 -- [function] Handle tabs
117 function schemedit.handle_tabs(pos, name, fields)
118 local tab = tonumber(fields.tabs)
119 if tab and tabs[tab] and forms[tabs[tab]] then
120 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
121 return true
125 -- [function] Show formspec
126 function schemedit.show_formspec(pos, player, tab, show, ...)
127 if forms[tab] then
128 if type(player) == "string" then
129 player = minetest.get_player_by_name(player)
131 local name = player:get_player_name()
133 if show ~= false then
134 if not form_data[name] then
135 form_data[name] = {}
138 local form = forms[tab].get(form_data[name], pos, name, ...)
139 if forms[tab].tab then
140 form = form..schemedit.generate_tabs(tab)
143 minetest.show_formspec(name, "schemedit:"..tab, form)
144 contexts[name] = pos
146 -- Update player attribute
147 if forms[tab].cache_name ~= false then
148 local pmeta = player:get_meta()
149 pmeta:set_string("schemedit:tab", tab)
151 else
152 minetest.close_formspec(pname, "schemedit:"..tab)
157 -- [event] On receive fields
158 minetest.register_on_player_receive_fields(function(player, formname, fields)
159 local formname = formname:split(":")
161 if formname[1] == "schemedit" and forms[formname[2]] then
162 local handle = forms[formname[2]].handle
163 local name = player:get_player_name()
164 if contexts[name] then
165 if not form_data[name] then
166 form_data[name] = {}
169 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
170 handle(form_data[name], contexts[name], name, fields)
174 end)
176 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
177 schemedit.scan_metadata = function(pos1, pos2)
178 local prob_list = {}
180 for x=pos1.x, pos2.x do
181 for y=pos1.y, pos2.y do
182 for z=pos1.z, pos2.z do
183 local scanpos = {x=x, y=y, z=z}
184 local node = minetest.get_node_or_nil(scanpos)
186 local prob, force_place
187 if node == nil or node.name == "schemedit:void" then
188 prob = 0
189 force_place = false
190 else
191 local meta = minetest.get_meta(scanpos)
193 prob = tonumber(meta:get_string("schemedit_prob")) or 255
194 local fp = meta:get_string("schemedit_force_place")
195 if fp == "true" then
196 force_place = true
197 else
198 force_place = false
202 local hashpos = minetest.hash_node_position(scanpos)
203 prob_list[hashpos] = {
204 pos = scanpos,
205 prob = prob,
206 force_place = force_place,
212 return prob_list
215 -- Sets probability and force_place metadata of an item.
216 -- Also updates item description.
217 -- The itemstack is updated in-place.
218 local function set_item_metadata(itemstack, prob, force_place)
219 local smeta = itemstack:get_meta()
220 local prob_desc = "\n"..S("Probability: @1", prob or
221 smeta:get_string("schemedit_prob") or S("Not Set"))
222 -- Update probability
223 if prob and prob >= 0 and prob < 255 then
224 smeta:set_string("schemedit_prob", tostring(prob))
225 elseif prob and prob == 255 then
226 -- Clear prob metadata for default probability
227 prob_desc = ""
228 smeta:set_string("schemedit_prob", nil)
229 else
230 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
231 S("Not Set"))
234 -- Update force place
235 if force_place == true then
236 smeta:set_string("schemedit_force_place", "true")
237 elseif force_place == false then
238 smeta:set_string("schemedit_force_place", nil)
241 -- Update description
242 local desc = minetest.registered_items[itemstack:get_name()].description
243 local meta_desc = smeta:get_string("description")
244 if meta_desc and meta_desc ~= "" then
245 desc = meta_desc
248 local original_desc = smeta:get_string("original_description")
249 if original_desc and original_desc ~= "" then
250 desc = original_desc
251 else
252 smeta:set_string("original_description", desc)
255 local force_desc = ""
256 if smeta:get_string("schemedit_force_place") == "true" then
257 force_desc = "\n"..S("Force placement")
260 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
262 smeta:set_string("description", desc)
264 return itemstack
268 --- Formspec Tabs
270 local import_btn = ""
271 if can_import then
272 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
274 schemedit.add_form("main", {
275 tab = true,
276 caption = S("Main"),
277 get = function(self, pos, name)
278 local meta = minetest.get_meta(pos):to_table().fields
279 local strpos = minetest.pos_to_string(pos)
280 local hashpos = minetest.hash_node_position(pos)
282 local border_button
283 if meta.schem_border == "true" and schemedit.markers[hashpos] then
284 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
285 else
286 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
289 local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
290 local size = {x=xs, y=ys, z=zs}
291 local schem_name = meta.schem_name or ""
293 local form = [[
294 size[7,8]
295 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
296 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
297 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name)))..[[]
298 label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
300 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name or "")..[[]
301 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
302 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
303 field_close_on_enter[name;false]
305 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
306 import_btn..[[
307 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
308 export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
309 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
310 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
311 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
312 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
313 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
314 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
315 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
316 field_close_on_enter[x;false]
317 field_close_on_enter[y;false]
318 field_close_on_enter[z;false]
319 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
320 ]]..
321 border_button
322 if minetest.get_modpath("doc") then
323 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
324 "tooltip[doc;"..F(S("Help")).."]"
326 return form
327 end,
328 handle = function(self, pos, name, fields)
329 if fields.doc then
330 doc.show_entry(name, "nodes", "schemedit:creator", true)
331 return
334 if not check_priv(name, fields.quit) then
335 return
338 local realmeta = minetest.get_meta(pos)
339 local meta = realmeta:to_table().fields
340 local hashpos = minetest.hash_node_position(pos)
342 -- Save size vector values
343 if (fields.x and fields.x ~= "") then
344 local x = tonumber(fields.x)
345 if x then
346 meta.x_size = math.max(x, 1)
349 if (fields.y and fields.y ~= "") then
350 local y = tonumber(fields.y)
351 if y then
352 meta.y_size = math.max(y, 1)
355 if (fields.z and fields.z ~= "") then
356 local z = tonumber(fields.z)
357 if z then
358 meta.z_size = math.max(z, 1)
362 -- Save schematic name
363 if fields.name then
364 meta.schem_name = fields.name
367 -- Node conversion
368 if (fields.air2void) then
369 local pos1, pos2 = schemedit.size(pos)
370 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
371 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"air"})
372 minetest.bulk_set_node(nodes, {name="schemedit:void"})
373 return
374 elseif (fields.void2air) then
375 local pos1, pos2 = schemedit.size(pos)
376 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
377 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"schemedit:void"})
378 minetest.bulk_set_node(nodes, {name="air"})
379 return
382 -- Toggle border
383 if fields.border then
384 if meta.schem_border == "true" and schemedit.markers[hashpos] then
385 schemedit.unmark(pos)
386 meta.schem_border = "false"
387 else
388 schemedit.mark(pos)
389 meta.schem_border = "true"
393 -- Export schematic
394 if fields.export and meta.schem_name and meta.schem_name ~= "" then
395 local pos1, pos2 = schemedit.size(pos)
396 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
397 local path = export_path_full .. DIR_DELIM
398 minetest.mkdir(path)
400 local plist = schemedit.scan_metadata(pos1, pos2)
401 local probability_list = {}
402 for hash, i in pairs(plist) do
403 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
404 if i.force_place == true then
405 prob = prob + 128
408 table.insert(probability_list, {
409 pos = minetest.get_position_from_hash(hash),
410 prob = prob,
414 local slist = minetest.deserialize(meta.slices)
415 local slice_list = {}
416 for _, i in pairs(slist) do
417 slice_list[#slice_list + 1] = {
418 ypos = pos.y + i.ypos,
419 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
423 local filepath = path..meta.schem_name..".mts"
424 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
426 if res then
427 minetest.chat_send_player(name, minetest.colorize("#00ff00",
428 S("Exported schematic to @1", filepath)))
429 -- Additional export to Lua file if MTS export was successful
430 local schematic = minetest.read_schematic(filepath, {})
431 if schematic and minetest.settings:get_bool("schemedit_export_lua") then
432 local filepath_lua = path..meta.schem_name..".lua"
433 res = export_schematic_to_lua(schematic, filepath_lua)
434 if res then
435 minetest.chat_send_player(name, minetest.colorize("#00ff00",
436 S("Exported schematic to @1", filepath_lua)))
439 else
440 minetest.chat_send_player(name, minetest.colorize("red",
441 S("Failed to export schematic to @1", filepath)))
445 -- Import schematic
446 if fields.import and meta.schem_name and meta.schem_name ~= "" then
447 if not can_import then
448 return
450 local pos1
451 local node = minetest.get_node(pos)
452 local path = export_path_full .. DIR_DELIM
454 local filepath = path..meta.schem_name..".mts"
455 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
456 local success = false
458 if schematic then
459 meta.x_size = schematic.size.x
460 meta.y_size = schematic.size.y
461 meta.z_size = schematic.size.z
462 meta.slices = minetest.serialize(renumber(schematic.yslice_prob))
463 local special_x_size = meta.x_size
464 local special_y_size = meta.y_size
465 local special_z_size = meta.z_size
467 if node.param2 == 1 then
468 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
469 meta.x_size, meta.z_size = meta.z_size, meta.x_size
470 elseif node.param2 == 2 then
471 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
472 elseif node.param2 == 3 then
473 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
474 meta.x_size, meta.z_size = meta.z_size, meta.x_size
475 else
476 pos1 = vector.add(pos, {x=0,y=0,z=1})
479 local schematic_for_meta = table.copy(schematic)
480 -- Strip probability data for placement
481 schematic.yslice_prob = {}
482 for d=1, #schematic.data do
483 schematic.data[d].prob = nil
486 -- Place schematic
487 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
489 -- Add special schematic data to nodes
490 if success then
491 local d = 1
492 for z=0, special_z_size-1 do
493 for y=0, special_y_size-1 do
494 for x=0, special_x_size-1 do
495 local data = schematic_for_meta.data[d]
496 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
497 if data.prob == 0 then
498 minetest.set_node(pp, {name="schemedit:void"})
499 else
500 local meta = minetest.get_meta(pp)
501 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
502 meta:set_string("schemedit_prob", tostring(data.prob))
503 else
504 meta:set_string("schemedit_prob", "")
506 if data.force_place then
507 meta:set_string("schemedit_force_place", "true")
508 else
509 meta:set_string("schemedit_force_place", "")
512 d = d + 1
518 if success then
519 minetest.chat_send_player(name, minetest.colorize("#00ff00",
520 S("Imported schematic from @1", filepath)))
521 else
522 minetest.chat_send_player(name, minetest.colorize("red",
523 S("Failed to import schematic from @1", filepath)))
529 -- Save meta before updating visuals
530 local inv = realmeta:get_inventory():get_lists()
531 realmeta:from_table({fields = meta, inventory = inv})
533 -- Update border
534 if not fields.border and meta.schem_border == "true" then
535 schemedit.mark(pos)
538 -- Update formspec
539 if not fields.quit then
540 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
542 end,
545 schemedit.add_form("slice", {
546 caption = S("Y Slices"),
547 tab = true,
548 get = function(self, pos, name, visible_panel)
549 local meta = minetest.get_meta(pos):to_table().fields
551 self.selected = self.selected or 1
552 local selected = tostring(self.selected)
553 local slice_list = minetest.deserialize(meta.slices)
554 local slices = ""
555 for _, i in pairs(slice_list) do
556 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
557 slices = slices..insert..","
559 slices = slices:sub(1, -2) -- Remove final comma
561 local form = [[
562 size[7,8]
563 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
566 if self.panel_add or self.panel_edit then
567 local ypos_default, prob_default = "", ""
568 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Add")).."]"
569 if self.panel_edit then
570 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Apply")).."]"
571 if slice_list[self.selected] then
572 ypos_default = slice_list[self.selected].ypos
573 prob_default = slice_list[self.selected].prob
577 local field_ypos = ""
578 if self.panel_add then
579 field_ypos = "field[0.3,7.5;2.5,1;ypos;"..F(S("Y position (max. @1):", (meta.y_size - 1)))..";"..ypos_default.."]"
582 form = form..[[
583 ]]..field_ypos..[[
584 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
585 field_close_on_enter[ypos;false]
586 field_close_on_enter[prob;false]
587 ]]..done_button
590 if not self.panel_edit then
591 if self.panel_add then
592 form = form.."button[0,6;2.4,1;add;"..F(S("Cancel")).."]"
593 else
594 form = form.."button[0,6;2.4,1;add;"..F(S("Add slice")).."]"
598 if slices ~= "" and self.selected and not self.panel_add then
599 if not self.panel_edit then
600 form = form..[[
601 button[2.4,6;2.4,1;remove;]]..F(S("Remove slice"))..[[]
602 button[4.8,6;2.4,1;edit;]]..F(S("Edit slice"))..[[]
604 else
605 form = form..[[
606 button[4.8,6;2.4,1;edit;]]..F(S("Back"))..[[]
611 return form
612 end,
613 handle = function(self, pos, name, fields)
614 if not check_priv(name, fields.quit) then
615 return
618 local meta = minetest.get_meta(pos)
619 local player = minetest.get_player_by_name(name)
621 if fields.slices then
622 local slices = fields.slices:split(":")
623 self.selected = tonumber(slices[2])
626 if fields.add then
627 if not self.panel_add then
628 self.panel_add = true
629 schemedit.show_formspec(pos, player, "slice")
630 else
631 self.panel_add = nil
632 schemedit.show_formspec(pos, player, "slice")
636 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
637 if fields.done_edit then
638 ypos = 0
640 if (fields.done_add or fields.done_edit) and ypos and prob and
641 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
642 local slice_list = minetest.deserialize(meta:get_string("slices"))
643 local index = #slice_list + 1
644 if fields.done_edit then
645 index = self.selected
648 local dupe = false
649 if fields.done_add then
650 for k,v in pairs(slice_list) do
651 if v.ypos == ypos then
652 v.prob = prob
653 dupe = true
657 if fields.done_edit and slice_list[index] then
658 ypos = slice_list[index].ypos
660 if not dupe then
661 slice_list[index] = {ypos = ypos, prob = prob}
664 meta:set_string("slices", minetest.serialize(slice_list))
666 -- Update and show formspec
667 self.panel_add = nil
668 schemedit.show_formspec(pos, player, "slice")
671 if fields.remove and self.selected then
672 local slice_list = minetest.deserialize(meta:get_string("slices"))
673 slice_list[self.selected] = nil
674 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
676 -- Update formspec
677 self.selected = math.max(1, self.selected-1)
678 self.panel_edit = nil
679 schemedit.show_formspec(pos, player, "slice")
682 if fields.edit then
683 if not self.panel_edit then
684 self.panel_edit = true
685 schemedit.show_formspec(pos, player, "slice")
686 else
687 self.panel_edit = nil
688 schemedit.show_formspec(pos, player, "slice")
691 end,
694 schemedit.add_form("probtool", {
695 cache_name = false,
696 caption = S("Schematic Node Probability Tool"),
697 get = function(self, pos, name)
698 local player = minetest.get_player_by_name(name)
699 if not player then
700 return
702 local probtool = player:get_wielded_item()
703 if probtool:get_name() ~= "schemedit:probtool" then
704 return
707 local meta = probtool:get_meta()
708 local prob = tonumber(meta:get_string("schemedit_prob"))
709 local force_place = meta:get_string("schemedit_force_place")
711 if not prob then
712 prob = 255
714 if force_place == nil or force_place == "" then
715 force_place = "false"
717 local form = "size[5,4]"..
718 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
719 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
720 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
721 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
722 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
723 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
724 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
725 "field_close_on_enter[prob;false]"
726 return form
727 end,
728 handle = function(self, pos, name, fields)
729 if not check_priv(name, fields.quit) then
730 return
733 if fields.submit then
734 local prob = tonumber(fields.prob)
735 if prob then
736 local player = minetest.get_player_by_name(name)
737 if not player then
738 return
740 local probtool = player:get_wielded_item()
741 if probtool:get_name() ~= "schemedit:probtool" then
742 return
745 local force_place = self.force_place == true
747 set_item_metadata(probtool, prob, force_place)
749 -- Repurpose the tool's wear bar to display the set probability
750 probtool:set_wear(math.floor(((255-prob)/255)*65535))
752 player:set_wielded_item(probtool)
755 if fields.force_place == "true" then
756 self.force_place = true
757 elseif fields.force_place == "false" then
758 self.force_place = false
760 end,
764 --- API
767 --- Copies and modifies positions `pos1` and `pos2` so that each component of
768 -- `pos1` is less than or equal to the corresponding component of `pos2`.
769 -- Returns the new positions.
770 function schemedit.sort_pos(pos1, pos2)
771 if not pos1 or not pos2 then
772 return
775 pos1, pos2 = table.copy(pos1), table.copy(pos2)
776 if pos1.x > pos2.x then
777 pos2.x, pos1.x = pos1.x, pos2.x
779 if pos1.y > pos2.y then
780 pos2.y, pos1.y = pos1.y, pos2.y
782 if pos1.z > pos2.z then
783 pos2.z, pos1.z = pos1.z, pos2.z
785 return pos1, pos2
788 -- [function] Prepare size
789 function schemedit.size(pos)
790 local pos1 = vector.new(pos)
791 local meta = minetest.get_meta(pos)
792 local node = minetest.get_node(pos)
793 local param2 = node.param2
794 local size = {
795 x = meta:get_int("x_size"),
796 y = math.max(meta:get_int("y_size") - 1, 0),
797 z = meta:get_int("z_size"),
800 if param2 == 1 then
801 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
802 pos1.x = pos1.x + 1
803 new_pos.z = new_pos.z + 1
804 return pos1, new_pos
805 elseif param2 == 2 then
806 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
807 pos1.z = pos1.z - 1
808 new_pos.x = new_pos.x + 1
809 return pos1, new_pos
810 elseif param2 == 3 then
811 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
812 pos1.x = pos1.x - 1
813 new_pos.z = new_pos.z - 1
814 return pos1, new_pos
815 else
816 local new_pos = vector.add(size, pos)
817 pos1.z = pos1.z + 1
818 new_pos.x = new_pos.x - 1
819 return pos1, new_pos
823 -- [function] Mark region
824 function schemedit.mark(pos)
825 schemedit.unmark(pos)
827 local id = minetest.hash_node_position(pos)
828 local owner = minetest.get_meta(pos):get_string("owner")
829 local pos1, pos2 = schemedit.size(pos)
830 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
832 local thickness = 0.2
833 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
834 local m = {}
835 local low = true
836 local offset
838 -- XY plane markers
839 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
840 if low then
841 offset = -0.01
842 else
843 offset = 0.01
845 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
846 if marker ~= nil then
847 marker:set_properties({
848 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
850 marker:get_luaentity().id = id
851 marker:get_luaentity().owner = owner
852 table.insert(m, marker)
854 low = false
857 low = true
858 -- YZ plane markers
859 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
860 if low then
861 offset = -0.01
862 else
863 offset = 0.01
866 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
867 if marker ~= nil then
868 marker:set_properties({
869 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
871 marker:set_rotation({x=0, y=math.pi / 2, z=0})
872 marker:get_luaentity().id = id
873 marker:get_luaentity().owner = owner
874 table.insert(m, marker)
876 low = false
879 low = true
880 -- XZ plane markers
881 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
882 if low then
883 offset = -0.01
884 else
885 offset = 0.01
888 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
889 if marker ~= nil then
890 marker:set_properties({
891 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
893 marker:set_rotation({x=math.pi/2, y=0, z=0})
894 marker:get_luaentity().id = id
895 marker:get_luaentity().owner = owner
896 table.insert(m, marker)
898 low = false
903 schemedit.markers[id] = m
904 return true
907 -- [function] Unmark region
908 function schemedit.unmark(pos)
909 local id = minetest.hash_node_position(pos)
910 if schemedit.markers[id] then
911 local retval
912 for _, entity in ipairs(schemedit.markers[id]) do
913 entity:remove()
914 retval = true
916 return retval
921 --- Mark node probability values near player
924 -- Show probability and force_place status of a particular position for player in HUD.
925 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
926 function schemedit.display_node_prob(player, pos, prob, force_place)
927 local wpstring
928 if prob and force_place == true then
929 wpstring = string.format("%s [F]", prob)
930 elseif prob and type(tonumber(prob)) == "number" then
931 wpstring = prob
932 elseif force_place == true then
933 wpstring = "[F]"
935 if wpstring then
936 return player:hud_add({
937 hud_elem_type = "waypoint",
938 name = wpstring,
939 precision = 0,
940 text = "m", -- For the distance artifact [legacy]
941 number = text_color_number,
942 world_pos = pos,
947 -- Display the node probabilities and force_place status of the nodes in a region.
948 -- By default, this is done for nodes near the player (distance: 5).
949 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
950 function schemedit.display_node_probs_region(player, pos1, pos2)
951 local playername = player:get_player_name()
952 local pos = vector.round(player:get_pos())
954 local dist = 5
955 -- Default: 5 nodes away from player in any direction
956 if not pos1 then
957 pos1 = vector.subtract(pos, dist)
959 if not pos2 then
960 pos2 = vector.add(pos, dist)
962 for x=pos1.x, pos2.x do
963 for y=pos1.y, pos2.y do
964 for z=pos1.z, pos2.z do
965 local checkpos = {x=x, y=y, z=z}
966 local nodehash = minetest.hash_node_position(checkpos)
968 -- If node is already displayed, remove it so it can re replaced later
969 if displayed_waypoints[playername][nodehash] then
970 player:hud_remove(displayed_waypoints[playername][nodehash])
971 displayed_waypoints[playername][nodehash] = nil
974 local prob, force_place
975 local meta = minetest.get_meta(checkpos)
976 prob = meta:get_string("schemedit_prob")
977 force_place = meta:get_string("schemedit_force_place") == "true"
978 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
979 if hud_id then
980 displayed_waypoints[playername][nodehash] = hud_id
981 displayed_waypoints[playername].display_active = true
988 -- Remove all active displayed node statuses.
989 function schemedit.clear_displayed_node_probs(player)
990 local playername = player:get_player_name()
991 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
992 if nodehash ~= "display_active" then
993 player:hud_remove(hud_id)
994 displayed_waypoints[playername][nodehash] = nil
995 displayed_waypoints[playername].display_active = false
1000 minetest.register_on_joinplayer(function(player)
1001 displayed_waypoints[player:get_player_name()] = {
1002 display_active = false -- If true, there *might* be at least one active node prob HUD display
1003 -- If false, no node probabilities are displayed for sure.
1005 end)
1007 minetest.register_on_leaveplayer(function(player)
1008 displayed_waypoints[player:get_player_name()] = nil
1009 end)
1011 -- Regularily clear the displayed node probabilities and force_place
1012 -- for all players who do not wield the probtool.
1013 -- This makes sure the screen is not spammed with information when it
1014 -- isn't needed.
1015 local cleartimer = 0
1016 minetest.register_globalstep(function(dtime)
1017 cleartimer = cleartimer + dtime
1018 if cleartimer > 2 then
1019 local players = minetest.get_connected_players()
1020 for p = 1, #players do
1021 local player = players[p]
1022 local pname = player:get_player_name()
1023 if displayed_waypoints[pname].display_active then
1024 local item = player:get_wielded_item()
1025 if item:get_name() ~= "schemedit:probtool" then
1026 schemedit.clear_displayed_node_probs(player)
1030 cleartimer = 0
1032 end)
1035 --- Registrations
1038 -- [priv] schematic_override
1039 minetest.register_privilege("schematic_override", {
1040 description = S("Allows you to access schemedit nodes not owned by you"),
1041 give_to_singleplayer = false,
1044 local help_import = ""
1045 if can_import then
1046 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"
1049 -- [node] Schematic creator
1050 minetest.register_node("schemedit:creator", {
1051 description = S("Schematic Creator"),
1052 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1053 _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"..
1054 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"..
1055 help_import..
1056 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1057 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"..
1058 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."),
1059 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1060 "schemedit_creator_sides.png"},
1061 groups = { dig_immediate = 2},
1062 paramtype2 = "facedir",
1063 is_ground_content = false,
1065 after_place_node = function(pos, player)
1066 local name = player:get_player_name()
1067 local meta = minetest.get_meta(pos)
1069 meta:set_string("owner", name)
1070 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
1071 meta:set_string("prob_list", minetest.serialize({}))
1072 meta:set_string("slices", minetest.serialize({}))
1074 local node = minetest.get_node(pos)
1075 local dir = minetest.facedir_to_dir(node.param2)
1077 meta:set_int("x_size", 1)
1078 meta:set_int("y_size", 1)
1079 meta:set_int("z_size", 1)
1081 -- Don't take item from itemstack
1082 return true
1083 end,
1084 can_dig = function(pos, player)
1085 local name = player:get_player_name()
1086 local meta = minetest.get_meta(pos)
1087 if meta:get_string("owner") == name or
1088 minetest.check_player_privs(player, "schematic_override") == true then
1089 return true
1092 return false
1093 end,
1094 on_rightclick = function(pos, node, player)
1095 local meta = minetest.get_meta(pos)
1096 local name = player:get_player_name()
1097 if meta:get_string("owner") == name or
1098 minetest.check_player_privs(player, "schematic_override") == true then
1099 -- Get player attribute
1100 local pmeta = player:get_meta()
1101 local tab = pmeta:get_string("schemedit:tab")
1102 if not forms[tab] or not tab then
1103 tab = "main"
1106 schemedit.show_formspec(pos, player, tab, true)
1108 end,
1109 after_destruct = function(pos)
1110 schemedit.unmark(pos)
1111 end,
1113 -- No support for Minetest Game's screwdriver
1114 on_rotate = false,
1117 minetest.register_tool("schemedit:probtool", {
1118 description = S("Schematic Node Probability Tool"),
1119 _doc_items_longdesc =
1120 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"..
1121 S("It allows you to set two things:").."\n"..
1122 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1123 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1124 _doc_items_usagehelp = "\n"..
1125 S("BASIC USAGE:").."\n"..
1126 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"..
1127 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"..
1128 S("NODE HUD:").."\n"..
1129 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"..
1130 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1131 S("UPDATING THE NODE HUD:").."\n"..
1132 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 simultaneously. 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."),
1133 wield_image = "schemedit_probtool.png",
1134 inventory_image = "schemedit_probtool.png",
1135 liquids_pointable = true,
1136 groups = { disable_repair = 1 },
1137 on_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 local ctrl = user:get_player_control()
1144 -- Simple use
1145 if not ctrl.sneak then
1146 -- Open dialog to change the probability to apply to nodes
1147 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1149 -- Use + sneak
1150 else
1151 -- Display the probability and force_place values for nodes.
1153 -- If a schematic creator was punched, only enable display for all nodes
1154 -- within the creator's region.
1155 local use_creator_region = false
1156 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1157 local punchpos = pointed_thing.under
1158 local node = minetest.get_node(punchpos)
1159 if node.name == "schemedit:creator" then
1160 local pos1, pos2 = schemedit.size(punchpos)
1161 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1162 schemedit.display_node_probs_region(user, pos1, pos2)
1163 return
1167 -- Otherwise, just display the region close to the player
1168 schemedit.display_node_probs_region(user)
1170 end,
1171 on_secondary_use = function(itemstack, user, pointed_thing)
1172 local uname = user:get_player_name()
1173 if uname and not check_priv(uname) then
1174 return
1177 schemedit.clear_displayed_node_probs(user)
1178 end,
1179 -- Set note probability and force_place and enable node probability display
1180 on_place = function(itemstack, placer, pointed_thing)
1181 local pname = placer:get_player_name()
1182 if pname and not check_priv(pname) then
1183 return
1186 -- Use pointed node's on_rightclick function first, if present
1187 local node = minetest.get_node(pointed_thing.under)
1188 if placer and not placer:get_player_control().sneak then
1189 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1190 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1194 -- This sets the node probability of pointed node to the
1195 -- currently used probability stored in the tool.
1196 local pos = pointed_thing.under
1197 local node = minetest.get_node(pos)
1198 -- Schematic void are ignored, they always have probability 0
1199 if node.name == "schemedit:void" then
1200 return itemstack
1202 local nmeta = minetest.get_meta(pos)
1203 local imeta = itemstack:get_meta()
1204 local prob = tonumber(imeta:get_string("schemedit_prob"))
1205 local force_place = imeta:get_string("schemedit_force_place")
1207 if not prob or prob == 255 then
1208 nmeta:set_string("schemedit_prob", nil)
1209 else
1210 nmeta:set_string("schemedit_prob", prob)
1212 if force_place == "true" then
1213 nmeta:set_string("schemedit_force_place", "true")
1214 else
1215 nmeta:set_string("schemedit_force_place", nil)
1218 -- Enable node probablity display
1219 schemedit.display_node_probs_region(placer)
1221 return itemstack
1222 end,
1225 local use_texture_alpha_void
1226 if minetest.features.use_texture_alpha_string_modes then
1227 use_texture_alpha_void = "clip"
1228 else
1229 use_texture_alpha_void = true
1232 minetest.register_node("schemedit:void", {
1233 description = S("Schematic Void"),
1234 _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."),
1235 _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."),
1236 tiles = { "schemedit_void.png" },
1237 use_texture_alpha = use_texture_alpha_void,
1238 drawtype = "nodebox",
1239 is_ground_content = false,
1240 paramtype = "light",
1241 walkable = false,
1242 sunlight_propagates = true,
1243 node_box = {
1244 type = "fixed",
1245 fixed = {
1246 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1249 groups = { dig_immediate = 3},
1252 -- [entity] Visible schematic border
1253 minetest.register_entity("schemedit:display", {
1254 visual = "upright_sprite",
1255 textures = {"schemedit_border.png"},
1256 visual_size = {x=10, y=10},
1257 pointable = false,
1258 physical = false,
1259 static_save = false,
1260 glow = minetest.LIGHT_MAX,
1262 on_step = function(self, dtime)
1263 if not self.id then
1264 self.object:remove()
1265 elseif not schemedit.markers[self.id] then
1266 self.object:remove()
1268 end,
1269 on_activate = function(self)
1270 self.object:set_armor_groups({immortal = 1})
1271 end,
1274 minetest.register_lbm({
1275 label = "Reset schematic creator border entities",
1276 name = "schemedit:reset_border",
1277 nodenames = "schemedit:creator",
1278 run_at_every_load = true,
1279 action = function(pos, node)
1280 local meta = minetest.get_meta(pos)
1281 meta:set_string("schem_border", "false")
1282 end,
1285 local function add_suffix(schem)
1286 -- Automatically add file name suffix if omitted
1287 local schem_full, schem_lua
1288 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1289 schem_full = schem
1290 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1291 else
1292 schem_full = schem .. ".mts"
1293 schem_lua = schem .. ".lua"
1295 return schem_full, schem_lua
1298 -- [chatcommand] Place schematic
1299 minetest.register_chatcommand("placeschem", {
1300 description = S("Place schematic at the position specified or the current player position (loaded from @1). “-c” will clear the area first", export_path_trunc),
1301 privs = {server = true},
1302 params = S("<schematic name>[.mts] [-c] [<x> <y> <z>]"),
1303 func = function(name, param)
1304 local schem, clear, p = string.match(param, "^([^ ]+) +(%-c) +(.*)$")
1305 if not schem then
1306 schem, p = string.match(param, "^([^ ]+) +(.*)$")
1308 clear = clear == "-c"
1310 local pos = minetest.string_to_pos(p)
1312 if not schem then
1313 return false, S("No schematic file specified.")
1316 if not pos then
1317 pos = minetest.get_player_by_name(name):get_pos()
1320 local schem_full, schem_lua = add_suffix(schem)
1321 local success = false
1322 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1323 if minetest.read_schematic then
1324 -- We don't call minetest.place_schematic with the path name directly because
1325 -- this would trigger the caching and we wouldn't get any updates to the schematic
1326 -- files when we reload. minetest.read_schematic circumvents that.
1327 local schematic = minetest.read_schematic(schem_path, {})
1328 if schematic then
1329 if clear then
1330 -- Clear same size for X and Z because
1331 -- because schematic is randomly rotated
1332 local max_xz = math.max(schematic.size.x, schematic.size.z)
1333 local posses = {}
1334 for z=pos.z, pos.z+max_xz-1 do
1335 for y=pos.y, pos.y+schematic.size.y-1 do
1336 for x=pos.x, pos.x+max_xz-1 do
1337 table.insert(posses, {x=x,y=y,z=z})
1341 minetest.bulk_set_node(posses, {name="air"})
1343 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1345 else
1346 -- Legacy support for Minetest versions that do not have minetest.read_schematic.
1347 -- Note: "-c" is ignored here.
1348 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1351 if success == nil then
1352 return false, S("Schematic file could not be loaded!")
1353 else
1354 return true
1356 end,
1359 if can_import then
1360 -- [chatcommand] Convert MTS schematic file to .lua file
1361 minetest.register_chatcommand("mts2lua", {
1362 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1363 privs = {server = true},
1364 params = S("<schematic name>[.mts] [comments]"),
1365 func = function(name, param)
1366 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1368 if not schem then
1369 return false, S("No schematic file specified.")
1372 local comments = comments_str == "comments"
1374 -- Automatically add file name suffix if omitted
1375 local schem_full, schem_lua = add_suffix(schem)
1376 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1377 local schematic = minetest.read_schematic(schem_path, {})
1379 if schematic then
1380 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1381 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1382 local file = io.open(lua_path, "w")
1383 if file and str then
1384 file:write(str)
1385 file:flush()
1386 file:close()
1387 return true, S("Exported schematic to @1", lua_path)
1388 else
1389 return false, S("Failed!")
1392 end,
1396 if MAKE_README then
1397 dofile(minetest.get_modpath("schemedit")..DIR_DELIM.."make_readme.lua")