Version 1.5.1
[minetest_schemedit.git] / init.lua
blobe1e24f51f856b824ca1aa34b5ae019d0b262fe1c
1 local S
2 if minetest.get_translator then
3 S = minetest.get_translator("schemedit")
4 else
5 S = function(s) return s end
6 end
7 local F = minetest.formspec_escape
9 local schemedit = {}
11 local DIR_DELIM = "/"
13 -- Set to true to enable `make_schemedit_readme` command
14 local MAKE_README = false
16 local export_path_full = table.concat({minetest.get_worldpath(), "schems"}, DIR_DELIM)
18 -- truncated export path so the server directory structure is not exposed publicly
19 local export_path_trunc = table.concat({S("<world path>"), "schems"}, DIR_DELIM)
21 local text_color = "#D79E9E"
22 local text_color_number = 0xD79E9E
24 local can_import = minetest.read_schematic ~= nil
26 schemedit.markers = {}
28 -- [local function] Renumber table
29 local function renumber(t)
30 local res = {}
31 for _, i in pairs(t) do
32 res[#res + 1] = i
33 end
34 return res
35 end
37 local NEEDED_PRIV = "server"
38 local function check_priv(player_name, quit)
39 local privs = minetest.get_player_privs(player_name)
40 if privs[NEEDED_PRIV] then
41 return true
42 else
43 if not quit then
44 minetest.chat_send_player(player_name, minetest.colorize("red",
45 S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV)))
46 end
47 return false
48 end
49 end
51 -- Lua export
52 local export_schematic_to_lua
53 if can_import then
54 export_schematic_to_lua = function(schematic, filepath, options)
55 if not options then options = {} end
56 local str = minetest.serialize_schematic(schematic, "lua", options)
57 local file = io.open(filepath, "w")
58 if file and str then
59 file:write(str)
60 file:flush()
61 file:close()
62 return true
63 else
64 return false
65 end
66 end
67 end
69 ---
70 --- Formspec API
71 ---
73 local contexts = {}
74 local form_data = {}
75 local tabs = {}
76 local forms = {}
77 local displayed_waypoints = {}
79 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
80 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
81 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
82 -- on an actual export to a schematic.
84 function schemedit.lua_prob_to_schematic_prob(lua_prob)
85 return math.floor(lua_prob / 2)
86 end
88 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
89 return schematic_prob * 2
91 end
93 -- [function] Add form
94 function schemedit.add_form(name, def)
95 def.name = name
96 forms[name] = def
98 if def.tab then
99 tabs[#tabs + 1] = name
103 -- [function] Generate tabs
104 function schemedit.generate_tabs(current)
105 local retval = "tabheader[0,0;tabs;"
106 for _, t in pairs(tabs) do
107 local f = forms[t]
108 if f.tab ~= false and f.caption then
109 retval = retval..f.caption..","
111 if type(current) ~= "number" and current == f.name then
112 current = _
116 retval = retval:sub(1, -2) -- Strip last comma
117 retval = retval..";"..current.."]" -- Close tabheader
118 return retval
121 -- [function] Handle tabs
122 function schemedit.handle_tabs(pos, name, fields)
123 local tab = tonumber(fields.tabs)
124 if tab and tabs[tab] and forms[tabs[tab]] then
125 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
126 return true
130 -- [function] Show formspec
131 function schemedit.show_formspec(pos, player, tab, show, ...)
132 if forms[tab] then
133 if type(player) == "string" then
134 player = minetest.get_player_by_name(player)
136 local name = player:get_player_name()
138 if show ~= false then
139 if not form_data[name] then
140 form_data[name] = {}
143 local form = forms[tab].get(form_data[name], pos, name, ...)
144 if forms[tab].tab then
145 form = form..schemedit.generate_tabs(tab)
148 minetest.show_formspec(name, "schemedit:"..tab, form)
149 contexts[name] = pos
151 -- Update player attribute
152 if forms[tab].cache_name ~= false then
153 local pmeta = player:get_meta()
154 pmeta:set_string("schemedit:tab", tab)
156 else
157 minetest.close_formspec(pname, "schemedit:"..tab)
162 -- [event] On receive fields
163 minetest.register_on_player_receive_fields(function(player, formname, fields)
164 local formname = formname:split(":")
166 if formname[1] == "schemedit" and forms[formname[2]] then
167 local handle = forms[formname[2]].handle
168 local name = player:get_player_name()
169 if contexts[name] then
170 if not form_data[name] then
171 form_data[name] = {}
174 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
175 handle(form_data[name], contexts[name], name, fields)
179 end)
181 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
182 schemedit.scan_metadata = function(pos1, pos2)
183 local prob_list = {}
185 for x=pos1.x, pos2.x do
186 for y=pos1.y, pos2.y do
187 for z=pos1.z, pos2.z do
188 local scanpos = {x=x, y=y, z=z}
189 local node = minetest.get_node_or_nil(scanpos)
191 local prob, force_place
192 if node == nil or node.name == "schemedit:void" then
193 prob = 0
194 force_place = false
195 else
196 local meta = minetest.get_meta(scanpos)
198 prob = tonumber(meta:get_string("schemedit_prob")) or 255
199 local fp = meta:get_string("schemedit_force_place")
200 if fp == "true" then
201 force_place = true
202 else
203 force_place = false
207 local hashpos = minetest.hash_node_position(scanpos)
208 prob_list[hashpos] = {
209 pos = scanpos,
210 prob = prob,
211 force_place = force_place,
217 return prob_list
220 -- Sets probability and force_place metadata of an item.
221 -- Also updates item description.
222 -- The itemstack is updated in-place.
223 local function set_item_metadata(itemstack, prob, force_place)
224 local smeta = itemstack:get_meta()
225 local prob_desc = "\n"..S("Probability: @1", prob or
226 smeta:get_string("schemedit_prob") or S("Not Set"))
227 -- Update probability
228 if prob and prob >= 0 and prob < 255 then
229 smeta:set_string("schemedit_prob", tostring(prob))
230 elseif prob and prob == 255 then
231 -- Clear prob metadata for default probability
232 prob_desc = ""
233 smeta:set_string("schemedit_prob", nil)
234 else
235 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
236 S("Not Set"))
239 -- Update force place
240 if force_place == true then
241 smeta:set_string("schemedit_force_place", "true")
242 elseif force_place == false then
243 smeta:set_string("schemedit_force_place", nil)
246 -- Update description
247 local desc = minetest.registered_items[itemstack:get_name()].description
248 local meta_desc = smeta:get_string("description")
249 if meta_desc and meta_desc ~= "" then
250 desc = meta_desc
253 local original_desc = smeta:get_string("original_description")
254 if original_desc and original_desc ~= "" then
255 desc = original_desc
256 else
257 smeta:set_string("original_description", desc)
260 local force_desc = ""
261 if smeta:get_string("schemedit_force_place") == "true" then
262 force_desc = "\n"..S("Force placement")
265 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
267 smeta:set_string("description", desc)
269 return itemstack
273 --- Formspec Tabs
275 local import_btn = ""
276 if can_import then
277 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
279 schemedit.add_form("main", {
280 tab = true,
281 caption = S("Main"),
282 get = function(self, pos, name)
283 local meta = minetest.get_meta(pos):to_table().fields
284 local strpos = minetest.pos_to_string(pos)
285 local hashpos = minetest.hash_node_position(pos)
287 local border_button
288 if meta.schem_border == "true" and schemedit.markers[hashpos] then
289 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
290 else
291 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
294 local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
295 local size = {x=xs, y=ys, z=zs}
296 local schem_name = meta.schem_name or ""
298 local form = [[
299 size[7,8]
300 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
301 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
302 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name)))..[[]
303 label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
305 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name or "")..[[]
306 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
307 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
308 field_close_on_enter[name;false]
310 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
311 import_btn..[[
312 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
313 export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
314 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
315 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
316 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
317 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
318 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
319 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
320 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
321 field_close_on_enter[x;false]
322 field_close_on_enter[y;false]
323 field_close_on_enter[z;false]
324 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
325 ]]..
326 border_button
327 if minetest.get_modpath("doc") then
328 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
329 "tooltip[doc;"..F(S("Help")).."]"
331 return form
332 end,
333 handle = function(self, pos, name, fields)
334 if fields.doc then
335 doc.show_entry(name, "nodes", "schemedit:creator", true)
336 return
339 if not check_priv(name, fields.quit) then
340 return
343 local realmeta = minetest.get_meta(pos)
344 local meta = realmeta:to_table().fields
345 local hashpos = minetest.hash_node_position(pos)
347 -- Save size vector values
348 if (fields.x and fields.x ~= "") then
349 local x = tonumber(fields.x)
350 if x then
351 meta.x_size = math.max(x, 1)
354 if (fields.y and fields.y ~= "") then
355 local y = tonumber(fields.y)
356 if y then
357 meta.y_size = math.max(y, 1)
360 if (fields.z and fields.z ~= "") then
361 local z = tonumber(fields.z)
362 if z then
363 meta.z_size = math.max(z, 1)
367 -- Save schematic name
368 if fields.name then
369 meta.schem_name = fields.name
372 -- Node conversion
373 if (fields.air2void) then
374 local pos1, pos2 = schemedit.size(pos)
375 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
376 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"air"})
377 minetest.bulk_set_node(nodes, {name="schemedit:void"})
378 return
379 elseif (fields.void2air) then
380 local pos1, pos2 = schemedit.size(pos)
381 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
382 local nodes = minetest.find_nodes_in_area(pos1, pos2, {"schemedit:void"})
383 minetest.bulk_set_node(nodes, {name="air"})
384 return
387 -- Toggle border
388 if fields.border then
389 if meta.schem_border == "true" and schemedit.markers[hashpos] then
390 schemedit.unmark(pos)
391 meta.schem_border = "false"
392 else
393 schemedit.mark(pos)
394 meta.schem_border = "true"
398 -- Export schematic
399 if fields.export and meta.schem_name and meta.schem_name ~= "" then
400 local pos1, pos2 = schemedit.size(pos)
401 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
402 local path = export_path_full .. DIR_DELIM
403 minetest.mkdir(path)
405 local plist = schemedit.scan_metadata(pos1, pos2)
406 local probability_list = {}
407 for hash, i in pairs(plist) do
408 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
409 if i.force_place == true then
410 prob = prob + 128
413 table.insert(probability_list, {
414 pos = minetest.get_position_from_hash(hash),
415 prob = prob,
419 local slist = minetest.deserialize(meta.slices)
420 local slice_list = {}
421 for _, i in pairs(slist) do
422 slice_list[#slice_list + 1] = {
423 ypos = i.ypos,
424 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
428 local filepath = path..meta.schem_name..".mts"
429 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
431 if res then
432 minetest.chat_send_player(name, minetest.colorize("#00ff00",
433 S("Exported schematic to @1", filepath)))
434 -- Additional export to Lua file if MTS export was successful
435 local schematic = minetest.read_schematic(filepath, {})
436 if schematic and minetest.settings:get_bool("schemedit_export_lua") then
437 local filepath_lua = path..meta.schem_name..".lua"
438 res = export_schematic_to_lua(schematic, filepath_lua)
439 if res then
440 minetest.chat_send_player(name, minetest.colorize("#00ff00",
441 S("Exported schematic to @1", filepath_lua)))
444 else
445 minetest.chat_send_player(name, minetest.colorize("red",
446 S("Failed to export schematic to @1", filepath)))
450 -- Import schematic
451 if fields.import and meta.schem_name and meta.schem_name ~= "" then
452 if not can_import then
453 return
455 local pos1
456 local node = minetest.get_node(pos)
457 local path = export_path_full .. DIR_DELIM
459 local filepath = path..meta.schem_name..".mts"
460 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
461 local success = false
463 if schematic then
464 meta.x_size = schematic.size.x
465 meta.y_size = schematic.size.y
466 meta.z_size = schematic.size.z
467 meta.slices = minetest.serialize(renumber(schematic.yslice_prob))
468 local special_x_size = meta.x_size
469 local special_y_size = meta.y_size
470 local special_z_size = meta.z_size
472 if node.param2 == 1 then
473 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
474 meta.x_size, meta.z_size = meta.z_size, meta.x_size
475 elseif node.param2 == 2 then
476 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
477 elseif node.param2 == 3 then
478 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
479 meta.x_size, meta.z_size = meta.z_size, meta.x_size
480 else
481 pos1 = vector.add(pos, {x=0,y=0,z=1})
484 local schematic_for_meta = table.copy(schematic)
485 -- Strip probability data for placement
486 schematic.yslice_prob = {}
487 for d=1, #schematic.data do
488 schematic.data[d].prob = nil
491 -- Place schematic
492 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
494 -- Add special schematic data to nodes
495 if success then
496 local d = 1
497 for z=0, special_z_size-1 do
498 for y=0, special_y_size-1 do
499 for x=0, special_x_size-1 do
500 local data = schematic_for_meta.data[d]
501 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
502 if data.prob == 0 then
503 minetest.set_node(pp, {name="schemedit:void"})
504 else
505 local meta = minetest.get_meta(pp)
506 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
507 meta:set_string("schemedit_prob", tostring(data.prob))
508 else
509 meta:set_string("schemedit_prob", "")
511 if data.force_place then
512 meta:set_string("schemedit_force_place", "true")
513 else
514 meta:set_string("schemedit_force_place", "")
517 d = d + 1
523 if success then
524 minetest.chat_send_player(name, minetest.colorize("#00ff00",
525 S("Imported schematic from @1", filepath)))
526 else
527 minetest.chat_send_player(name, minetest.colorize("red",
528 S("Failed to import schematic from @1", filepath)))
534 -- Save meta before updating visuals
535 local inv = realmeta:get_inventory():get_lists()
536 realmeta:from_table({fields = meta, inventory = inv})
538 -- Update border
539 if not fields.border and meta.schem_border == "true" then
540 schemedit.mark(pos)
543 -- Update formspec
544 if not fields.quit then
545 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
547 end,
550 schemedit.add_form("slice", {
551 caption = S("Y Slices"),
552 tab = true,
553 get = function(self, pos, name, visible_panel)
554 local meta = minetest.get_meta(pos):to_table().fields
556 self.selected = self.selected or 1
557 local selected = tostring(self.selected)
558 local slice_list = minetest.deserialize(meta.slices)
559 local slices = ""
560 for _, i in pairs(slice_list) do
561 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
562 slices = slices..insert..","
564 slices = slices:sub(1, -2) -- Remove final comma
566 local form = [[
567 size[7,8]
568 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
571 -- Close edit panel if no slices
572 if self.panel_edit and slices == "" then
573 self.panel_edit = nil
576 if self.panel_add or self.panel_edit then
577 local ypos_default, prob_default = "", ""
578 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Add")).."]"
579 if self.panel_edit then
580 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Apply")).."]"
581 if slice_list[self.selected] then
582 ypos_default = slice_list[self.selected].ypos
583 prob_default = slice_list[self.selected].prob
587 local field_ypos = ""
588 if self.panel_add then
589 field_ypos = "field[0.3,7.5;2.5,1;ypos;"..F(S("Y position (max. @1):", (meta.y_size - 1)))..";"..ypos_default.."]"
592 form = form..[[
593 ]]..field_ypos..[[
594 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
595 field_close_on_enter[ypos;false]
596 field_close_on_enter[prob;false]
597 ]]..done_button
600 if not self.panel_edit then
601 if self.panel_add then
602 form = form.."button[0,6;2.4,1;add;"..F(S("Cancel")).."]"
603 else
604 form = form.."button[0,6;2.4,1;add;"..F(S("Add slice")).."]"
608 if slices ~= "" and self.selected and not self.panel_add then
609 if not self.panel_edit then
610 form = form..[[
611 button[2.4,6;2.4,1;remove;]]..F(S("Remove slice"))..[[]
612 button[4.8,6;2.4,1;edit;]]..F(S("Edit slice"))..[[]
614 else
615 form = form..[[
616 button[4.8,6;2.4,1;edit;]]..F(S("Back"))..[[]
621 return form
622 end,
623 handle = function(self, pos, name, fields)
624 if not check_priv(name, fields.quit) then
625 return
628 local meta = minetest.get_meta(pos)
629 local player = minetest.get_player_by_name(name)
631 if fields.slices then
632 local slices = fields.slices:split(":")
633 self.selected = tonumber(slices[2])
636 if fields.add then
637 if not self.panel_add then
638 self.panel_add = true
639 schemedit.show_formspec(pos, player, "slice")
640 else
641 self.panel_add = nil
642 schemedit.show_formspec(pos, player, "slice")
646 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
647 if fields.done_edit then
648 ypos = 0
650 if (fields.done_add or fields.done_edit) and ypos and prob and
651 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
652 local slice_list = minetest.deserialize(meta:get_string("slices"))
653 local index = #slice_list + 1
654 if fields.done_edit then
655 index = self.selected
658 local dupe = false
659 if fields.done_add then
660 for k,v in pairs(slice_list) do
661 if v.ypos == ypos then
662 v.prob = prob
663 dupe = true
667 if fields.done_edit and slice_list[index] then
668 ypos = slice_list[index].ypos
670 if not dupe then
671 slice_list[index] = {ypos = ypos, prob = prob}
674 meta:set_string("slices", minetest.serialize(slice_list))
676 -- Update and show formspec
677 self.panel_add = nil
678 schemedit.show_formspec(pos, player, "slice")
681 if fields.remove and self.selected then
682 local slice_list = minetest.deserialize(meta:get_string("slices"))
683 slice_list[self.selected] = nil
684 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
686 -- Update formspec
687 self.selected = math.max(1, self.selected-1)
688 self.panel_edit = nil
689 schemedit.show_formspec(pos, player, "slice")
692 if fields.edit then
693 if not self.panel_edit then
694 self.panel_edit = true
695 schemedit.show_formspec(pos, player, "slice")
696 else
697 self.panel_edit = nil
698 schemedit.show_formspec(pos, player, "slice")
701 end,
704 schemedit.add_form("probtool", {
705 cache_name = false,
706 caption = S("Schematic Node Probability Tool"),
707 get = function(self, pos, name)
708 local player = minetest.get_player_by_name(name)
709 if not player then
710 return
712 local probtool = player:get_wielded_item()
713 if probtool:get_name() ~= "schemedit:probtool" then
714 return
717 local meta = probtool:get_meta()
718 local prob = tonumber(meta:get_string("schemedit_prob"))
719 local force_place = meta:get_string("schemedit_force_place")
721 if not prob then
722 prob = 255
724 if force_place == nil or force_place == "" then
725 force_place = "false"
727 local form = "size[5,4]"..
728 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
729 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
730 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
731 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
732 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
733 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
734 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
735 "field_close_on_enter[prob;false]"
736 return form
737 end,
738 handle = function(self, pos, name, fields)
739 if not check_priv(name, fields.quit) then
740 return
743 if fields.submit then
744 local prob = tonumber(fields.prob)
745 if prob then
746 local player = minetest.get_player_by_name(name)
747 if not player then
748 return
750 local probtool = player:get_wielded_item()
751 if probtool:get_name() ~= "schemedit:probtool" then
752 return
755 local force_place = self.force_place == true
757 set_item_metadata(probtool, prob, force_place)
759 -- Repurpose the tool's wear bar to display the set probability
760 probtool:set_wear(math.floor(((255-prob)/255)*65535))
762 player:set_wielded_item(probtool)
765 if fields.force_place == "true" then
766 self.force_place = true
767 elseif fields.force_place == "false" then
768 self.force_place = false
770 end,
774 --- API
777 --- Copies and modifies positions `pos1` and `pos2` so that each component of
778 -- `pos1` is less than or equal to the corresponding component of `pos2`.
779 -- Returns the new positions.
780 function schemedit.sort_pos(pos1, pos2)
781 if not pos1 or not pos2 then
782 return
785 pos1, pos2 = table.copy(pos1), table.copy(pos2)
786 if pos1.x > pos2.x then
787 pos2.x, pos1.x = pos1.x, pos2.x
789 if pos1.y > pos2.y then
790 pos2.y, pos1.y = pos1.y, pos2.y
792 if pos1.z > pos2.z then
793 pos2.z, pos1.z = pos1.z, pos2.z
795 return pos1, pos2
798 -- [function] Prepare size
799 function schemedit.size(pos)
800 local pos1 = vector.new(pos)
801 local meta = minetest.get_meta(pos)
802 local node = minetest.get_node(pos)
803 local param2 = node.param2
804 local size = {
805 x = meta:get_int("x_size"),
806 y = math.max(meta:get_int("y_size") - 1, 0),
807 z = meta:get_int("z_size"),
810 if param2 == 1 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 elseif param2 == 2 then
816 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
817 pos1.z = pos1.z - 1
818 new_pos.x = new_pos.x + 1
819 return pos1, new_pos
820 elseif param2 == 3 then
821 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
822 pos1.x = pos1.x - 1
823 new_pos.z = new_pos.z - 1
824 return pos1, new_pos
825 else
826 local new_pos = vector.add(size, pos)
827 pos1.z = pos1.z + 1
828 new_pos.x = new_pos.x - 1
829 return pos1, new_pos
833 -- [function] Mark region
834 function schemedit.mark(pos)
835 schemedit.unmark(pos)
837 local id = minetest.hash_node_position(pos)
838 local owner = minetest.get_meta(pos):get_string("owner")
839 local pos1, pos2 = schemedit.size(pos)
840 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
842 local thickness = 0.2
843 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
844 local m = {}
845 local low = true
846 local offset
848 -- XY plane markers
849 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
850 if low then
851 offset = -0.01
852 else
853 offset = 0.01
855 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
856 if marker ~= nil then
857 marker:set_properties({
858 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
860 marker:get_luaentity().id = id
861 marker:get_luaentity().owner = owner
862 table.insert(m, marker)
864 low = false
867 low = true
868 -- YZ plane markers
869 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
870 if low then
871 offset = -0.01
872 else
873 offset = 0.01
876 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
877 if marker ~= nil then
878 marker:set_properties({
879 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
881 marker:set_rotation({x=0, y=math.pi / 2, z=0})
882 marker:get_luaentity().id = id
883 marker:get_luaentity().owner = owner
884 table.insert(m, marker)
886 low = false
889 low = true
890 -- XZ plane markers
891 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
892 if low then
893 offset = -0.01
894 else
895 offset = 0.01
898 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
899 if marker ~= nil then
900 marker:set_properties({
901 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
903 marker:set_rotation({x=math.pi/2, y=0, z=0})
904 marker:get_luaentity().id = id
905 marker:get_luaentity().owner = owner
906 table.insert(m, marker)
908 low = false
913 schemedit.markers[id] = m
914 return true
917 -- [function] Unmark region
918 function schemedit.unmark(pos)
919 local id = minetest.hash_node_position(pos)
920 if schemedit.markers[id] then
921 local retval
922 for _, entity in ipairs(schemedit.markers[id]) do
923 entity:remove()
924 retval = true
926 return retval
931 --- Mark node probability values near player
934 -- Show probability and force_place status of a particular position for player in HUD.
935 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
936 function schemedit.display_node_prob(player, pos, prob, force_place)
937 local wpstring
938 if prob and force_place == true then
939 wpstring = string.format("%s [F]", prob)
940 elseif prob and type(tonumber(prob)) == "number" then
941 wpstring = prob
942 elseif force_place == true then
943 wpstring = "[F]"
945 if wpstring then
946 return player:hud_add({
947 hud_elem_type = "waypoint",
948 name = wpstring,
949 precision = 0,
950 text = "m", -- For the distance artifact [legacy]
951 number = text_color_number,
952 world_pos = pos,
953 z_index = -300,
958 -- Display the node probabilities and force_place status of the nodes in a region.
959 -- By default, this is done for nodes near the player (distance: 5).
960 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
961 function schemedit.display_node_probs_region(player, pos1, pos2)
962 local playername = player:get_player_name()
963 local pos = vector.round(player:get_pos())
965 local dist = 5
966 -- Default: 5 nodes away from player in any direction
967 if not pos1 then
968 pos1 = vector.subtract(pos, dist)
970 if not pos2 then
971 pos2 = vector.add(pos, dist)
973 for x=pos1.x, pos2.x do
974 for y=pos1.y, pos2.y do
975 for z=pos1.z, pos2.z do
976 local checkpos = {x=x, y=y, z=z}
977 local nodehash = minetest.hash_node_position(checkpos)
979 -- If node is already displayed, remove it so it can re replaced later
980 if displayed_waypoints[playername][nodehash] then
981 player:hud_remove(displayed_waypoints[playername][nodehash])
982 displayed_waypoints[playername][nodehash] = nil
985 local prob, force_place
986 local meta = minetest.get_meta(checkpos)
987 prob = meta:get_string("schemedit_prob")
988 force_place = meta:get_string("schemedit_force_place") == "true"
989 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
990 if hud_id then
991 displayed_waypoints[playername][nodehash] = hud_id
992 displayed_waypoints[playername].display_active = true
999 -- Remove all active displayed node statuses.
1000 function schemedit.clear_displayed_node_probs(player)
1001 local playername = player:get_player_name()
1002 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
1003 if nodehash ~= "display_active" then
1004 player:hud_remove(hud_id)
1005 displayed_waypoints[playername][nodehash] = nil
1006 displayed_waypoints[playername].display_active = false
1011 minetest.register_on_joinplayer(function(player)
1012 displayed_waypoints[player:get_player_name()] = {
1013 display_active = false -- If true, there *might* be at least one active node prob HUD display
1014 -- If false, no node probabilities are displayed for sure.
1016 end)
1018 minetest.register_on_leaveplayer(function(player)
1019 displayed_waypoints[player:get_player_name()] = nil
1020 end)
1022 -- Regularily clear the displayed node probabilities and force_place
1023 -- for all players who do not wield the probtool.
1024 -- This makes sure the screen is not spammed with information when it
1025 -- isn't needed.
1026 local cleartimer = 0
1027 minetest.register_globalstep(function(dtime)
1028 cleartimer = cleartimer + dtime
1029 if cleartimer > 2 then
1030 local players = minetest.get_connected_players()
1031 for p = 1, #players do
1032 local player = players[p]
1033 local pname = player:get_player_name()
1034 if displayed_waypoints[pname].display_active then
1035 local item = player:get_wielded_item()
1036 if item:get_name() ~= "schemedit:probtool" then
1037 schemedit.clear_displayed_node_probs(player)
1041 cleartimer = 0
1043 end)
1046 --- Registrations
1049 -- [priv] schematic_override
1050 minetest.register_privilege("schematic_override", {
1051 description = S("Allows you to access schemedit nodes not owned by you"),
1052 give_to_singleplayer = false,
1055 local help_import = ""
1056 if can_import then
1057 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"
1060 -- [node] Schematic creator
1061 minetest.register_node("schemedit:creator", {
1062 description = S("Schematic Creator"),
1063 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1064 _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"..
1065 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"..
1066 help_import..
1067 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1068 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"..
1069 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."),
1070 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1071 "schemedit_creator_sides.png"},
1072 groups = { dig_immediate = 2},
1073 paramtype2 = "facedir",
1074 is_ground_content = false,
1076 after_place_node = function(pos, player)
1077 local name = player:get_player_name()
1078 local meta = minetest.get_meta(pos)
1080 meta:set_string("owner", name)
1081 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
1082 meta:set_string("prob_list", minetest.serialize({}))
1083 meta:set_string("slices", minetest.serialize({}))
1085 local node = minetest.get_node(pos)
1086 local dir = minetest.facedir_to_dir(node.param2)
1088 meta:set_int("x_size", 1)
1089 meta:set_int("y_size", 1)
1090 meta:set_int("z_size", 1)
1092 -- Don't take item from itemstack
1093 return true
1094 end,
1095 can_dig = function(pos, player)
1096 local name = player:get_player_name()
1097 local meta = minetest.get_meta(pos)
1098 if meta:get_string("owner") == name or
1099 minetest.check_player_privs(player, "schematic_override") == true then
1100 return true
1103 return false
1104 end,
1105 on_rightclick = function(pos, node, player)
1106 local meta = minetest.get_meta(pos)
1107 local name = player:get_player_name()
1108 if meta:get_string("owner") == name or
1109 minetest.check_player_privs(player, "schematic_override") == true then
1110 -- Get player attribute
1111 local pmeta = player:get_meta()
1112 local tab = pmeta:get_string("schemedit:tab")
1113 if not forms[tab] or not tab then
1114 tab = "main"
1117 schemedit.show_formspec(pos, player, tab, true)
1119 end,
1120 after_destruct = function(pos)
1121 schemedit.unmark(pos)
1122 end,
1124 -- No support for Minetest Game's screwdriver
1125 on_rotate = false,
1128 minetest.register_tool("schemedit:probtool", {
1129 description = S("Schematic Node Probability Tool"),
1130 _doc_items_longdesc =
1131 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"..
1132 S("It allows you to set two things:").."\n"..
1133 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1134 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1135 _doc_items_usagehelp = "\n"..
1136 S("BASIC USAGE:").."\n"..
1137 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"..
1138 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"..
1139 S("NODE HUD:").."\n"..
1140 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"..
1141 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1142 S("UPDATING THE NODE HUD:").."\n"..
1143 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."),
1144 wield_image = "schemedit_probtool.png",
1145 inventory_image = "schemedit_probtool.png",
1146 liquids_pointable = true,
1147 groups = { disable_repair = 1 },
1148 on_use = function(itemstack, user, pointed_thing)
1149 local uname = user:get_player_name()
1150 if uname and not check_priv(uname) then
1151 return
1154 local ctrl = user:get_player_control()
1155 -- Simple use
1156 if not ctrl.sneak then
1157 -- Open dialog to change the probability to apply to nodes
1158 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1160 -- Use + sneak
1161 else
1162 -- Display the probability and force_place values for nodes.
1164 -- If a schematic creator was punched, only enable display for all nodes
1165 -- within the creator's region.
1166 local use_creator_region = false
1167 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1168 local punchpos = pointed_thing.under
1169 local node = minetest.get_node(punchpos)
1170 if node.name == "schemedit:creator" then
1171 local pos1, pos2 = schemedit.size(punchpos)
1172 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1173 schemedit.display_node_probs_region(user, pos1, pos2)
1174 return
1178 -- Otherwise, just display the region close to the player
1179 schemedit.display_node_probs_region(user)
1181 end,
1182 on_secondary_use = function(itemstack, user, pointed_thing)
1183 local uname = user:get_player_name()
1184 if uname and not check_priv(uname) then
1185 return
1188 schemedit.clear_displayed_node_probs(user)
1189 end,
1190 -- Set note probability and force_place and enable node probability display
1191 on_place = function(itemstack, placer, pointed_thing)
1192 local pname = placer:get_player_name()
1193 if pname and not check_priv(pname) then
1194 return
1197 -- Use pointed node's on_rightclick function first, if present
1198 local node = minetest.get_node(pointed_thing.under)
1199 if placer and not placer:get_player_control().sneak then
1200 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1201 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1205 -- This sets the node probability of pointed node to the
1206 -- currently used probability stored in the tool.
1207 local pos = pointed_thing.under
1208 local node = minetest.get_node(pos)
1209 -- Schematic void are ignored, they always have probability 0
1210 if node.name == "schemedit:void" then
1211 return itemstack
1213 local nmeta = minetest.get_meta(pos)
1214 local imeta = itemstack:get_meta()
1215 local prob = tonumber(imeta:get_string("schemedit_prob"))
1216 local force_place = imeta:get_string("schemedit_force_place")
1218 if not prob or prob == 255 then
1219 nmeta:set_string("schemedit_prob", nil)
1220 else
1221 nmeta:set_string("schemedit_prob", prob)
1223 if force_place == "true" then
1224 nmeta:set_string("schemedit_force_place", "true")
1225 else
1226 nmeta:set_string("schemedit_force_place", nil)
1229 -- Enable node probablity display
1230 schemedit.display_node_probs_region(placer)
1232 return itemstack
1233 end,
1236 local use_texture_alpha_void
1237 if minetest.features.use_texture_alpha_string_modes then
1238 use_texture_alpha_void = "clip"
1239 else
1240 use_texture_alpha_void = true
1243 minetest.register_node("schemedit:void", {
1244 description = S("Schematic Void"),
1245 _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."),
1246 _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."),
1247 tiles = { "schemedit_void.png" },
1248 use_texture_alpha = use_texture_alpha_void,
1249 drawtype = "nodebox",
1250 is_ground_content = false,
1251 paramtype = "light",
1252 walkable = false,
1253 sunlight_propagates = true,
1254 node_box = {
1255 type = "fixed",
1256 fixed = {
1257 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1260 groups = { dig_immediate = 3},
1263 -- [entity] Visible schematic border
1264 minetest.register_entity("schemedit:display", {
1265 visual = "upright_sprite",
1266 textures = {"schemedit_border.png"},
1267 visual_size = {x=10, y=10},
1268 pointable = false,
1269 physical = false,
1270 static_save = false,
1271 glow = minetest.LIGHT_MAX,
1273 on_step = function(self, dtime)
1274 if not self.id then
1275 self.object:remove()
1276 elseif not schemedit.markers[self.id] then
1277 self.object:remove()
1279 end,
1280 on_activate = function(self)
1281 self.object:set_armor_groups({immortal = 1})
1282 end,
1285 minetest.register_lbm({
1286 label = "Reset schematic creator border entities",
1287 name = "schemedit:reset_border",
1288 nodenames = "schemedit:creator",
1289 run_at_every_load = true,
1290 action = function(pos, node)
1291 local meta = minetest.get_meta(pos)
1292 meta:set_string("schem_border", "false")
1293 end,
1296 local function add_suffix(schem)
1297 -- Automatically add file name suffix if omitted
1298 local schem_full, schem_lua
1299 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1300 schem_full = schem
1301 schem_lua = string.sub(schem, 1, -5) .. ".lua"
1302 else
1303 schem_full = schem .. ".mts"
1304 schem_lua = schem .. ".lua"
1306 return schem_full, schem_lua
1309 -- [chatcommand] Place schematic
1310 minetest.register_chatcommand("placeschem", {
1311 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),
1312 privs = {server = true},
1313 params = S("<schematic name>[.mts] [-c] [<x> <y> <z>]"),
1314 func = function(name, param)
1315 local schem, clear, p = string.match(param, "^([^ ]+) +(%-c) *(.*)$")
1316 if not schem then
1317 schem, p = string.match(param, "^([^ ]+) *(.*)$")
1319 clear = clear == "-c"
1321 local pos = minetest.string_to_pos(p)
1323 if not schem then
1324 return false, S("No schematic file specified.")
1327 if not pos then
1328 pos = minetest.get_player_by_name(name):get_pos()
1331 local schem_full, schem_lua = add_suffix(schem)
1332 local success = false
1333 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1334 if minetest.read_schematic then
1335 -- We don't call minetest.place_schematic with the path name directly because
1336 -- this would trigger the caching and we wouldn't get any updates to the schematic
1337 -- files when we reload. minetest.read_schematic circumvents that.
1338 local schematic = minetest.read_schematic(schem_path, {})
1339 if schematic then
1340 if clear then
1341 -- Clear same size for X and Z because
1342 -- because schematic is randomly rotated
1343 local max_xz = math.max(schematic.size.x, schematic.size.z)
1344 local posses = {}
1345 for z=pos.z, pos.z+max_xz-1 do
1346 for y=pos.y, pos.y+schematic.size.y-1 do
1347 for x=pos.x, pos.x+max_xz-1 do
1348 table.insert(posses, {x=x,y=y,z=z})
1352 minetest.bulk_set_node(posses, {name="air"})
1354 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1356 else
1357 -- Legacy support for Minetest versions that do not have minetest.read_schematic.
1358 -- Note: "-c" is ignored here.
1359 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1362 if success == nil then
1363 return false, S("Schematic file could not be loaded!")
1364 else
1365 return true
1367 end,
1370 minetest.register_chatcommand("listschems", {
1371 description = S("List schematic files in world path"),
1372 privs = {server = true},
1373 params = "",
1374 func = function(name, param)
1375 local files = minetest.get_dir_list(minetest.get_worldpath()..DIR_DELIM.."schems", false)
1376 if not files then
1377 return false
1379 local out_files = {}
1380 -- Only show files with “.mts” suffix
1381 for f=#files, 1, -1 do
1382 if string.sub(string.lower(files[f]), -4, -1) == ".mts" then
1383 table.insert(out_files, files[f])
1386 table.sort(out_files)
1387 local str = table.concat(out_files, ", ")
1388 if str == "" then
1389 return true, S("No schematic files.")
1391 return true, str
1392 end,
1395 if can_import then
1396 -- [chatcommand] Convert MTS schematic file to .lua file
1397 minetest.register_chatcommand("mts2lua", {
1398 description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
1399 privs = {server = true},
1400 params = S("<schematic name>[.mts] [comments]"),
1401 func = function(name, param)
1402 local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
1404 if not schem then
1405 return false, S("No schematic file specified.")
1408 local comments = comments_str == "comments"
1410 -- Automatically add file name suffix if omitted
1411 local schem_full, schem_lua = add_suffix(schem)
1412 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1413 local schematic = minetest.read_schematic(schem_path, {})
1415 if schematic then
1416 local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
1417 local lua_path = export_path_full .. DIR_DELIM .. schem_lua
1418 local file = io.open(lua_path, "w")
1419 if file and str then
1420 file:write(str)
1421 file:flush()
1422 file:close()
1423 return true, S("Exported schematic to @1", lua_path)
1424 else
1425 return false, S("Failed!")
1428 end,
1432 if MAKE_README then
1433 dofile(minetest.get_modpath("schemedit")..DIR_DELIM.."make_readme.lua")