1 local S
= minetest
.get_translator("schemedit")
2 local F
= minetest
.formspec_escape
8 local export_path_full
= table.concat({minetest
.get_worldpath(), "schems"}, DIR_DELIM
)
10 -- truncated export path so the server directory structure is not exposed publicly
11 local export_path_trunc
= table.concat({S("<world path>"), "schems"}, DIR_DELIM
)
13 local text_color
= "#D79E9E"
14 local text_color_number
= 0xD79E9E
16 local can_import
= minetest
.read_schematic
~= nil
18 schemedit
.markers
= {}
20 -- [local function] Renumber table
21 local function renumber(t
)
23 for _
, i
in pairs(t
) do
29 local NEEDED_PRIV
= "server"
30 local function check_priv(player_name
, quit
)
31 local privs
= minetest
.get_player_privs(player_name
)
32 if privs
[NEEDED_PRIV
] then
36 minetest
.chat_send_player(player_name
, minetest
.colorize("red",
37 S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV
)))
44 local export_schematic_to_lua
46 export_schematic_to_lua
= function(schematic
, filepath
, options
)
47 if not options
then options
= {} end
48 local str
= minetest
.serialize_schematic(schematic
, "lua", options
)
49 local file
= io
.open(filepath
, "w")
69 local displayed_waypoints
= {}
71 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
72 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
73 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
74 -- on an actual export to a schematic.
76 function schemedit
.lua_prob_to_schematic_prob(lua_prob
)
77 return math
.floor(lua_prob
/ 2)
80 function schemedit
.schematic_prob_to_lua_prob(schematic_prob
)
81 return schematic_prob
* 2
85 -- [function] Add form
86 function schemedit
.add_form(name
, def
)
91 tabs
[#tabs
+ 1] = name
95 -- [function] Generate tabs
96 function schemedit
.generate_tabs(current
)
97 local retval
= "tabheader[0,0;tabs;"
98 for _
, t
in pairs(tabs
) do
100 if f
.tab
~= false and f
.caption
then
101 retval
= retval
..f
.caption
..","
103 if type(current
) ~= "number" and current
== f
.name
then
108 retval
= retval
:sub(1, -2) -- Strip last comma
109 retval
= retval
..";"..current
.."]" -- Close tabheader
113 -- [function] Handle tabs
114 function schemedit
.handle_tabs(pos
, name
, fields
)
115 local tab
= tonumber(fields
.tabs
)
116 if tab
and tabs
[tab
] and forms
[tabs
[tab]]
then
117 schemedit
.show_formspec(pos
, name
, forms
[tabs
[tab]]
.name
)
122 -- [function] Show formspec
123 function schemedit
.show_formspec(pos
, player
, tab
, show
, ...)
125 if type(player
) == "string" then
126 player
= minetest
.get_player_by_name(player
)
128 local name
= player
:get_player_name()
130 if show
~= false then
131 if not form_data
[name
] then
135 local form
= forms
[tab
].get(form_data
[name
], pos
, name
, ...)
136 if forms
[tab
].tab
then
137 form
= form
..schemedit
.generate_tabs(tab
)
140 minetest
.show_formspec(name
, "schemedit:"..tab
, form
)
143 -- Update player attribute
144 if forms
[tab
].cache_name
~= false then
145 local pmeta
= player
:get_meta()
146 pmeta
:set_string("schemedit:tab", tab
)
149 minetest
.close_formspec(pname
, "schemedit:"..tab
)
154 -- [event] On receive fields
155 minetest
.register_on_player_receive_fields(function(player
, formname
, fields
)
156 local formname
= formname
:split(":")
158 if formname
[1] == "schemedit" and forms
[formname
[2]]
then
159 local handle
= forms
[formname
[2]]
.handle
160 local name
= player
:get_player_name()
161 if contexts
[name
] then
162 if not form_data
[name
] then
166 if not schemedit
.handle_tabs(contexts
[name
], name
, fields
) and handle
then
167 handle(form_data
[name
], contexts
[name
], name
, fields
)
173 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
174 schemedit
.scan_metadata
= function(pos1
, pos2
)
177 for x
=pos1
.x
, pos2
.x
do
178 for y
=pos1
.y
, pos2
.y
do
179 for z
=pos1
.z
, pos2
.z
do
180 local scanpos
= {x
=x
, y
=y
, z
=z
}
181 local node
= minetest
.get_node_or_nil(scanpos
)
183 local prob
, force_place
184 if node
== nil or node
.name
== "schemedit:void" then
188 local meta
= minetest
.get_meta(scanpos
)
190 prob
= tonumber(meta
:get_string("schemedit_prob")) or 255
191 local fp
= meta
:get_string("schemedit_force_place")
199 local hashpos
= minetest
.hash_node_position(scanpos
)
200 prob_list
[hashpos
] = {
203 force_place
= force_place
,
212 -- Sets probability and force_place metadata of an item.
213 -- Also updates item description.
214 -- The itemstack is updated in-place.
215 local function set_item_metadata(itemstack
, prob
, force_place
)
216 local smeta
= itemstack
:get_meta()
217 local prob_desc
= "\n"..S("Probability: @1", prob
or
218 smeta
:get_string("schemedit_prob") or S("Not Set"))
219 -- Update probability
220 if prob
and prob
>= 0 and prob
< 255 then
221 smeta
:set_string("schemedit_prob", tostring(prob
))
222 elseif prob
and prob
== 255 then
223 -- Clear prob metadata for default probability
225 smeta
:set_string("schemedit_prob", nil)
227 prob_desc
= "\n"..S("Probability: @1", smeta
:get_string("schemedit_prob") or
231 -- Update force place
232 if force_place
== true then
233 smeta
:set_string("schemedit_force_place", "true")
234 elseif force_place
== false then
235 smeta
:set_string("schemedit_force_place", nil)
238 -- Update description
239 local desc
= minetest
.registered_items
[itemstack
:get_name()].description
240 local meta_desc
= smeta
:get_string("description")
241 if meta_desc
and meta_desc
~= "" then
245 local original_desc
= smeta
:get_string("original_description")
246 if original_desc
and original_desc
~= "" then
249 smeta
:set_string("original_description", desc
)
252 local force_desc
= ""
253 if smeta
:get_string("schemedit_force_place") == "true" then
254 force_desc
= "\n"..S("Force placement")
257 desc
= desc
..minetest
.colorize(text_color
, prob_desc
..force_desc
)
259 smeta
:set_string("description", desc
)
267 local import_btn
= ""
269 import_btn
= "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
271 schemedit
.add_form("main", {
274 get
= function(self
, pos
, name
)
275 local meta
= minetest
.get_meta(pos
):to_table().fields
276 local strpos
= minetest
.pos_to_string(pos
)
277 local hashpos
= minetest
.hash_node_position(pos
)
280 if meta
.schem_border
== "true" and schemedit
.markers
[hashpos
] then
281 border_button
= "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
283 border_button
= "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
286 local xs
, ys
, zs
= meta
.x_size
or 1, meta
.y_size
or 1, meta
.z_size
or 1
287 local size
= {x
=xs
, y
=ys
, z
=zs
}
288 local schem_name
= meta
.schem_name
or ""
292 label[0.5,-0.1;]]..F(S("Position: @1", strpos
))..[[]
293 label[3,-0.1;]]..F(S("Owner: @1", name
))..[[]
294 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name
)))..[[]
295 label[0.5,0.9;]]..F(S("Size: @1", minetest
.pos_to_string(size
)))..[[]
297 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name
or "")..[[]
298 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
299 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
300 field_close_on_enter[name;false]
302 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
304 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
305 export_path_trunc
.. DIR_DELIM
.. F(S("<name>"))..".mts"))..[[;]
306 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
307 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
308 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
309 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
310 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs
..[[]
311 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys
..[[]
312 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs
..[[]
313 field_close_on_enter[x;false]
314 field_close_on_enter[y;false]
315 field_close_on_enter[z;false]
316 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
319 if minetest
.get_modpath("doc") then
320 form
= form
.. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
321 "tooltip[doc;"..F(S("Help")).."]"
325 handle
= function(self
, pos
, name
, fields
)
327 doc
.show_entry(name
, "nodes", "schemedit:creator", true)
331 if not check_priv(name
, fields
.quit
) then
335 local realmeta
= minetest
.get_meta(pos
)
336 local meta
= realmeta
:to_table().fields
337 local hashpos
= minetest
.hash_node_position(pos
)
339 -- Save size vector values
340 if (fields
.x
and fields
.x
~= "") then
341 local x
= tonumber(fields
.x
)
343 meta
.x_size
= math
.max(x
, 1)
346 if (fields
.y
and fields
.y
~= "") then
347 local y
= tonumber(fields
.y
)
349 meta
.y_size
= math
.max(y
, 1)
352 if (fields
.z
and fields
.z
~= "") then
353 local z
= tonumber(fields
.z
)
355 meta
.z_size
= math
.max(z
, 1)
359 -- Save schematic name
361 meta
.schem_name
= fields
.name
365 if (fields
.air2void
) then
366 local pos1
, pos2
= schemedit
.size(pos
)
367 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
368 local nodes
= minetest
.find_nodes_in_area(pos1
, pos2
, {"air"})
369 minetest
.bulk_set_node(nodes
, {name
="schemedit:void"})
371 elseif (fields
.void2air
) then
372 local pos1
, pos2
= schemedit
.size(pos
)
373 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
374 local nodes
= minetest
.find_nodes_in_area(pos1
, pos2
, {"schemedit:void"})
375 minetest
.bulk_set_node(nodes
, {name
="air"})
380 if fields
.border
then
381 if meta
.schem_border
== "true" and schemedit
.markers
[hashpos
] then
382 schemedit
.unmark(pos
)
383 meta
.schem_border
= "false"
386 meta
.schem_border
= "true"
391 if fields
.export
and meta
.schem_name
and meta
.schem_name
~= "" then
392 local pos1
, pos2
= schemedit
.size(pos
)
393 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
394 local path
= export_path_full
.. DIR_DELIM
397 local plist
= schemedit
.scan_metadata(pos1
, pos2
)
398 local probability_list
= {}
399 for hash
, i
in pairs(plist
) do
400 local prob
= schemedit
.lua_prob_to_schematic_prob(i
.prob
)
401 if i
.force_place
== true then
405 table.insert(probability_list
, {
406 pos
= minetest
.get_position_from_hash(hash
),
411 local slist
= minetest
.deserialize(meta
.slices
)
412 local slice_list
= {}
413 for _
, i
in pairs(slist
) do
414 slice_list
[#slice_list
+ 1] = {
415 ypos
= pos
.y
+ i
.ypos
,
416 prob
= schemedit
.lua_prob_to_schematic_prob(i
.prob
),
420 local filepath
= path
..meta
.schem_name
..".mts"
421 local res
= minetest
.create_schematic(pos1
, pos2
, probability_list
, filepath
, slice_list
)
424 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
425 S("Exported schematic to @1", filepath
)))
426 -- Additional export to Lua file if MTS export was successful
427 local schematic
= minetest
.read_schematic(filepath
, {})
428 if schematic
and minetest
.settings
:get_bool("schemedit_export_lua") then
429 local filepath_lua
= path
..meta
.schem_name
..".lua"
430 res
= export_schematic_to_lua(schematic
, filepath_lua
)
432 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
433 S("Exported schematic to @1", filepath_lua
)))
437 minetest
.chat_send_player(name
, minetest
.colorize("red",
438 S("Failed to export schematic to @1", filepath
)))
443 if fields
.import
and meta
.schem_name
and meta
.schem_name
~= "" then
444 if not can_import
then
448 local node
= minetest
.get_node(pos
)
449 local path
= export_path_full
.. DIR_DELIM
451 local filepath
= path
..meta
.schem_name
..".mts"
452 local schematic
= minetest
.read_schematic(filepath
, {write_yslice_prob
="low"})
453 local success
= false
456 meta
.x_size
= schematic
.size
.x
457 meta
.y_size
= schematic
.size
.y
458 meta
.z_size
= schematic
.size
.z
459 meta
.slices
= minetest
.serialize(schematic
.yslice_prob
)
460 local special_x_size
= meta
.x_size
461 local special_y_size
= meta
.y_size
462 local special_z_size
= meta
.z_size
464 if node
.param2
== 1 then
465 pos1
= vector
.add(pos
, {x
=1,y
=0,z
=-meta
.z_size
+1})
466 meta
.x_size
, meta
.z_size
= meta
.z_size
, meta
.x_size
467 elseif node
.param2
== 2 then
468 pos1
= vector
.add(pos
, {x
=-meta
.x_size
+1,y
=0,z
=-meta
.z_size
})
469 elseif node
.param2
== 3 then
470 pos1
= vector
.add(pos
, {x
=-meta
.x_size
,y
=0,z
=0})
471 meta
.x_size
, meta
.z_size
= meta
.z_size
, meta
.x_size
473 pos1
= vector
.add(pos
, {x
=0,y
=0,z
=1})
476 local schematic_for_meta
= table.copy(schematic
)
477 -- Strip probability data for placement
478 schematic
.yslice_prob
= {}
479 for d
=1, #schematic
.data
do
480 schematic
.data
[d
].prob
= nil
484 success
= minetest
.place_schematic(pos1
, schematic
, "0", nil, true)
486 -- Add special schematic data to nodes
489 for z
=0, special_z_size
-1 do
490 for y
=0, special_y_size
-1 do
491 for x
=0, special_x_size
-1 do
492 local data
= schematic_for_meta
.data
[d
]
493 local pp
= {x
=pos1
.x
+x
, y
=pos1
.y
+y
, z
=pos1
.z
+z
}
494 if data
.prob
== 0 then
495 minetest
.set_node(pp
, {name
="schemedit:void"})
497 local meta
= minetest
.get_meta(pp
)
498 if data
.prob
and data
.prob
~= 255 and data
.prob
~= 254 then
499 meta
:set_string("schemedit_prob", tostring(data
.prob
))
501 meta
:set_string("schemedit_prob", "")
503 if data
.force_place
then
504 meta
:set_string("schemedit_force_place", "true")
506 meta
:set_string("schemedit_force_place", "")
516 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
517 S("Imported schematic from @1", filepath
)))
519 minetest
.chat_send_player(name
, minetest
.colorize("red",
520 S("Failed to import schematic from @1", filepath
)))
526 -- Save meta before updating visuals
527 local inv
= realmeta
:get_inventory():get_lists()
528 realmeta
:from_table({fields
= meta
, inventory
= inv
})
531 if not fields
.border
and meta
.schem_border
== "true" then
536 if not fields
.quit
then
537 schemedit
.show_formspec(pos
, minetest
.get_player_by_name(name
), "main")
542 schemedit
.add_form("slice", {
543 caption
= S("Y Slices"),
545 get
= function(self
, pos
, name
, visible_panel
)
546 local meta
= minetest
.get_meta(pos
):to_table().fields
548 self
.selected
= self
.selected
or 1
549 local selected
= tostring(self
.selected
)
550 local slice_list
= minetest
.deserialize(meta
.slices
)
552 for _
, i
in pairs(slice_list
) do
553 local insert
= F(S("Y = @1; Probability = @2", tostring(i
.ypos
), tostring(i
.prob
)))
554 slices
= slices
..insert
..","
556 slices
= slices
:sub(1, -2) -- Remove final comma
560 table[0,0;6.8,6;slices;]]..slices
..[[;]]..selected
..[[]
563 if self
.panel_add
or self
.panel_edit
then
564 local ypos_default
, prob_default
= "", ""
565 local done_button
= "button[5,7.18;2,1;done_add;"..F(S("Add")).."]"
566 if self
.panel_edit
then
567 done_button
= "button[5,7.18;2,1;done_edit;"..F(S("Apply")).."]"
568 if slice_list
[self
.selected
] then
569 ypos_default
= slice_list
[self
.selected
].ypos
570 prob_default
= slice_list
[self
.selected
].prob
575 field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta
.y_size
- 1)))..[[;]]..ypos_default
..[[]
576 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default
..[[]
577 field_close_on_enter[ypos;false]
578 field_close_on_enter[prob;false]
582 if not self
.panel_edit
then
583 if self
.panel_add
then
584 form
= form
.."button[0,6;2.4,1;add;"..F(S("Cancel")).."]"
586 form
= form
.."button[0,6;2.4,1;add;"..F(S("Add slice")).."]"
590 if slices
~= "" and self
.selected
and not self
.panel_add
then
591 if not self
.panel_edit
then
593 button[2.4,6;2.4,1;remove;]]..F(S("Remove slice"))..[[]
594 button[4.8,6;2.4,1;edit;]]..F(S("Edit slice"))..[[]
598 button[4.8,6;2.4,1;edit;]]..F(S("Back"))..[[]
605 handle
= function(self
, pos
, name
, fields
)
606 if not check_priv(name
, fields
.quit
) then
610 local meta
= minetest
.get_meta(pos
)
611 local player
= minetest
.get_player_by_name(name
)
613 if fields
.slices
then
614 local slices
= fields
.slices
:split(":")
615 self
.selected
= tonumber(slices
[2])
619 if not self
.panel_add
then
620 self
.panel_add
= true
621 schemedit
.show_formspec(pos
, player
, "slice")
624 schemedit
.show_formspec(pos
, player
, "slice")
628 local ypos
, prob
= tonumber(fields
.ypos
), tonumber(fields
.prob
)
629 if (fields
.done_add
or fields
.done_edit
) and fields
.ypos
and fields
.prob
and
630 fields
.ypos
~= "" and fields
.prob
~= "" and ypos
and prob
and
631 ypos
<= (meta
:get_int("y_size") - 1) and prob
>= 0 and prob
<= 255 then
632 local slice_list
= minetest
.deserialize(meta
:get_string("slices"))
633 local index
= #slice_list
+ 1
634 if fields
.done_edit
then
635 index
= self
.selected
638 slice_list
[index
] = {ypos
= ypos
, prob
= prob
}
640 meta
:set_string("slices", minetest
.serialize(slice_list
))
642 -- Update and show formspec
644 schemedit
.show_formspec(pos
, player
, "slice")
647 if fields
.remove and self
.selected
then
648 local slice_list
= minetest
.deserialize(meta
:get_string("slices"))
649 slice_list
[self
.selected
] = nil
650 meta
:set_string("slices", minetest
.serialize(renumber(slice_list
)))
654 self
.panel_edit
= nil
655 schemedit
.show_formspec(pos
, player
, "slice")
659 if not self
.panel_edit
then
660 self
.panel_edit
= true
661 schemedit
.show_formspec(pos
, player
, "slice")
663 self
.panel_edit
= nil
664 schemedit
.show_formspec(pos
, player
, "slice")
670 schemedit
.add_form("probtool", {
672 caption
= S("Schematic Node Probability Tool"),
673 get
= function(self
, pos
, name
)
674 local player
= minetest
.get_player_by_name(name
)
678 local probtool
= player
:get_wielded_item()
679 if probtool
:get_name() ~= "schemedit:probtool" then
683 local meta
= probtool
:get_meta()
684 local prob
= tonumber(meta
:get_string("schemedit_prob"))
685 local force_place
= meta
:get_string("schemedit_force_place")
690 if force_place
== nil or force_place
== "" then
691 force_place
= "false"
693 local form
= "size[5,4]"..
694 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
695 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob
.."]"..
696 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place
.. "]" ..
697 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
698 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
699 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
700 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
701 "field_close_on_enter[prob;false]"
704 handle
= function(self
, pos
, name
, fields
)
705 if not check_priv(name
, fields
.quit
) then
709 if fields
.submit
then
710 local prob
= tonumber(fields
.prob
)
712 local player
= minetest
.get_player_by_name(name
)
716 local probtool
= player
:get_wielded_item()
717 if probtool
:get_name() ~= "schemedit:probtool" then
721 local force_place
= self
.force_place
== true
723 set_item_metadata(probtool
, prob
, force_place
)
725 -- Repurpose the tool's wear bar to display the set probability
726 probtool
:set_wear(math
.floor(((255-prob
)/255)*65535))
728 player
:set_wielded_item(probtool
)
731 if fields
.force_place
== "true" then
732 self
.force_place
= true
733 elseif fields
.force_place
== "false" then
734 self
.force_place
= false
743 --- Copies and modifies positions `pos1` and `pos2` so that each component of
744 -- `pos1` is less than or equal to the corresponding component of `pos2`.
745 -- Returns the new positions.
746 function schemedit
.sort_pos(pos1
, pos2
)
747 if not pos1
or not pos2
then
751 pos1
, pos2
= table.copy(pos1
), table.copy(pos2
)
752 if pos1
.x
> pos2
.x
then
753 pos2
.x
, pos1
.x
= pos1
.x
, pos2
.x
755 if pos1
.y
> pos2
.y
then
756 pos2
.y
, pos1
.y
= pos1
.y
, pos2
.y
758 if pos1
.z
> pos2
.z
then
759 pos2
.z
, pos1
.z
= pos1
.z
, pos2
.z
764 -- [function] Prepare size
765 function schemedit
.size(pos
)
766 local pos1
= vector
.new(pos
)
767 local meta
= minetest
.get_meta(pos
)
768 local node
= minetest
.get_node(pos
)
769 local param2
= node
.param2
771 x
= meta
:get_int("x_size"),
772 y
= math
.max(meta
:get_int("y_size") - 1, 0),
773 z
= meta
:get_int("z_size"),
777 local new_pos
= vector
.add({x
= size
.z
, y
= size
.y
, z
= -size
.x
}, pos
)
779 new_pos
.z
= new_pos
.z
+ 1
781 elseif param2
== 2 then
782 local new_pos
= vector
.add({x
= -size
.x
, y
= size
.y
, z
= -size
.z
}, pos
)
784 new_pos
.x
= new_pos
.x
+ 1
786 elseif param2
== 3 then
787 local new_pos
= vector
.add({x
= -size
.z
, y
= size
.y
, z
= size
.x
}, pos
)
789 new_pos
.z
= new_pos
.z
- 1
792 local new_pos
= vector
.add(size
, pos
)
794 new_pos
.x
= new_pos
.x
- 1
799 -- [function] Mark region
800 function schemedit
.mark(pos
)
801 schemedit
.unmark(pos
)
803 local id
= minetest
.hash_node_position(pos
)
804 local owner
= minetest
.get_meta(pos
):get_string("owner")
805 local pos1
, pos2
= schemedit
.size(pos
)
806 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
808 local thickness
= 0.2
809 local sizex
, sizey
, sizez
= (1 + pos2
.x
- pos1
.x
) / 2, (1 + pos2
.y
- pos1
.y
) / 2, (1 + pos2
.z
- pos1
.z
) / 2
815 for _
, z
in ipairs({pos1
.z
- 0.5, pos2
.z
+ 0.5}) do
821 local marker
= minetest
.add_entity({x
= pos1
.x
+ sizex
- 0.5, y
= pos1
.y
+ sizey
- 0.5, z
= z
+ offset
}, "schemedit:display")
822 if marker
~= nil then
823 marker
:set_properties({
824 visual_size
={x
=(sizex
+0.01) * 2, y
=(sizey
+0.01) * 2},
826 marker
:get_luaentity().id
= id
827 marker
:get_luaentity().owner
= owner
828 table.insert(m
, marker
)
835 for _
, x
in ipairs({pos1
.x
- 0.5, pos2
.x
+ 0.5}) do
842 local marker
= minetest
.add_entity({x
= x
+ offset
, y
= pos1
.y
+ sizey
- 0.5, z
= pos1
.z
+ sizez
- 0.5}, "schemedit:display")
843 if marker
~= nil then
844 marker
:set_properties({
845 visual_size
={x
=(sizez
+0.01) * 2, y
=(sizey
+0.01) * 2},
847 marker
:set_rotation({x
=0, y
=math
.pi
/ 2, z
=0})
848 marker
:get_luaentity().id
= id
849 marker
:get_luaentity().owner
= owner
850 table.insert(m
, marker
)
857 for _
, y
in ipairs({pos1
.y
- 0.5, pos2
.y
+ 0.5}) do
864 local marker
= minetest
.add_entity({x
= pos1
.x
+ sizex
- 0.5, y
= y
+ offset
, z
= pos1
.z
+ sizez
- 0.5}, "schemedit:display")
865 if marker
~= nil then
866 marker
:set_properties({
867 visual_size
={x
=(sizex
+0.01) * 2, y
=(sizez
+0.01) * 2},
869 marker
:set_rotation({x
=math
.pi
/2, y
=0, z
=0})
870 marker
:get_luaentity().id
= id
871 marker
:get_luaentity().owner
= owner
872 table.insert(m
, marker
)
879 schemedit
.markers
[id
] = m
883 -- [function] Unmark region
884 function schemedit
.unmark(pos
)
885 local id
= minetest
.hash_node_position(pos
)
886 if schemedit
.markers
[id
] then
888 for _
, entity
in ipairs(schemedit
.markers
[id
]) do
897 --- Mark node probability values near player
900 -- Show probability and force_place status of a particular position for player in HUD.
901 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
902 -- The distance to the node is also displayed below that. This can't be avoided and is
903 -- and artifact of the waypoint HUD element.
904 function schemedit
.display_node_prob(player
, pos
, prob
, force_place
)
906 if prob
and force_place
== true then
907 wpstring
= string.format("%s [F]", prob
)
908 elseif prob
and type(tonumber(prob
)) == "number" then
910 elseif force_place
== true then
914 return player
:hud_add({
915 hud_elem_type
= "waypoint",
918 text
= "m", -- For the distance artifact
919 number = text_color_number
,
925 -- Display the node probabilities and force_place status of the nodes in a region.
926 -- By default, this is done for nodes near the player (distance: 5).
927 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
928 function schemedit
.display_node_probs_region(player
, pos1
, pos2
)
929 local playername
= player
:get_player_name()
930 local pos
= vector
.round(player
:get_pos())
933 -- Default: 5 nodes away from player in any direction
935 pos1
= vector
.subtract(pos
, dist
)
938 pos2
= vector
.add(pos
, dist
)
940 for x
=pos1
.x
, pos2
.x
do
941 for y
=pos1
.y
, pos2
.y
do
942 for z
=pos1
.z
, pos2
.z
do
943 local checkpos
= {x
=x
, y
=y
, z
=z
}
944 local nodehash
= minetest
.hash_node_position(checkpos
)
946 -- If node is already displayed, remove it so it can re replaced later
947 if displayed_waypoints
[playername
][nodehash
] then
948 player
:hud_remove(displayed_waypoints
[playername
][nodehash
])
949 displayed_waypoints
[playername
][nodehash
] = nil
952 local prob
, force_place
953 local meta
= minetest
.get_meta(checkpos
)
954 prob
= meta
:get_string("schemedit_prob")
955 force_place
= meta
:get_string("schemedit_force_place") == "true"
956 local hud_id
= schemedit
.display_node_prob(player
, checkpos
, prob
, force_place
)
958 displayed_waypoints
[playername
][nodehash
] = hud_id
959 displayed_waypoints
[playername
].display_active
= true
966 -- Remove all active displayed node statuses.
967 function schemedit
.clear_displayed_node_probs(player
)
968 local playername
= player
:get_player_name()
969 for nodehash
, hud_id
in pairs(displayed_waypoints
[playername
]) do
970 player
:hud_remove(hud_id
)
971 displayed_waypoints
[playername
][nodehash
] = nil
972 displayed_waypoints
[playername
].display_active
= false
976 minetest
.register_on_joinplayer(function(player
)
977 displayed_waypoints
[player
:get_player_name()] = {
978 display_active
= false -- If true, there *might* be at least one active node prob HUD display
979 -- If false, no node probabilities are displayed for sure.
983 minetest
.register_on_leaveplayer(function(player
)
984 displayed_waypoints
[player
:get_player_name()] = nil
987 -- Regularily clear the displayed node probabilities and force_place
988 -- for all players who do not wield the probtool.
989 -- This makes sure the screen is not spammed with information when it
992 minetest
.register_globalstep(function(dtime
)
993 cleartimer
= cleartimer
+ dtime
994 if cleartimer
> 2 then
995 local players
= minetest
.get_connected_players()
996 for p
= 1, #players
do
997 local player
= players
[p
]
998 local pname
= player
:get_player_name()
999 if displayed_waypoints
[pname
].display_active
then
1000 local item
= player
:get_wielded_item()
1001 if item
:get_name() ~= "schemedit:probtool" then
1002 schemedit
.clear_displayed_node_probs(player
)
1014 -- [priv] schematic_override
1015 minetest
.register_privilege("schematic_override", {
1016 description
= S("Allows you to access schemedit nodes not owned by you"),
1017 give_to_singleplayer
= false,
1020 local help_import
= ""
1022 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"
1025 -- [node] Schematic creator
1026 minetest
.register_node("schemedit:creator", {
1027 description
= S("Schematic Creator"),
1028 _doc_items_longdesc
= S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1029 _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"..
1030 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"..
1032 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1033 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"..
1034 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."),
1035 tiles
= {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1036 "schemedit_creator_sides.png"},
1037 groups
= { dig_immediate
= 2},
1038 paramtype2
= "facedir",
1039 is_ground_content
= false,
1041 after_place_node
= function(pos
, player
)
1042 local name
= player
:get_player_name()
1043 local meta
= minetest
.get_meta(pos
)
1045 meta
:set_string("owner", name
)
1046 meta
:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name
))
1047 meta
:set_string("prob_list", minetest
.serialize({}))
1048 meta
:set_string("slices", minetest
.serialize({}))
1050 local node
= minetest
.get_node(pos
)
1051 local dir
= minetest
.facedir_to_dir(node
.param2
)
1053 meta
:set_int("x_size", 1)
1054 meta
:set_int("y_size", 1)
1055 meta
:set_int("z_size", 1)
1057 -- Don't take item from itemstack
1060 can_dig
= function(pos
, player
)
1061 local name
= player
:get_player_name()
1062 local meta
= minetest
.get_meta(pos
)
1063 if meta
:get_string("owner") == name
or
1064 minetest
.check_player_privs(player
, "schematic_override") == true then
1070 on_rightclick
= function(pos
, node
, player
)
1071 local meta
= minetest
.get_meta(pos
)
1072 local name
= player
:get_player_name()
1073 if meta
:get_string("owner") == name
or
1074 minetest
.check_player_privs(player
, "schematic_override") == true then
1075 -- Get player attribute
1076 local pmeta
= player
:get_meta()
1077 local tab
= pmeta
:get_string("schemedit:tab")
1078 if not forms
[tab
] or not tab
then
1082 schemedit
.show_formspec(pos
, player
, tab
, true)
1085 after_destruct
= function(pos
)
1086 schemedit
.unmark(pos
)
1089 -- No support for Minetest Game's screwdriver
1093 minetest
.register_tool("schemedit:probtool", {
1094 description
= S("Schematic Node Probability Tool"),
1095 _doc_items_longdesc
=
1096 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"..
1097 S("It allows you to set two things:").."\n"..
1098 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1099 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1100 _doc_items_usagehelp
= "\n"..
1101 S("BASIC USAGE:").."\n"..
1102 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"..
1103 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"..
1104 S("NODE HUD:").."\n"..
1105 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"..
1106 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1107 S("UPDATING THE NODE HUD:").."\n"..
1108 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."),
1109 wield_image
= "schemedit_probtool.png",
1110 inventory_image
= "schemedit_probtool.png",
1111 liquids_pointable
= true,
1112 groups
= { disable_repair
= 1 },
1113 on_use
= function(itemstack
, user
, pointed_thing
)
1114 local uname
= user
:get_player_name()
1115 if uname
and not check_priv(uname
) then
1119 local ctrl
= user
:get_player_control()
1121 if not ctrl
.sneak
then
1122 -- Open dialog to change the probability to apply to nodes
1123 schemedit
.show_formspec(user
:get_pos(), user
, "probtool", true)
1127 -- Display the probability and force_place values for nodes.
1129 -- If a schematic creator was punched, only enable display for all nodes
1130 -- within the creator's region.
1131 local use_creator_region
= false
1132 if pointed_thing
and pointed_thing
.type == "node" and pointed_thing
.under
then
1133 local punchpos
= pointed_thing
.under
1134 local node
= minetest
.get_node(punchpos
)
1135 if node
.name
== "schemedit:creator" then
1136 local pos1
, pos2
= schemedit
.size(punchpos
)
1137 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
1138 schemedit
.display_node_probs_region(user
, pos1
, pos2
)
1143 -- Otherwise, just display the region close to the player
1144 schemedit
.display_node_probs_region(user
)
1147 on_secondary_use
= function(itemstack
, user
, pointed_thing
)
1148 local uname
= user
:get_player_name()
1149 if uname
and not check_priv(uname
) then
1153 schemedit
.clear_displayed_node_probs(user
)
1155 -- Set note probability and force_place and enable node probability display
1156 on_place
= function(itemstack
, placer
, pointed_thing
)
1157 local pname
= placer
:get_player_name()
1158 if pname
and not check_priv(pname
) then
1162 -- Use pointed node's on_rightclick function first, if present
1163 local node
= minetest
.get_node(pointed_thing
.under
)
1164 if placer
and not placer
:get_player_control().sneak
then
1165 if minetest
.registered_nodes
[node
.name
] and minetest
.registered_nodes
[node
.name
].on_rightclick
then
1166 return minetest
.registered_nodes
[node
.name
].on_rightclick(pointed_thing
.under
, node
, placer
, itemstack
) or itemstack
1170 -- This sets the node probability of pointed node to the
1171 -- currently used probability stored in the tool.
1172 local pos
= pointed_thing
.under
1173 local node
= minetest
.get_node(pos
)
1174 -- Schematic void are ignored, they always have probability 0
1175 if node
.name
== "schemedit:void" then
1178 local nmeta
= minetest
.get_meta(pos
)
1179 local imeta
= itemstack
:get_meta()
1180 local prob
= tonumber(imeta
:get_string("schemedit_prob"))
1181 local force_place
= imeta
:get_string("schemedit_force_place")
1183 if not prob
or prob
== 255 then
1184 nmeta
:set_string("schemedit_prob", nil)
1186 nmeta
:set_string("schemedit_prob", prob
)
1188 if force_place
== "true" then
1189 nmeta
:set_string("schemedit_force_place", "true")
1191 nmeta
:set_string("schemedit_force_place", nil)
1194 -- Enable node probablity display
1195 schemedit
.display_node_probs_region(placer
)
1201 minetest
.register_node("schemedit:void", {
1202 description
= S("Schematic Void"),
1203 _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."),
1204 _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."),
1205 tiles
= { "schemedit_void.png" },
1206 use_texture_alpha
= "clip",
1207 drawtype
= "nodebox",
1208 is_ground_content
= false,
1209 paramtype
= "light",
1211 sunlight_propagates
= true,
1215 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1218 groups
= { dig_immediate
= 3},
1221 -- [entity] Visible schematic border
1222 minetest
.register_entity("schemedit:display", {
1223 visual
= "upright_sprite",
1224 textures
= {"schemedit_border.png"},
1225 visual_size
= {x
=10, y
=10},
1228 static_save
= false,
1229 glow
= minetest
.LIGHT_MAX
,
1231 on_step
= function(self
, dtime
)
1233 self
.object
:remove()
1234 elseif not schemedit
.markers
[self
.id
] then
1235 self
.object
:remove()
1238 on_activate
= function(self
)
1239 self
.object
:set_armor_groups({immortal
= 1})
1243 minetest
.register_lbm({
1244 label
= "Reset schematic creator border entities",
1245 name
= "schemedit:reset_border",
1246 nodenames
= "schemedit:creator",
1247 run_at_every_load
= true,
1248 action
= function(pos
, node
)
1249 local meta
= minetest
.get_meta(pos
)
1250 meta
:set_string("schem_border", "false")
1254 local function add_suffix(schem
)
1255 -- Automatically add file name suffix if omitted
1256 local schem_full
, schem_lua
1257 if string.sub(schem
, string.len(schem
)-3, string.len(schem
)) == ".mts" then
1259 schem_lua
= string.sub(schem
, 1, -5) .. ".lua"
1261 schem_full
= schem
.. ".mts"
1262 schem_lua
= schem
.. ".lua"
1264 return schem_full
, schem_lua
1267 -- [chatcommand] Place schematic
1268 minetest
.register_chatcommand("placeschem", {
1269 description
= S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc
),
1270 privs
= {server
= true},
1271 params
= S("<schematic name>[.mts] [<x> <y> <z>]"),
1272 func
= function(name
, param
)
1273 local schem
, p
= string.match(param
, "^([^ ]+) *(.*)$")
1274 local pos
= minetest
.string_to_pos(p
)
1277 return false, S("No schematic file specified.")
1281 pos
= minetest
.get_player_by_name(name
):get_pos()
1284 local schem_full
, schem_lua
= add_suffix(schem
)
1285 local success
= false
1286 local schem_path
= export_path_full
.. DIR_DELIM
.. schem_full
1287 if minetest
.read_schematic
then
1288 -- We don't call minetest.place_schematic with the path name directly because
1289 -- this would trigger the caching and we wouldn't get any updates to the schematic
1290 -- files when we reload. minetest.read_schematic circumvents that.
1291 local schematic
= minetest
.read_schematic(schem_path
, {})
1293 success
= minetest
.place_schematic(pos
, schematic
, "random", nil, false)
1296 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1297 success
= minetest
.place_schematic(schem_path
, schematic
, "random", nil, false)
1300 if success
== nil then
1301 return false, S("Schematic file could not be loaded!")
1309 -- [chatcommand] Convert MTS schematic file to .lua file
1310 minetest
.register_chatcommand("mts2lua", {
1311 description
= S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc
),
1312 privs
= {server
= true},
1313 params
= S("<schematic name>[.mts] [comments]"),
1314 func
= function(name
, param
)
1315 local schem
, comments_str
= string.match(param
, "^([^ ]+) *(.*)$")
1318 return false, S("No schematic file specified.")
1321 local comments
= comments_str
== "comments"
1323 -- Automatically add file name suffix if omitted
1324 local schem_full
, schem_lua
= add_suffix(schem
)
1325 local schem_path
= export_path_full
.. DIR_DELIM
.. schem_full
1326 local schematic
= minetest
.read_schematic(schem_path
, {})
1329 local str
= minetest
.serialize_schematic(schematic
, "lua", {lua_use_comments
=comments
})
1330 local lua_path
= export_path_full
.. DIR_DELIM
.. schem_lua
1331 local file
= io
.open(lua_path
, "w")
1332 if file
and str
then
1336 return true, S("Exported schematic to @1", lua_path
)
1338 return false, S("Failed!")