2 if minetest
.get_translator
then
3 S
= minetest
.get_translator("schemedit")
5 S
= function(s
) return s
end
7 local F
= minetest
.formspec_escape
13 -- Set to true to enable `make_schemedit_readme` command
14 local MAKE_README
= false
16 local export_path_full
= table.concat({minetest
.get_worldpath(), "schems"}, DIR_DELIM
)
18 -- truncated export path so the server directory structure is not exposed publicly
19 local export_path_trunc
= table.concat({S("<world path>"), "schems"}, DIR_DELIM
)
21 local text_color
= "#D79E9E"
22 local text_color_number
= 0xD79E9E
24 local can_import
= minetest
.read_schematic
~= nil
26 schemedit
.markers
= {}
28 -- [local function] Renumber table
29 local function renumber(t
)
31 for _
, i
in pairs(t
) do
37 local NEEDED_PRIV
= "server"
38 local function check_priv(player_name
, quit
)
39 local privs
= minetest
.get_player_privs(player_name
)
40 if privs
[NEEDED_PRIV
] then
44 minetest
.chat_send_player(player_name
, minetest
.colorize("red",
45 S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV
)))
52 local export_schematic_to_lua
54 export_schematic_to_lua
= function(schematic
, filepath
, options
)
55 if not options
then options
= {} end
56 local str
= minetest
.serialize_schematic(schematic
, "lua", options
)
57 local file
= io
.open(filepath
, "w")
77 local displayed_waypoints
= {}
79 -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
80 -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
81 -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
82 -- on an actual export to a schematic.
84 function schemedit
.lua_prob_to_schematic_prob(lua_prob
)
85 return math
.floor(lua_prob
/ 2)
88 function schemedit
.schematic_prob_to_lua_prob(schematic_prob
)
89 return schematic_prob
* 2
93 -- [function] Add form
94 function schemedit
.add_form(name
, def
)
99 tabs
[#tabs
+ 1] = name
103 -- [function] Generate tabs
104 function schemedit
.generate_tabs(current
)
105 local retval
= "tabheader[0,0;tabs;"
106 for _
, t
in pairs(tabs
) do
108 if f
.tab
~= false and f
.caption
then
109 retval
= retval
..f
.caption
..","
111 if type(current
) ~= "number" and current
== f
.name
then
116 retval
= retval
:sub(1, -2) -- Strip last comma
117 retval
= retval
..";"..current
.."]" -- Close tabheader
121 -- [function] Handle tabs
122 function schemedit
.handle_tabs(pos
, name
, fields
)
123 local tab
= tonumber(fields
.tabs
)
124 if tab
and tabs
[tab
] and forms
[tabs
[tab]]
then
125 schemedit
.show_formspec(pos
, name
, forms
[tabs
[tab]]
.name
)
130 -- [function] Show formspec
131 function schemedit
.show_formspec(pos
, player
, tab
, show
, ...)
133 if type(player
) == "string" then
134 player
= minetest
.get_player_by_name(player
)
136 local name
= player
:get_player_name()
138 if show
~= false then
139 if not form_data
[name
] then
143 local form
= forms
[tab
].get(form_data
[name
], pos
, name
, ...)
144 if forms
[tab
].tab
then
145 form
= form
..schemedit
.generate_tabs(tab
)
148 minetest
.show_formspec(name
, "schemedit:"..tab
, form
)
151 -- Update player attribute
152 if forms
[tab
].cache_name
~= false then
153 local pmeta
= player
:get_meta()
154 pmeta
:set_string("schemedit:tab", tab
)
157 minetest
.close_formspec(pname
, "schemedit:"..tab
)
162 -- [event] On receive fields
163 minetest
.register_on_player_receive_fields(function(player
, formname
, fields
)
164 local formname
= formname
:split(":")
166 if formname
[1] == "schemedit" and forms
[formname
[2]]
then
167 local handle
= forms
[formname
[2]]
.handle
168 local name
= player
:get_player_name()
169 if contexts
[name
] then
170 if not form_data
[name
] then
174 if not schemedit
.handle_tabs(contexts
[name
], name
, fields
) and handle
then
175 handle(form_data
[name
], contexts
[name
], name
, fields
)
181 -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
182 schemedit
.scan_metadata
= function(pos1
, pos2
)
185 for x
=pos1
.x
, pos2
.x
do
186 for y
=pos1
.y
, pos2
.y
do
187 for z
=pos1
.z
, pos2
.z
do
188 local scanpos
= {x
=x
, y
=y
, z
=z
}
189 local node
= minetest
.get_node_or_nil(scanpos
)
191 local prob
, force_place
192 if node
== nil or node
.name
== "schemedit:void" then
196 local meta
= minetest
.get_meta(scanpos
)
198 prob
= tonumber(meta
:get_string("schemedit_prob")) or 255
199 local fp
= meta
:get_string("schemedit_force_place")
207 local hashpos
= minetest
.hash_node_position(scanpos
)
208 prob_list
[hashpos
] = {
211 force_place
= force_place
,
220 -- Sets probability and force_place metadata of an item.
221 -- Also updates item description.
222 -- The itemstack is updated in-place.
223 local function set_item_metadata(itemstack
, prob
, force_place
)
224 local smeta
= itemstack
:get_meta()
225 local prob_desc
= "\n"..S("Probability: @1", prob
or
226 smeta
:get_string("schemedit_prob") or S("Not Set"))
227 -- Update probability
228 if prob
and prob
>= 0 and prob
< 255 then
229 smeta
:set_string("schemedit_prob", tostring(prob
))
230 elseif prob
and prob
== 255 then
231 -- Clear prob metadata for default probability
233 smeta
:set_string("schemedit_prob", nil)
235 prob_desc
= "\n"..S("Probability: @1", smeta
:get_string("schemedit_prob") or
239 -- Update force place
240 if force_place
== true then
241 smeta
:set_string("schemedit_force_place", "true")
242 elseif force_place
== false then
243 smeta
:set_string("schemedit_force_place", nil)
246 -- Update description
247 local desc
= minetest
.registered_items
[itemstack
:get_name()].description
248 local meta_desc
= smeta
:get_string("description")
249 if meta_desc
and meta_desc
~= "" then
253 local original_desc
= smeta
:get_string("original_description")
254 if original_desc
and original_desc
~= "" then
257 smeta
:set_string("original_description", desc
)
260 local force_desc
= ""
261 if smeta
:get_string("schemedit_force_place") == "true" then
262 force_desc
= "\n"..S("Force placement")
265 desc
= desc
..minetest
.colorize(text_color
, prob_desc
..force_desc
)
267 smeta
:set_string("description", desc
)
275 local import_btn
= ""
277 import_btn
= "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
279 schemedit
.add_form("main", {
282 get
= function(self
, pos
, name
)
283 local meta
= minetest
.get_meta(pos
):to_table().fields
284 local strpos
= minetest
.pos_to_string(pos
)
285 local hashpos
= minetest
.hash_node_position(pos
)
288 if meta
.schem_border
== "true" and schemedit
.markers
[hashpos
] then
289 border_button
= "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
291 border_button
= "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
294 local xs
, ys
, zs
= meta
.x_size
or 1, meta
.y_size
or 1, meta
.z_size
or 1
295 local size
= {x
=xs
, y
=ys
, z
=zs
}
296 local schem_name
= meta
.schem_name
or ""
300 label[0.5,-0.1;]]..F(S("Position: @1", strpos
))..[[]
301 label[3,-0.1;]]..F(S("Owner: @1", name
))..[[]
302 label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name
)))..[[]
303 label[0.5,0.9;]]..F(S("Size: @1", minetest
.pos_to_string(size
)))..[[]
305 field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name
or "")..[[]
306 button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
307 tooltip[save_name;]]..F(S("Save schematic name"))..[[]
308 field_close_on_enter[name;false]
310 button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
312 textarea[0.8,4.5;6.2,1;;]]..F(S("Export/import path:\n@1",
313 export_path_trunc
.. DIR_DELIM
.. F(S("<name>"))..".mts"))..[[;]
314 button[0.5,5.5;3,1;air2void;]]..F(S("Air to voids"))..[[]
315 button[3.5,5.5;3,1;void2air;]]..F(S("Voids to air"))..[[]
316 tooltip[air2void;]]..F(S("Turn all air nodes into schematic void nodes"))..[[]
317 tooltip[void2air;]]..F(S("Turn all schematic void nodes into air nodes"))..[[]
318 field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs
..[[]
319 field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys
..[[]
320 field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs
..[[]
321 field_close_on_enter[x;false]
322 field_close_on_enter[y;false]
323 field_close_on_enter[z;false]
324 button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
327 if minetest
.get_modpath("doc") then
328 form
= form
.. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
329 "tooltip[doc;"..F(S("Help")).."]"
333 handle
= function(self
, pos
, name
, fields
)
335 doc
.show_entry(name
, "nodes", "schemedit:creator", true)
339 if not check_priv(name
, fields
.quit
) then
343 local realmeta
= minetest
.get_meta(pos
)
344 local meta
= realmeta
:to_table().fields
345 local hashpos
= minetest
.hash_node_position(pos
)
347 -- Save size vector values
348 if (fields
.x
and fields
.x
~= "") then
349 local x
= tonumber(fields
.x
)
351 meta
.x_size
= math
.max(x
, 1)
354 if (fields
.y
and fields
.y
~= "") then
355 local y
= tonumber(fields
.y
)
357 meta
.y_size
= math
.max(y
, 1)
360 if (fields
.z
and fields
.z
~= "") then
361 local z
= tonumber(fields
.z
)
363 meta
.z_size
= math
.max(z
, 1)
367 -- Save schematic name
369 meta
.schem_name
= fields
.name
373 if (fields
.air2void
) then
374 local pos1
, pos2
= schemedit
.size(pos
)
375 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
376 local nodes
= minetest
.find_nodes_in_area(pos1
, pos2
, {"air"})
377 minetest
.bulk_set_node(nodes
, {name
="schemedit:void"})
379 elseif (fields
.void2air
) then
380 local pos1
, pos2
= schemedit
.size(pos
)
381 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
382 local nodes
= minetest
.find_nodes_in_area(pos1
, pos2
, {"schemedit:void"})
383 minetest
.bulk_set_node(nodes
, {name
="air"})
388 if fields
.border
then
389 if meta
.schem_border
== "true" and schemedit
.markers
[hashpos
] then
390 schemedit
.unmark(pos
)
391 meta
.schem_border
= "false"
394 meta
.schem_border
= "true"
399 if fields
.export
and meta
.schem_name
and meta
.schem_name
~= "" then
400 local pos1
, pos2
= schemedit
.size(pos
)
401 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
402 local path
= export_path_full
.. DIR_DELIM
405 local plist
= schemedit
.scan_metadata(pos1
, pos2
)
406 local probability_list
= {}
407 for hash
, i
in pairs(plist
) do
408 local prob
= schemedit
.lua_prob_to_schematic_prob(i
.prob
)
409 if i
.force_place
== true then
413 table.insert(probability_list
, {
414 pos
= minetest
.get_position_from_hash(hash
),
419 local slist
= minetest
.deserialize(meta
.slices
)
420 local slice_list
= {}
421 for _
, i
in pairs(slist
) do
422 slice_list
[#slice_list
+ 1] = {
424 prob
= schemedit
.lua_prob_to_schematic_prob(i
.prob
),
428 local filepath
= path
..meta
.schem_name
..".mts"
429 local res
= minetest
.create_schematic(pos1
, pos2
, probability_list
, filepath
, slice_list
)
432 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
433 S("Exported schematic to @1", filepath
)))
434 -- Additional export to Lua file if MTS export was successful
435 local schematic
= minetest
.read_schematic(filepath
, {})
436 if schematic
and minetest
.settings
:get_bool("schemedit_export_lua") then
437 local filepath_lua
= path
..meta
.schem_name
..".lua"
438 res
= export_schematic_to_lua(schematic
, filepath_lua
)
440 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
441 S("Exported schematic to @1", filepath_lua
)))
445 minetest
.chat_send_player(name
, minetest
.colorize("red",
446 S("Failed to export schematic to @1", filepath
)))
451 if fields
.import
and meta
.schem_name
and meta
.schem_name
~= "" then
452 if not can_import
then
456 local node
= minetest
.get_node(pos
)
457 local path
= export_path_full
.. DIR_DELIM
459 local filepath
= path
..meta
.schem_name
..".mts"
460 local schematic
= minetest
.read_schematic(filepath
, {write_yslice_prob
="low"})
461 local success
= false
464 meta
.x_size
= schematic
.size
.x
465 meta
.y_size
= schematic
.size
.y
466 meta
.z_size
= schematic
.size
.z
467 meta
.slices
= minetest
.serialize(renumber(schematic
.yslice_prob
))
468 local special_x_size
= meta
.x_size
469 local special_y_size
= meta
.y_size
470 local special_z_size
= meta
.z_size
472 if node
.param2
== 1 then
473 pos1
= vector
.add(pos
, {x
=1,y
=0,z
=-meta
.z_size
+1})
474 meta
.x_size
, meta
.z_size
= meta
.z_size
, meta
.x_size
475 elseif node
.param2
== 2 then
476 pos1
= vector
.add(pos
, {x
=-meta
.x_size
+1,y
=0,z
=-meta
.z_size
})
477 elseif node
.param2
== 3 then
478 pos1
= vector
.add(pos
, {x
=-meta
.x_size
,y
=0,z
=0})
479 meta
.x_size
, meta
.z_size
= meta
.z_size
, meta
.x_size
481 pos1
= vector
.add(pos
, {x
=0,y
=0,z
=1})
484 local schematic_for_meta
= table.copy(schematic
)
485 -- Strip probability data for placement
486 schematic
.yslice_prob
= {}
487 for d
=1, #schematic
.data
do
488 schematic
.data
[d
].prob
= nil
492 success
= minetest
.place_schematic(pos1
, schematic
, "0", nil, true)
494 -- Add special schematic data to nodes
497 for z
=0, special_z_size
-1 do
498 for y
=0, special_y_size
-1 do
499 for x
=0, special_x_size
-1 do
500 local data
= schematic_for_meta
.data
[d
]
501 local pp
= {x
=pos1
.x
+x
, y
=pos1
.y
+y
, z
=pos1
.z
+z
}
502 if data
.prob
== 0 then
503 minetest
.set_node(pp
, {name
="schemedit:void"})
505 local meta
= minetest
.get_meta(pp
)
506 if data
.prob
and data
.prob
~= 255 and data
.prob
~= 254 then
507 meta
:set_string("schemedit_prob", tostring(data
.prob
))
509 meta
:set_string("schemedit_prob", "")
511 if data
.force_place
then
512 meta
:set_string("schemedit_force_place", "true")
514 meta
:set_string("schemedit_force_place", "")
524 minetest
.chat_send_player(name
, minetest
.colorize("#00ff00",
525 S("Imported schematic from @1", filepath
)))
527 minetest
.chat_send_player(name
, minetest
.colorize("red",
528 S("Failed to import schematic from @1", filepath
)))
534 -- Save meta before updating visuals
535 local inv
= realmeta
:get_inventory():get_lists()
536 realmeta
:from_table({fields
= meta
, inventory
= inv
})
539 if not fields
.border
and meta
.schem_border
== "true" then
544 if not fields
.quit
then
545 schemedit
.show_formspec(pos
, minetest
.get_player_by_name(name
), "main")
550 schemedit
.add_form("slice", {
551 caption
= S("Y Slices"),
553 get
= function(self
, pos
, name
, visible_panel
)
554 local meta
= minetest
.get_meta(pos
):to_table().fields
556 self
.selected
= self
.selected
or 1
557 local selected
= tostring(self
.selected
)
558 local slice_list
= minetest
.deserialize(meta
.slices
)
560 for _
, i
in pairs(slice_list
) do
561 local insert
= F(S("Y = @1; Probability = @2", tostring(i
.ypos
), tostring(i
.prob
)))
562 slices
= slices
..insert
..","
564 slices
= slices
:sub(1, -2) -- Remove final comma
568 table[0,0;6.8,6;slices;]]..slices
..[[;]]..selected
..[[]
571 -- Close edit panel if no slices
572 if self
.panel_edit
and slices
== "" then
573 self
.panel_edit
= nil
576 if self
.panel_add
or self
.panel_edit
then
577 local ypos_default
, prob_default
= "", ""
578 local done_button
= "button[5,7.18;2,1;done_add;"..F(S("Add")).."]"
579 if self
.panel_edit
then
580 done_button
= "button[5,7.18;2,1;done_edit;"..F(S("Apply")).."]"
581 if slice_list
[self
.selected
] then
582 ypos_default
= slice_list
[self
.selected
].ypos
583 prob_default
= slice_list
[self
.selected
].prob
587 local field_ypos
= ""
588 if self
.panel_add
then
589 field_ypos
= "field[0.3,7.5;2.5,1;ypos;"..F(S("Y position (max. @1):", (meta
.y_size
- 1)))..";"..ypos_default
.."]"
594 field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default
..[[]
595 field_close_on_enter[ypos;false]
596 field_close_on_enter[prob;false]
600 if not self
.panel_edit
then
601 if self
.panel_add
then
602 form
= form
.."button[0,6;2.4,1;add;"..F(S("Cancel")).."]"
604 form
= form
.."button[0,6;2.4,1;add;"..F(S("Add slice")).."]"
608 if slices
~= "" and self
.selected
and not self
.panel_add
then
609 if not self
.panel_edit
then
611 button[2.4,6;2.4,1;remove;]]..F(S("Remove slice"))..[[]
612 button[4.8,6;2.4,1;edit;]]..F(S("Edit slice"))..[[]
616 button[4.8,6;2.4,1;edit;]]..F(S("Back"))..[[]
623 handle
= function(self
, pos
, name
, fields
)
624 if not check_priv(name
, fields
.quit
) then
628 local meta
= minetest
.get_meta(pos
)
629 local player
= minetest
.get_player_by_name(name
)
631 if fields
.slices
then
632 local slices
= fields
.slices
:split(":")
633 self
.selected
= tonumber(slices
[2])
637 if not self
.panel_add
then
638 self
.panel_add
= true
639 schemedit
.show_formspec(pos
, player
, "slice")
642 schemedit
.show_formspec(pos
, player
, "slice")
646 local ypos
, prob
= tonumber(fields
.ypos
), tonumber(fields
.prob
)
647 if fields
.done_edit
then
650 if (fields
.done_add
or fields
.done_edit
) and ypos
and prob
and
651 ypos
<= (meta
:get_int("y_size") - 1) and prob
>= 0 and prob
<= 255 then
652 local slice_list
= minetest
.deserialize(meta
:get_string("slices"))
653 local index
= #slice_list
+ 1
654 if fields
.done_edit
then
655 index
= self
.selected
659 if fields
.done_add
then
660 for k
,v
in pairs(slice_list
) do
661 if v
.ypos
== ypos
then
667 if fields
.done_edit
and slice_list
[index
] then
668 ypos
= slice_list
[index
].ypos
671 slice_list
[index
] = {ypos
= ypos
, prob
= prob
}
674 meta
:set_string("slices", minetest
.serialize(slice_list
))
676 -- Update and show formspec
678 schemedit
.show_formspec(pos
, player
, "slice")
681 if fields
.remove and self
.selected
then
682 local slice_list
= minetest
.deserialize(meta
:get_string("slices"))
683 slice_list
[self
.selected
] = nil
684 meta
:set_string("slices", minetest
.serialize(renumber(slice_list
)))
687 self
.selected
= math
.max(1, self
.selected
-1)
688 self
.panel_edit
= nil
689 schemedit
.show_formspec(pos
, player
, "slice")
693 if not self
.panel_edit
then
694 self
.panel_edit
= true
695 schemedit
.show_formspec(pos
, player
, "slice")
697 self
.panel_edit
= nil
698 schemedit
.show_formspec(pos
, player
, "slice")
704 schemedit
.add_form("probtool", {
706 caption
= S("Schematic Node Probability Tool"),
707 get
= function(self
, pos
, name
)
708 local player
= minetest
.get_player_by_name(name
)
712 local probtool
= player
:get_wielded_item()
713 if probtool
:get_name() ~= "schemedit:probtool" then
717 local meta
= probtool
:get_meta()
718 local prob
= tonumber(meta
:get_string("schemedit_prob"))
719 local force_place
= meta
:get_string("schemedit_force_place")
724 if force_place
== nil or force_place
== "" then
725 force_place
= "false"
727 local form
= "size[5,4]"..
728 "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
729 "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob
.."]"..
730 "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place
.. "]" ..
731 "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
732 "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
733 "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
734 "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
735 "field_close_on_enter[prob;false]"
738 handle
= function(self
, pos
, name
, fields
)
739 if not check_priv(name
, fields
.quit
) then
743 if fields
.submit
then
744 local prob
= tonumber(fields
.prob
)
746 local player
= minetest
.get_player_by_name(name
)
750 local probtool
= player
:get_wielded_item()
751 if probtool
:get_name() ~= "schemedit:probtool" then
755 local force_place
= self
.force_place
== true
757 set_item_metadata(probtool
, prob
, force_place
)
759 -- Repurpose the tool's wear bar to display the set probability
760 probtool
:set_wear(math
.floor(((255-prob
)/255)*65535))
762 player
:set_wielded_item(probtool
)
765 if fields
.force_place
== "true" then
766 self
.force_place
= true
767 elseif fields
.force_place
== "false" then
768 self
.force_place
= false
777 --- Copies and modifies positions `pos1` and `pos2` so that each component of
778 -- `pos1` is less than or equal to the corresponding component of `pos2`.
779 -- Returns the new positions.
780 function schemedit
.sort_pos(pos1
, pos2
)
781 if not pos1
or not pos2
then
785 pos1
, pos2
= table.copy(pos1
), table.copy(pos2
)
786 if pos1
.x
> pos2
.x
then
787 pos2
.x
, pos1
.x
= pos1
.x
, pos2
.x
789 if pos1
.y
> pos2
.y
then
790 pos2
.y
, pos1
.y
= pos1
.y
, pos2
.y
792 if pos1
.z
> pos2
.z
then
793 pos2
.z
, pos1
.z
= pos1
.z
, pos2
.z
798 -- [function] Prepare size
799 function schemedit
.size(pos
)
800 local pos1
= vector
.new(pos
)
801 local meta
= minetest
.get_meta(pos
)
802 local node
= minetest
.get_node(pos
)
803 local param2
= node
.param2
805 x
= meta
:get_int("x_size"),
806 y
= math
.max(meta
:get_int("y_size") - 1, 0),
807 z
= meta
:get_int("z_size"),
811 local new_pos
= vector
.add({x
= size
.z
, y
= size
.y
, z
= -size
.x
}, pos
)
813 new_pos
.z
= new_pos
.z
+ 1
815 elseif param2
== 2 then
816 local new_pos
= vector
.add({x
= -size
.x
, y
= size
.y
, z
= -size
.z
}, pos
)
818 new_pos
.x
= new_pos
.x
+ 1
820 elseif param2
== 3 then
821 local new_pos
= vector
.add({x
= -size
.z
, y
= size
.y
, z
= size
.x
}, pos
)
823 new_pos
.z
= new_pos
.z
- 1
826 local new_pos
= vector
.add(size
, pos
)
828 new_pos
.x
= new_pos
.x
- 1
833 -- [function] Mark region
834 function schemedit
.mark(pos
)
835 schemedit
.unmark(pos
)
837 local id
= minetest
.hash_node_position(pos
)
838 local owner
= minetest
.get_meta(pos
):get_string("owner")
839 local pos1
, pos2
= schemedit
.size(pos
)
840 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
842 local thickness
= 0.2
843 local sizex
, sizey
, sizez
= (1 + pos2
.x
- pos1
.x
) / 2, (1 + pos2
.y
- pos1
.y
) / 2, (1 + pos2
.z
- pos1
.z
) / 2
849 for _
, z
in ipairs({pos1
.z
- 0.5, pos2
.z
+ 0.5}) do
855 local marker
= minetest
.add_entity({x
= pos1
.x
+ sizex
- 0.5, y
= pos1
.y
+ sizey
- 0.5, z
= z
+ offset
}, "schemedit:display")
856 if marker
~= nil then
857 marker
:set_properties({
858 visual_size
={x
=(sizex
+0.01) * 2, y
=(sizey
+0.01) * 2},
860 marker
:get_luaentity().id
= id
861 marker
:get_luaentity().owner
= owner
862 table.insert(m
, marker
)
869 for _
, x
in ipairs({pos1
.x
- 0.5, pos2
.x
+ 0.5}) do
876 local marker
= minetest
.add_entity({x
= x
+ offset
, y
= pos1
.y
+ sizey
- 0.5, z
= pos1
.z
+ sizez
- 0.5}, "schemedit:display")
877 if marker
~= nil then
878 marker
:set_properties({
879 visual_size
={x
=(sizez
+0.01) * 2, y
=(sizey
+0.01) * 2},
881 marker
:set_rotation({x
=0, y
=math
.pi
/ 2, z
=0})
882 marker
:get_luaentity().id
= id
883 marker
:get_luaentity().owner
= owner
884 table.insert(m
, marker
)
891 for _
, y
in ipairs({pos1
.y
- 0.5, pos2
.y
+ 0.5}) do
898 local marker
= minetest
.add_entity({x
= pos1
.x
+ sizex
- 0.5, y
= y
+ offset
, z
= pos1
.z
+ sizez
- 0.5}, "schemedit:display")
899 if marker
~= nil then
900 marker
:set_properties({
901 visual_size
={x
=(sizex
+0.01) * 2, y
=(sizez
+0.01) * 2},
903 marker
:set_rotation({x
=math
.pi
/2, y
=0, z
=0})
904 marker
:get_luaentity().id
= id
905 marker
:get_luaentity().owner
= owner
906 table.insert(m
, marker
)
913 schemedit
.markers
[id
] = m
917 -- [function] Unmark region
918 function schemedit
.unmark(pos
)
919 local id
= minetest
.hash_node_position(pos
)
920 if schemedit
.markers
[id
] then
922 for _
, entity
in ipairs(schemedit
.markers
[id
]) do
931 --- Mark node probability values near player
934 -- Show probability and force_place status of a particular position for player in HUD.
935 -- Probability is shown as a number followed by “[F]” if the node is force-placed.
936 function schemedit
.display_node_prob(player
, pos
, prob
, force_place
)
938 if prob
and force_place
== true then
939 wpstring
= string.format("%s [F]", prob
)
940 elseif prob
and type(tonumber(prob
)) == "number" then
942 elseif force_place
== true then
946 return player
:hud_add({
947 hud_elem_type
= "waypoint",
950 text
= "m", -- For the distance artifact [legacy]
951 number = text_color_number
,
958 -- Display the node probabilities and force_place status of the nodes in a region.
959 -- By default, this is done for nodes near the player (distance: 5).
960 -- But the boundaries can optionally be set explicitly with pos1 and pos2.
961 function schemedit
.display_node_probs_region(player
, pos1
, pos2
)
962 local playername
= player
:get_player_name()
963 local pos
= vector
.round(player
:get_pos())
966 -- Default: 5 nodes away from player in any direction
968 pos1
= vector
.subtract(pos
, dist
)
971 pos2
= vector
.add(pos
, dist
)
973 for x
=pos1
.x
, pos2
.x
do
974 for y
=pos1
.y
, pos2
.y
do
975 for z
=pos1
.z
, pos2
.z
do
976 local checkpos
= {x
=x
, y
=y
, z
=z
}
977 local nodehash
= minetest
.hash_node_position(checkpos
)
979 -- If node is already displayed, remove it so it can re replaced later
980 if displayed_waypoints
[playername
][nodehash
] then
981 player
:hud_remove(displayed_waypoints
[playername
][nodehash
])
982 displayed_waypoints
[playername
][nodehash
] = nil
985 local prob
, force_place
986 local meta
= minetest
.get_meta(checkpos
)
987 prob
= meta
:get_string("schemedit_prob")
988 force_place
= meta
:get_string("schemedit_force_place") == "true"
989 local hud_id
= schemedit
.display_node_prob(player
, checkpos
, prob
, force_place
)
991 displayed_waypoints
[playername
][nodehash
] = hud_id
992 displayed_waypoints
[playername
].display_active
= true
999 -- Remove all active displayed node statuses.
1000 function schemedit
.clear_displayed_node_probs(player
)
1001 local playername
= player
:get_player_name()
1002 for nodehash
, hud_id
in pairs(displayed_waypoints
[playername
]) do
1003 if nodehash
~= "display_active" then
1004 player
:hud_remove(hud_id
)
1005 displayed_waypoints
[playername
][nodehash
] = nil
1006 displayed_waypoints
[playername
].display_active
= false
1011 minetest
.register_on_joinplayer(function(player
)
1012 displayed_waypoints
[player
:get_player_name()] = {
1013 display_active
= false -- If true, there *might* be at least one active node prob HUD display
1014 -- If false, no node probabilities are displayed for sure.
1018 minetest
.register_on_leaveplayer(function(player
)
1019 displayed_waypoints
[player
:get_player_name()] = nil
1022 -- Regularily clear the displayed node probabilities and force_place
1023 -- for all players who do not wield the probtool.
1024 -- This makes sure the screen is not spammed with information when it
1026 local cleartimer
= 0
1027 minetest
.register_globalstep(function(dtime
)
1028 cleartimer
= cleartimer
+ dtime
1029 if cleartimer
> 2 then
1030 local players
= minetest
.get_connected_players()
1031 for p
= 1, #players
do
1032 local player
= players
[p
]
1033 local pname
= player
:get_player_name()
1034 if displayed_waypoints
[pname
].display_active
then
1035 local item
= player
:get_wielded_item()
1036 if item
:get_name() ~= "schemedit:probtool" then
1037 schemedit
.clear_displayed_node_probs(player
)
1049 -- [priv] schematic_override
1050 minetest
.register_privilege("schematic_override", {
1051 description
= S("Allows you to access schemedit nodes not owned by you"),
1052 give_to_singleplayer
= false,
1055 local help_import
= ""
1057 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"
1060 -- [node] Schematic creator
1061 minetest
.register_node("schemedit:creator", {
1062 description
= S("Schematic Creator"),
1063 _doc_items_longdesc
= S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
1064 _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"..
1065 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"..
1067 S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
1068 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"..
1069 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."),
1070 tiles
= {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
1071 "schemedit_creator_sides.png"},
1072 groups
= { dig_immediate
= 2},
1073 paramtype2
= "facedir",
1074 is_ground_content
= false,
1076 after_place_node
= function(pos
, player
)
1077 local name
= player
:get_player_name()
1078 local meta
= minetest
.get_meta(pos
)
1080 meta
:set_string("owner", name
)
1081 meta
:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name
))
1082 meta
:set_string("prob_list", minetest
.serialize({}))
1083 meta
:set_string("slices", minetest
.serialize({}))
1085 local node
= minetest
.get_node(pos
)
1086 local dir
= minetest
.facedir_to_dir(node
.param2
)
1088 meta
:set_int("x_size", 1)
1089 meta
:set_int("y_size", 1)
1090 meta
:set_int("z_size", 1)
1092 -- Don't take item from itemstack
1095 can_dig
= function(pos
, player
)
1096 local name
= player
:get_player_name()
1097 local meta
= minetest
.get_meta(pos
)
1098 if meta
:get_string("owner") == name
or
1099 minetest
.check_player_privs(player
, "schematic_override") == true then
1105 on_rightclick
= function(pos
, node
, player
)
1106 local meta
= minetest
.get_meta(pos
)
1107 local name
= player
:get_player_name()
1108 if meta
:get_string("owner") == name
or
1109 minetest
.check_player_privs(player
, "schematic_override") == true then
1110 -- Get player attribute
1111 local pmeta
= player
:get_meta()
1112 local tab
= pmeta
:get_string("schemedit:tab")
1113 if not forms
[tab
] or not tab
then
1117 schemedit
.show_formspec(pos
, player
, tab
, true)
1120 after_destruct
= function(pos
)
1121 schemedit
.unmark(pos
)
1124 -- No support for Minetest Game's screwdriver
1128 minetest
.register_tool("schemedit:probtool", {
1129 description
= S("Schematic Node Probability Tool"),
1130 _doc_items_longdesc
=
1131 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"..
1132 S("It allows you to set two things:").."\n"..
1133 S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
1134 S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
1135 _doc_items_usagehelp
= "\n"..
1136 S("BASIC USAGE:").."\n"..
1137 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"..
1138 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"..
1139 S("NODE HUD:").."\n"..
1140 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"..
1141 S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
1142 S("UPDATING THE NODE HUD:").."\n"..
1143 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."),
1144 wield_image
= "schemedit_probtool.png",
1145 inventory_image
= "schemedit_probtool.png",
1146 liquids_pointable
= true,
1147 groups
= { disable_repair
= 1 },
1148 on_use
= function(itemstack
, user
, pointed_thing
)
1149 local uname
= user
:get_player_name()
1150 if uname
and not check_priv(uname
) then
1154 local ctrl
= user
:get_player_control()
1156 if not ctrl
.sneak
then
1157 -- Open dialog to change the probability to apply to nodes
1158 schemedit
.show_formspec(user
:get_pos(), user
, "probtool", true)
1162 -- Display the probability and force_place values for nodes.
1164 -- If a schematic creator was punched, only enable display for all nodes
1165 -- within the creator's region.
1166 local use_creator_region
= false
1167 if pointed_thing
and pointed_thing
.type == "node" and pointed_thing
.under
then
1168 local punchpos
= pointed_thing
.under
1169 local node
= minetest
.get_node(punchpos
)
1170 if node
.name
== "schemedit:creator" then
1171 local pos1
, pos2
= schemedit
.size(punchpos
)
1172 pos1
, pos2
= schemedit
.sort_pos(pos1
, pos2
)
1173 schemedit
.display_node_probs_region(user
, pos1
, pos2
)
1178 -- Otherwise, just display the region close to the player
1179 schemedit
.display_node_probs_region(user
)
1182 on_secondary_use
= function(itemstack
, user
, pointed_thing
)
1183 local uname
= user
:get_player_name()
1184 if uname
and not check_priv(uname
) then
1188 schemedit
.clear_displayed_node_probs(user
)
1190 -- Set note probability and force_place and enable node probability display
1191 on_place
= function(itemstack
, placer
, pointed_thing
)
1192 local pname
= placer
:get_player_name()
1193 if pname
and not check_priv(pname
) then
1197 -- Use pointed node's on_rightclick function first, if present
1198 local node
= minetest
.get_node(pointed_thing
.under
)
1199 if placer
and not placer
:get_player_control().sneak
then
1200 if minetest
.registered_nodes
[node
.name
] and minetest
.registered_nodes
[node
.name
].on_rightclick
then
1201 return minetest
.registered_nodes
[node
.name
].on_rightclick(pointed_thing
.under
, node
, placer
, itemstack
) or itemstack
1205 -- This sets the node probability of pointed node to the
1206 -- currently used probability stored in the tool.
1207 local pos
= pointed_thing
.under
1208 local node
= minetest
.get_node(pos
)
1209 -- Schematic void are ignored, they always have probability 0
1210 if node
.name
== "schemedit:void" then
1213 local nmeta
= minetest
.get_meta(pos
)
1214 local imeta
= itemstack
:get_meta()
1215 local prob
= tonumber(imeta
:get_string("schemedit_prob"))
1216 local force_place
= imeta
:get_string("schemedit_force_place")
1218 if not prob
or prob
== 255 then
1219 nmeta
:set_string("schemedit_prob", nil)
1221 nmeta
:set_string("schemedit_prob", prob
)
1223 if force_place
== "true" then
1224 nmeta
:set_string("schemedit_force_place", "true")
1226 nmeta
:set_string("schemedit_force_place", nil)
1229 -- Enable node probablity display
1230 schemedit
.display_node_probs_region(placer
)
1236 local use_texture_alpha_void
1237 if minetest
.features
.use_texture_alpha_string_modes
then
1238 use_texture_alpha_void
= "clip"
1240 use_texture_alpha_void
= true
1243 minetest
.register_node("schemedit:void", {
1244 description
= S("Schematic Void"),
1245 _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."),
1246 _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."),
1247 tiles
= { "schemedit_void.png" },
1248 use_texture_alpha
= use_texture_alpha_void
,
1249 drawtype
= "nodebox",
1250 is_ground_content
= false,
1251 paramtype
= "light",
1253 sunlight_propagates
= true,
1257 { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
1260 groups
= { dig_immediate
= 3},
1263 -- [entity] Visible schematic border
1264 minetest
.register_entity("schemedit:display", {
1265 visual
= "upright_sprite",
1266 textures
= {"schemedit_border.png"},
1267 visual_size
= {x
=10, y
=10},
1270 static_save
= false,
1271 glow
= minetest
.LIGHT_MAX
,
1273 on_step
= function(self
, dtime
)
1275 self
.object
:remove()
1276 elseif not schemedit
.markers
[self
.id
] then
1277 self
.object
:remove()
1280 on_activate
= function(self
)
1281 self
.object
:set_armor_groups({immortal
= 1})
1285 minetest
.register_lbm({
1286 label
= "Reset schematic creator border entities",
1287 name
= "schemedit:reset_border",
1288 nodenames
= "schemedit:creator",
1289 run_at_every_load
= true,
1290 action
= function(pos
, node
)
1291 local meta
= minetest
.get_meta(pos
)
1292 meta
:set_string("schem_border", "false")
1296 local function add_suffix(schem
)
1297 -- Automatically add file name suffix if omitted
1298 local schem_full
, schem_lua
1299 if string.sub(schem
, string.len(schem
)-3, string.len(schem
)) == ".mts" then
1301 schem_lua
= string.sub(schem
, 1, -5) .. ".lua"
1303 schem_full
= schem
.. ".mts"
1304 schem_lua
= schem
.. ".lua"
1306 return schem_full
, schem_lua
1309 -- [chatcommand] Place schematic
1310 minetest
.register_chatcommand("placeschem", {
1311 description
= S("Place schematic at the position specified or the current player position (loaded from @1). “-c” will clear the area first", export_path_trunc
),
1312 privs
= {server
= true},
1313 params
= S("<schematic name>[.mts] [-c] [<x> <y> <z>]"),
1314 func
= function(name
, param
)
1315 local schem
, clear
, p
= string.match(param
, "^([^ ]+) +(%-c) *(.*)$")
1317 schem
, p
= string.match(param
, "^([^ ]+) *(.*)$")
1319 clear
= clear
== "-c"
1321 local pos
= minetest
.string_to_pos(p
)
1324 return false, S("No schematic file specified.")
1328 pos
= minetest
.get_player_by_name(name
):get_pos()
1331 local schem_full
, schem_lua
= add_suffix(schem
)
1332 local success
= false
1333 local schem_path
= export_path_full
.. DIR_DELIM
.. schem_full
1334 if minetest
.read_schematic
then
1335 -- We don't call minetest.place_schematic with the path name directly because
1336 -- this would trigger the caching and we wouldn't get any updates to the schematic
1337 -- files when we reload. minetest.read_schematic circumvents that.
1338 local schematic
= minetest
.read_schematic(schem_path
, {})
1341 -- Clear same size for X and Z because
1342 -- because schematic is randomly rotated
1343 local max_xz
= math
.max(schematic
.size
.x
, schematic
.size
.z
)
1345 for z
=pos
.z
, pos
.z
+max_xz
-1 do
1346 for y
=pos
.y
, pos
.y
+schematic
.size
.y
-1 do
1347 for x
=pos
.x
, pos
.x
+max_xz
-1 do
1348 table.insert(posses
, {x
=x
,y
=y
,z
=z
})
1352 minetest
.bulk_set_node(posses
, {name
="air"})
1354 success
= minetest
.place_schematic(pos
, schematic
, "random", nil, false)
1357 -- Legacy support for Minetest versions that do not have minetest.read_schematic.
1358 -- Note: "-c" is ignored here.
1359 success
= minetest
.place_schematic(schem_path
, schematic
, "random", nil, false)
1362 if success
== nil then
1363 return false, S("Schematic file could not be loaded!")
1370 minetest
.register_chatcommand("listschems", {
1371 description
= S("List schematic files in world path"),
1372 privs
= {server
= true},
1374 func
= function(name
, param
)
1375 local files
= minetest
.get_dir_list(minetest
.get_worldpath()..DIR_DELIM
.."schems", false)
1379 local out_files
= {}
1380 -- Only show files with “.mts” suffix
1381 for f
=#files
, 1, -1 do
1382 if string.sub(string.lower(files
[f
]), -4, -1) == ".mts" then
1383 table.insert(out_files
, files
[f
])
1386 table.sort(out_files
)
1387 local str
= table.concat(out_files
, ", ")
1389 return true, S("No schematic files.")
1396 -- [chatcommand] Convert MTS schematic file to .lua file
1397 minetest
.register_chatcommand("mts2lua", {
1398 description
= S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc
),
1399 privs
= {server
= true},
1400 params
= S("<schematic name>[.mts] [comments]"),
1401 func
= function(name
, param
)
1402 local schem
, comments_str
= string.match(param
, "^([^ ]+) *(.*)$")
1405 return false, S("No schematic file specified.")
1408 local comments
= comments_str
== "comments"
1410 -- Automatically add file name suffix if omitted
1411 local schem_full
, schem_lua
= add_suffix(schem
)
1412 local schem_path
= export_path_full
.. DIR_DELIM
.. schem_full
1413 local schematic
= minetest
.read_schematic(schem_path
, {})
1416 local str
= minetest
.serialize_schematic(schematic
, "lua", {lua_use_comments
=comments
})
1417 local lua_path
= export_path_full
.. DIR_DELIM
.. schem_lua
1418 local file
= io
.open(lua_path
, "w")
1419 if file
and str
then
1423 return true, S("Exported schematic to @1", lua_path
)
1425 return false, S("Failed!")
1433 dofile(minetest
.get_modpath("schemedit")..DIR_DELIM
.."make_readme.lua")