1 local S
= minetest
.get_translator("schemedit")
2 local F
= minetest
.formspec_escape
8 -- Set to true to enable `make_schemedit_readme` command
9 local MAKE_README
= false
11 local export_path_full
= table.concat({minetest
.get_worldpath(), "schems"}, DIR_DELIM
)
13 -- truncated export path so the server directory structure is not exposed publicly
14 local export_path_trunc
= table.concat({S("<world path>"), "schems"}, DIR_DELIM
)
16 local text_color
= "#D79E9E"
17 local text_color_number
= 0xD79E9E
19 local can_import
= minetest
.read_schematic
~= nil
21 schemedit
.markers
= {}
23 -- [local function] Renumber table
24 local function renumber(t
)
26 for _
, i
in pairs(t
) do
32 local NEEDED_PRIV
= "server"
33 local function check_priv(player_name
, quit
)
34 local privs
= minetest
.get_player_privs(player_name
)
35 if privs
[NEEDED_PRIV
] then
39 minetest
.chat_send_player(player_name
, minetest
.colorize("red",
40 S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV
)))
47 local export_schematic_to_lua
49 export_schematic_to_lua
= function(schematic
, filepath
, options
)
50 if not options
then options
= {} end
51 local str
= minetest
.serialize_schematic(schematic
, "lua", options
)
52 local file
= io
.open(filepath
, "w")
72 local displayed_waypoints
= {}
74 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
75 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
76 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
77 -- on an actual export to a schematic.
79 function schemedit
.lua_prob_to_schematic_prob(lua_prob
)
80 return math
.floor(lua_prob
/ 2)
83 function schemedit
.schematic_prob_to_lua_prob(schematic_prob
)
84 return schematic_prob
* 2
88 -- [function] Add form
89 function schemedit
.add_form(name
, def
)
94 tabs
[#tabs
+ 1] = name
98 -- [function] Generate tabs
99 function schemedit
.generate_tabs(current
)
100 local retval
= "tabheader[0,0;tabs;"
101 for _
, t
in pairs(tabs
) do
103 if f
.tab
~= false and f
.caption
then
104 retval
= retval
..f
.caption
..","
106 if type(current
) ~= "number" and current
== f
.name
then
111 retval
= retval
:sub(1, -2) -- Strip last comma
112 retval
= retval
..";"..current
.."]" -- Close tabheader
116 -- [function] Handle tabs
117 function schemedit
.handle_tabs(pos
, name
, fields
)
118 local tab
= tonumber(fields
.tabs
)
119 if tab
and tabs
[tab
] and forms
[tabs
[tab]]
then
120 schemedit
.show_formspec(pos
, name
, forms
[tabs
[tab]]
.name
)
125 -- [function] Show formspec
126 function schemedit
.show_formspec(pos
, player
, tab
, show
, ...)
128 if type(player
) == "string" then
129 player
= minetest
.get_player_by_name(player
)
131 local name
= player
:get_player_name()
133 if show
~= false then
134 if not form_data
[name
] then
138 local form
= forms
[tab
].get(form_data
[name
], pos
, name
, ...)
139 if forms
[tab
].tab
then
140 form
= form
..schemedit
.generate_tabs(tab
)
143 minetest
.show_formspec(name
, "schemedit:"..tab
, form
)
146 -- Update player attribute
147 if forms
[tab
].cache_name
~= false then
148 local pmeta
= player
:get_meta()
149 pmeta
:set_string("schemedit:tab", tab
)
152 minetest
.close_formspec(pname
, "schemedit:"..tab
)
157 -- [event] On receive fields
158 minetest
.register_on_player_receive_fields(function(player
, formname
, fields
)
159 local formname
= formname
:split(":")
161 if formname
[1] == "schemedit" and forms
[formname
[2]]
then
162 local handle
= forms
[formname
[2]]
.handle
163 local name
= player
:get_player_name()
164 if contexts
[name
] then
165 if not form_data
[name
] then
169 if not schemedit
.handle_tabs(contexts
[name
], name
, fields
) and handle
then
170 handle(form_data
[name
], contexts
[name
], name
, fields
)
176 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
177 schemedit
.scan_metadata
= function(pos1
, pos2
)
180 for x
=pos1
.x
, pos2
.x
do
181 for y
=pos1
.y
, pos2
.y
do
182 for z
=pos1
.z
, pos2
.z
do
183 local scanpos
= {x
=x
, y
=y
, z
=z
}
184 local node
= minetest
.get_node_or_nil(scanpos
)
186 local prob
, force_place
187 if node
== nil or node
.name
== "schemedit:void" then
191 local meta
= minetest
.get_meta(scanpos
)
193 prob
= tonumber(meta
:get_string("schemedit_prob")) or 255
194 local fp
= meta
:get_string("schemedit_force_place")
202 local hashpos
= minetest
.hash_node_position(scanpos
)
203 prob_list
[hashpos
] = {
206 force_place
= force_place
,
215 -- Sets probability and force_place metadata of an item.
216 -- Also updates item description.
217 -- The itemstack is updated in-place.
218 local function set_item_metadata(itemstack
, prob
, force_place
)
219 local smeta
= itemstack
:get_meta()
220 local prob_desc
= "\n"..S("Probability: @1", prob
or
221 smeta
:get_string("schemedit_prob") or S("Not Set"))
222 -- Update probability
223 if prob
and prob
>= 0 and prob
< 255 then
224 smeta
:set_string("schemedit_prob", tostring(prob
))
225 elseif prob
and prob
== 255 then
226 -- Clear prob metadata for default probability
228 smeta
:set_string("schemedit_prob", nil)
230 prob_desc
= "\n"..S("Probability: @1", smeta
:get_string("schemedit_prob") or
234 -- Update force place
235 if force_place
== true then
236 smeta
:set_string("schemedit_force_place", "true")
237 elseif force_place
== false then
238 smeta
:set_string("schemedit_force_place", nil)
241 -- Update description
242 local desc
= minetest
.registered_items
[itemstack
:get_name()].description
243 local meta_desc
= smeta
:get_string("description")
244 if meta_desc
and meta_desc
~= "" then
248 local original_desc
= smeta
:get_string("original_description")
249 if original_desc
and original_desc
~= "" then
252 smeta
:set_string("original_description", desc
)
255 local force_desc
= ""
256 if smeta
:get_string("schemedit_force_place") == "true" then
257 force_desc
= "\n"..S("Force placement")
260 desc
= desc
..minetest
.colorize(text_color
, prob_desc
..force_desc
)
262 smeta
:set_string("description", desc
)
270 local import_btn
= ""
272 import_btn
= "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
274 schemedit
.add_form("main", {
277 get
= function(self
, pos
, name
)
278 local meta
= minetest
.get_meta(pos
):to_table().fields
279 local strpos
= minetest
.pos_to_string(pos
)
280 local hashpos
= minetest
.hash_node_position(pos
)
283 if meta
.schem_border
== "true" and schemedit
.markers
[hashpos
] then
284 border_button
= "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
286 border_button
= "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
289 local xs
, ys
, zs
= meta
.x_size
or 1, meta
.y_size
or 1, meta
.z_size
or 1
290 local size
= {x
=xs
, y
=ys
, z
=zs
}
291 local schem_name
= meta
.schem_name
or ""
295 label[0.5,-0.1;]]..F(S("Position: @1", strpos
))..[[]
296 label[3,-0.1;]]..F(S("Owner: @1", name
))..[[]
297 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name
)))..[[]
298 label[0.5,0.9;]]..F(S("Size: @1", minetest
.pos_to_string(size
)))..[[]
300 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name
or "")..[[]
301 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
302 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
303 field_close_on_enter[name;false]
305 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
307 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
308 export_path_trunc
.. DIR_DELIM
.. F(S("<name>"))..".mts"))..[[;]
309 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
310 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
311 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
312 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
313 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs
..[[]
314 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys
..[[]
315 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs
..[[]
316 field_close_on_enter[x;false]
317 field_close_on_enter[y;false]
318 field_close_on_enter[z;false]
319 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
322 if minetest
.get_modpath("doc") then
323 form
= form
.. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
324 "tooltip[doc;"..F(S("Help")).."]"
328 handle
= function(self
, pos
, name
, fields
)
330 doc
.show_entry(name
, "nodes", "schemedit:creator", true)
334 if not check_priv(name
, fields
.quit
) then
338 local realmeta
= minetest
.get_meta(pos
)
339 local meta
= realmeta
:to_table().fields
340 local hashpos
= minetest
.hash_node_position(pos
)
342 -- Save size vector values
343 if (fields
.x
and fields
.x
~= "") then
344 local x
= tonumber(fields
.x
)
346 meta
.x_size
= math
.max(x
, 1)
349 if (fields
.y
and fields
.y
~= "") then
350 local y
= tonumber(fields
.y
)
352 meta
.y_size
= math
.max(y
, 1)
355 if (fields
.z
and fields
.z
~= "") then
356 local z
= tonumber(fields
.z
)
358 meta
.z_size
= math
.max(z
, 1)
362 -- Save schematic name
364 meta
.schem_name
= fields
.name
368 if (fields
.air2void
) then
369 local pos1
, pos2
= schemedit
.size(pos
)
370 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
371 local nodes
= minetest
.find_nodes_in_area(pos1
, pos2
, {"air"})
372 minetest
.bulk_set_node(nodes
, {name
="schemedit:void"})
374 elseif (fields
.void2air
) then
375 local pos1
, pos2
= schemedit
.size(pos
)
376 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
377 local nodes
= minetest
.find_nodes_in_area(pos1
, pos2
, {"schemedit:void"})
378 minetest
.bulk_set_node(nodes
, {name
="air"})
383 if fields
.border
then
384 if meta
.schem_border
== "true" and schemedit
.markers
[hashpos
] then
385 schemedit
.unmark(pos
)
386 meta
.schem_border
= "false"
389 meta
.schem_border
= "true"
394 if fields
.export
and meta
.schem_name
and meta
.schem_name
~= "" then
395 local pos1
, pos2
= schemedit
.size(pos
)
396 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
397 local path
= export_path_full
.. DIR_DELIM
400 local plist
= schemedit
.scan_metadata(pos1
, pos2
)
401 local probability_list
= {}
402 for hash
, i
in pairs(plist
) do
403 local prob
= schemedit
.lua_prob_to_schematic_prob(i
.prob
)
404 if i
.force_place
== true then
408 table.insert(probability_list
, {
409 pos
= minetest
.get_position_from_hash(hash
),
414 local slist
= minetest
.deserialize(meta
.slices
)
415 local slice_list
= {}
416 for _
, i
in pairs(slist
) do
417 slice_list
[#slice_list
+ 1] = {
418 ypos
= pos
.y
+ i
.ypos
,
419 prob
= schemedit
.lua_prob_to_schematic_prob(i
.prob
),
423 local filepath
= path
..meta
.schem_name
..".mts"
424 local res
= minetest
.create_schematic(pos1
, pos2
, probability_list
, filepath
, slice_list
)
427 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
428 S("Exported schematic to @1", filepath
)))
429 -- Additional export to Lua file if MTS export was successful
430 local schematic
= minetest
.read_schematic(filepath
, {})
431 if schematic
and minetest
.settings
:get_bool("schemedit_export_lua") then
432 local filepath_lua
= path
..meta
.schem_name
..".lua"
433 res
= export_schematic_to_lua(schematic
, filepath_lua
)
435 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
436 S("Exported schematic to @1", filepath_lua
)))
440 minetest
.chat_send_player(name
, minetest
.colorize("red",
441 S("Failed to export schematic to @1", filepath
)))
446 if fields
.import
and meta
.schem_name
and meta
.schem_name
~= "" then
447 if not can_import
then
451 local node
= minetest
.get_node(pos
)
452 local path
= export_path_full
.. DIR_DELIM
454 local filepath
= path
..meta
.schem_name
..".mts"
455 local schematic
= minetest
.read_schematic(filepath
, {write_yslice_prob
="low"})
456 local success
= false
459 meta
.x_size
= schematic
.size
.x
460 meta
.y_size
= schematic
.size
.y
461 meta
.z_size
= schematic
.size
.z
462 meta
.slices
= minetest
.serialize(renumber(schematic
.yslice_prob
))
463 local special_x_size
= meta
.x_size
464 local special_y_size
= meta
.y_size
465 local special_z_size
= meta
.z_size
467 if node
.param2
== 1 then
468 pos1
= vector
.add(pos
, {x
=1,y
=0,z
=-meta
.z_size
+1})
469 meta
.x_size
, meta
.z_size
= meta
.z_size
, meta
.x_size
470 elseif node
.param2
== 2 then
471 pos1
= vector
.add(pos
, {x
=-meta
.x_size
+1,y
=0,z
=-meta
.z_size
})
472 elseif node
.param2
== 3 then
473 pos1
= vector
.add(pos
, {x
=-meta
.x_size
,y
=0,z
=0})
474 meta
.x_size
, meta
.z_size
= meta
.z_size
, meta
.x_size
476 pos1
= vector
.add(pos
, {x
=0,y
=0,z
=1})
479 local schematic_for_meta
= table.copy(schematic
)
480 -- Strip probability data for placement
481 schematic
.yslice_prob
= {}
482 for d
=1, #schematic
.data
do
483 schematic
.data
[d
].prob
= nil
487 success
= minetest
.place_schematic(pos1
, schematic
, "0", nil, true)
489 -- Add special schematic data to nodes
492 for z
=0, special_z_size
-1 do
493 for y
=0, special_y_size
-1 do
494 for x
=0, special_x_size
-1 do
495 local data
= schematic_for_meta
.data
[d
]
496 local pp
= {x
=pos1
.x
+x
, y
=pos1
.y
+y
, z
=pos1
.z
+z
}
497 if data
.prob
== 0 then
498 minetest
.set_node(pp
, {name
="schemedit:void"})
500 local meta
= minetest
.get_meta(pp
)
501 if data
.prob
and data
.prob
~= 255 and data
.prob
~= 254 then
502 meta
:set_string("schemedit_prob", tostring(data
.prob
))
504 meta
:set_string("schemedit_prob", "")
506 if data
.force_place
then
507 meta
:set_string("schemedit_force_place", "true")
509 meta
:set_string("schemedit_force_place", "")
519 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
520 S("Imported schematic from @1", filepath
)))
522 minetest
.chat_send_player(name
, minetest
.colorize("red",
523 S("Failed to import schematic from @1", filepath
)))
529 -- Save meta before updating visuals
530 local inv
= realmeta
:get_inventory():get_lists()
531 realmeta
:from_table({fields
= meta
, inventory
= inv
})
534 if not fields
.border
and meta
.schem_border
== "true" then
539 if not fields
.quit
then
540 schemedit
.show_formspec(pos
, minetest
.get_player_by_name(name
), "main")
545 schemedit
.add_form("slice", {
546 caption
= S("Y Slices"),
548 get
= function(self
, pos
, name
, visible_panel
)
549 local meta
= minetest
.get_meta(pos
):to_table().fields
551 self
.selected
= self
.selected
or 1
552 local selected
= tostring(self
.selected
)
553 local slice_list
= minetest
.deserialize(meta
.slices
)
555 for _
, i
in pairs(slice_list
) do
556 local insert
= F(S("Y = @1; Probability = @2", tostring(i
.ypos
), tostring(i
.prob
)))
557 slices
= slices
..insert
..","
559 slices
= slices
:sub(1, -2) -- Remove final comma
563 table[0,0;6.8,6;slices;]]..slices
..[[;]]..selected
..[[]
566 if self
.panel_add
or self
.panel_edit
then
567 local ypos_default
, prob_default
= "", ""
568 local done_button
= "button[5,7.18;2,1;done_add;"..F(S("Add")).."]"
569 if self
.panel_edit
then
570 done_button
= "button[5,7.18;2,1;done_edit;"..F(S("Apply")).."]"
571 if slice_list
[self
.selected
] then
572 ypos_default
= slice_list
[self
.selected
].ypos
573 prob_default
= slice_list
[self
.selected
].prob
577 local field_ypos
= ""
578 if self
.panel_add
then
579 field_ypos
= "field[0.3,7.5;2.5,1;ypos;"..F(S("Y position (max. @1):", (meta
.y_size
- 1)))..";"..ypos_default
.."]"
584 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default
..[[]
585 field_close_on_enter[ypos;false]
586 field_close_on_enter[prob;false]
590 if not self
.panel_edit
then
591 if self
.panel_add
then
592 form
= form
.."button[0,6;2.4,1;add;"..F(S("Cancel")).."]"
594 form
= form
.."button[0,6;2.4,1;add;"..F(S("Add slice")).."]"
598 if slices
~= "" and self
.selected
and not self
.panel_add
then
599 if not self
.panel_edit
then
601 button[2.4,6;2.4,1;remove;]]..F(S("Remove slice"))..[[]
602 button[4.8,6;2.4,1;edit;]]..F(S("Edit slice"))..[[]
606 button[4.8,6;2.4,1;edit;]]..F(S("Back"))..[[]
613 handle
= function(self
, pos
, name
, fields
)
614 if not check_priv(name
, fields
.quit
) then
618 local meta
= minetest
.get_meta(pos
)
619 local player
= minetest
.get_player_by_name(name
)
621 if fields
.slices
then
622 local slices
= fields
.slices
:split(":")
623 self
.selected
= tonumber(slices
[2])
627 if not self
.panel_add
then
628 self
.panel_add
= true
629 schemedit
.show_formspec(pos
, player
, "slice")
632 schemedit
.show_formspec(pos
, player
, "slice")
636 local ypos
, prob
= tonumber(fields
.ypos
), tonumber(fields
.prob
)
637 if fields
.done_edit
then
640 if (fields
.done_add
or fields
.done_edit
) and ypos
and prob
and
641 ypos
<= (meta
:get_int("y_size") - 1) and prob
>= 0 and prob
<= 255 then
642 local slice_list
= minetest
.deserialize(meta
:get_string("slices"))
643 local index
= #slice_list
+ 1
644 if fields
.done_edit
then
645 index
= self
.selected
649 if fields
.done_add
then
650 for k
,v
in pairs(slice_list
) do
651 if v
.ypos
== ypos
then
657 if fields
.done_edit
and slice_list
[index
] then
658 ypos
= slice_list
[index
].ypos
661 slice_list
[index
] = {ypos
= ypos
, prob
= prob
}
664 meta
:set_string("slices", minetest
.serialize(slice_list
))
666 -- Update and show formspec
668 schemedit
.show_formspec(pos
, player
, "slice")
671 if fields
.remove and self
.selected
then
672 local slice_list
= minetest
.deserialize(meta
:get_string("slices"))
673 slice_list
[self
.selected
] = nil
674 meta
:set_string("slices", minetest
.serialize(renumber(slice_list
)))
677 self
.selected
= math
.max(1, self
.selected
-1)
678 self
.panel_edit
= nil
679 schemedit
.show_formspec(pos
, player
, "slice")
683 if not self
.panel_edit
then
684 self
.panel_edit
= true
685 schemedit
.show_formspec(pos
, player
, "slice")
687 self
.panel_edit
= nil
688 schemedit
.show_formspec(pos
, player
, "slice")
694 schemedit
.add_form("probtool", {
696 caption
= S("Schematic Node Probability Tool"),
697 get
= function(self
, pos
, name
)
698 local player
= minetest
.get_player_by_name(name
)
702 local probtool
= player
:get_wielded_item()
703 if probtool
:get_name() ~= "schemedit:probtool" then
707 local meta
= probtool
:get_meta()
708 local prob
= tonumber(meta
:get_string("schemedit_prob"))
709 local force_place
= meta
:get_string("schemedit_force_place")
714 if force_place
== nil or force_place
== "" then
715 force_place
= "false"
717 local form
= "size[5,4]"..
718 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
719 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob
.."]"..
720 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place
.. "]" ..
721 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
722 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
723 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
724 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
725 "field_close_on_enter[prob;false]"
728 handle
= function(self
, pos
, name
, fields
)
729 if not check_priv(name
, fields
.quit
) then
733 if fields
.submit
then
734 local prob
= tonumber(fields
.prob
)
736 local player
= minetest
.get_player_by_name(name
)
740 local probtool
= player
:get_wielded_item()
741 if probtool
:get_name() ~= "schemedit:probtool" then
745 local force_place
= self
.force_place
== true
747 set_item_metadata(probtool
, prob
, force_place
)
749 -- Repurpose the tool's wear bar to display the set probability
750 probtool
:set_wear(math
.floor(((255-prob
)/255)*65535))
752 player
:set_wielded_item(probtool
)
755 if fields
.force_place
== "true" then
756 self
.force_place
= true
757 elseif fields
.force_place
== "false" then
758 self
.force_place
= false
767 --- Copies and modifies positions `pos1` and `pos2` so that each component of
768 -- `pos1` is less than or equal to the corresponding component of `pos2`.
769 -- Returns the new positions.
770 function schemedit
.sort_pos(pos1
, pos2
)
771 if not pos1
or not pos2
then
775 pos1
, pos2
= table.copy(pos1
), table.copy(pos2
)
776 if pos1
.x
> pos2
.x
then
777 pos2
.x
, pos1
.x
= pos1
.x
, pos2
.x
779 if pos1
.y
> pos2
.y
then
780 pos2
.y
, pos1
.y
= pos1
.y
, pos2
.y
782 if pos1
.z
> pos2
.z
then
783 pos2
.z
, pos1
.z
= pos1
.z
, pos2
.z
788 -- [function] Prepare size
789 function schemedit
.size(pos
)
790 local pos1
= vector
.new(pos
)
791 local meta
= minetest
.get_meta(pos
)
792 local node
= minetest
.get_node(pos
)
793 local param2
= node
.param2
795 x
= meta
:get_int("x_size"),
796 y
= math
.max(meta
:get_int("y_size") - 1, 0),
797 z
= meta
:get_int("z_size"),
801 local new_pos
= vector
.add({x
= size
.z
, y
= size
.y
, z
= -size
.x
}, pos
)
803 new_pos
.z
= new_pos
.z
+ 1
805 elseif param2
== 2 then
806 local new_pos
= vector
.add({x
= -size
.x
, y
= size
.y
, z
= -size
.z
}, pos
)
808 new_pos
.x
= new_pos
.x
+ 1
810 elseif param2
== 3 then
811 local new_pos
= vector
.add({x
= -size
.z
, y
= size
.y
, z
= size
.x
}, pos
)
813 new_pos
.z
= new_pos
.z
- 1
816 local new_pos
= vector
.add(size
, pos
)
818 new_pos
.x
= new_pos
.x
- 1
823 -- [function] Mark region
824 function schemedit
.mark(pos
)
825 schemedit
.unmark(pos
)
827 local id
= minetest
.hash_node_position(pos
)
828 local owner
= minetest
.get_meta(pos
):get_string("owner")
829 local pos1
, pos2
= schemedit
.size(pos
)
830 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
832 local thickness
= 0.2
833 local sizex
, sizey
, sizez
= (1 + pos2
.x
- pos1
.x
) / 2, (1 + pos2
.y
- pos1
.y
) / 2, (1 + pos2
.z
- pos1
.z
) / 2
839 for _
, z
in ipairs({pos1
.z
- 0.5, pos2
.z
+ 0.5}) do
845 local marker
= minetest
.add_entity({x
= pos1
.x
+ sizex
- 0.5, y
= pos1
.y
+ sizey
- 0.5, z
= z
+ offset
}, "schemedit:display")
846 if marker
~= nil then
847 marker
:set_properties({
848 visual_size
={x
=(sizex
+0.01) * 2, y
=(sizey
+0.01) * 2},
850 marker
:get_luaentity().id
= id
851 marker
:get_luaentity().owner
= owner
852 table.insert(m
, marker
)
859 for _
, x
in ipairs({pos1
.x
- 0.5, pos2
.x
+ 0.5}) do
866 local marker
= minetest
.add_entity({x
= x
+ offset
, y
= pos1
.y
+ sizey
- 0.5, z
= pos1
.z
+ sizez
- 0.5}, "schemedit:display")
867 if marker
~= nil then
868 marker
:set_properties({
869 visual_size
={x
=(sizez
+0.01) * 2, y
=(sizey
+0.01) * 2},
871 marker
:set_rotation({x
=0, y
=math
.pi
/ 2, z
=0})
872 marker
:get_luaentity().id
= id
873 marker
:get_luaentity().owner
= owner
874 table.insert(m
, marker
)
881 for _
, y
in ipairs({pos1
.y
- 0.5, pos2
.y
+ 0.5}) do
888 local marker
= minetest
.add_entity({x
= pos1
.x
+ sizex
- 0.5, y
= y
+ offset
, z
= pos1
.z
+ sizez
- 0.5}, "schemedit:display")
889 if marker
~= nil then
890 marker
:set_properties({
891 visual_size
={x
=(sizex
+0.01) * 2, y
=(sizez
+0.01) * 2},
893 marker
:set_rotation({x
=math
.pi
/2, y
=0, z
=0})
894 marker
:get_luaentity().id
= id
895 marker
:get_luaentity().owner
= owner
896 table.insert(m
, marker
)
903 schemedit
.markers
[id
] = m
907 -- [function] Unmark region
908 function schemedit
.unmark(pos
)
909 local id
= minetest
.hash_node_position(pos
)
910 if schemedit
.markers
[id
] then
912 for _
, entity
in ipairs(schemedit
.markers
[id
]) do
921 --- Mark node probability values near player
924 -- Show probability and force_place status of a particular position for player in HUD.
925 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
926 -- The distance to the node is also displayed below that. This can't be avoided and is
927 -- and artifact of the waypoint HUD element.
928 function schemedit
.display_node_prob(player
, pos
, prob
, force_place
)
930 if prob
and force_place
== true then
931 wpstring
= string.format("%s [F]", prob
)
932 elseif prob
and type(tonumber(prob
)) == "number" then
934 elseif force_place
== true then
938 return player
:hud_add({
939 hud_elem_type
= "waypoint",
942 text
= "m", -- For the distance artifact
943 number = text_color_number
,
949 -- Display the node probabilities and force_place status of the nodes in a region.
950 -- By default, this is done for nodes near the player (distance: 5).
951 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
952 function schemedit
.display_node_probs_region(player
, pos1
, pos2
)
953 local playername
= player
:get_player_name()
954 local pos
= vector
.round(player
:get_pos())
957 -- Default: 5 nodes away from player in any direction
959 pos1
= vector
.subtract(pos
, dist
)
962 pos2
= vector
.add(pos
, dist
)
964 for x
=pos1
.x
, pos2
.x
do
965 for y
=pos1
.y
, pos2
.y
do
966 for z
=pos1
.z
, pos2
.z
do
967 local checkpos
= {x
=x
, y
=y
, z
=z
}
968 local nodehash
= minetest
.hash_node_position(checkpos
)
970 -- If node is already displayed, remove it so it can re replaced later
971 if displayed_waypoints
[playername
][nodehash
] then
972 player
:hud_remove(displayed_waypoints
[playername
][nodehash
])
973 displayed_waypoints
[playername
][nodehash
] = nil
976 local prob
, force_place
977 local meta
= minetest
.get_meta(checkpos
)
978 prob
= meta
:get_string("schemedit_prob")
979 force_place
= meta
:get_string("schemedit_force_place") == "true"
980 local hud_id
= schemedit
.display_node_prob(player
, checkpos
, prob
, force_place
)
982 displayed_waypoints
[playername
][nodehash
] = hud_id
983 displayed_waypoints
[playername
].display_active
= true
990 -- Remove all active displayed node statuses.
991 function schemedit
.clear_displayed_node_probs(player
)
992 local playername
= player
:get_player_name()
993 for nodehash
, hud_id
in pairs(displayed_waypoints
[playername
]) do
994 if nodehash
~= "display_active" then
995 player
:hud_remove(hud_id
)
996 displayed_waypoints
[playername
][nodehash
] = nil
997 displayed_waypoints
[playername
].display_active
= false
1002 minetest
.register_on_joinplayer(function(player
)
1003 displayed_waypoints
[player
:get_player_name()] = {
1004 display_active
= false -- If true, there *might* be at least one active node prob HUD display
1005 -- If false, no node probabilities are displayed for sure.
1009 minetest
.register_on_leaveplayer(function(player
)
1010 displayed_waypoints
[player
:get_player_name()] = nil
1013 -- Regularily clear the displayed node probabilities and force_place
1014 -- for all players who do not wield the probtool.
1015 -- This makes sure the screen is not spammed with information when it
1017 local cleartimer
= 0
1018 minetest
.register_globalstep(function(dtime
)
1019 cleartimer
= cleartimer
+ dtime
1020 if cleartimer
> 2 then
1021 local players
= minetest
.get_connected_players()
1022 for p
= 1, #players
do
1023 local player
= players
[p
]
1024 local pname
= player
:get_player_name()
1025 if displayed_waypoints
[pname
].display_active
then
1026 local item
= player
:get_wielded_item()
1027 if item
:get_name() ~= "schemedit:probtool" then
1028 schemedit
.clear_displayed_node_probs(player
)
1040 -- [priv] schematic_override
1041 minetest
.register_privilege("schematic_override", {
1042 description
= S("Allows you to access schemedit nodes not owned by you"),
1043 give_to_singleplayer
= false,
1046 local help_import
= ""
1048 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"
1051 -- [node] Schematic creator
1052 minetest
.register_node("schemedit:creator", {
1053 description
= S("Schematic Creator"),
1054 _doc_items_longdesc
= S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1055 _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"..
1056 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"..
1058 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1059 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"..
1060 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."),
1061 tiles
= {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1062 "schemedit_creator_sides.png"},
1063 groups
= { dig_immediate
= 2},
1064 paramtype2
= "facedir",
1065 is_ground_content
= false,
1067 after_place_node
= function(pos
, player
)
1068 local name
= player
:get_player_name()
1069 local meta
= minetest
.get_meta(pos
)
1071 meta
:set_string("owner", name
)
1072 meta
:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name
))
1073 meta
:set_string("prob_list", minetest
.serialize({}))
1074 meta
:set_string("slices", minetest
.serialize({}))
1076 local node
= minetest
.get_node(pos
)
1077 local dir
= minetest
.facedir_to_dir(node
.param2
)
1079 meta
:set_int("x_size", 1)
1080 meta
:set_int("y_size", 1)
1081 meta
:set_int("z_size", 1)
1083 -- Don't take item from itemstack
1086 can_dig
= function(pos
, player
)
1087 local name
= player
:get_player_name()
1088 local meta
= minetest
.get_meta(pos
)
1089 if meta
:get_string("owner") == name
or
1090 minetest
.check_player_privs(player
, "schematic_override") == true then
1096 on_rightclick
= function(pos
, node
, player
)
1097 local meta
= minetest
.get_meta(pos
)
1098 local name
= player
:get_player_name()
1099 if meta
:get_string("owner") == name
or
1100 minetest
.check_player_privs(player
, "schematic_override") == true then
1101 -- Get player attribute
1102 local pmeta
= player
:get_meta()
1103 local tab
= pmeta
:get_string("schemedit:tab")
1104 if not forms
[tab
] or not tab
then
1108 schemedit
.show_formspec(pos
, player
, tab
, true)
1111 after_destruct
= function(pos
)
1112 schemedit
.unmark(pos
)
1115 -- No support for Minetest Game's screwdriver
1119 minetest
.register_tool("schemedit:probtool", {
1120 description
= S("Schematic Node Probability Tool"),
1121 _doc_items_longdesc
=
1122 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"..
1123 S("It allows you to set two things:").."\n"..
1124 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1125 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1126 _doc_items_usagehelp
= "\n"..
1127 S("BASIC USAGE:").."\n"..
1128 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"..
1129 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"..
1130 S("NODE HUD:").."\n"..
1131 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"..
1132 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1133 S("UPDATING THE NODE HUD:").."\n"..
1134 S("The node HUD is not updated automatically and may be outdated. The node HUD only updates the HUD for nodes close to you whenever you place the tool or press the punch and sneak keys simultaneously. If you sneak-punch a schematic creator, then the node HUD is updated for all nodes within the schematic creator's region, even if this region is very big."),
1135 wield_image
= "schemedit_probtool.png",
1136 inventory_image
= "schemedit_probtool.png",
1137 liquids_pointable
= true,
1138 groups
= { disable_repair
= 1 },
1139 on_use
= function(itemstack
, user
, pointed_thing
)
1140 local uname
= user
:get_player_name()
1141 if uname
and not check_priv(uname
) then
1145 local ctrl
= user
:get_player_control()
1147 if not ctrl
.sneak
then
1148 -- Open dialog to change the probability to apply to nodes
1149 schemedit
.show_formspec(user
:get_pos(), user
, "probtool", true)
1153 -- Display the probability and force_place values for nodes.
1155 -- If a schematic creator was punched, only enable display for all nodes
1156 -- within the creator's region.
1157 local use_creator_region
= false
1158 if pointed_thing
and pointed_thing
.type == "node" and pointed_thing
.under
then
1159 local punchpos
= pointed_thing
.under
1160 local node
= minetest
.get_node(punchpos
)
1161 if node
.name
== "schemedit:creator" then
1162 local pos1
, pos2
= schemedit
.size(punchpos
)
1163 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
1164 schemedit
.display_node_probs_region(user
, pos1
, pos2
)
1169 -- Otherwise, just display the region close to the player
1170 schemedit
.display_node_probs_region(user
)
1173 on_secondary_use
= function(itemstack
, user
, pointed_thing
)
1174 local uname
= user
:get_player_name()
1175 if uname
and not check_priv(uname
) then
1179 schemedit
.clear_displayed_node_probs(user
)
1181 -- Set note probability and force_place and enable node probability display
1182 on_place
= function(itemstack
, placer
, pointed_thing
)
1183 local pname
= placer
:get_player_name()
1184 if pname
and not check_priv(pname
) then
1188 -- Use pointed node's on_rightclick function first, if present
1189 local node
= minetest
.get_node(pointed_thing
.under
)
1190 if placer
and not placer
:get_player_control().sneak
then
1191 if minetest
.registered_nodes
[node
.name
] and minetest
.registered_nodes
[node
.name
].on_rightclick
then
1192 return minetest
.registered_nodes
[node
.name
].on_rightclick(pointed_thing
.under
, node
, placer
, itemstack
) or itemstack
1196 -- This sets the node probability of pointed node to the
1197 -- currently used probability stored in the tool.
1198 local pos
= pointed_thing
.under
1199 local node
= minetest
.get_node(pos
)
1200 -- Schematic void are ignored, they always have probability 0
1201 if node
.name
== "schemedit:void" then
1204 local nmeta
= minetest
.get_meta(pos
)
1205 local imeta
= itemstack
:get_meta()
1206 local prob
= tonumber(imeta
:get_string("schemedit_prob"))
1207 local force_place
= imeta
:get_string("schemedit_force_place")
1209 if not prob
or prob
== 255 then
1210 nmeta
:set_string("schemedit_prob", nil)
1212 nmeta
:set_string("schemedit_prob", prob
)
1214 if force_place
== "true" then
1215 nmeta
:set_string("schemedit_force_place", "true")
1217 nmeta
:set_string("schemedit_force_place", nil)
1220 -- Enable node probablity display
1221 schemedit
.display_node_probs_region(placer
)
1227 local use_texture_alpha_void
1228 if minetest
.features
.use_texture_alpha_string_modes
then
1229 use_texture_alpha_void
= "clip"
1231 use_texture_alpha_void
= true
1234 minetest
.register_node("schemedit:void", {
1235 description
= S("Schematic Void"),
1236 _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."),
1237 _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."),
1238 tiles
= { "schemedit_void.png" },
1239 use_texture_alpha
= use_texture_alpha_void
,
1240 drawtype
= "nodebox",
1241 is_ground_content
= false,
1242 paramtype
= "light",
1244 sunlight_propagates
= true,
1248 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1251 groups
= { dig_immediate
= 3},
1254 -- [entity] Visible schematic border
1255 minetest
.register_entity("schemedit:display", {
1256 visual
= "upright_sprite",
1257 textures
= {"schemedit_border.png"},
1258 visual_size
= {x
=10, y
=10},
1261 static_save
= false,
1262 glow
= minetest
.LIGHT_MAX
,
1264 on_step
= function(self
, dtime
)
1266 self
.object
:remove()
1267 elseif not schemedit
.markers
[self
.id
] then
1268 self
.object
:remove()
1271 on_activate
= function(self
)
1272 self
.object
:set_armor_groups({immortal
= 1})
1276 minetest
.register_lbm({
1277 label
= "Reset schematic creator border entities",
1278 name
= "schemedit:reset_border",
1279 nodenames
= "schemedit:creator",
1280 run_at_every_load
= true,
1281 action
= function(pos
, node
)
1282 local meta
= minetest
.get_meta(pos
)
1283 meta
:set_string("schem_border", "false")
1287 local function add_suffix(schem
)
1288 -- Automatically add file name suffix if omitted
1289 local schem_full
, schem_lua
1290 if string.sub(schem
, string.len(schem
)-3, string.len(schem
)) == ".mts" then
1292 schem_lua
= string.sub(schem
, 1, -5) .. ".lua"
1294 schem_full
= schem
.. ".mts"
1295 schem_lua
= schem
.. ".lua"
1297 return schem_full
, schem_lua
1300 -- [chatcommand] Place schematic
1301 minetest
.register_chatcommand("placeschem", {
1302 description
= S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc
),
1303 privs
= {server
= true},
1304 params
= S("<schematic name>[.mts] [<x> <y> <z>]"),
1305 func
= function(name
, param
)
1306 local schem
, p
= string.match(param
, "^([^ ]+) *(.*)$")
1307 local pos
= minetest
.string_to_pos(p
)
1310 return false, S("No schematic file specified.")
1314 pos
= minetest
.get_player_by_name(name
):get_pos()
1317 local schem_full
, schem_lua
= add_suffix(schem
)
1318 local success
= false
1319 local schem_path
= export_path_full
.. DIR_DELIM
.. schem_full
1320 if minetest
.read_schematic
then
1321 -- We don't call minetest.place_schematic with the path name directly because
1322 -- this would trigger the caching and we wouldn't get any updates to the schematic
1323 -- files when we reload. minetest.read_schematic circumvents that.
1324 local schematic
= minetest
.read_schematic(schem_path
, {})
1326 success
= minetest
.place_schematic(pos
, schematic
, "random", nil, false)
1329 -- Legacy support for Minetest versions that do not have minetest.read_schematic
1330 success
= minetest
.place_schematic(schem_path
, schematic
, "random", nil, false)
1333 if success
== nil then
1334 return false, S("Schematic file could not be loaded!")
1342 -- [chatcommand] Convert MTS schematic file to .lua file
1343 minetest
.register_chatcommand("mts2lua", {
1344 description
= S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc
),
1345 privs
= {server
= true},
1346 params
= S("<schematic name>[.mts] [comments]"),
1347 func
= function(name
, param
)
1348 local schem
, comments_str
= string.match(param
, "^([^ ]+) *(.*)$")
1351 return false, S("No schematic file specified.")
1354 local comments
= comments_str
== "comments"
1356 -- Automatically add file name suffix if omitted
1357 local schem_full
, schem_lua
= add_suffix(schem
)
1358 local schem_path
= export_path_full
.. DIR_DELIM
.. schem_full
1359 local schematic
= minetest
.read_schematic(schem_path
, {})
1362 local str
= minetest
.serialize_schematic(schematic
, "lua", {lua_use_comments
=comments
})
1363 local lua_path
= export_path_full
.. DIR_DELIM
.. schem_lua
1364 local file
= io
.open(lua_path
, "w")
1365 if file
and str
then
1369 return true, S("Exported schematic to @1", lua_path
)
1371 return false, S("Failed!")
1379 dofile(minetest
.get_modpath("schemedit")..DIR_DELIM
.."make_readme.lua")