Localize DIR_DELIM
[minetest_schemedit.git] / init.lua
blob62fe52eee7060cf48a196c946cf51b13b51a93c7
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 -- Lua export
30 local export_schematic_to_lua
31 if can_import then
32 export_schematic_to_lua = function(schematic, filepath, options)
33 if not options then options = {} end
34 local str = minetest.serialize_schematic(schematic, "lua", options)
35 local file = io.open(filepath, "w")
36 if file and str then
37 file:write(str)
38 file:flush()
39 file:close()
40 return true
41 else
42 return false
43 end
44 end
45 end
47 ---
48 --- Formspec API
49 ---
51 local contexts = {}
52 local form_data = {}
53 local tabs = {}
54 local forms = {}
55 local displayed_waypoints = {}
57 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
58 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
59 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
60 -- on an actual export to a schematic.
62 function schemedit.lua_prob_to_schematic_prob(lua_prob)
63 return math.floor(lua_prob / 2)
64 end
66 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
67 return schematic_prob * 2
69 end
71 -- [function] Add form
72 function schemedit.add_form(name, def)
73 def.name = name
74 forms[name] = def
76 if def.tab then
77 tabs[#tabs + 1] = name
78 end
79 end
81 -- [function] Generate tabs
82 function schemedit.generate_tabs(current)
83 local retval = "tabheader[0,0;tabs;"
84 for _, t in pairs(tabs) do
85 local f = forms[t]
86 if f.tab ~= false and f.caption then
87 retval = retval..f.caption..","
89 if type(current) ~= "number" and current == f.name then
90 current = _
91 end
92 end
93 end
94 retval = retval:sub(1, -2) -- Strip last comma
95 retval = retval..";"..current.."]" -- Close tabheader
96 return retval
97 end
99 -- [function] Handle tabs
100 function schemedit.handle_tabs(pos, name, fields)
101 local tab = tonumber(fields.tabs)
102 if tab and tabs[tab] and forms[tabs[tab]] then
103 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
104 return true
108 -- [function] Show formspec
109 function schemedit.show_formspec(pos, player, tab, show, ...)
110 if forms[tab] then
111 if type(player) == "string" then
112 player = minetest.get_player_by_name(player)
114 local name = player:get_player_name()
116 if show ~= false then
117 if not form_data[name] then
118 form_data[name] = {}
121 local form = forms[tab].get(form_data[name], pos, name, ...)
122 if forms[tab].tab then
123 form = form..schemedit.generate_tabs(tab)
126 minetest.show_formspec(name, "schemedit:"..tab, form)
127 contexts[name] = pos
129 -- Update player attribute
130 if forms[tab].cache_name ~= false then
131 player:set_attribute("schemedit:tab", tab)
133 else
134 minetest.close_formspec(pname, "schemedit:"..tab)
139 -- [event] On receive fields
140 minetest.register_on_player_receive_fields(function(player, formname, fields)
141 local formname = formname:split(":")
143 if formname[1] == "schemedit" and forms[formname[2]] then
144 local handle = forms[formname[2]].handle
145 local name = player:get_player_name()
146 if contexts[name] then
147 if not form_data[name] then
148 form_data[name] = {}
151 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
152 handle(form_data[name], contexts[name], name, fields)
156 end)
158 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
159 schemedit.scan_metadata = function(pos1, pos2)
160 local prob_list = {}
162 for x=pos1.x, pos2.x do
163 for y=pos1.y, pos2.y do
164 for z=pos1.z, pos2.z do
165 local scanpos = {x=x, y=y, z=z}
166 local node = minetest.get_node_or_nil(scanpos)
168 local prob, force_place
169 if node == nil or node.name == "schemedit:void" then
170 prob = 0
171 force_place = false
172 else
173 local meta = minetest.get_meta(scanpos)
175 prob = tonumber(meta:get_string("schemedit_prob")) or 255
176 local fp = meta:get_string("schemedit_force_place")
177 if fp == "true" then
178 force_place = true
179 else
180 force_place = false
184 local hashpos = minetest.hash_node_position(scanpos)
185 prob_list[hashpos] = {
186 pos = scanpos,
187 prob = prob,
188 force_place = force_place,
194 return prob_list
197 -- Sets probability and force_place metadata of an item.
198 -- Also updates item description.
199 -- The itemstack is updated in-place.
200 local function set_item_metadata(itemstack, prob, force_place)
201 local smeta = itemstack:get_meta()
202 local prob_desc = "\n"..S("Probability: @1", prob or
203 smeta:get_string("schemedit_prob") or S("Not Set"))
204 -- Update probability
205 if prob and prob >= 0 and prob < 255 then
206 smeta:set_string("schemedit_prob", tostring(prob))
207 elseif prob and prob == 255 then
208 -- Clear prob metadata for default probability
209 prob_desc = ""
210 smeta:set_string("schemedit_prob", nil)
211 else
212 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
213 S("Not Set"))
216 -- Update force place
217 if force_place == true then
218 smeta:set_string("schemedit_force_place", "true")
219 elseif force_place == false then
220 smeta:set_string("schemedit_force_place", nil)
223 -- Update description
224 local desc = minetest.registered_items[itemstack:get_name()].description
225 local meta_desc = smeta:get_string("description")
226 if meta_desc and meta_desc ~= "" then
227 desc = meta_desc
230 local original_desc = smeta:get_string("original_description")
231 if original_desc and original_desc ~= "" then
232 desc = original_desc
233 else
234 smeta:set_string("original_description", desc)
237 local force_desc = ""
238 if smeta:get_string("schemedit_force_place") == "true" then
239 force_desc = "\n"..S("Force placement")
242 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
244 smeta:set_string("description", desc)
246 return itemstack
250 --- Formspec Tabs
252 local import_btn = ""
253 if can_import then
254 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
256 schemedit.add_form("main", {
257 tab = true,
258 caption = S("Main"),
259 get = function(self, pos, name)
260 local meta = minetest.get_meta(pos):to_table().fields
261 local strpos = minetest.pos_to_string(pos)
262 local hashpos = minetest.hash_node_position(pos)
264 local border_button
265 if meta.schem_border == "true" and schemedit.markers[hashpos] then
266 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
267 else
268 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
271 local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
272 local size = {x=xs, y=ys, z=zs}
274 local form = [[
275 size[7,8]
276 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
277 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
278 label[0.5,0.4;]]..F(S("Schematic name: @1", meta.schem_name))..[[]
279 label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
281 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(meta.schem_name or "")..[[]
282 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
283 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
284 field_close_on_enter[name;false]
286 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
287 import_btn..[[
288 textarea[0.8,4.5;6.2,5;;]]..F(S("Export/import path:\n@1",
289 export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
290 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
291 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
292 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
293 field_close_on_enter[x;false]
294 field_close_on_enter[y;false]
295 field_close_on_enter[z;false]
297 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
298 ]]..
299 border_button
300 if minetest.get_modpath("doc") then
301 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
302 "tooltip[doc;"..F(S("Help")).."]"
304 return form
305 end,
306 handle = function(self, pos, name, fields)
307 local realmeta = minetest.get_meta(pos)
308 local meta = realmeta:to_table().fields
309 local hashpos = minetest.hash_node_position(pos)
311 -- Save size vector values
312 if (fields.x and fields.x ~= "") then
313 local x = tonumber(fields.x)
314 if x then
315 meta.x_size = math.max(x, 1)
318 if (fields.y and fields.y ~= "") then
319 local y = tonumber(fields.y)
320 if y then
321 meta.y_size = math.max(y, 1)
324 if (fields.z and fields.z ~= "") then
325 local z = tonumber(fields.z)
326 if z then
327 meta.z_size = math.max(z, 1)
331 -- Save schematic name
332 if fields.name then
333 meta.schem_name = fields.name
336 if fields.doc then
337 doc.show_entry(name, "nodes", "schemedit:creator", true)
338 return
341 -- Toggle border
342 if fields.border then
343 if meta.schem_border == "true" and schemedit.markers[hashpos] then
344 schemedit.unmark(pos)
345 meta.schem_border = "false"
346 else
347 schemedit.mark(pos)
348 meta.schem_border = "true"
352 -- Export schematic
353 if fields.export and meta.schem_name and meta.schem_name ~= "" then
354 local pos1, pos2 = schemedit.size(pos)
355 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
356 local path = export_path_full .. DIR_DELIM
357 minetest.mkdir(path)
359 local plist = schemedit.scan_metadata(pos1, pos2)
360 local probability_list = {}
361 for hash, i in pairs(plist) do
362 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
363 if i.force_place == true then
364 prob = prob + 128
367 table.insert(probability_list, {
368 pos = minetest.get_position_from_hash(hash),
369 prob = prob,
373 local slist = minetest.deserialize(meta.slices)
374 local slice_list = {}
375 for _, i in pairs(slist) do
376 slice_list[#slice_list + 1] = {
377 ypos = pos.y + i.ypos,
378 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
382 local filepath = path..meta.schem_name..".mts"
383 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
385 if res then
386 minetest.chat_send_player(name, minetest.colorize("#00ff00",
387 S("Exported schematic to @1", filepath)))
388 -- Additional export to Lua file if MTS export was successful
389 local schematic = minetest.read_schematic(filepath, {})
390 if schematic and minetest.settings:get_bool("schemedit_export_lua") then
391 local filepath_lua = path..meta.schem_name..".lua"
392 res = export_schematic_to_lua(schematic, filepath_lua)
393 if res then
394 minetest.chat_send_player(name, minetest.colorize("#00ff00",
395 S("Exported schematic to @1", filepath_lua)))
398 else
399 minetest.chat_send_player(name, minetest.colorize("red",
400 S("Failed to export schematic to @1", filepath)))
404 -- Import schematic
405 if fields.import and meta.schem_name and meta.schem_name ~= "" then
406 if not minetest.get_player_privs(name).debug then
407 minetest.chat_send_player(name, minetest.colorize("red",
408 S("Insufficient privileges! You need the “debug” privilege to do this.")))
409 return
412 if not can_import then
413 return
415 local pos1
416 local node = minetest.get_node(pos)
417 local path = export_path_full .. DIR_DELIM
419 local filepath = path..meta.schem_name..".mts"
420 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
421 local success = false
423 if schematic then
424 meta.x_size = schematic.size.x
425 meta.y_size = schematic.size.y
426 meta.z_size = schematic.size.z
427 meta.slices = minetest.serialize(schematic.yslice_prob)
429 if node.param2 == 1 then
430 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
431 elseif node.param2 == 2 then
432 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
433 elseif node.param2 == 3 then
434 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
435 else
436 pos1 = vector.add(pos, {x=0,y=0,z=1})
439 local schematic_for_meta = table.copy(schematic)
440 -- Strip probability data for placement
441 schematic.yslice_prob = {}
442 for d=1, #schematic.data do
443 schematic.data[d].prob = nil
446 -- Place schematic
447 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
449 -- Add special schematic data to nodes
450 if success then
451 local d = 1
452 for z=0, meta.z_size-1 do
453 for y=0, meta.y_size-1 do
454 for x=0, meta.x_size-1 do
455 local data = schematic_for_meta.data[d]
456 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
457 if data.prob == 0 then
458 minetest.set_node(pp, {name="schemedit:void"})
459 else
460 local meta = minetest.get_meta(pp)
461 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
462 meta:set_string("schemedit_prob", tostring(data.prob))
463 else
464 meta:set_string("schemedit_prob", "")
466 if data.force_place then
467 meta:set_string("schemedit_force_place", "true")
468 else
469 meta:set_string("schemedit_force_place", "")
472 d = d + 1
478 if success then
479 minetest.chat_send_player(name, minetest.colorize("#00ff00",
480 S("Imported schematic from @1", filepath)))
481 else
482 minetest.chat_send_player(name, minetest.colorize("red",
483 S("Failed to import schematic from @1", filepath)))
489 -- Save meta before updating visuals
490 local inv = realmeta:get_inventory():get_lists()
491 realmeta:from_table({fields = meta, inventory = inv})
493 -- Update border
494 if not fields.border and meta.schem_border == "true" then
495 schemedit.mark(pos)
498 -- Update formspec
499 if not fields.quit then
500 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
502 end,
505 schemedit.add_form("slice", {
506 caption = S("Y Slices"),
507 tab = true,
508 get = function(self, pos, name, visible_panel)
509 local meta = minetest.get_meta(pos):to_table().fields
511 self.selected = self.selected or 1
512 local selected = tostring(self.selected)
513 local slice_list = minetest.deserialize(meta.slices)
514 local slices = ""
515 for _, i in pairs(slice_list) do
516 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
517 slices = slices..insert..","
519 slices = slices:sub(1, -2) -- Remove final comma
521 local form = [[
522 size[7,8]
523 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
526 if self.panel_add or self.panel_edit then
527 local ypos_default, prob_default = "", ""
528 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
529 if self.panel_edit then
530 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
531 if slice_list[self.selected] then
532 ypos_default = slice_list[self.selected].ypos
533 prob_default = slice_list[self.selected].prob
537 form = form..[[
538 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
539 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
540 field_close_on_enter[ypos;false]
541 field_close_on_enter[prob;false]
542 ]]..done_button
545 if not self.panel_edit then
546 form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
549 if slices ~= "" and self.selected and not self.panel_add then
550 if not self.panel_edit then
551 form = form..[[
552 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
553 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
555 else
556 form = form..[[
557 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
558 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
563 return form
564 end,
565 handle = function(self, pos, name, fields)
566 local meta = minetest.get_meta(pos)
567 local player = minetest.get_player_by_name(name)
569 if fields.slices then
570 local slices = fields.slices:split(":")
571 self.selected = tonumber(slices[2])
574 if fields.add then
575 if not self.panel_add then
576 self.panel_add = true
577 schemedit.show_formspec(pos, player, "slice")
578 else
579 self.panel_add = nil
580 schemedit.show_formspec(pos, player, "slice")
584 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
585 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
586 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
587 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
588 local slice_list = minetest.deserialize(meta:get_string("slices"))
589 local index = #slice_list + 1
590 if fields.done_edit then
591 index = self.selected
594 slice_list[index] = {ypos = ypos, prob = prob}
596 meta:set_string("slices", minetest.serialize(slice_list))
598 -- Update and show formspec
599 self.panel_add = nil
600 schemedit.show_formspec(pos, player, "slice")
603 if fields.remove and self.selected then
604 local slice_list = minetest.deserialize(meta:get_string("slices"))
605 slice_list[self.selected] = nil
606 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
608 -- Update formspec
609 self.selected = 1
610 self.panel_edit = nil
611 schemedit.show_formspec(pos, player, "slice")
614 if fields.edit then
615 if not self.panel_edit then
616 self.panel_edit = true
617 schemedit.show_formspec(pos, player, "slice")
618 else
619 self.panel_edit = nil
620 schemedit.show_formspec(pos, player, "slice")
623 end,
626 schemedit.add_form("probtool", {
627 cache_name = false,
628 caption = S("Schematic Node Probability Tool"),
629 get = function(self, pos, name)
630 local player = minetest.get_player_by_name(name)
631 if not player then
632 return
634 local probtool = player:get_wielded_item()
635 if probtool:get_name() ~= "schemedit:probtool" then
636 return
639 local meta = probtool:get_meta()
640 local prob = tonumber(meta:get_string("schemedit_prob"))
641 local force_place = meta:get_string("schemedit_force_place")
643 if not prob then
644 prob = 255
646 if force_place == nil or force_place == "" then
647 force_place = "false"
649 local form = "size[5,4]"..
650 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
651 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
652 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
653 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
654 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
655 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
656 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
657 "field_close_on_enter[prob;false]"
658 return form
659 end,
660 handle = function(self, pos, name, fields)
661 if fields.submit then
662 local prob = tonumber(fields.prob)
663 if prob then
664 local player = minetest.get_player_by_name(name)
665 if not player then
666 return
668 local probtool = player:get_wielded_item()
669 if probtool:get_name() ~= "schemedit:probtool" then
670 return
673 local force_place = self.force_place == true
675 set_item_metadata(probtool, prob, force_place)
677 -- Repurpose the tool's wear bar to display the set probability
678 probtool:set_wear(math.floor(((255-prob)/255)*65535))
680 player:set_wielded_item(probtool)
683 if fields.force_place == "true" then
684 self.force_place = true
685 elseif fields.force_place == "false" then
686 self.force_place = false
688 end,
692 --- API
695 --- Copies and modifies positions `pos1` and `pos2` so that each component of
696 -- `pos1` is less than or equal to the corresponding component of `pos2`.
697 -- Returns the new positions.
698 function schemedit.sort_pos(pos1, pos2)
699 if not pos1 or not pos2 then
700 return
703 pos1, pos2 = table.copy(pos1), table.copy(pos2)
704 if pos1.x > pos2.x then
705 pos2.x, pos1.x = pos1.x, pos2.x
707 if pos1.y > pos2.y then
708 pos2.y, pos1.y = pos1.y, pos2.y
710 if pos1.z > pos2.z then
711 pos2.z, pos1.z = pos1.z, pos2.z
713 return pos1, pos2
716 -- [function] Prepare size
717 function schemedit.size(pos)
718 local pos1 = vector.new(pos)
719 local meta = minetest.get_meta(pos)
720 local node = minetest.get_node(pos)
721 local param2 = node.param2
722 local size = {
723 x = meta:get_int("x_size"),
724 y = math.max(meta:get_int("y_size") - 1, 0),
725 z = meta:get_int("z_size"),
728 if param2 == 1 then
729 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
730 pos1.x = pos1.x + 1
731 new_pos.z = new_pos.z + 1
732 return pos1, new_pos
733 elseif param2 == 2 then
734 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
735 pos1.z = pos1.z - 1
736 new_pos.x = new_pos.x + 1
737 return pos1, new_pos
738 elseif param2 == 3 then
739 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
740 pos1.x = pos1.x - 1
741 new_pos.z = new_pos.z - 1
742 return pos1, new_pos
743 else
744 local new_pos = vector.add(size, pos)
745 pos1.z = pos1.z + 1
746 new_pos.x = new_pos.x - 1
747 return pos1, new_pos
751 -- [function] Mark region
752 function schemedit.mark(pos)
753 schemedit.unmark(pos)
755 local id = minetest.hash_node_position(pos)
756 local owner = minetest.get_meta(pos):get_string("owner")
757 local pos1, pos2 = schemedit.size(pos)
758 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
760 local thickness = 0.2
761 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
762 local m = {}
763 local low = true
764 local offset
766 -- XY plane markers
767 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
768 if low then
769 offset = -0.01
770 else
771 offset = 0.01
773 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
774 if marker ~= nil then
775 marker:set_properties({
776 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
778 marker:get_luaentity().id = id
779 marker:get_luaentity().owner = owner
780 table.insert(m, marker)
782 low = false
785 low = true
786 -- YZ plane markers
787 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
788 if low then
789 offset = -0.01
790 else
791 offset = 0.01
794 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
795 if marker ~= nil then
796 marker:set_properties({
797 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
799 marker:set_rotation({x=0, y=math.pi / 2, z=0})
800 marker:get_luaentity().id = id
801 marker:get_luaentity().owner = owner
802 table.insert(m, marker)
804 low = false
807 low = true
808 -- XZ plane markers
809 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
810 if low then
811 offset = -0.01
812 else
813 offset = 0.01
816 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
817 if marker ~= nil then
818 marker:set_properties({
819 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
821 marker:set_rotation({x=math.pi/2, y=0, z=0})
822 marker:get_luaentity().id = id
823 marker:get_luaentity().owner = owner
824 table.insert(m, marker)
826 low = false
831 schemedit.markers[id] = m
832 return true
835 -- [function] Unmark region
836 function schemedit.unmark(pos)
837 local id = minetest.hash_node_position(pos)
838 if schemedit.markers[id] then
839 local retval
840 for _, entity in ipairs(schemedit.markers[id]) do
841 entity:remove()
842 retval = true
844 return retval
849 --- Mark node probability values near player
852 -- Show probability and force_place status of a particular position for player in HUD.
853 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
854 -- The distance to the node is also displayed below that. This can't be avoided and is
855 -- and artifact of the waypoint HUD element.
856 function schemedit.display_node_prob(player, pos, prob, force_place)
857 local wpstring
858 if prob and force_place == true then
859 wpstring = string.format("%s [F]", prob)
860 elseif prob and type(tonumber(prob)) == "number" then
861 wpstring = prob
862 elseif force_place == true then
863 wpstring = "[F]"
865 if wpstring then
866 return player:hud_add({
867 hud_elem_type = "waypoint",
868 name = wpstring,
869 precision = 0,
870 text = "m", -- For the distance artifact
871 number = text_color_number,
872 world_pos = pos,
877 -- Display the node probabilities and force_place status of the nodes in a region.
878 -- By default, this is done for nodes near the player (distance: 5).
879 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
880 function schemedit.display_node_probs_region(player, pos1, pos2)
881 local playername = player:get_player_name()
882 local pos = vector.round(player:get_pos())
884 local dist = 5
885 -- Default: 5 nodes away from player in any direction
886 if not pos1 then
887 pos1 = vector.subtract(pos, dist)
889 if not pos2 then
890 pos2 = vector.add(pos, dist)
892 for x=pos1.x, pos2.x do
893 for y=pos1.y, pos2.y do
894 for z=pos1.z, pos2.z do
895 local checkpos = {x=x, y=y, z=z}
896 local nodehash = minetest.hash_node_position(checkpos)
898 -- If node is already displayed, remove it so it can re replaced later
899 if displayed_waypoints[playername][nodehash] then
900 player:hud_remove(displayed_waypoints[playername][nodehash])
901 displayed_waypoints[playername][nodehash] = nil
904 local prob, force_place
905 local meta = minetest.get_meta(checkpos)
906 prob = meta:get_string("schemedit_prob")
907 force_place = meta:get_string("schemedit_force_place") == "true"
908 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
909 if hud_id then
910 displayed_waypoints[playername][nodehash] = hud_id
911 displayed_waypoints[playername].display_active = true
918 -- Remove all active displayed node statuses.
919 function schemedit.clear_displayed_node_probs(player)
920 local playername = player:get_player_name()
921 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
922 player:hud_remove(hud_id)
923 displayed_waypoints[playername][nodehash] = nil
924 displayed_waypoints[playername].display_active = false
928 minetest.register_on_joinplayer(function(player)
929 displayed_waypoints[player:get_player_name()] = {
930 display_active = false -- If true, there *might* be at least one active node prob HUD display
931 -- If false, no node probabilities are displayed for sure.
933 end)
935 minetest.register_on_leaveplayer(function(player)
936 displayed_waypoints[player:get_player_name()] = nil
937 end)
939 -- Regularily clear the displayed node probabilities and force_place
940 -- for all players who do not wield the probtool.
941 -- This makes sure the screen is not spammed with information when it
942 -- isn't needed.
943 local cleartimer = 0
944 minetest.register_globalstep(function(dtime)
945 cleartimer = cleartimer + dtime
946 if cleartimer > 2 then
947 local players = minetest.get_connected_players()
948 for p = 1, #players do
949 local player = players[p]
950 local pname = player:get_player_name()
951 if displayed_waypoints[pname].display_active then
952 local item = player:get_wielded_item()
953 if item:get_name() ~= "schemedit:probtool" then
954 schemedit.clear_displayed_node_probs(player)
958 cleartimer = 0
960 end)
963 --- Registrations
966 -- [priv] schematic_override
967 minetest.register_privilege("schematic_override", {
968 description = S("Allows you to access schemedit nodes not owned by you"),
969 give_to_singleplayer = false,
972 local help_import = ""
973 if can_import then
974 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"
977 -- [node] Schematic creator
978 minetest.register_node("schemedit:creator", {
979 description = S("Schematic Creator"),
980 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
981 _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"..
982 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"..
983 help_import..
984 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
985 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"..
986 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."),
987 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
988 "schemedit_creator_sides.png"},
989 groups = { dig_immediate = 2},
990 paramtype2 = "facedir",
991 is_ground_content = false,
993 after_place_node = function(pos, player)
994 local name = player:get_player_name()
995 local meta = minetest.get_meta(pos)
997 meta:set_string("owner", name)
998 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
999 meta:set_string("prob_list", minetest.serialize({}))
1000 meta:set_string("slices", minetest.serialize({}))
1002 local node = minetest.get_node(pos)
1003 local dir = minetest.facedir_to_dir(node.param2)
1005 meta:set_int("x_size", 1)
1006 meta:set_int("y_size", 1)
1007 meta:set_int("z_size", 1)
1009 -- Don't take item from itemstack
1010 return true
1011 end,
1012 can_dig = function(pos, player)
1013 local name = player:get_player_name()
1014 local meta = minetest.get_meta(pos)
1015 if meta:get_string("owner") == name or
1016 minetest.check_player_privs(player, "schematic_override") == true then
1017 return true
1020 return false
1021 end,
1022 on_rightclick = function(pos, node, player)
1023 local meta = minetest.get_meta(pos)
1024 local name = player:get_player_name()
1025 if meta:get_string("owner") == name or
1026 minetest.check_player_privs(player, "schematic_override") == true then
1027 -- Get player attribute
1028 local tab = player:get_attribute("schemedit:tab")
1029 if not forms[tab] or not tab then
1030 tab = "main"
1033 schemedit.show_formspec(pos, player, tab, true)
1035 end,
1036 after_destruct = function(pos)
1037 schemedit.unmark(pos)
1038 end,
1040 -- No support for Minetest Game's screwdriver
1041 on_rotate = false,
1044 minetest.register_tool("schemedit:probtool", {
1045 description = S("Schematic Node Probability Tool"),
1046 _doc_items_longdesc =
1047 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"..
1048 S("It allows you to set two things:").."\n"..
1049 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1050 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1051 _doc_items_usagehelp = "\n"..
1052 S("BASIC USAGE:").."\n"..
1053 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"..
1054 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"..
1055 S("NODE HUD:").."\n"..
1056 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"..
1057 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1058 S("UPDATING THE NODE HUD:").."\n"..
1059 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."),
1060 wield_image = "schemedit_probtool.png",
1061 inventory_image = "schemedit_probtool.png",
1062 liquids_pointable = true,
1063 groups = { disable_repair = 1 },
1064 on_use = function(itemstack, user, pointed_thing)
1065 local ctrl = user:get_player_control()
1066 -- Simple use
1067 if not ctrl.sneak then
1068 -- Open dialog to change the probability to apply to nodes
1069 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1071 -- Use + sneak
1072 else
1073 -- Display the probability and force_place values for nodes.
1075 -- If a schematic creator was punched, only enable display for all nodes
1076 -- within the creator's region.
1077 local use_creator_region = false
1078 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1079 local punchpos = pointed_thing.under
1080 local node = minetest.get_node(punchpos)
1081 if node.name == "schemedit:creator" then
1082 local pos1, pos2 = schemedit.size(punchpos)
1083 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1084 schemedit.display_node_probs_region(user, pos1, pos2)
1085 return
1089 -- Otherwise, just display the region close to the player
1090 schemedit.display_node_probs_region(user)
1092 end,
1093 on_secondary_use = function(itemstack, user, pointed_thing)
1094 schemedit.clear_displayed_node_probs(user)
1095 end,
1096 -- Set note probability and force_place and enable node probability display
1097 on_place = function(itemstack, placer, pointed_thing)
1098 -- Use pointed node's on_rightclick function first, if present
1099 local node = minetest.get_node(pointed_thing.under)
1100 if placer and not placer:get_player_control().sneak then
1101 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1102 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1106 -- This sets the node probability of pointed node to the
1107 -- currently used probability stored in the tool.
1108 local pos = pointed_thing.under
1109 local node = minetest.get_node(pos)
1110 -- Schematic void are ignored, they always have probability 0
1111 if node.name == "schemedit:void" then
1112 return itemstack
1114 local nmeta = minetest.get_meta(pos)
1115 local imeta = itemstack:get_meta()
1116 local prob = tonumber(imeta:get_string("schemedit_prob"))
1117 local force_place = imeta:get_string("schemedit_force_place")
1119 if not prob or prob == 255 then
1120 nmeta:set_string("schemedit_prob", nil)
1121 else
1122 nmeta:set_string("schemedit_prob", prob)
1124 if force_place == "true" then
1125 nmeta:set_string("schemedit_force_place", "true")
1126 else
1127 nmeta:set_string("schemedit_force_place", nil)
1130 -- Enable node probablity display
1131 schemedit.display_node_probs_region(placer)
1133 return itemstack
1134 end,
1137 minetest.register_node("schemedit:void", {
1138 description = S("Schematic Void"),
1139 _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."),
1140 _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."),
1141 tiles = { "schemedit_void.png" },
1142 drawtype = "nodebox",
1143 is_ground_content = false,
1144 paramtype = "light",
1145 walkable = false,
1146 sunlight_propagates = true,
1147 node_box = {
1148 type = "fixed",
1149 fixed = {
1150 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1153 groups = { dig_immediate = 3},
1156 -- [entity] Visible schematic border
1157 minetest.register_entity("schemedit:display", {
1158 visual = "upright_sprite",
1159 textures = {"schemedit_border.png"},
1160 visual_size = {x=10, y=10},
1161 pointable = false,
1162 physical = false,
1163 static_save = false,
1164 glow = minetest.LIGHT_MAX,
1166 on_step = function(self, dtime)
1167 if not self.id then
1168 self.object:remove()
1169 elseif not schemedit.markers[self.id] then
1170 self.object:remove()
1172 end,
1173 on_activate = function(self)
1174 self.object:set_armor_groups({immortal = 1})
1175 end,
1178 minetest.register_lbm({
1179 label = "Reset schematic creator border entities",
1180 name = "schemedit:reset_border",
1181 nodenames = "schemedit:creator",
1182 run_at_every_load = true,
1183 action = function(pos, node)
1184 local meta = minetest.get_meta(pos)
1185 meta:set_string("schem_border", "false")
1186 end,
1189 local function add_suffix(schem)
1190 -- Automatically add file name suffix if omitted
1191 local schem_full, schem_lua
1192 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1193 schem_full = schem
1194 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1195 else
1196 schem_full = schem .. ".mts"
1197 schem_lua = schem .. ".lua"
1199 return schem_full, schem_lua
1202 -- [chatcommand] Place schematic
1203 minetest.register_chatcommand("placeschem", {
1204 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1205 privs = {debug = true},
1206 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1207 func = function(name, param)
1208 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1209 local pos = minetest.string_to_pos(p)
1211 if not schem then
1212 return false, S("No schematic file specified.")
1215 if not pos then
1216 pos = minetest.get_player_by_name(name):get_pos()
1219 local schem_full, schem_lua = add_suffix(schem)
1220 local success = false
1221 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1222 if minetest.read_schematic then
1223 -- We don't call minetest.place_schematic with the path name directly because
1224 -- this would trigger the caching and we wouldn't get any updates to the schematic
1225 -- files when we reload. minetest.read_schematic circumvents that.
1226 local schematic = minetest.read_schematic(schem_path, {})
1227 if schematic then
1228 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1230 else
1231 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1232 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1235 if success == nil then
1236 return false, S("Schematic file could not be loaded!")
1237 else
1238 return true
1240 end,
1243 if can_import then
1244 -- [chatcommand] Convert MTS schematic file to .lua file
1245 minetest.register_chatcommand("mts2lua", {
1246 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1247 privs = {debug = true},
1248 params = S("<schematic name>[.mts] [comments]"),
1249 func = function(name, param)
1250 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1252 if not schem then
1253 return false, S("No schematic file specified.")
1256 local comments = comments_str == "comments"
1258 -- Automatically add file name suffix if omitted
1259 local schem_full, schem_lua = add_suffix(schem)
1260 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1261 local schematic = minetest.read_schematic(schem_path, {})
1263 if schematic then
1264 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1265 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1266 local file = io.open(lua_path, "w")
1267 if file and str then
1268 file:write(str)
1269 file:flush()
1270 file:close()
1271 return true, S("Exported schematic to @1", lua_path)
1272 else
1273 return false, S("Failed!")
1276 end,