Easier name and size editing
[minetest_schemedit.git] / init.lua
blob71d7e712aaaa73af8411ae1391cd8b11b4a8c66d
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 -- Save size vector values
290 if (fields.x and fields.x ~= "") then
291 local x = tonumber(fields.x)
292 if x then
293 meta.x_size = math.max(x, 1)
296 if (fields.y and fields.y ~= "") then
297 local y = tonumber(fields.y)
298 if y then
299 meta.y_size = math.max(y, 1)
302 if (fields.z and fields.z ~= "") then
303 local z = tonumber(fields.z)
304 if z then
305 meta.z_size = math.max(z, 1)
309 -- Save schematic name
310 if fields.name then
311 meta.schem_name = fields.name
314 if fields.doc then
315 doc.show_entry(name, "nodes", "schemedit:creator", true)
316 return
319 -- Toggle border
320 if fields.border then
321 if meta.schem_border == "true" and schemedit.markers[hashpos] then
322 schemedit.unmark(pos)
323 meta.schem_border = "false"
324 else
325 schemedit.mark(pos)
326 meta.schem_border = "true"
330 -- Export schematic
331 if fields.export and meta.schem_name and meta.schem_name ~= "" then
332 local pos1, pos2 = schemedit.size(pos)
333 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
334 local path = export_path_full .. DIR_DELIM
335 minetest.mkdir(path)
337 local plist = schemedit.scan_metadata(pos1, pos2)
338 local probability_list = {}
339 for hash, i in pairs(plist) do
340 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
341 if i.force_place == true then
342 prob = prob + 128
345 table.insert(probability_list, {
346 pos = minetest.get_position_from_hash(hash),
347 prob = prob,
351 local slist = minetest.deserialize(meta.slices)
352 local slice_list = {}
353 for _, i in pairs(slist) do
354 slice_list[#slice_list + 1] = {
355 ypos = pos.y + i.ypos,
356 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
360 local filepath = path..meta.schem_name..".mts"
361 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
363 if res then
364 minetest.chat_send_player(name, minetest.colorize("#00ff00",
365 S("Exported schematic to @1", filepath)))
366 else
367 minetest.chat_send_player(name, minetest.colorize("red",
368 S("Failed to export schematic to @1", filepath)))
372 -- Import schematic
373 if fields.import and meta.schem_name and meta.schem_name ~= "" then
374 if not minetest.read_schematic then
375 return
377 local pos1
378 local node = minetest.get_node(pos)
379 local path = export_path_full .. DIR_DELIM
381 local filepath = path..meta.schem_name..".mts"
382 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
383 local success = false
385 if schematic then
386 meta.x_size = schematic.size.x
387 meta.y_size = schematic.size.y
388 meta.z_size = schematic.size.z
389 meta.slices = minetest.serialize(schematic.yslice_prob)
391 if node.param2 == 1 then
392 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
393 elseif node.param2 == 2 then
394 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
395 elseif node.param2 == 3 then
396 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
397 else
398 pos1 = vector.add(pos, {x=0,y=0,z=1})
400 schematic.yslice_prob = {}
402 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
404 if success then
405 minetest.chat_send_player(name, minetest.colorize("#00ff00",
406 S("Imported schematic from @1", filepath)))
407 else
408 minetest.chat_send_player(name, minetest.colorize("red",
409 S("Failed to import schematic from @1", filepath)))
415 -- Save meta before updating visuals
416 local inv = realmeta:get_inventory():get_lists()
417 realmeta:from_table({fields = meta, inventory = inv})
419 -- Update border
420 if not fields.border and meta.schem_border == "true" then
421 schemedit.mark(pos)
424 -- Update formspec
425 if not fields.quit then
426 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
428 end,
431 schemedit.add_form("slice", {
432 caption = S("Y Slices"),
433 tab = true,
434 get = function(self, pos, name, visible_panel)
435 local meta = minetest.get_meta(pos):to_table().fields
437 self.selected = self.selected or 1
438 local selected = tostring(self.selected)
439 local slice_list = minetest.deserialize(meta.slices)
440 local slices = ""
441 for _, i in pairs(slice_list) do
442 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
443 slices = slices..insert..","
445 slices = slices:sub(1, -2) -- Remove final comma
447 local form = [[
448 size[7,8]
449 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
452 if self.panel_add or self.panel_edit then
453 local ypos_default, prob_default = "", ""
454 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
455 if self.panel_edit then
456 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
457 if slice_list[self.selected] then
458 ypos_default = slice_list[self.selected].ypos
459 prob_default = slice_list[self.selected].prob
463 form = form..[[
464 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
465 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
466 field_close_on_enter[ypos;false]
467 field_close_on_enter[prob;false]
468 ]]..done_button
471 if not self.panel_edit then
472 form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
475 if slices ~= "" and self.selected and not self.panel_add then
476 if not self.panel_edit then
477 form = form..[[
478 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
479 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
481 else
482 form = form..[[
483 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
484 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
489 return form
490 end,
491 handle = function(self, pos, name, fields)
492 local meta = minetest.get_meta(pos)
493 local player = minetest.get_player_by_name(name)
495 if fields.slices then
496 local slices = fields.slices:split(":")
497 self.selected = tonumber(slices[2])
500 if fields.add then
501 if not self.panel_add then
502 self.panel_add = true
503 schemedit.show_formspec(pos, player, "slice")
504 else
505 self.panel_add = nil
506 schemedit.show_formspec(pos, player, "slice")
510 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
511 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
512 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
513 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
514 local slice_list = minetest.deserialize(meta:get_string("slices"))
515 local index = #slice_list + 1
516 if fields.done_edit then
517 index = self.selected
520 slice_list[index] = {ypos = ypos, prob = prob}
522 meta:set_string("slices", minetest.serialize(slice_list))
524 -- Update and show formspec
525 self.panel_add = nil
526 schemedit.show_formspec(pos, player, "slice")
529 if fields.remove and self.selected then
530 local slice_list = minetest.deserialize(meta:get_string("slices"))
531 slice_list[self.selected] = nil
532 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
534 -- Update formspec
535 self.selected = 1
536 self.panel_edit = nil
537 schemedit.show_formspec(pos, player, "slice")
540 if fields.edit then
541 if not self.panel_edit then
542 self.panel_edit = true
543 schemedit.show_formspec(pos, player, "slice")
544 else
545 self.panel_edit = nil
546 schemedit.show_formspec(pos, player, "slice")
549 end,
552 schemedit.add_form("probtool", {
553 cache_name = false,
554 caption = S("Schematic Node Probability Tool"),
555 get = function(self, pos, name)
556 local player = minetest.get_player_by_name(name)
557 if not player then
558 return
560 local probtool = player:get_wielded_item()
561 if probtool:get_name() ~= "schemedit:probtool" then
562 return
565 local meta = probtool:get_meta()
566 local prob = tonumber(meta:get_string("schemedit_prob"))
567 local force_place = meta:get_string("schemedit_force_place")
569 if not prob then
570 prob = 255
572 if force_place == nil or force_place == "" then
573 force_place = "false"
575 local form = "size[5,4]"..
576 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
577 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
578 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
579 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
580 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
581 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
582 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
583 "field_close_on_enter[prob;false]"
584 return form
585 end,
586 handle = function(self, pos, name, fields)
587 if fields.submit then
588 local prob = tonumber(fields.prob)
589 if prob then
590 local player = minetest.get_player_by_name(name)
591 if not player then
592 return
594 local probtool = player:get_wielded_item()
595 if probtool:get_name() ~= "schemedit:probtool" then
596 return
599 local force_place = self.force_place == true
601 set_item_metadata(probtool, prob, force_place)
603 -- Repurpose the tool's wear bar to display the set probability
604 probtool:set_wear(math.floor(((255-prob)/255)*65535))
606 player:set_wielded_item(probtool)
609 if fields.force_place == "true" then
610 self.force_place = true
611 elseif fields.force_place == "false" then
612 self.force_place = false
614 end,
618 --- API
621 --- Copies and modifies positions `pos1` and `pos2` so that each component of
622 -- `pos1` is less than or equal to the corresponding component of `pos2`.
623 -- Returns the new positions.
624 function schemedit.sort_pos(pos1, pos2)
625 if not pos1 or not pos2 then
626 return
629 pos1, pos2 = table.copy(pos1), table.copy(pos2)
630 if pos1.x > pos2.x then
631 pos2.x, pos1.x = pos1.x, pos2.x
633 if pos1.y > pos2.y then
634 pos2.y, pos1.y = pos1.y, pos2.y
636 if pos1.z > pos2.z then
637 pos2.z, pos1.z = pos1.z, pos2.z
639 return pos1, pos2
642 -- [function] Prepare size
643 function schemedit.size(pos)
644 local pos1 = vector.new(pos)
645 local meta = minetest.get_meta(pos)
646 local node = minetest.get_node(pos)
647 local param2 = node.param2
648 local size = {
649 x = meta:get_int("x_size"),
650 y = math.max(meta:get_int("y_size") - 1, 0),
651 z = meta:get_int("z_size"),
654 if param2 == 1 then
655 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
656 pos1.x = pos1.x + 1
657 new_pos.z = new_pos.z + 1
658 return pos1, new_pos
659 elseif param2 == 2 then
660 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
661 pos1.z = pos1.z - 1
662 new_pos.x = new_pos.x + 1
663 return pos1, new_pos
664 elseif param2 == 3 then
665 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
666 pos1.x = pos1.x - 1
667 new_pos.z = new_pos.z - 1
668 return pos1, new_pos
669 else
670 local new_pos = vector.add(size, pos)
671 pos1.z = pos1.z + 1
672 new_pos.x = new_pos.x - 1
673 return pos1, new_pos
677 -- [function] Mark region
678 function schemedit.mark(pos)
679 schemedit.unmark(pos)
681 local id = minetest.hash_node_position(pos)
682 local owner = minetest.get_meta(pos):get_string("owner")
683 local pos1, pos2 = schemedit.size(pos)
684 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
686 local thickness = 0.2
687 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
688 local m = {}
689 local low = true
690 local offset
692 -- XY plane markers
693 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
694 if low then
695 offset = -0.01
696 else
697 offset = 0.01
699 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
700 if marker ~= nil then
701 marker:set_properties({
702 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
704 marker:get_luaentity().id = id
705 marker:get_luaentity().owner = owner
706 table.insert(m, marker)
708 low = false
711 low = true
712 -- YZ plane markers
713 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
714 if low then
715 offset = -0.01
716 else
717 offset = 0.01
720 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
721 if marker ~= nil then
722 marker:set_properties({
723 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
725 marker:set_rotation({x=0, y=math.pi / 2, z=0})
726 marker:get_luaentity().id = id
727 marker:get_luaentity().owner = owner
728 table.insert(m, marker)
730 low = false
733 low = true
734 -- XZ plane markers
735 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
736 if low then
737 offset = -0.01
738 else
739 offset = 0.01
742 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
743 if marker ~= nil then
744 marker:set_properties({
745 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
747 marker:set_rotation({x=math.pi/2, y=0, z=0})
748 marker:get_luaentity().id = id
749 marker:get_luaentity().owner = owner
750 table.insert(m, marker)
752 low = false
757 schemedit.markers[id] = m
758 return true
761 -- [function] Unmark region
762 function schemedit.unmark(pos)
763 local id = minetest.hash_node_position(pos)
764 if schemedit.markers[id] then
765 local retval
766 for _, entity in ipairs(schemedit.markers[id]) do
767 entity:remove()
768 retval = true
770 return retval
775 --- Mark node probability values near player
778 -- Show probability and force_place status of a particular position for player in HUD.
779 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
780 -- The distance to the node is also displayed below that. This can't be avoided and is
781 -- and artifact of the waypoint HUD element. TODO: Hide displayed distance.
782 function schemedit.display_node_prob(player, pos, prob, force_place)
783 local wpstring
784 if prob and force_place == true then
785 wpstring = string.format("%d [F]", prob)
786 elseif prob then
787 wpstring = prob
788 elseif force_place == true then
789 wpstring = "[F]"
791 if wpstring then
792 return player:hud_add({
793 hud_elem_type = "waypoint",
794 name = wpstring,
795 text = "m", -- For the distance artifact
796 number = text_color_number,
797 world_pos = pos,
802 -- Display the node probabilities and force_place status of the nodes in a region.
803 -- By default, this is done for nodes near the player (distance: 5).
804 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
805 function schemedit.display_node_probs_region(player, pos1, pos2)
806 local playername = player:get_player_name()
807 local pos = vector.round(player:get_pos())
809 local dist = 5
810 -- Default: 5 nodes away from player in any direction
811 if not pos1 then
812 pos1 = vector.subtract(pos, dist)
814 if not pos2 then
815 pos2 = vector.add(pos, dist)
817 for x=pos1.x, pos2.x do
818 for y=pos1.y, pos2.y do
819 for z=pos1.z, pos2.z do
820 local checkpos = {x=x, y=y, z=z}
821 local nodehash = minetest.hash_node_position(checkpos)
823 -- If node is already displayed, remove it so it can re replaced later
824 if displayed_waypoints[playername][nodehash] then
825 player:hud_remove(displayed_waypoints[playername][nodehash])
826 displayed_waypoints[playername][nodehash] = nil
829 local prob, force_place
830 local meta = minetest.get_meta(checkpos)
831 prob = tonumber(meta:get_string("schemedit_prob"))
832 force_place = meta:get_string("schemedit_force_place") == "true"
833 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
834 if hud_id then
835 displayed_waypoints[playername][nodehash] = hud_id
836 displayed_waypoints[playername].display_active = true
843 -- Remove all active displayed node statuses.
844 function schemedit.clear_displayed_node_probs(player)
845 local playername = player:get_player_name()
846 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
847 player:hud_remove(hud_id)
848 displayed_waypoints[playername][nodehash] = nil
849 displayed_waypoints[playername].display_active = false
853 minetest.register_on_joinplayer(function(player)
854 displayed_waypoints[player:get_player_name()] = {
855 display_active = false -- If true, there *might* be at least one active node prob HUD display
856 -- If false, no node probabilities are displayed for sure.
858 end)
860 minetest.register_on_leaveplayer(function(player)
861 displayed_waypoints[player:get_player_name()] = nil
862 end)
864 -- Regularily clear the displayed node probabilities and force_place
865 -- for all players who do not wield the probtool.
866 -- This makes sure the screen is not spammed with information when it
867 -- isn't needed.
868 local cleartimer = 0
869 minetest.register_globalstep(function(dtime)
870 cleartimer = cleartimer + dtime
871 if cleartimer > 2 then
872 local players = minetest.get_connected_players()
873 for p = 1, #players do
874 local player = players[p]
875 local pname = player:get_player_name()
876 if displayed_waypoints[pname].display_active then
877 local item = player:get_wielded_item()
878 if item:get_name() ~= "schemedit:probtool" then
879 schemedit.clear_displayed_node_probs(player)
883 cleartimer = 0
885 end)
888 --- Registrations
891 -- [priv] schematic_override
892 minetest.register_privilege("schematic_override", {
893 description = S("Allows you to access schemedit nodes not owned by you"),
894 give_to_singleplayer = false,
897 -- [node] Schematic creator
898 minetest.register_node("schemedit:creator", {
899 description = S("Schematic Creator"),
900 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
901 _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"..
902 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"..
903 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
904 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"..
905 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."),
906 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
907 "schemedit_creator_sides.png"},
908 groups = { dig_immediate = 2},
909 paramtype2 = "facedir",
910 is_ground_content = false,
912 after_place_node = function(pos, player)
913 local name = player:get_player_name()
914 local meta = minetest.get_meta(pos)
916 meta:set_string("owner", name)
917 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
918 meta:set_string("prob_list", minetest.serialize({}))
919 meta:set_string("slices", minetest.serialize({}))
921 local node = minetest.get_node(pos)
922 local dir = minetest.facedir_to_dir(node.param2)
924 meta:set_int("x_size", 1)
925 meta:set_int("y_size", 1)
926 meta:set_int("z_size", 1)
928 -- Don't take item from itemstack
929 return true
930 end,
931 can_dig = function(pos, player)
932 local name = player:get_player_name()
933 local meta = minetest.get_meta(pos)
934 if meta:get_string("owner") == name or
935 minetest.check_player_privs(player, "schematic_override") == true then
936 return true
939 return false
940 end,
941 on_rightclick = function(pos, node, player)
942 local meta = minetest.get_meta(pos)
943 local name = player:get_player_name()
944 if meta:get_string("owner") == name or
945 minetest.check_player_privs(player, "schematic_override") == true then
946 -- Get player attribute
947 local tab = player:get_attribute("schemedit:tab")
948 if not forms[tab] or not tab then
949 tab = "main"
952 schemedit.show_formspec(pos, player, tab, true)
954 end,
955 after_destruct = function(pos)
956 schemedit.unmark(pos)
957 end,
959 -- No support for Minetest Game's screwdriver
960 on_rotate = false,
963 minetest.register_tool("schemedit:probtool", {
964 description = S("Schematic Node Probability Tool"),
965 _doc_items_longdesc =
966 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"..
967 S("It allows you to set two things:").."\n"..
968 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
969 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
970 _doc_items_usagehelp = "\n"..
971 S("BASIC USAGE:").."\n"..
972 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"..
973 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"..
974 S("NODE HUD:").."\n"..
975 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"..
976 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
977 S("UPDATING THE NODE HUD:").."\n"..
978 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."),
979 wield_image = "schemedit_probtool.png",
980 inventory_image = "schemedit_probtool.png",
981 liquids_pointable = true,
982 groups = { disable_repair = 1 },
983 on_use = function(itemstack, user, pointed_thing)
984 local ctrl = user:get_player_control()
985 -- Simple use
986 if not ctrl.sneak then
987 -- Open dialog to change the probability to apply to nodes
988 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
990 -- Use + sneak
991 else
992 -- Display the probability and force_place values for nodes.
994 -- If a schematic creator was punched, only enable display for all nodes
995 -- within the creator's region.
996 local use_creator_region = false
997 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
998 punchpos = pointed_thing.under
999 local node = minetest.get_node(punchpos)
1000 if node.name == "schemedit:creator" then
1001 local pos1, pos2 = schemedit.size(punchpos)
1002 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1003 schemedit.display_node_probs_region(user, pos1, pos2)
1004 return
1008 -- Otherwise, just display the region close to the player
1009 schemedit.display_node_probs_region(user)
1011 end,
1012 on_secondary_use = function(itemstack, user, pointed_thing)
1013 schemedit.clear_displayed_node_probs(user)
1014 end,
1015 -- Set note probability and force_place and enable node probability display
1016 on_place = function(itemstack, placer, pointed_thing)
1017 -- Use pointed node's on_rightclick function first, if present
1018 local node = minetest.get_node(pointed_thing.under)
1019 if placer and not placer:get_player_control().sneak then
1020 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1021 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1025 -- This sets the node probability of pointed node to the
1026 -- currently used probability stored in the tool.
1027 local pos = pointed_thing.under
1028 local node = minetest.get_node(pos)
1029 -- Schematic void are ignored, they always have probability 0
1030 if node.name == "schemedit:void" then
1031 return itemstack
1033 local nmeta = minetest.get_meta(pos)
1034 local imeta = itemstack:get_meta()
1035 local prob = tonumber(imeta:get_string("schemedit_prob"))
1036 local force_place = imeta:get_string("schemedit_force_place")
1038 if not prob or prob == 255 then
1039 nmeta:set_string("schemedit_prob", nil)
1040 else
1041 nmeta:set_string("schemedit_prob", prob)
1043 if force_place == "true" then
1044 nmeta:set_string("schemedit_force_place", "true")
1045 else
1046 nmeta:set_string("schemedit_force_place", nil)
1049 -- Enable node probablity display
1050 schemedit.display_node_probs_region(placer)
1052 return itemstack
1053 end,
1056 minetest.register_node("schemedit:void", {
1057 description = S("Schematic Void"),
1058 _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."),
1059 _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."),
1060 tiles = { "schemedit_void.png" },
1061 drawtype = "nodebox",
1062 is_ground_content = false,
1063 paramtype = "light",
1064 walkable = false,
1065 sunlight_propagates = true,
1066 node_box = {
1067 type = "fixed",
1068 fixed = {
1069 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1072 groups = { dig_immediate = 3},
1075 -- [entity] Visible schematic border
1076 minetest.register_entity("schemedit:display", {
1077 visual = "upright_sprite",
1078 textures = {"schemedit_border.png"},
1079 visual_size = {x=10, y=10},
1080 pointable = false,
1081 physical = false,
1082 static_save = false,
1083 glow = minetest.LIGHT_MAX,
1085 on_step = function(self, dtime)
1086 if not self.id then
1087 self.object:remove()
1088 elseif not schemedit.markers[self.id] then
1089 self.object:remove()
1091 end,
1092 on_activate = function(self)
1093 self.object:set_armor_groups({immortal = 1})
1094 end,
1097 minetest.register_lbm({
1098 label = "Reset schematic creator border entities",
1099 name = "schemedit:reset_border",
1100 nodenames = "schemedit:creator",
1101 run_at_every_load = true,
1102 action = function(pos, node)
1103 local meta = minetest.get_meta(pos)
1104 meta:set_string("schem_border", "false")
1105 end,
1108 -- [chatcommand] Place schematic
1109 minetest.register_chatcommand("placeschem", {
1110 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1111 privs = {debug = true},
1112 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1113 func = function(name, param)
1114 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1115 local pos = minetest.string_to_pos(p)
1117 if not schem then
1118 return false, S("No schematic file specified.")
1121 if not pos then
1122 pos = minetest.get_player_by_name(name):get_pos()
1125 -- Automatically add file name suffix if omitted
1126 local schem_full
1127 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1128 schem_full = schem
1129 else
1130 schem_full = schem .. ".mts"
1133 local success = false
1134 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1135 if minetest.read_schematic then
1136 -- We don't call minetest.place_schematic with the path name directly because
1137 -- this would trigger the caching and we wouldn't get any updates to the schematic
1138 -- files when we reload. minetest.read_schematic circumvents that.
1139 local schematic = minetest.read_schematic(schem_path, {})
1140 if schematic then
1141 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1143 else
1144 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1145 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1148 if success == nil then
1149 return false, S("Schematic file could not be loaded!")
1150 else
1151 return true
1153 end,