Backwards-compat for versions w/o read_schematic
[minetest_schemedit.git] / init.lua
blob65f1c6f9730546327701e524a2d59fe77c24e43b
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 local can_import = minetest.read_schematic ~= nil
20 schemedit.markers = {}
22 -- [local function] Renumber table
23 local function renumber(t)
24 local res = {}
25 for _, i in pairs(t) do
26 res[#res + 1] = i
27 end
28 return res
29 end
31 ---
32 --- Formspec API
33 ---
35 local contexts = {}
36 local form_data = {}
37 local tabs = {}
38 local forms = {}
39 local displayed_waypoints = {}
41 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
42 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
43 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
44 -- on an actual export to a schematic.
46 function schemedit.lua_prob_to_schematic_prob(lua_prob)
47 return math.floor(lua_prob / 2)
48 end
50 function schemedit.schematic_prob_to_lua_prob(schematic_prob)
51 return schematic_prob * 2
53 end
55 -- [function] Add form
56 function schemedit.add_form(name, def)
57 def.name = name
58 forms[name] = def
60 if def.tab then
61 tabs[#tabs + 1] = name
62 end
63 end
65 -- [function] Generate tabs
66 function schemedit.generate_tabs(current)
67 local retval = "tabheader[0,0;tabs;"
68 for _, t in pairs(tabs) do
69 local f = forms[t]
70 if f.tab ~= false and f.caption then
71 retval = retval..f.caption..","
73 if type(current) ~= "number" and current == f.name then
74 current = _
75 end
76 end
77 end
78 retval = retval:sub(1, -2) -- Strip last comma
79 retval = retval..";"..current.."]" -- Close tabheader
80 return retval
81 end
83 -- [function] Handle tabs
84 function schemedit.handle_tabs(pos, name, fields)
85 local tab = tonumber(fields.tabs)
86 if tab and tabs[tab] and forms[tabs[tab]] then
87 schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
88 return true
89 end
90 end
92 -- [function] Show formspec
93 function schemedit.show_formspec(pos, player, tab, show, ...)
94 if forms[tab] then
95 if type(player) == "string" then
96 player = minetest.get_player_by_name(player)
97 end
98 local name = player:get_player_name()
100 if show ~= false then
101 if not form_data[name] then
102 form_data[name] = {}
105 local form = forms[tab].get(form_data[name], pos, name, ...)
106 if forms[tab].tab then
107 form = form..schemedit.generate_tabs(tab)
110 minetest.show_formspec(name, "schemedit:"..tab, form)
111 contexts[name] = pos
113 -- Update player attribute
114 if forms[tab].cache_name ~= false then
115 player:set_attribute("schemedit:tab", tab)
117 else
118 minetest.close_formspec(pname, "schemedit:"..tab)
123 -- [event] On receive fields
124 minetest.register_on_player_receive_fields(function(player, formname, fields)
125 local formname = formname:split(":")
127 if formname[1] == "schemedit" and forms[formname[2]] then
128 local handle = forms[formname[2]].handle
129 local name = player:get_player_name()
130 if contexts[name] then
131 if not form_data[name] then
132 form_data[name] = {}
135 if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
136 handle(form_data[name], contexts[name], name, fields)
140 end)
142 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
143 schemedit.scan_metadata = function(pos1, pos2)
144 local prob_list = {}
146 for x=pos1.x, pos2.x do
147 for y=pos1.y, pos2.y do
148 for z=pos1.z, pos2.z do
149 local scanpos = {x=x, y=y, z=z}
150 local node = minetest.get_node_or_nil(scanpos)
152 local prob, force_place
153 if node == nil or node.name == "schemedit:void" then
154 prob = 0
155 force_place = false
156 else
157 local meta = minetest.get_meta(scanpos)
159 prob = tonumber(meta:get_string("schemedit_prob")) or 255
160 local fp = meta:get_string("schemedit_force_place")
161 if fp == "true" then
162 force_place = true
163 else
164 force_place = false
168 local hashpos = minetest.hash_node_position(scanpos)
169 prob_list[hashpos] = {
170 pos = scanpos,
171 prob = prob,
172 force_place = force_place,
178 return prob_list
181 -- Sets probability and force_place metadata of an item.
182 -- Also updates item description.
183 -- The itemstack is updated in-place.
184 local function set_item_metadata(itemstack, prob, force_place)
185 local smeta = itemstack:get_meta()
186 local prob_desc = "\n"..S("Probability: @1", prob or
187 smeta:get_string("schemedit_prob") or S("Not Set"))
188 -- Update probability
189 if prob and prob >= 0 and prob < 255 then
190 smeta:set_string("schemedit_prob", tostring(prob))
191 elseif prob and prob == 255 then
192 -- Clear prob metadata for default probability
193 prob_desc = ""
194 smeta:set_string("schemedit_prob", nil)
195 else
196 prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
197 S("Not Set"))
200 -- Update force place
201 if force_place == true then
202 smeta:set_string("schemedit_force_place", "true")
203 elseif force_place == false then
204 smeta:set_string("schemedit_force_place", nil)
207 -- Update description
208 local desc = minetest.registered_items[itemstack:get_name()].description
209 local meta_desc = smeta:get_string("description")
210 if meta_desc and meta_desc ~= "" then
211 desc = meta_desc
214 local original_desc = smeta:get_string("original_description")
215 if original_desc and original_desc ~= "" then
216 desc = original_desc
217 else
218 smeta:set_string("original_description", desc)
221 local force_desc = ""
222 if smeta:get_string("schemedit_force_place") == "true" then
223 force_desc = "\n"..S("Force placement")
226 desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
228 smeta:set_string("description", desc)
230 return itemstack
234 --- Formspec Tabs
236 local import_btn = ""
237 if can_import then
238 import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
240 schemedit.add_form("main", {
241 tab = true,
242 caption = S("Main"),
243 get = function(self, pos, name)
244 local meta = minetest.get_meta(pos):to_table().fields
245 local strpos = minetest.pos_to_string(pos)
246 local hashpos = minetest.hash_node_position(pos)
248 local border_button
249 if meta.schem_border == "true" and schemedit.markers[hashpos] then
250 border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
251 else
252 border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
255 -- TODO: Show information regarding volume, pos1, pos2, etc... in formspec
256 local form = [[
257 size[7,8]
258 label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
259 label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
261 field[0.8,1;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(meta.schem_name or "")..[[]
262 button[5.3,0.69;1.2,1;save_name;]]..F(S("OK"))..[[]
263 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
264 field_close_on_enter[name;false]
266 button[0.5,1.5;6,1;export;]]..F(S("Export schematic")).."]"..
267 import_btn..[[
268 textarea[0.8,3.5;6.2,5;;]]..F(S("The schematic will be exported as a .mts file and stored in\n@1",
269 export_path_trunc .. DIR_DELIM .. "<name>.mts."))..[[;]
270 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..meta.x_size..[[]
271 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..meta.y_size..[[]
272 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..meta.z_size..[[]
273 field_close_on_enter[x;false]
274 field_close_on_enter[y;false]
275 field_close_on_enter[z;false]
277 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
278 ]]..
279 border_button
280 if minetest.get_modpath("doc") then
281 form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
282 "tooltip[doc;"..F(S("Help")).."]"
284 return form
285 end,
286 handle = function(self, pos, name, fields)
287 local realmeta = minetest.get_meta(pos)
288 local meta = realmeta:to_table().fields
289 local hashpos = minetest.hash_node_position(pos)
291 -- Save size vector values
292 if (fields.x and fields.x ~= "") then
293 local x = tonumber(fields.x)
294 if x then
295 meta.x_size = math.max(x, 1)
298 if (fields.y and fields.y ~= "") then
299 local y = tonumber(fields.y)
300 if y then
301 meta.y_size = math.max(y, 1)
304 if (fields.z and fields.z ~= "") then
305 local z = tonumber(fields.z)
306 if z then
307 meta.z_size = math.max(z, 1)
311 -- Save schematic name
312 if fields.name then
313 meta.schem_name = fields.name
316 if fields.doc then
317 doc.show_entry(name, "nodes", "schemedit:creator", true)
318 return
321 -- Toggle border
322 if fields.border then
323 if meta.schem_border == "true" and schemedit.markers[hashpos] then
324 schemedit.unmark(pos)
325 meta.schem_border = "false"
326 else
327 schemedit.mark(pos)
328 meta.schem_border = "true"
332 -- Export schematic
333 if fields.export and meta.schem_name and meta.schem_name ~= "" then
334 local pos1, pos2 = schemedit.size(pos)
335 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
336 local path = export_path_full .. DIR_DELIM
337 minetest.mkdir(path)
339 local plist = schemedit.scan_metadata(pos1, pos2)
340 local probability_list = {}
341 for hash, i in pairs(plist) do
342 local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
343 if i.force_place == true then
344 prob = prob + 128
347 table.insert(probability_list, {
348 pos = minetest.get_position_from_hash(hash),
349 prob = prob,
353 local slist = minetest.deserialize(meta.slices)
354 local slice_list = {}
355 for _, i in pairs(slist) do
356 slice_list[#slice_list + 1] = {
357 ypos = pos.y + i.ypos,
358 prob = schemedit.lua_prob_to_schematic_prob(i.prob),
362 local filepath = path..meta.schem_name..".mts"
363 local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
365 if res then
366 minetest.chat_send_player(name, minetest.colorize("#00ff00",
367 S("Exported schematic to @1", filepath)))
368 else
369 minetest.chat_send_player(name, minetest.colorize("red",
370 S("Failed to export schematic to @1", filepath)))
374 -- Import schematic
375 if fields.import and meta.schem_name and meta.schem_name ~= "" then
376 if not minetest.get_player_privs(name).debug then
377 minetest.chat_send_player(name, minetest.colorize("red",
378 S("Insufficient privileges! You need the “debug” privilege to do this.")))
379 return
382 if not can_import then
383 return
385 local pos1
386 local node = minetest.get_node(pos)
387 local path = export_path_full .. DIR_DELIM
389 local filepath = path..meta.schem_name..".mts"
390 local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
391 local success = false
393 if schematic then
394 meta.x_size = schematic.size.x
395 meta.y_size = schematic.size.y
396 meta.z_size = schematic.size.z
397 meta.slices = minetest.serialize(schematic.yslice_prob)
399 if node.param2 == 1 then
400 pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
401 elseif node.param2 == 2 then
402 pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
403 elseif node.param2 == 3 then
404 pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
405 else
406 pos1 = vector.add(pos, {x=0,y=0,z=1})
409 local schematic_for_meta = table.copy(schematic)
410 -- Strip probability data for placement
411 schematic.yslice_prob = {}
412 for d=1, #schematic.data do
413 schematic.data[d].prob = nil
416 -- Place schematic
417 success = minetest.place_schematic(pos1, schematic, "0", nil, true)
419 -- Add special schematic data to nodes
420 if success then
421 local d = 1
422 for z=0, meta.z_size-1 do
423 for y=0, meta.y_size-1 do
424 for x=0, meta.x_size-1 do
425 local data = schematic_for_meta.data[d]
426 local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
427 if data.prob == 0 then
428 minetest.set_node(pp, {name="schemedit:void"})
429 else
430 local meta = minetest.get_meta(pp)
431 if data.prob and data.prob ~= 255 and data.prob ~= 254 then
432 meta:set_string("schemedit_prob", tostring(data.prob))
433 else
434 meta:set_string("schemedit_prob", "")
436 if data.force_place then
437 meta:set_string("schemedit_force_place", "true")
438 else
439 meta:set_string("schemedit_force_place", "")
442 d = d + 1
448 if success then
449 minetest.chat_send_player(name, minetest.colorize("#00ff00",
450 S("Imported schematic from @1", filepath)))
451 else
452 minetest.chat_send_player(name, minetest.colorize("red",
453 S("Failed to import schematic from @1", filepath)))
459 -- Save meta before updating visuals
460 local inv = realmeta:get_inventory():get_lists()
461 realmeta:from_table({fields = meta, inventory = inv})
463 -- Update border
464 if not fields.border and meta.schem_border == "true" then
465 schemedit.mark(pos)
468 -- Update formspec
469 if not fields.quit then
470 schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
472 end,
475 schemedit.add_form("slice", {
476 caption = S("Y Slices"),
477 tab = true,
478 get = function(self, pos, name, visible_panel)
479 local meta = minetest.get_meta(pos):to_table().fields
481 self.selected = self.selected or 1
482 local selected = tostring(self.selected)
483 local slice_list = minetest.deserialize(meta.slices)
484 local slices = ""
485 for _, i in pairs(slice_list) do
486 local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
487 slices = slices..insert..","
489 slices = slices:sub(1, -2) -- Remove final comma
491 local form = [[
492 size[7,8]
493 table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
496 if self.panel_add or self.panel_edit then
497 local ypos_default, prob_default = "", ""
498 local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
499 if self.panel_edit then
500 done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
501 if slice_list[self.selected] then
502 ypos_default = slice_list[self.selected].ypos
503 prob_default = slice_list[self.selected].prob
507 form = form..[[
508 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
509 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
510 field_close_on_enter[ypos;false]
511 field_close_on_enter[prob;false]
512 ]]..done_button
515 if not self.panel_edit then
516 form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
519 if slices ~= "" and self.selected and not self.panel_add then
520 if not self.panel_edit then
521 form = form..[[
522 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
523 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
525 else
526 form = form..[[
527 button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
528 button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
533 return form
534 end,
535 handle = function(self, pos, name, fields)
536 local meta = minetest.get_meta(pos)
537 local player = minetest.get_player_by_name(name)
539 if fields.slices then
540 local slices = fields.slices:split(":")
541 self.selected = tonumber(slices[2])
544 if fields.add then
545 if not self.panel_add then
546 self.panel_add = true
547 schemedit.show_formspec(pos, player, "slice")
548 else
549 self.panel_add = nil
550 schemedit.show_formspec(pos, player, "slice")
554 local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
555 if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
556 fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
557 ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
558 local slice_list = minetest.deserialize(meta:get_string("slices"))
559 local index = #slice_list + 1
560 if fields.done_edit then
561 index = self.selected
564 slice_list[index] = {ypos = ypos, prob = prob}
566 meta:set_string("slices", minetest.serialize(slice_list))
568 -- Update and show formspec
569 self.panel_add = nil
570 schemedit.show_formspec(pos, player, "slice")
573 if fields.remove and self.selected then
574 local slice_list = minetest.deserialize(meta:get_string("slices"))
575 slice_list[self.selected] = nil
576 meta:set_string("slices", minetest.serialize(renumber(slice_list)))
578 -- Update formspec
579 self.selected = 1
580 self.panel_edit = nil
581 schemedit.show_formspec(pos, player, "slice")
584 if fields.edit then
585 if not self.panel_edit then
586 self.panel_edit = true
587 schemedit.show_formspec(pos, player, "slice")
588 else
589 self.panel_edit = nil
590 schemedit.show_formspec(pos, player, "slice")
593 end,
596 schemedit.add_form("probtool", {
597 cache_name = false,
598 caption = S("Schematic Node Probability Tool"),
599 get = function(self, pos, name)
600 local player = minetest.get_player_by_name(name)
601 if not player then
602 return
604 local probtool = player:get_wielded_item()
605 if probtool:get_name() ~= "schemedit:probtool" then
606 return
609 local meta = probtool:get_meta()
610 local prob = tonumber(meta:get_string("schemedit_prob"))
611 local force_place = meta:get_string("schemedit_force_place")
613 if not prob then
614 prob = 255
616 if force_place == nil or force_place == "" then
617 force_place = "false"
619 local form = "size[5,4]"..
620 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
621 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
622 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
623 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
624 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
625 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
626 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
627 "field_close_on_enter[prob;false]"
628 return form
629 end,
630 handle = function(self, pos, name, fields)
631 if fields.submit then
632 local prob = tonumber(fields.prob)
633 if prob then
634 local player = minetest.get_player_by_name(name)
635 if not player then
636 return
638 local probtool = player:get_wielded_item()
639 if probtool:get_name() ~= "schemedit:probtool" then
640 return
643 local force_place = self.force_place == true
645 set_item_metadata(probtool, prob, force_place)
647 -- Repurpose the tool's wear bar to display the set probability
648 probtool:set_wear(math.floor(((255-prob)/255)*65535))
650 player:set_wielded_item(probtool)
653 if fields.force_place == "true" then
654 self.force_place = true
655 elseif fields.force_place == "false" then
656 self.force_place = false
658 end,
662 --- API
665 --- Copies and modifies positions `pos1` and `pos2` so that each component of
666 -- `pos1` is less than or equal to the corresponding component of `pos2`.
667 -- Returns the new positions.
668 function schemedit.sort_pos(pos1, pos2)
669 if not pos1 or not pos2 then
670 return
673 pos1, pos2 = table.copy(pos1), table.copy(pos2)
674 if pos1.x > pos2.x then
675 pos2.x, pos1.x = pos1.x, pos2.x
677 if pos1.y > pos2.y then
678 pos2.y, pos1.y = pos1.y, pos2.y
680 if pos1.z > pos2.z then
681 pos2.z, pos1.z = pos1.z, pos2.z
683 return pos1, pos2
686 -- [function] Prepare size
687 function schemedit.size(pos)
688 local pos1 = vector.new(pos)
689 local meta = minetest.get_meta(pos)
690 local node = minetest.get_node(pos)
691 local param2 = node.param2
692 local size = {
693 x = meta:get_int("x_size"),
694 y = math.max(meta:get_int("y_size") - 1, 0),
695 z = meta:get_int("z_size"),
698 if param2 == 1 then
699 local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
700 pos1.x = pos1.x + 1
701 new_pos.z = new_pos.z + 1
702 return pos1, new_pos
703 elseif param2 == 2 then
704 local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
705 pos1.z = pos1.z - 1
706 new_pos.x = new_pos.x + 1
707 return pos1, new_pos
708 elseif param2 == 3 then
709 local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
710 pos1.x = pos1.x - 1
711 new_pos.z = new_pos.z - 1
712 return pos1, new_pos
713 else
714 local new_pos = vector.add(size, pos)
715 pos1.z = pos1.z + 1
716 new_pos.x = new_pos.x - 1
717 return pos1, new_pos
721 -- [function] Mark region
722 function schemedit.mark(pos)
723 schemedit.unmark(pos)
725 local id = minetest.hash_node_position(pos)
726 local owner = minetest.get_meta(pos):get_string("owner")
727 local pos1, pos2 = schemedit.size(pos)
728 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
730 local thickness = 0.2
731 local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
732 local m = {}
733 local low = true
734 local offset
736 -- XY plane markers
737 for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
738 if low then
739 offset = -0.01
740 else
741 offset = 0.01
743 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
744 if marker ~= nil then
745 marker:set_properties({
746 visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
748 marker:get_luaentity().id = id
749 marker:get_luaentity().owner = owner
750 table.insert(m, marker)
752 low = false
755 low = true
756 -- YZ plane markers
757 for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
758 if low then
759 offset = -0.01
760 else
761 offset = 0.01
764 local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
765 if marker ~= nil then
766 marker:set_properties({
767 visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
769 marker:set_rotation({x=0, y=math.pi / 2, z=0})
770 marker:get_luaentity().id = id
771 marker:get_luaentity().owner = owner
772 table.insert(m, marker)
774 low = false
777 low = true
778 -- XZ plane markers
779 for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
780 if low then
781 offset = -0.01
782 else
783 offset = 0.01
786 local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
787 if marker ~= nil then
788 marker:set_properties({
789 visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
791 marker:set_rotation({x=math.pi/2, y=0, z=0})
792 marker:get_luaentity().id = id
793 marker:get_luaentity().owner = owner
794 table.insert(m, marker)
796 low = false
801 schemedit.markers[id] = m
802 return true
805 -- [function] Unmark region
806 function schemedit.unmark(pos)
807 local id = minetest.hash_node_position(pos)
808 if schemedit.markers[id] then
809 local retval
810 for _, entity in ipairs(schemedit.markers[id]) do
811 entity:remove()
812 retval = true
814 return retval
819 --- Mark node probability values near player
822 -- Show probability and force_place status of a particular position for player in HUD.
823 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
824 -- The distance to the node is also displayed below that. This can't be avoided and is
825 -- and artifact of the waypoint HUD element. TODO: Hide displayed distance.
826 function schemedit.display_node_prob(player, pos, prob, force_place)
827 local wpstring
828 if prob and force_place == true then
829 wpstring = string.format("%s [F]", prob)
830 elseif prob and type(tonumber(prob)) == "number" then
831 wpstring = prob
832 elseif force_place == true then
833 wpstring = "[F]"
835 if wpstring then
836 return player:hud_add({
837 hud_elem_type = "waypoint",
838 name = wpstring,
839 precision = 0,
840 text = "m", -- For the distance artifact
841 number = text_color_number,
842 world_pos = pos,
847 -- Display the node probabilities and force_place status of the nodes in a region.
848 -- By default, this is done for nodes near the player (distance: 5).
849 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
850 function schemedit.display_node_probs_region(player, pos1, pos2)
851 local playername = player:get_player_name()
852 local pos = vector.round(player:get_pos())
854 local dist = 5
855 -- Default: 5 nodes away from player in any direction
856 if not pos1 then
857 pos1 = vector.subtract(pos, dist)
859 if not pos2 then
860 pos2 = vector.add(pos, dist)
862 for x=pos1.x, pos2.x do
863 for y=pos1.y, pos2.y do
864 for z=pos1.z, pos2.z do
865 local checkpos = {x=x, y=y, z=z}
866 local nodehash = minetest.hash_node_position(checkpos)
868 -- If node is already displayed, remove it so it can re replaced later
869 if displayed_waypoints[playername][nodehash] then
870 player:hud_remove(displayed_waypoints[playername][nodehash])
871 displayed_waypoints[playername][nodehash] = nil
874 local prob, force_place
875 local meta = minetest.get_meta(checkpos)
876 prob = meta:get_string("schemedit_prob")
877 force_place = meta:get_string("schemedit_force_place") == "true"
878 local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
879 if hud_id then
880 displayed_waypoints[playername][nodehash] = hud_id
881 displayed_waypoints[playername].display_active = true
888 -- Remove all active displayed node statuses.
889 function schemedit.clear_displayed_node_probs(player)
890 local playername = player:get_player_name()
891 for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
892 player:hud_remove(hud_id)
893 displayed_waypoints[playername][nodehash] = nil
894 displayed_waypoints[playername].display_active = false
898 minetest.register_on_joinplayer(function(player)
899 displayed_waypoints[player:get_player_name()] = {
900 display_active = false -- If true, there *might* be at least one active node prob HUD display
901 -- If false, no node probabilities are displayed for sure.
903 end)
905 minetest.register_on_leaveplayer(function(player)
906 displayed_waypoints[player:get_player_name()] = nil
907 end)
909 -- Regularily clear the displayed node probabilities and force_place
910 -- for all players who do not wield the probtool.
911 -- This makes sure the screen is not spammed with information when it
912 -- isn't needed.
913 local cleartimer = 0
914 minetest.register_globalstep(function(dtime)
915 cleartimer = cleartimer + dtime
916 if cleartimer > 2 then
917 local players = minetest.get_connected_players()
918 for p = 1, #players do
919 local player = players[p]
920 local pname = player:get_player_name()
921 if displayed_waypoints[pname].display_active then
922 local item = player:get_wielded_item()
923 if item:get_name() ~= "schemedit:probtool" then
924 schemedit.clear_displayed_node_probs(player)
928 cleartimer = 0
930 end)
933 --- Registrations
936 -- [priv] schematic_override
937 minetest.register_privilege("schematic_override", {
938 description = S("Allows you to access schemedit nodes not owned by you"),
939 give_to_singleplayer = false,
942 local help_import = ""
943 if can_import then
944 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"
947 -- [node] Schematic creator
948 minetest.register_node("schemedit:creator", {
949 description = S("Schematic Creator"),
950 _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
951 _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"..
952 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"..
953 help_import..
954 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
955 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"..
956 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."),
957 tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
958 "schemedit_creator_sides.png"},
959 groups = { dig_immediate = 2},
960 paramtype2 = "facedir",
961 is_ground_content = false,
963 after_place_node = function(pos, player)
964 local name = player:get_player_name()
965 local meta = minetest.get_meta(pos)
967 meta:set_string("owner", name)
968 meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
969 meta:set_string("prob_list", minetest.serialize({}))
970 meta:set_string("slices", minetest.serialize({}))
972 local node = minetest.get_node(pos)
973 local dir = minetest.facedir_to_dir(node.param2)
975 meta:set_int("x_size", 1)
976 meta:set_int("y_size", 1)
977 meta:set_int("z_size", 1)
979 -- Don't take item from itemstack
980 return true
981 end,
982 can_dig = function(pos, player)
983 local name = player:get_player_name()
984 local meta = minetest.get_meta(pos)
985 if meta:get_string("owner") == name or
986 minetest.check_player_privs(player, "schematic_override") == true then
987 return true
990 return false
991 end,
992 on_rightclick = function(pos, node, player)
993 local meta = minetest.get_meta(pos)
994 local name = player:get_player_name()
995 if meta:get_string("owner") == name or
996 minetest.check_player_privs(player, "schematic_override") == true then
997 -- Get player attribute
998 local tab = player:get_attribute("schemedit:tab")
999 if not forms[tab] or not tab then
1000 tab = "main"
1003 schemedit.show_formspec(pos, player, tab, true)
1005 end,
1006 after_destruct = function(pos)
1007 schemedit.unmark(pos)
1008 end,
1010 -- No support for Minetest Game's screwdriver
1011 on_rotate = false,
1014 minetest.register_tool("schemedit:probtool", {
1015 description = S("Schematic Node Probability Tool"),
1016 _doc_items_longdesc =
1017 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"..
1018 S("It allows you to set two things:").."\n"..
1019 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1020 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1021 _doc_items_usagehelp = "\n"..
1022 S("BASIC USAGE:").."\n"..
1023 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"..
1024 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"..
1025 S("NODE HUD:").."\n"..
1026 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"..
1027 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1028 S("UPDATING THE NODE HUD:").."\n"..
1029 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."),
1030 wield_image = "schemedit_probtool.png",
1031 inventory_image = "schemedit_probtool.png",
1032 liquids_pointable = true,
1033 groups = { disable_repair = 1 },
1034 on_use = function(itemstack, user, pointed_thing)
1035 local ctrl = user:get_player_control()
1036 -- Simple use
1037 if not ctrl.sneak then
1038 -- Open dialog to change the probability to apply to nodes
1039 schemedit.show_formspec(user:get_pos(), user, "probtool", true)
1041 -- Use + sneak
1042 else
1043 -- Display the probability and force_place values for nodes.
1045 -- If a schematic creator was punched, only enable display for all nodes
1046 -- within the creator's region.
1047 local use_creator_region = false
1048 if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
1049 local punchpos = pointed_thing.under
1050 local node = minetest.get_node(punchpos)
1051 if node.name == "schemedit:creator" then
1052 local pos1, pos2 = schemedit.size(punchpos)
1053 pos1, pos2 = schemedit.sort_pos(pos1, pos2)
1054 schemedit.display_node_probs_region(user, pos1, pos2)
1055 return
1059 -- Otherwise, just display the region close to the player
1060 schemedit.display_node_probs_region(user)
1062 end,
1063 on_secondary_use = function(itemstack, user, pointed_thing)
1064 schemedit.clear_displayed_node_probs(user)
1065 end,
1066 -- Set note probability and force_place and enable node probability display
1067 on_place = function(itemstack, placer, pointed_thing)
1068 -- Use pointed node's on_rightclick function first, if present
1069 local node = minetest.get_node(pointed_thing.under)
1070 if placer and not placer:get_player_control().sneak then
1071 if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
1072 return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
1076 -- This sets the node probability of pointed node to the
1077 -- currently used probability stored in the tool.
1078 local pos = pointed_thing.under
1079 local node = minetest.get_node(pos)
1080 -- Schematic void are ignored, they always have probability 0
1081 if node.name == "schemedit:void" then
1082 return itemstack
1084 local nmeta = minetest.get_meta(pos)
1085 local imeta = itemstack:get_meta()
1086 local prob = tonumber(imeta:get_string("schemedit_prob"))
1087 local force_place = imeta:get_string("schemedit_force_place")
1089 if not prob or prob == 255 then
1090 nmeta:set_string("schemedit_prob", nil)
1091 else
1092 nmeta:set_string("schemedit_prob", prob)
1094 if force_place == "true" then
1095 nmeta:set_string("schemedit_force_place", "true")
1096 else
1097 nmeta:set_string("schemedit_force_place", nil)
1100 -- Enable node probablity display
1101 schemedit.display_node_probs_region(placer)
1103 return itemstack
1104 end,
1107 minetest.register_node("schemedit:void", {
1108 description = S("Schematic Void"),
1109 _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."),
1110 _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."),
1111 tiles = { "schemedit_void.png" },
1112 drawtype = "nodebox",
1113 is_ground_content = false,
1114 paramtype = "light",
1115 walkable = false,
1116 sunlight_propagates = true,
1117 node_box = {
1118 type = "fixed",
1119 fixed = {
1120 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1123 groups = { dig_immediate = 3},
1126 -- [entity] Visible schematic border
1127 minetest.register_entity("schemedit:display", {
1128 visual = "upright_sprite",
1129 textures = {"schemedit_border.png"},
1130 visual_size = {x=10, y=10},
1131 pointable = false,
1132 physical = false,
1133 static_save = false,
1134 glow = minetest.LIGHT_MAX,
1136 on_step = function(self, dtime)
1137 if not self.id then
1138 self.object:remove()
1139 elseif not schemedit.markers[self.id] then
1140 self.object:remove()
1142 end,
1143 on_activate = function(self)
1144 self.object:set_armor_groups({immortal = 1})
1145 end,
1148 minetest.register_lbm({
1149 label = "Reset schematic creator border entities",
1150 name = "schemedit:reset_border",
1151 nodenames = "schemedit:creator",
1152 run_at_every_load = true,
1153 action = function(pos, node)
1154 local meta = minetest.get_meta(pos)
1155 meta:set_string("schem_border", "false")
1156 end,
1159 -- [chatcommand] Place schematic
1160 minetest.register_chatcommand("placeschem", {
1161 description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
1162 privs = {debug = true},
1163 params = S("<schematic name>[.mts] [<x> <y> <z>]"),
1164 func = function(name, param)
1165 local schem, p = string.match(param, "^([^ ]+) *(.*)$")
1166 local pos = minetest.string_to_pos(p)
1168 if not schem then
1169 return false, S("No schematic file specified.")
1172 if not pos then
1173 pos = minetest.get_player_by_name(name):get_pos()
1176 -- Automatically add file name suffix if omitted
1177 local schem_full
1178 if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
1179 schem_full = schem
1180 else
1181 schem_full = schem .. ".mts"
1184 local success = false
1185 local schem_path = export_path_full .. DIR_DELIM .. schem_full
1186 if minetest.read_schematic then
1187 -- We don't call minetest.place_schematic with the path name directly because
1188 -- this would trigger the caching and we wouldn't get any updates to the schematic
1189 -- files when we reload. minetest.read_schematic circumvents that.
1190 local schematic = minetest.read_schematic(schem_path, {})
1191 if schematic then
1192 success = minetest.place_schematic(pos, schematic, "random", nil, false)
1194 else
1195 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1196 success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
1199 if success == nil then
1200 return false, S("Schematic file could not be loaded!")
1201 else
1202 return true
1204 end,