Add schematic import functionality
[minetest_schemedit.git] / init.lua
blobac1f2d7722ff0aa5c91e2bdb0e41ea9a68a12daf
1 local S = minetest.get_translator("schemedit")
2 local F = minetest.formspec_escape
4 local schemedit = {}
6 -- Directory delimeter fallback (normally comes from builtin)
7 if not DIR_DELIM then
8 DIR_DELIM = "/"
9 end
10 local export_path_full = table.concat({minetest.get_worldpath(), "schems"}, DIR_DELIM)
12 -- truncated export path so the server directory structure is not exposed publicly
13 local export_path_trunc = table.concat({S("<world path>"), "schems"}, DIR_DELIM)
15 local text_color = "#D79E9E"
16 local text_color_number = 0xD79E9E
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 ---
30 --- Formspec API
31 ---
33 local contexts = {}
34 local form_data = {}
35 local tabs = {}
36 local forms = {}
37 local displayed_waypoints = {}
39 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
40 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
41 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
42 -- on an actual export to a schematic.
44 function schemedit.lua_prob_to_schematic_prob(lua_prob)
45 return math.floor(lua_prob / 2)
46 end
48 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
49 return schematic_prob * 2
51 end
53 -- [function] Add form
54 function schemedit.add_form(name, def)
55 def.name = name
56 forms[name] = def
58 if def.tab then
59 tabs[#tabs + 1] = name
60 end
61 end
63 -- [function] Generate tabs
64 function schemedit.generate_tabs(current)
65 local retval = "tabheader[0,0;tabs;"
66 for _, t in pairs(tabs) do
67 local f = forms[t]
68 if f.tab ~= false and f.caption then
69 retval = retval..f.caption..","
71 if type(current) ~= "number" and current == f.name then
72 current = _
73 end
74 end
75 end
76 retval = retval:sub(1, -2) -- Strip last comma
77 retval = retval..";"..current.."]" -- Close tabheader
78 return retval
79 end
81 -- [function] Handle tabs
82 function schemedit.handle_tabs(pos, name, fields)
83 local tab = tonumber(fields.tabs)
84 if tab and tabs[tab] and forms[tabs[tab]] then
85 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
86 return true
87 end
88 end
90 -- [function] Show formspec
91 function schemedit.show_formspec(pos, player, tab, show, ...)
92 if forms[tab] then
93 if type(player) == "string" then
94 player = minetest.get_player_by_name(player)
95 end
96 local name = player:get_player_name()
98 if show ~= false then
99 if not form_data[name] then
100 form_data[name] = {}
103 local form = forms[tab].get(form_data[name], pos, name, ...)
104 if forms[tab].tab then
105 form = form..schemedit.generate_tabs(tab)
108 minetest.show_formspec(name, "schemedit:"..tab, form)
109 contexts[name] = pos
111 -- Update player attribute
112 if forms[tab].cache_name ~= false then
113 player:set_attribute("schemedit:tab", tab)
115 else
116 minetest.close_formspec(pname, "schemedit:"..tab)
121 -- [event] On receive fields
122 minetest.register_on_player_receive_fields(function(player, formname, fields)
123 local formname = formname:split(":")
125 if formname[1] == "schemedit" and forms[formname[2]] then
126 local handle = forms[formname[2]].handle
127 local name = player:get_player_name()
128 if contexts[name] then
129 if not form_data[name] then
130 form_data[name] = {}
133 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
134 handle(form_data[name], contexts[name], name, fields)
138 end)
140 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
141 schemedit.scan_metadata = function(pos1, pos2)
142 local prob_list = {}
144 for x=pos1.x, pos2.x do
145 for y=pos1.y, pos2.y do
146 for z=pos1.z, pos2.z do
147 local scanpos = {x=x, y=y, z=z}
148 local node = minetest.get_node_or_nil(scanpos)
150 local prob, force_place
151 if node == nil or node.name == "schemedit:void" then
152 prob = 0
153 force_place = false
154 else
155 local meta = minetest.get_meta(scanpos)
157 prob = tonumber(meta:get_string("schemedit_prob")) or 255
158 local fp = meta:get_string("schemedit_force_place")
159 if fp == "true" then
160 force_place = true
161 else
162 force_place = false
166 local hashpos = minetest.hash_node_position(scanpos)
167 prob_list[hashpos] = {
168 pos = scanpos,
169 prob = prob,
170 force_place = force_place,
176 return prob_list
179 -- Sets probability and force_place metadata of an item.
180 -- Also updates item description.
181 -- The itemstack is updated in-place.
182 local function set_item_metadata(itemstack, prob, force_place)
183 local smeta = itemstack:get_meta()
184 local prob_desc = "\n"..S("Probability: @1", prob or
185 smeta:get_string("schemedit_prob") or S("Not Set"))
186 -- Update probability
187 if prob and prob >= 0 and prob < 255 then
188 smeta:set_string("schemedit_prob", tostring(prob))
189 elseif prob and prob == 255 then
190 -- Clear prob metadata for default probability
191 prob_desc = ""
192 smeta:set_string("schemedit_prob", nil)
193 else
194 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
195 S("Not Set"))
198 -- Update force place
199 if force_place == true then
200 smeta:set_string("schemedit_force_place", "true")
201 elseif force_place == false then
202 smeta:set_string("schemedit_force_place", nil)
205 -- Update description
206 local desc = minetest.registered_items[itemstack:get_name()].description
207 local meta_desc = smeta:get_string("description")
208 if meta_desc and meta_desc ~= "" then
209 desc = meta_desc
212 local original_desc = smeta:get_string("original_description")
213 if original_desc and original_desc ~= "" then
214 desc = original_desc
215 else
216 smeta:set_string("original_description", desc)
219 local force_desc = ""
220 if smeta:get_string("schemedit_force_place") == "true" then
221 force_desc = "\n"..S("Force placement")
224 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
226 smeta:set_string("description", desc)
228 return itemstack
232 --- Formspec Tabs
234 local import_btn = ""
235 if minetest.read_schematic then
236 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
238 schemedit.add_form("main", {
239 tab = true,
240 caption = S("Main"),
241 get = function(self, pos, name)
242 local meta = minetest.get_meta(pos):to_table().fields
243 local strpos = minetest.pos_to_string(pos)
244 local hashpos = minetest.hash_node_position(pos)
246 local border_button
247 if meta.schem_border == "true" and schemedit.markers[hashpos] then
248 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
249 else
250 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
253 -- TODO: Show information regarding volume, pos1, pos2, etc... in formspec
254 local form = [[
255 size[7,8]
256 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
257 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
259 field[0.8,1;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(meta.schem_name or "")..[[]
260 button[5.3,0.69;1.2,1;save_name;]]..F(S("Save"))..[[]
261 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
262 field_close_on_enter[name;false]
264 button[0.5,1.5;6,1;export;]]..F(S("Export schematic")).."]"..
265 import_btn..[[
266 textarea[0.8,3.5;6.2,5;;]]..F(S("The schematic will be exported as a .mts file and stored in\n@1",
267 export_path_trunc .. DIR_DELIM .. "<name>.mts."))..[[;]
268 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..meta.x_size..[[]
269 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..meta.y_size..[[]
270 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..meta.z_size..[[]
271 field_close_on_enter[x;false]
272 field_close_on_enter[y;false]
273 field_close_on_enter[z;false]
275 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
276 ]]..
277 border_button
278 if minetest.get_modpath("doc") then
279 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
280 "tooltip[doc;"..F(S("Help")).."]"
282 return form
283 end,
284 handle = function(self, pos, name, fields)
285 local realmeta = minetest.get_meta(pos)
286 local meta = realmeta:to_table().fields
287 local hashpos = minetest.hash_node_position(pos)
289 if fields.doc then
290 doc.show_entry(name, "nodes", "schemedit:creator", true)
291 return
294 -- Toggle border
295 if fields.border then
296 if meta.schem_border == "true" and schemedit.markers[hashpos] then
297 schemedit.unmark(pos)
298 meta.schem_border = "false"
299 else
300 schemedit.mark(pos)
301 meta.schem_border = "true"
305 -- Save size vector values
306 if (fields.save or fields.key_enter_field == "x" or
307 fields.key_enter_field == "y" or fields.key_enter_field == "z")
308 and (fields.x and fields.y and fields.z and fields.x ~= ""
309 and fields.y ~= "" and fields.z ~= "") then
310 local x, y, z = tonumber(fields.x), tonumber(fields.y), tonumber(fields.z)
312 if x then
313 meta.x_size = math.max(x, 1)
315 if y then
316 meta.y_size = math.max(y, 1)
318 if z then
319 meta.z_size = math.max(z, 1)
323 -- Save schematic name
324 if fields.save_name or fields.key_enter_field == "name" and fields.name and
325 fields.name ~= "" then
326 meta.schem_name = fields.name
329 -- Export schematic
330 if fields.export and meta.schem_name and meta.schem_name ~= "" then
331 local pos1, pos2 = schemedit.size(pos)
332 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
333 local path = export_path_full .. DIR_DELIM
334 minetest.mkdir(path)
336 local plist = schemedit.scan_metadata(pos1, pos2)
337 local probability_list = {}
338 for hash, i in pairs(plist) do
339 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
340 if i.force_place == true then
341 prob = prob + 128
344 table.insert(probability_list, {
345 pos = minetest.get_position_from_hash(hash),
346 prob = prob,
350 local slist = minetest.deserialize(meta.slices)
351 local slice_list = {}
352 for _, i in pairs(slist) do
353 slice_list[#slice_list + 1] = {
354 ypos = pos.y + i.ypos,
355 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
359 local filepath = path..meta.schem_name..".mts"
360 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
362 if res then
363 minetest.chat_send_player(name, minetest.colorize("#00ff00",
364 S("Exported schematic to @1", filepath)))
365 else
366 minetest.chat_send_player(name, minetest.colorize("red",
367 S("Failed to export schematic to @1", filepath)))
371 -- Import schematic
372 if fields.import and meta.schem_name and meta.schem_name ~= "" then
373 if not minetest.read_schematic then
374 return
376 local pos1
377 local node = minetest.get_node(pos)
378 local path = export_path_full .. DIR_DELIM
380 local filepath = path..meta.schem_name..".mts"
381 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
382 local success = false
384 if schematic then
385 meta.x_size = schematic.size.x
386 meta.y_size = schematic.size.y
387 meta.z_size = schematic.size.z
388 meta.slices = minetest.serialize(schematic.yslice_prob)
390 if node.param2 == 1 then
391 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
392 elseif node.param2 == 2 then
393 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
394 elseif node.param2 == 3 then
395 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
396 else
397 pos1 = vector.add(pos, {x=0,y=0,z=1})
399 schematic.yslice_prob = {}
401 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
403 if success then
404 minetest.chat_send_player(name, minetest.colorize("#00ff00",
405 S("Imported schematic from @1", filepath)))
406 else
407 minetest.chat_send_player(name, minetest.colorize("red",
408 S("Failed to import schematic from @1", filepath)))
414 -- Save meta before updating visuals
415 local inv = realmeta:get_inventory():get_lists()
416 realmeta:from_table({fields = meta, inventory = inv})
418 -- Update border
419 if not fields.border and meta.schem_border == "true" then
420 schemedit.mark(pos)
423 -- Update formspec
424 if not fields.quit then
425 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
427 end,
430 schemedit.add_form("slice", {
431 caption = S("Y Slices"),
432 tab = true,
433 get = function(self, pos, name, visible_panel)
434 local meta = minetest.get_meta(pos):to_table().fields
436 self.selected = self.selected or 1
437 local selected = tostring(self.selected)
438 local slice_list = minetest.deserialize(meta.slices)
439 local slices = ""
440 for _, i in pairs(slice_list) do
441 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
442 slices = slices..insert..","
444 slices = slices:sub(1, -2) -- Remove final comma
446 local form = [[
447 size[7,8]
448 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
451 if self.panel_add or self.panel_edit then
452 local ypos_default, prob_default = "", ""
453 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
454 if self.panel_edit then
455 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
456 if slice_list[self.selected] then
457 ypos_default = slice_list[self.selected].ypos
458 prob_default = slice_list[self.selected].prob
462 form = form..[[
463 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
464 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
465 field_close_on_enter[ypos;false]
466 field_close_on_enter[prob;false]
467 ]]..done_button
470 if not self.panel_edit then
471 form = form.."button[0,6;2,1;add;"..F(S("+ Add slice")).."]"
474 if slices ~= "" and self.selected and not self.panel_add then
475 if not self.panel_edit then
476 form = form..[[
477 button[2,6;2,1;remove;]]..F(S("- Remove slice"))..[[]
478 button[4,6;2,1;edit;]]..F(S("+/- Edit slice"))..[[]
480 else
481 form = form..[[
482 button[2,6;2,1;remove;]]..F(S("- Remove slice"))..[[]
483 button[4,6;2,1;edit;]]..F(S("+/- Edit slice"))..[[]
488 return form
489 end,
490 handle = function(self, pos, name, fields)
491 local meta = minetest.get_meta(pos)
492 local player = minetest.get_player_by_name(name)
494 if fields.slices then
495 local slices = fields.slices:split(":")
496 self.selected = tonumber(slices[2])
499 if fields.add then
500 if not self.panel_add then
501 self.panel_add = true
502 schemedit.show_formspec(pos, player, "slice")
503 else
504 self.panel_add = nil
505 schemedit.show_formspec(pos, player, "slice")
509 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
510 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
511 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
512 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
513 local slice_list = minetest.deserialize(meta:get_string("slices"))
514 local index = #slice_list + 1
515 if fields.done_edit then
516 index = self.selected
519 slice_list[index] = {ypos = ypos, prob = prob}
521 meta:set_string("slices", minetest.serialize(slice_list))
523 -- Update and show formspec
524 self.panel_add = nil
525 schemedit.show_formspec(pos, player, "slice")
528 if fields.remove and self.selected then
529 local slice_list = minetest.deserialize(meta:get_string("slices"))
530 slice_list[self.selected] = nil
531 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
533 -- Update formspec
534 self.selected = 1
535 self.panel_edit = nil
536 schemedit.show_formspec(pos, player, "slice")
539 if fields.edit then
540 if not self.panel_edit then
541 self.panel_edit = true
542 schemedit.show_formspec(pos, player, "slice")
543 else
544 self.panel_edit = nil
545 schemedit.show_formspec(pos, player, "slice")
548 end,
551 schemedit.add_form("probtool", {
552 cache_name = false,
553 caption = S("Schematic Node Probability Tool"),
554 get = function(self, pos, name)
555 local player = minetest.get_player_by_name(name)
556 if not player then
557 return
559 local probtool = player:get_wielded_item()
560 if probtool:get_name() ~= "schemedit:probtool" then
561 return
564 local meta = probtool:get_meta()
565 local prob = tonumber(meta:get_string("schemedit_prob"))
566 local force_place = meta:get_string("schemedit_force_place")
568 if not prob then
569 prob = 255
571 if force_place == nil or force_place == "" then
572 force_place = "false"
574 local form = "size[5,4]"..
575 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
576 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
577 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
578 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
579 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
580 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
581 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
582 "field_close_on_enter[prob;false]"
583 return form
584 end,
585 handle = function(self, pos, name, fields)
586 if fields.submit then
587 local prob = tonumber(fields.prob)
588 if prob then
589 local player = minetest.get_player_by_name(name)
590 if not player then
591 return
593 local probtool = player:get_wielded_item()
594 if probtool:get_name() ~= "schemedit:probtool" then
595 return
598 local force_place = self.force_place == true
600 set_item_metadata(probtool, prob, force_place)
602 -- Repurpose the tool's wear bar to display the set probability
603 probtool:set_wear(math.floor(((255-prob)/255)*65535))
605 player:set_wielded_item(probtool)
608 if fields.force_place == "true" then
609 self.force_place = true
610 elseif fields.force_place == "false" then
611 self.force_place = false
613 end,
617 --- API
620 --- Copies and modifies positions `pos1` and `pos2` so that each component of
621 -- `pos1` is less than or equal to the corresponding component of `pos2`.
622 -- Returns the new positions.
623 function schemedit.sort_pos(pos1, pos2)
624 if not pos1 or not pos2 then
625 return
628 pos1, pos2 = table.copy(pos1), table.copy(pos2)
629 if pos1.x > pos2.x then
630 pos2.x, pos1.x = pos1.x, pos2.x
632 if pos1.y > pos2.y then
633 pos2.y, pos1.y = pos1.y, pos2.y
635 if pos1.z > pos2.z then
636 pos2.z, pos1.z = pos1.z, pos2.z
638 return pos1, pos2
641 -- [function] Prepare size
642 function schemedit.size(pos)
643 local pos1 = vector.new(pos)
644 local meta = minetest.get_meta(pos)
645 local node = minetest.get_node(pos)
646 local param2 = node.param2
647 local size = {
648 x = meta:get_int("x_size"),
649 y = math.max(meta:get_int("y_size") - 1, 0),
650 z = meta:get_int("z_size"),
653 if param2 == 1 then
654 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
655 pos1.x = pos1.x + 1
656 new_pos.z = new_pos.z + 1
657 return pos1, new_pos
658 elseif param2 == 2 then
659 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
660 pos1.z = pos1.z - 1
661 new_pos.x = new_pos.x + 1
662 return pos1, new_pos
663 elseif param2 == 3 then
664 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
665 pos1.x = pos1.x - 1
666 new_pos.z = new_pos.z - 1
667 return pos1, new_pos
668 else
669 local new_pos = vector.add(size, pos)
670 pos1.z = pos1.z + 1
671 new_pos.x = new_pos.x - 1
672 return pos1, new_pos
676 -- [function] Mark region
677 function schemedit.mark(pos)
678 schemedit.unmark(pos)
680 local id = minetest.hash_node_position(pos)
681 local owner = minetest.get_meta(pos):get_string("owner")
682 local pos1, pos2 = schemedit.size(pos)
683 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
685 local thickness = 0.2
686 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
687 local m = {}
688 local low = true
689 local offset
691 -- XY plane markers
692 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
693 if low then
694 offset = -0.01
695 else
696 offset = 0.01
698 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
699 if marker ~= nil then
700 marker:set_properties({
701 visual_size={x=(sizex+0.01) * 2, y=sizey * 2},
703 marker:get_luaentity().id = id
704 marker:get_luaentity().owner = owner
705 table.insert(m, marker)
707 low = false
710 low = true
711 -- YZ plane markers
712 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
713 if low then
714 offset = -0.01
715 else
716 offset = 0.01
719 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
720 if marker ~= nil then
721 marker:set_properties({
722 visual_size={x=(sizez+0.01) * 2, y=sizey * 2},
724 marker:set_yaw(math.pi / 2)
725 marker:get_luaentity().id = id
726 marker:get_luaentity().owner = owner
727 table.insert(m, marker)
729 low = false
732 schemedit.markers[id] = m
733 return true
736 -- [function] Unmark region
737 function schemedit.unmark(pos)
738 local id = minetest.hash_node_position(pos)
739 if schemedit.markers[id] then
740 local retval
741 for _, entity in ipairs(schemedit.markers[id]) do
742 entity:remove()
743 retval = true
745 return retval
750 --- Mark node probability values near player
753 -- Show probability and force_place status of a particular position for player in HUD.
754 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
755 -- The distance to the node is also displayed below that. This can't be avoided and is
756 -- and artifact of the waypoint HUD element. TODO: Hide displayed distance.
757 function schemedit.display_node_prob(player, pos, prob, force_place)
758 local wpstring
759 if prob and force_place == true then
760 wpstring = string.format("%d [F]", prob)
761 elseif prob then
762 wpstring = prob
763 elseif force_place == true then
764 wpstring = "[F]"
766 if wpstring then
767 return player:hud_add({
768 hud_elem_type = "waypoint",
769 name = wpstring,
770 text = "m", -- For the distance artifact
771 number = text_color_number,
772 world_pos = pos,
777 -- Display the node probabilities and force_place status of the nodes in a region.
778 -- By default, this is done for nodes near the player (distance: 5).
779 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
780 function schemedit.display_node_probs_region(player, pos1, pos2)
781 local playername = player:get_player_name()
782 local pos = vector.round(player:get_pos())
784 local dist = 5
785 -- Default: 5 nodes away from player in any direction
786 if not pos1 then
787 pos1 = vector.subtract(pos, dist)
789 if not pos2 then
790 pos2 = vector.add(pos, dist)
792 for x=pos1.x, pos2.x do
793 for y=pos1.y, pos2.y do
794 for z=pos1.z, pos2.z do
795 local checkpos = {x=x, y=y, z=z}
796 local nodehash = minetest.hash_node_position(checkpos)
798 -- If node is already displayed, remove it so it can re replaced later
799 if displayed_waypoints[playername][nodehash] then
800 player:hud_remove(displayed_waypoints[playername][nodehash])
801 displayed_waypoints[playername][nodehash] = nil
804 local prob, force_place
805 local meta = minetest.get_meta(checkpos)
806 prob = tonumber(meta:get_string("schemedit_prob"))
807 force_place = meta:get_string("schemedit_force_place") == "true"
808 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
809 if hud_id then
810 displayed_waypoints[playername][nodehash] = hud_id
811 displayed_waypoints[playername].display_active = true
818 -- Remove all active displayed node statuses.
819 function schemedit.clear_displayed_node_probs(player)
820 local playername = player:get_player_name()
821 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
822 player:hud_remove(hud_id)
823 displayed_waypoints[playername][nodehash] = nil
824 displayed_waypoints[playername].display_active = false
828 minetest.register_on_joinplayer(function(player)
829 displayed_waypoints[player:get_player_name()] = {
830 display_active = false -- If true, there *might* be at least one active node prob HUD display
831 -- If false, no node probabilities are displayed for sure.
833 end)
835 minetest.register_on_leaveplayer(function(player)
836 displayed_waypoints[player:get_player_name()] = nil
837 end)
839 -- Regularily clear the displayed node probabilities and force_place
840 -- for all players who do not wield the probtool.
841 -- This makes sure the screen is not spammed with information when it
842 -- isn't needed.
843 local cleartimer = 0
844 minetest.register_globalstep(function(dtime)
845 cleartimer = cleartimer + dtime
846 if cleartimer > 2 then
847 local players = minetest.get_connected_players()
848 for p = 1, #players do
849 local player = players[p]
850 local pname = player:get_player_name()
851 if displayed_waypoints[pname].display_active then
852 local item = player:get_wielded_item()
853 if item:get_name() ~= "schemedit:probtool" then
854 schemedit.clear_displayed_node_probs(player)
858 cleartimer = 0
860 end)
863 --- Registrations
866 -- [priv] schematic_override
867 minetest.register_privilege("schematic_override", {
868 description = S("Allows you to access schemedit nodes not owned by you"),
869 give_to_singleplayer = false,
872 -- [node] Schematic creator
873 minetest.register_node("schemedit:creator", {
874 description = S("Schematic Creator"),
875 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
876 _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"..
877 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"..
878 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
879 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"..
880 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."),
881 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
882 "schemedit_creator_sides.png"},
883 groups = { dig_immediate = 2},
884 paramtype2 = "facedir",
885 is_ground_content = false,
887 after_place_node = function(pos, player)
888 local name = player:get_player_name()
889 local meta = minetest.get_meta(pos)
891 meta:set_string("owner", name)
892 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
893 meta:set_string("prob_list", minetest.serialize({}))
894 meta:set_string("slices", minetest.serialize({}))
896 local node = minetest.get_node(pos)
897 local dir = minetest.facedir_to_dir(node.param2)
899 meta:set_int("x_size", 1)
900 meta:set_int("y_size", 1)
901 meta:set_int("z_size", 1)
903 -- Don't take item from itemstack
904 return true
905 end,
906 can_dig = function(pos, player)
907 local name = player:get_player_name()
908 local meta = minetest.get_meta(pos)
909 if meta:get_string("owner") == name or
910 minetest.check_player_privs(player, "schematic_override") == true then
911 return true
914 return false
915 end,
916 on_rightclick = function(pos, node, player)
917 local meta = minetest.get_meta(pos)
918 local name = player:get_player_name()
919 if meta:get_string("owner") == name or
920 minetest.check_player_privs(player, "schematic_override") == true then
921 -- Get player attribute
922 local tab = player:get_attribute("schemedit:tab")
923 if not forms[tab] or not tab then
924 tab = "main"
927 schemedit.show_formspec(pos, player, tab, true)
929 end,
930 after_destruct = function(pos)
931 schemedit.unmark(pos)
932 end,
935 minetest.register_tool("schemedit:probtool", {
936 description = S("Schematic Node Probability Tool"),
937 _doc_items_longdesc =
938 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"..
939 S("It allows you to set two things:").."\n"..
940 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
941 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
942 _doc_items_usagehelp = "\n"..
943 S("BASIC USAGE:").."\n"..
944 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"..
945 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"..
946 S("NODE HUD:").."\n"..
947 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"..
948 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
949 S("UPDATING THE NODE HUD:").."\n"..
950 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."),
951 wield_image = "schemedit_probtool.png",
952 inventory_image = "schemedit_probtool.png",
953 liquids_pointable = true,
954 groups = { disable_repair = 1 },
955 on_use = function(itemstack, user, pointed_thing)
956 local ctrl = user:get_player_control()
957 -- Simple use
958 if not ctrl.sneak then
959 -- Open dialog to change the probability to apply to nodes
960 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
962 -- Use + sneak
963 else
964 -- Display the probability and force_place values for nodes.
966 -- If a schematic creator was punched, only enable display for all nodes
967 -- within the creator's region.
968 local use_creator_region = false
969 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
970 punchpos = pointed_thing.under
971 local node = minetest.get_node(punchpos)
972 if node.name == "schemedit:creator" then
973 local pos1, pos2 = schemedit.size(punchpos)
974 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
975 schemedit.display_node_probs_region(user, pos1, pos2)
976 return
980 -- Otherwise, just display the region close to the player
981 schemedit.display_node_probs_region(user)
983 end,
984 on_secondary_use = function(itemstack, user, pointed_thing)
985 schemedit.clear_displayed_node_probs(user)
986 end,
987 -- Set note probability and force_place and enable node probability display
988 on_place = function(itemstack, placer, pointed_thing)
989 -- Use pointed node's on_rightclick function first, if present
990 local node = minetest.get_node(pointed_thing.under)
991 if placer and not placer:get_player_control().sneak then
992 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
993 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
997 -- This sets the node probability of pointed node to the
998 -- currently used probability stored in the tool.
999 local pos = pointed_thing.under
1000 local node = minetest.get_node(pos)
1001 -- Schematic void are ignored, they always have probability 0
1002 if node.name == "schemedit:void" then
1003 return itemstack
1005 local nmeta = minetest.get_meta(pos)
1006 local imeta = itemstack:get_meta()
1007 local prob = tonumber(imeta:get_string("schemedit_prob"))
1008 local force_place = imeta:get_string("schemedit_force_place")
1010 if not prob or prob == 255 then
1011 nmeta:set_string("schemedit_prob", nil)
1012 else
1013 nmeta:set_string("schemedit_prob", prob)
1015 if force_place == "true" then
1016 nmeta:set_string("schemedit_force_place", "true")
1017 else
1018 nmeta:set_string("schemedit_force_place", nil)
1021 -- Enable node probablity display
1022 schemedit.display_node_probs_region(placer)
1024 return itemstack
1025 end,
1028 minetest.register_node("schemedit:void", {
1029 description = S("Schematic Void"),
1030 _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."),
1031 _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."),
1032 tiles = { "schemedit_void.png" },
1033 drawtype = "nodebox",
1034 is_ground_content = false,
1035 paramtype = "light",
1036 walkable = false,
1037 sunlight_propagates = true,
1038 node_box = {
1039 type = "fixed",
1040 fixed = {
1041 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1044 groups = { dig_immediate = 3},
1047 -- [entity] Visible schematic border
1048 minetest.register_entity("schemedit:display", {
1049 visual = "upright_sprite",
1050 textures = {"schemedit_border.png"},
1051 visual_size = {x=10, y=10},
1052 pointable = false,
1053 physical = false,
1054 static_save = false,
1055 glow = minetest.LIGHT_MAX,
1057 on_step = function(self, dtime)
1058 if not self.id then
1059 self.object:remove()
1060 elseif not schemedit.markers[self.id] then
1061 self.object:remove()
1063 end,
1064 on_activate = function(self)
1065 self.object:set_armor_groups({immortal = 1})
1066 end,
1069 minetest.register_lbm({
1070 label = "Reset schematic creator border entities",
1071 name = "schemedit:reset_border",
1072 nodenames = "schemedit:creator",
1073 run_at_every_load = true,
1074 action = function(pos, node)
1075 local meta = minetest.get_meta(pos)
1076 meta:set_string("schem_border", "false")
1077 end,
1080 -- [chatcommand] Place schematic
1081 minetest.register_chatcommand("placeschem", {
1082 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1083 privs = {debug = true},
1084 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1085 func = function(name, param)
1086 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1087 local pos = minetest.string_to_pos(p)
1089 if not schem then
1090 return false, S("No schematic file specified.")
1093 if not pos then
1094 pos = minetest.get_player_by_name(name):get_pos()
1097 -- Automatically add file name suffix if omitted
1098 local schem_full
1099 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1100 schem_full = schem
1101 else
1102 schem_full = schem .. ".mts"
1105 local success = false
1106 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1107 if minetest.read_schematic then
1108 -- We don't call minetest.place_schematic with the path name directly because
1109 -- this would trigger the caching and we wouldn't get any updates to the schematic
1110 -- files when we reload. minetest.read_schematic circumvents that.
1111 local schematic = minetest.read_schematic(schem_path, {})
1112 if schematic then
1113 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1115 else
1116 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1117 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1120 if success == nil then
1121 return false, S("Schematic file could not be loaded!")
1122 else
1123 return true
1125 end,