Miroir du mod minetest_schemedit.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1315 lines
40KB

  1. local S = minetest.get_translator("schemedit")
  2. local F = minetest.formspec_escape
  3. local schemedit = {}
  4. local DIR_DELIM = "/"
  5. local export_path_full = table.concat({minetest.get_worldpath(), "schems"}, DIR_DELIM)
  6. -- truncated export path so the server directory structure is not exposed publicly
  7. local export_path_trunc = table.concat({S("<world path>"), "schems"}, DIR_DELIM)
  8. local text_color = "#D79E9E"
  9. local text_color_number = 0xD79E9E
  10. local can_import = minetest.read_schematic ~= nil
  11. schemedit.markers = {}
  12. -- [local function] Renumber table
  13. local function renumber(t)
  14. local res = {}
  15. for _, i in pairs(t) do
  16. res[#res + 1] = i
  17. end
  18. return res
  19. end
  20. local NEEDED_PRIV = "server"
  21. local function check_priv(player_name, quit)
  22. local privs = minetest.get_player_privs(player_name)
  23. if privs[NEEDED_PRIV] then
  24. return true
  25. else
  26. if not quit then
  27. minetest.chat_send_player(player_name, minetest.colorize("red",
  28. S("Insufficient privileges! You need the “@1” privilege to use this.", NEEDED_PRIV)))
  29. end
  30. return false
  31. end
  32. end
  33. -- Lua export
  34. local export_schematic_to_lua
  35. if can_import then
  36. export_schematic_to_lua = function(schematic, filepath, options)
  37. if not options then options = {} end
  38. local str = minetest.serialize_schematic(schematic, "lua", options)
  39. local file = io.open(filepath, "w")
  40. if file and str then
  41. file:write(str)
  42. file:flush()
  43. file:close()
  44. return true
  45. else
  46. return false
  47. end
  48. end
  49. end
  50. ---
  51. --- Formspec API
  52. ---
  53. local contexts = {}
  54. local form_data = {}
  55. local tabs = {}
  56. local forms = {}
  57. local displayed_waypoints = {}
  58. -- Sadly, the probabilities presented in Lua (0-255) are not identical to the REAL probabilities in the
  59. -- schematic file (0-127). There are two converter functions to convert from one probability type to another.
  60. -- This mod tries to retain the “Lua probability” as long as possible and only switches to “schematic probability”
  61. -- on an actual export to a schematic.
  62. function schemedit.lua_prob_to_schematic_prob(lua_prob)
  63. return math.floor(lua_prob / 2)
  64. end
  65. function schemedit.schematic_prob_to_lua_prob(schematic_prob)
  66. return schematic_prob * 2
  67. end
  68. -- [function] Add form
  69. function schemedit.add_form(name, def)
  70. def.name = name
  71. forms[name] = def
  72. if def.tab then
  73. tabs[#tabs + 1] = name
  74. end
  75. end
  76. -- [function] Generate tabs
  77. function schemedit.generate_tabs(current)
  78. local retval = "tabheader[0,0;tabs;"
  79. for _, t in pairs(tabs) do
  80. local f = forms[t]
  81. if f.tab ~= false and f.caption then
  82. retval = retval..f.caption..","
  83. if type(current) ~= "number" and current == f.name then
  84. current = _
  85. end
  86. end
  87. end
  88. retval = retval:sub(1, -2) -- Strip last comma
  89. retval = retval..";"..current.."]" -- Close tabheader
  90. return retval
  91. end
  92. -- [function] Handle tabs
  93. function schemedit.handle_tabs(pos, name, fields)
  94. local tab = tonumber(fields.tabs)
  95. if tab and tabs[tab] and forms[tabs[tab]] then
  96. schemedit.show_formspec(pos, name, forms[tabs[tab]].name)
  97. return true
  98. end
  99. end
  100. -- [function] Show formspec
  101. function schemedit.show_formspec(pos, player, tab, show, ...)
  102. if forms[tab] then
  103. if type(player) == "string" then
  104. player = minetest.get_player_by_name(player)
  105. end
  106. local name = player:get_player_name()
  107. if show ~= false then
  108. if not form_data[name] then
  109. form_data[name] = {}
  110. end
  111. local form = forms[tab].get(form_data[name], pos, name, ...)
  112. if forms[tab].tab then
  113. form = form..schemedit.generate_tabs(tab)
  114. end
  115. minetest.show_formspec(name, "schemedit:"..tab, form)
  116. contexts[name] = pos
  117. -- Update player attribute
  118. if forms[tab].cache_name ~= false then
  119. player:set_attribute("schemedit:tab", tab)
  120. end
  121. else
  122. minetest.close_formspec(pname, "schemedit:"..tab)
  123. end
  124. end
  125. end
  126. -- [event] On receive fields
  127. minetest.register_on_player_receive_fields(function(player, formname, fields)
  128. local formname = formname:split(":")
  129. if formname[1] == "schemedit" and forms[formname[2]] then
  130. local handle = forms[formname[2]].handle
  131. local name = player:get_player_name()
  132. if contexts[name] then
  133. if not form_data[name] then
  134. form_data[name] = {}
  135. end
  136. if not schemedit.handle_tabs(contexts[name], name, fields) and handle then
  137. handle(form_data[name], contexts[name], name, fields)
  138. end
  139. end
  140. end
  141. end)
  142. -- Helper function. Scans probabilities of all nodes in the given area and returns a prob_list
  143. schemedit.scan_metadata = function(pos1, pos2)
  144. local prob_list = {}
  145. for x=pos1.x, pos2.x do
  146. for y=pos1.y, pos2.y do
  147. for z=pos1.z, pos2.z do
  148. local scanpos = {x=x, y=y, z=z}
  149. local node = minetest.get_node_or_nil(scanpos)
  150. local prob, force_place
  151. if node == nil or node.name == "schemedit:void" then
  152. prob = 0
  153. force_place = false
  154. else
  155. local meta = minetest.get_meta(scanpos)
  156. prob = tonumber(meta:get_string("schemedit_prob")) or 255
  157. local fp = meta:get_string("schemedit_force_place")
  158. if fp == "true" then
  159. force_place = true
  160. else
  161. force_place = false
  162. end
  163. end
  164. local hashpos = minetest.hash_node_position(scanpos)
  165. prob_list[hashpos] = {
  166. pos = scanpos,
  167. prob = prob,
  168. force_place = force_place,
  169. }
  170. end
  171. end
  172. end
  173. return prob_list
  174. end
  175. -- Sets probability and force_place metadata of an item.
  176. -- Also updates item description.
  177. -- The itemstack is updated in-place.
  178. local function set_item_metadata(itemstack, prob, force_place)
  179. local smeta = itemstack:get_meta()
  180. local prob_desc = "\n"..S("Probability: @1", prob or
  181. smeta:get_string("schemedit_prob") or S("Not Set"))
  182. -- Update probability
  183. if prob and prob >= 0 and prob < 255 then
  184. smeta:set_string("schemedit_prob", tostring(prob))
  185. elseif prob and prob == 255 then
  186. -- Clear prob metadata for default probability
  187. prob_desc = ""
  188. smeta:set_string("schemedit_prob", nil)
  189. else
  190. prob_desc = "\n"..S("Probability: @1", smeta:get_string("schemedit_prob") or
  191. S("Not Set"))
  192. end
  193. -- Update force place
  194. if force_place == true then
  195. smeta:set_string("schemedit_force_place", "true")
  196. elseif force_place == false then
  197. smeta:set_string("schemedit_force_place", nil)
  198. end
  199. -- Update description
  200. local desc = minetest.registered_items[itemstack:get_name()].description
  201. local meta_desc = smeta:get_string("description")
  202. if meta_desc and meta_desc ~= "" then
  203. desc = meta_desc
  204. end
  205. local original_desc = smeta:get_string("original_description")
  206. if original_desc and original_desc ~= "" then
  207. desc = original_desc
  208. else
  209. smeta:set_string("original_description", desc)
  210. end
  211. local force_desc = ""
  212. if smeta:get_string("schemedit_force_place") == "true" then
  213. force_desc = "\n"..S("Force placement")
  214. end
  215. desc = desc..minetest.colorize(text_color, prob_desc..force_desc)
  216. smeta:set_string("description", desc)
  217. return itemstack
  218. end
  219. ---
  220. --- Formspec Tabs
  221. ---
  222. local import_btn = ""
  223. if can_import then
  224. import_btn = "button[0.5,2.5;6,1;import;"..F(S("Import schematic")).."]"
  225. end
  226. schemedit.add_form("main", {
  227. tab = true,
  228. caption = S("Main"),
  229. get = function(self, pos, name)
  230. local meta = minetest.get_meta(pos):to_table().fields
  231. local strpos = minetest.pos_to_string(pos)
  232. local hashpos = minetest.hash_node_position(pos)
  233. local border_button
  234. if meta.schem_border == "true" and schemedit.markers[hashpos] then
  235. border_button = "button[3.5,7.5;3,1;border;"..F(S("Hide border")).."]"
  236. else
  237. border_button = "button[3.5,7.5;3,1;border;"..F(S("Show border")).."]"
  238. end
  239. local xs, ys, zs = meta.x_size or 1, meta.y_size or 1, meta.z_size or 1
  240. local size = {x=xs, y=ys, z=zs}
  241. local schem_name = meta.schem_name or ""
  242. local form = [[
  243. size[7,8]
  244. label[0.5,-0.1;]]..F(S("Position: @1", strpos))..[[]
  245. label[3,-0.1;]]..F(S("Owner: @1", name))..[[]
  246. label[0.5,0.4;]]..F(S("Schematic name: @1", F(schem_name)))..[[]
  247. label[0.5,0.9;]]..F(S("Size: @1", minetest.pos_to_string(size)))..[[]
  248. field[0.8,2;5,1;name;]]..F(S("Schematic name:"))..[[;]]..F(schem_name or "")..[[]
  249. button[5.3,1.69;1.2,1;save_name;]]..F(S("OK"))..[[]
  250. tooltip[save_name;]]..F(S("Save schematic name"))..[[]
  251. field_close_on_enter[name;false]
  252. button[0.5,3.5;6,1;export;]]..F(S("Export schematic")).."]"..
  253. import_btn..[[
  254. textarea[0.8,4.5;6.2,5;;]]..F(S("Export/import path:\n@1",
  255. export_path_trunc .. DIR_DELIM .. F(S("<name>"))..".mts"))..[[;]
  256. field[0.8,7;2,1;x;]]..F(S("X size:"))..[[;]]..xs..[[]
  257. field[2.8,7;2,1;y;]]..F(S("Y size:"))..[[;]]..ys..[[]
  258. field[4.8,7;2,1;z;]]..F(S("Z size:"))..[[;]]..zs..[[]
  259. field_close_on_enter[x;false]
  260. field_close_on_enter[y;false]
  261. field_close_on_enter[z;false]
  262. button[0.5,7.5;3,1;save;]]..F(S("Save size"))..[[]
  263. ]]..
  264. border_button
  265. if minetest.get_modpath("doc") then
  266. form = form .. "image_button[6.4,-0.2;0.8,0.8;doc_button_icon_lores.png;doc;]" ..
  267. "tooltip[doc;"..F(S("Help")).."]"
  268. end
  269. return form
  270. end,
  271. handle = function(self, pos, name, fields)
  272. if fields.doc then
  273. doc.show_entry(name, "nodes", "schemedit:creator", true)
  274. return
  275. end
  276. if not check_priv(name, fields.quit) then
  277. return
  278. end
  279. local realmeta = minetest.get_meta(pos)
  280. local meta = realmeta:to_table().fields
  281. local hashpos = minetest.hash_node_position(pos)
  282. -- Save size vector values
  283. if (fields.x and fields.x ~= "") then
  284. local x = tonumber(fields.x)
  285. if x then
  286. meta.x_size = math.max(x, 1)
  287. end
  288. end
  289. if (fields.y and fields.y ~= "") then
  290. local y = tonumber(fields.y)
  291. if y then
  292. meta.y_size = math.max(y, 1)
  293. end
  294. end
  295. if (fields.z and fields.z ~= "") then
  296. local z = tonumber(fields.z)
  297. if z then
  298. meta.z_size = math.max(z, 1)
  299. end
  300. end
  301. -- Save schematic name
  302. if fields.name then
  303. meta.schem_name = fields.name
  304. end
  305. -- Toggle border
  306. if fields.border then
  307. if meta.schem_border == "true" and schemedit.markers[hashpos] then
  308. schemedit.unmark(pos)
  309. meta.schem_border = "false"
  310. else
  311. schemedit.mark(pos)
  312. meta.schem_border = "true"
  313. end
  314. end
  315. -- Export schematic
  316. if fields.export and meta.schem_name and meta.schem_name ~= "" then
  317. local pos1, pos2 = schemedit.size(pos)
  318. pos1, pos2 = schemedit.sort_pos(pos1, pos2)
  319. local path = export_path_full .. DIR_DELIM
  320. minetest.mkdir(path)
  321. local plist = schemedit.scan_metadata(pos1, pos2)
  322. local probability_list = {}
  323. for hash, i in pairs(plist) do
  324. local prob = schemedit.lua_prob_to_schematic_prob(i.prob)
  325. if i.force_place == true then
  326. prob = prob + 128
  327. end
  328. table.insert(probability_list, {
  329. pos = minetest.get_position_from_hash(hash),
  330. prob = prob,
  331. })
  332. end
  333. local slist = minetest.deserialize(meta.slices)
  334. local slice_list = {}
  335. for _, i in pairs(slist) do
  336. slice_list[#slice_list + 1] = {
  337. ypos = pos.y + i.ypos,
  338. prob = schemedit.lua_prob_to_schematic_prob(i.prob),
  339. }
  340. end
  341. local filepath = path..meta.schem_name..".mts"
  342. local res = minetest.create_schematic(pos1, pos2, probability_list, filepath, slice_list)
  343. if res then
  344. minetest.chat_send_player(name, minetest.colorize("#00ff00",
  345. S("Exported schematic to @1", filepath)))
  346. -- Additional export to Lua file if MTS export was successful
  347. local schematic = minetest.read_schematic(filepath, {})
  348. if schematic and minetest.settings:get_bool("schemedit_export_lua") then
  349. local filepath_lua = path..meta.schem_name..".lua"
  350. res = export_schematic_to_lua(schematic, filepath_lua)
  351. if res then
  352. minetest.chat_send_player(name, minetest.colorize("#00ff00",
  353. S("Exported schematic to @1", filepath_lua)))
  354. end
  355. end
  356. else
  357. minetest.chat_send_player(name, minetest.colorize("red",
  358. S("Failed to export schematic to @1", filepath)))
  359. end
  360. end
  361. -- Import schematic
  362. if fields.import and meta.schem_name and meta.schem_name ~= "" then
  363. if not can_import then
  364. return
  365. end
  366. local pos1
  367. local node = minetest.get_node(pos)
  368. local path = export_path_full .. DIR_DELIM
  369. local filepath = path..meta.schem_name..".mts"
  370. local schematic = minetest.read_schematic(filepath, {write_yslice_prob="low"})
  371. local success = false
  372. if schematic then
  373. meta.x_size = schematic.size.x
  374. meta.y_size = schematic.size.y
  375. meta.z_size = schematic.size.z
  376. meta.slices = minetest.serialize(schematic.yslice_prob)
  377. if node.param2 == 1 then
  378. pos1 = vector.add(pos, {x=1,y=0,z=-meta.z_size+1})
  379. elseif node.param2 == 2 then
  380. pos1 = vector.add(pos, {x=-meta.x_size+1,y=0,z=-meta.z_size})
  381. elseif node.param2 == 3 then
  382. pos1 = vector.add(pos, {x=-meta.x_size,y=0,z=0})
  383. else
  384. pos1 = vector.add(pos, {x=0,y=0,z=1})
  385. end
  386. local schematic_for_meta = table.copy(schematic)
  387. -- Strip probability data for placement
  388. schematic.yslice_prob = {}
  389. for d=1, #schematic.data do
  390. schematic.data[d].prob = nil
  391. end
  392. -- Place schematic
  393. success = minetest.place_schematic(pos1, schematic, "0", nil, true)
  394. -- Add special schematic data to nodes
  395. if success then
  396. local d = 1
  397. for z=0, meta.z_size-1 do
  398. for y=0, meta.y_size-1 do
  399. for x=0, meta.x_size-1 do
  400. local data = schematic_for_meta.data[d]
  401. local pp = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
  402. if data.prob == 0 then
  403. minetest.set_node(pp, {name="schemedit:void"})
  404. else
  405. local meta = minetest.get_meta(pp)
  406. if data.prob and data.prob ~= 255 and data.prob ~= 254 then
  407. meta:set_string("schemedit_prob", tostring(data.prob))
  408. else
  409. meta:set_string("schemedit_prob", "")
  410. end
  411. if data.force_place then
  412. meta:set_string("schemedit_force_place", "true")
  413. else
  414. meta:set_string("schemedit_force_place", "")
  415. end
  416. end
  417. d = d + 1
  418. end
  419. end
  420. end
  421. end
  422. end
  423. if success then
  424. minetest.chat_send_player(name, minetest.colorize("#00ff00",
  425. S("Imported schematic from @1", filepath)))
  426. else
  427. minetest.chat_send_player(name, minetest.colorize("red",
  428. S("Failed to import schematic from @1", filepath)))
  429. end
  430. end
  431. -- Save meta before updating visuals
  432. local inv = realmeta:get_inventory():get_lists()
  433. realmeta:from_table({fields = meta, inventory = inv})
  434. -- Update border
  435. if not fields.border and meta.schem_border == "true" then
  436. schemedit.mark(pos)
  437. end
  438. -- Update formspec
  439. if not fields.quit then
  440. schemedit.show_formspec(pos, minetest.get_player_by_name(name), "main")
  441. end
  442. end,
  443. })
  444. schemedit.add_form("slice", {
  445. caption = S("Y Slices"),
  446. tab = true,
  447. get = function(self, pos, name, visible_panel)
  448. local meta = minetest.get_meta(pos):to_table().fields
  449. self.selected = self.selected or 1
  450. local selected = tostring(self.selected)
  451. local slice_list = minetest.deserialize(meta.slices)
  452. local slices = ""
  453. for _, i in pairs(slice_list) do
  454. local insert = F(S("Y = @1; Probability = @2", tostring(i.ypos), tostring(i.prob)))
  455. slices = slices..insert..","
  456. end
  457. slices = slices:sub(1, -2) -- Remove final comma
  458. local form = [[
  459. size[7,8]
  460. table[0,0;6.8,6;slices;]]..slices..[[;]]..selected..[[]
  461. ]]
  462. if self.panel_add or self.panel_edit then
  463. local ypos_default, prob_default = "", ""
  464. local done_button = "button[5,7.18;2,1;done_add;"..F(S("Done")).."]"
  465. if self.panel_edit then
  466. done_button = "button[5,7.18;2,1;done_edit;"..F(S("Done")).."]"
  467. if slice_list[self.selected] then
  468. ypos_default = slice_list[self.selected].ypos
  469. prob_default = slice_list[self.selected].prob
  470. end
  471. end
  472. form = form..[[
  473. field[0.3,7.5;2.5,1;ypos;]]..F(S("Y position (max. @1):", (meta.y_size - 1)))..[[;]]..ypos_default..[[]
  474. field[2.8,7.5;2.5,1;prob;]]..F(S("Probability (0-255):"))..[[;]]..prob_default..[[]
  475. field_close_on_enter[ypos;false]
  476. field_close_on_enter[prob;false]
  477. ]]..done_button
  478. end
  479. if not self.panel_edit then
  480. form = form.."button[0,6;2.4,1;add;"..F(S("+ Add slice")).."]"
  481. end
  482. if slices ~= "" and self.selected and not self.panel_add then
  483. if not self.panel_edit then
  484. form = form..[[
  485. button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
  486. button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
  487. ]]
  488. else
  489. form = form..[[
  490. button[2.4,6;2.4,1;remove;]]..F(S("- Remove slice"))..[[]
  491. button[4.8,6;2.4,1;edit;]]..F(S("+/- Edit slice"))..[[]
  492. ]]
  493. end
  494. end
  495. return form
  496. end,
  497. handle = function(self, pos, name, fields)
  498. if not check_priv(name, fields.quit) then
  499. return
  500. end
  501. local meta = minetest.get_meta(pos)
  502. local player = minetest.get_player_by_name(name)
  503. if fields.slices then
  504. local slices = fields.slices:split(":")
  505. self.selected = tonumber(slices[2])
  506. end
  507. if fields.add then
  508. if not self.panel_add then
  509. self.panel_add = true
  510. schemedit.show_formspec(pos, player, "slice")
  511. else
  512. self.panel_add = nil
  513. schemedit.show_formspec(pos, player, "slice")
  514. end
  515. end
  516. local ypos, prob = tonumber(fields.ypos), tonumber(fields.prob)
  517. if (fields.done_add or fields.done_edit) and fields.ypos and fields.prob and
  518. fields.ypos ~= "" and fields.prob ~= "" and ypos and prob and
  519. ypos <= (meta:get_int("y_size") - 1) and prob >= 0 and prob <= 255 then
  520. local slice_list = minetest.deserialize(meta:get_string("slices"))
  521. local index = #slice_list + 1
  522. if fields.done_edit then
  523. index = self.selected
  524. end
  525. slice_list[index] = {ypos = ypos, prob = prob}
  526. meta:set_string("slices", minetest.serialize(slice_list))
  527. -- Update and show formspec
  528. self.panel_add = nil
  529. schemedit.show_formspec(pos, player, "slice")
  530. end
  531. if fields.remove and self.selected then
  532. local slice_list = minetest.deserialize(meta:get_string("slices"))
  533. slice_list[self.selected] = nil
  534. meta:set_string("slices", minetest.serialize(renumber(slice_list)))
  535. -- Update formspec
  536. self.selected = 1
  537. self.panel_edit = nil
  538. schemedit.show_formspec(pos, player, "slice")
  539. end
  540. if fields.edit then
  541. if not self.panel_edit then
  542. self.panel_edit = true
  543. schemedit.show_formspec(pos, player, "slice")
  544. else
  545. self.panel_edit = nil
  546. schemedit.show_formspec(pos, player, "slice")
  547. end
  548. end
  549. end,
  550. })
  551. schemedit.add_form("probtool", {
  552. cache_name = false,
  553. caption = S("Schematic Node Probability Tool"),
  554. get = function(self, pos, name)
  555. local player = minetest.get_player_by_name(name)
  556. if not player then
  557. return
  558. end
  559. local probtool = player:get_wielded_item()
  560. if probtool:get_name() ~= "schemedit:probtool" then
  561. return
  562. end
  563. local meta = probtool:get_meta()
  564. local prob = tonumber(meta:get_string("schemedit_prob"))
  565. local force_place = meta:get_string("schemedit_force_place")
  566. if not prob then
  567. prob = 255
  568. end
  569. if force_place == nil or force_place == "" then
  570. force_place = "false"
  571. end
  572. local form = "size[5,4]"..
  573. "label[0,0;"..F(S("Schematic Node Probability Tool")).."]"..
  574. "field[0.75,1;4,1;prob;"..F(S("Probability (0-255)"))..";"..prob.."]"..
  575. "checkbox[0.60,1.5;force_place;"..F(S("Force placement"))..";" .. force_place .. "]" ..
  576. "button_exit[0.25,3;2,1;cancel;"..F(S("Cancel")).."]"..
  577. "button_exit[2.75,3;2,1;submit;"..F(S("Apply")).."]"..
  578. "tooltip[prob;"..F(S("Probability that the node will be placed")).."]"..
  579. "tooltip[force_place;"..F(S("If enabled, the node will replace nodes other than air and ignore")).."]"..
  580. "field_close_on_enter[prob;false]"
  581. return form
  582. end,
  583. handle = function(self, pos, name, fields)
  584. if not check_priv(name, fields.quit) then
  585. return
  586. end
  587. if fields.submit then
  588. local prob = tonumber(fields.prob)
  589. if prob then
  590. local player = minetest.get_player_by_name(name)
  591. if not player then
  592. return
  593. end
  594. local probtool = player:get_wielded_item()
  595. if probtool:get_name() ~= "schemedit:probtool" then
  596. return
  597. end
  598. local force_place = self.force_place == true
  599. set_item_metadata(probtool, prob, force_place)
  600. -- Repurpose the tool's wear bar to display the set probability
  601. probtool:set_wear(math.floor(((255-prob)/255)*65535))
  602. player:set_wielded_item(probtool)
  603. end
  604. end
  605. if fields.force_place == "true" then
  606. self.force_place = true
  607. elseif fields.force_place == "false" then
  608. self.force_place = false
  609. end
  610. end,
  611. })
  612. ---
  613. --- API
  614. ---
  615. --- Copies and modifies positions `pos1` and `pos2` so that each component of
  616. -- `pos1` is less than or equal to the corresponding component of `pos2`.
  617. -- Returns the new positions.
  618. function schemedit.sort_pos(pos1, pos2)
  619. if not pos1 or not pos2 then
  620. return
  621. end
  622. pos1, pos2 = table.copy(pos1), table.copy(pos2)
  623. if pos1.x > pos2.x then
  624. pos2.x, pos1.x = pos1.x, pos2.x
  625. end
  626. if pos1.y > pos2.y then
  627. pos2.y, pos1.y = pos1.y, pos2.y
  628. end
  629. if pos1.z > pos2.z then
  630. pos2.z, pos1.z = pos1.z, pos2.z
  631. end
  632. return pos1, pos2
  633. end
  634. -- [function] Prepare size
  635. function schemedit.size(pos)
  636. local pos1 = vector.new(pos)
  637. local meta = minetest.get_meta(pos)
  638. local node = minetest.get_node(pos)
  639. local param2 = node.param2
  640. local size = {
  641. x = meta:get_int("x_size"),
  642. y = math.max(meta:get_int("y_size") - 1, 0),
  643. z = meta:get_int("z_size"),
  644. }
  645. if param2 == 1 then
  646. local new_pos = vector.add({x = size.z, y = size.y, z = -size.x}, pos)
  647. pos1.x = pos1.x + 1
  648. new_pos.z = new_pos.z + 1
  649. return pos1, new_pos
  650. elseif param2 == 2 then
  651. local new_pos = vector.add({x = -size.x, y = size.y, z = -size.z}, pos)
  652. pos1.z = pos1.z - 1
  653. new_pos.x = new_pos.x + 1
  654. return pos1, new_pos
  655. elseif param2 == 3 then
  656. local new_pos = vector.add({x = -size.z, y = size.y, z = size.x}, pos)
  657. pos1.x = pos1.x - 1
  658. new_pos.z = new_pos.z - 1
  659. return pos1, new_pos
  660. else
  661. local new_pos = vector.add(size, pos)
  662. pos1.z = pos1.z + 1
  663. new_pos.x = new_pos.x - 1
  664. return pos1, new_pos
  665. end
  666. end
  667. -- [function] Mark region
  668. function schemedit.mark(pos)
  669. schemedit.unmark(pos)
  670. local id = minetest.hash_node_position(pos)
  671. local owner = minetest.get_meta(pos):get_string("owner")
  672. local pos1, pos2 = schemedit.size(pos)
  673. pos1, pos2 = schemedit.sort_pos(pos1, pos2)
  674. local thickness = 0.2
  675. local sizex, sizey, sizez = (1 + pos2.x - pos1.x) / 2, (1 + pos2.y - pos1.y) / 2, (1 + pos2.z - pos1.z) / 2
  676. local m = {}
  677. local low = true
  678. local offset
  679. -- XY plane markers
  680. for _, z in ipairs({pos1.z - 0.5, pos2.z + 0.5}) do
  681. if low then
  682. offset = -0.01
  683. else
  684. offset = 0.01
  685. end
  686. local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = pos1.y + sizey - 0.5, z = z + offset}, "schemedit:display")
  687. if marker ~= nil then
  688. marker:set_properties({
  689. visual_size={x=(sizex+0.01) * 2, y=(sizey+0.01) * 2},
  690. })
  691. marker:get_luaentity().id = id
  692. marker:get_luaentity().owner = owner
  693. table.insert(m, marker)
  694. end
  695. low = false
  696. end
  697. low = true
  698. -- YZ plane markers
  699. for _, x in ipairs({pos1.x - 0.5, pos2.x + 0.5}) do
  700. if low then
  701. offset = -0.01
  702. else
  703. offset = 0.01
  704. end
  705. local marker = minetest.add_entity({x = x + offset, y = pos1.y + sizey - 0.5, z = pos1.z + sizez - 0.5}, "schemedit:display")
  706. if marker ~= nil then
  707. marker:set_properties({
  708. visual_size={x=(sizez+0.01) * 2, y=(sizey+0.01) * 2},
  709. })
  710. marker:set_rotation({x=0, y=math.pi / 2, z=0})
  711. marker:get_luaentity().id = id
  712. marker:get_luaentity().owner = owner
  713. table.insert(m, marker)
  714. end
  715. low = false
  716. end
  717. low = true
  718. -- XZ plane markers
  719. for _, y in ipairs({pos1.y - 0.5, pos2.y + 0.5}) do
  720. if low then
  721. offset = -0.01
  722. else
  723. offset = 0.01
  724. end
  725. local marker = minetest.add_entity({x = pos1.x + sizex - 0.5, y = y + offset, z = pos1.z + sizez - 0.5}, "schemedit:display")
  726. if marker ~= nil then
  727. marker:set_properties({
  728. visual_size={x=(sizex+0.01) * 2, y=(sizez+0.01) * 2},
  729. })
  730. marker:set_rotation({x=math.pi/2, y=0, z=0})
  731. marker:get_luaentity().id = id
  732. marker:get_luaentity().owner = owner
  733. table.insert(m, marker)
  734. end
  735. low = false
  736. end
  737. schemedit.markers[id] = m
  738. return true
  739. end
  740. -- [function] Unmark region
  741. function schemedit.unmark(pos)
  742. local id = minetest.hash_node_position(pos)
  743. if schemedit.markers[id] then
  744. local retval
  745. for _, entity in ipairs(schemedit.markers[id]) do
  746. entity:remove()
  747. retval = true
  748. end
  749. return retval
  750. end
  751. end
  752. ---
  753. --- Mark node probability values near player
  754. ---
  755. -- Show probability and force_place status of a particular position for player in HUD.
  756. -- Probability is shown as a number followed by “[F]” if the node is force-placed.
  757. -- The distance to the node is also displayed below that. This can't be avoided and is
  758. -- and artifact of the waypoint HUD element.
  759. function schemedit.display_node_prob(player, pos, prob, force_place)
  760. local wpstring
  761. if prob and force_place == true then
  762. wpstring = string.format("%s [F]", prob)
  763. elseif prob and type(tonumber(prob)) == "number" then
  764. wpstring = prob
  765. elseif force_place == true then
  766. wpstring = "[F]"
  767. end
  768. if wpstring then
  769. return player:hud_add({
  770. hud_elem_type = "waypoint",
  771. name = wpstring,
  772. precision = 0,
  773. text = "m", -- For the distance artifact
  774. number = text_color_number,
  775. world_pos = pos,
  776. })
  777. end
  778. end
  779. -- Display the node probabilities and force_place status of the nodes in a region.
  780. -- By default, this is done for nodes near the player (distance: 5).
  781. -- But the boundaries can optionally be set explicitly with pos1 and pos2.
  782. function schemedit.display_node_probs_region(player, pos1, pos2)
  783. local playername = player:get_player_name()
  784. local pos = vector.round(player:get_pos())
  785. local dist = 5
  786. -- Default: 5 nodes away from player in any direction
  787. if not pos1 then
  788. pos1 = vector.subtract(pos, dist)
  789. end
  790. if not pos2 then
  791. pos2 = vector.add(pos, dist)
  792. end
  793. for x=pos1.x, pos2.x do
  794. for y=pos1.y, pos2.y do
  795. for z=pos1.z, pos2.z do
  796. local checkpos = {x=x, y=y, z=z}
  797. local nodehash = minetest.hash_node_position(checkpos)
  798. -- If node is already displayed, remove it so it can re replaced later
  799. if displayed_waypoints[playername][nodehash] then
  800. player:hud_remove(displayed_waypoints[playername][nodehash])
  801. displayed_waypoints[playername][nodehash] = nil
  802. end
  803. local prob, force_place
  804. local meta = minetest.get_meta(checkpos)
  805. prob = meta:get_string("schemedit_prob")
  806. force_place = meta:get_string("schemedit_force_place") == "true"
  807. local hud_id = schemedit.display_node_prob(player, checkpos, prob, force_place)
  808. if hud_id then
  809. displayed_waypoints[playername][nodehash] = hud_id
  810. displayed_waypoints[playername].display_active = true
  811. end
  812. end
  813. end
  814. end
  815. end
  816. -- Remove all active displayed node statuses.
  817. function schemedit.clear_displayed_node_probs(player)
  818. local playername = player:get_player_name()
  819. for nodehash, hud_id in pairs(displayed_waypoints[playername]) do
  820. player:hud_remove(hud_id)
  821. displayed_waypoints[playername][nodehash] = nil
  822. displayed_waypoints[playername].display_active = false
  823. end
  824. end
  825. minetest.register_on_joinplayer(function(player)
  826. displayed_waypoints[player:get_player_name()] = {
  827. display_active = false -- If true, there *might* be at least one active node prob HUD display
  828. -- If false, no node probabilities are displayed for sure.
  829. }
  830. end)
  831. minetest.register_on_leaveplayer(function(player)
  832. displayed_waypoints[player:get_player_name()] = nil
  833. end)
  834. -- Regularily clear the displayed node probabilities and force_place
  835. -- for all players who do not wield the probtool.
  836. -- This makes sure the screen is not spammed with information when it
  837. -- isn't needed.
  838. local cleartimer = 0
  839. minetest.register_globalstep(function(dtime)
  840. cleartimer = cleartimer + dtime
  841. if cleartimer > 2 then
  842. local players = minetest.get_connected_players()
  843. for p = 1, #players do
  844. local player = players[p]
  845. local pname = player:get_player_name()
  846. if displayed_waypoints[pname].display_active then
  847. local item = player:get_wielded_item()
  848. if item:get_name() ~= "schemedit:probtool" then
  849. schemedit.clear_displayed_node_probs(player)
  850. end
  851. end
  852. end
  853. cleartimer = 0
  854. end
  855. end)
  856. ---
  857. --- Registrations
  858. ---
  859. -- [priv] schematic_override
  860. minetest.register_privilege("schematic_override", {
  861. description = S("Allows you to access schemedit nodes not owned by you"),
  862. give_to_singleplayer = false,
  863. })
  864. local help_import = ""
  865. if can_import then
  866. 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"
  867. end
  868. -- [node] Schematic creator
  869. minetest.register_node("schemedit:creator", {
  870. description = S("Schematic Creator"),
  871. _doc_items_longdesc = S("The schematic creator is used to save a region of the world into a schematic file (.mts)."),
  872. _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"..
  873. 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"..
  874. help_import..
  875. S("The other features of the schematic creator are optional and are used to allow to add randomness and fine-tuning.").."\n\n"..
  876. 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"..
  877. 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."),
  878. tiles = {"schemedit_creator_top.png", "schemedit_creator_bottom.png",
  879. "schemedit_creator_sides.png"},
  880. groups = { dig_immediate = 2},
  881. paramtype2 = "facedir",
  882. is_ground_content = false,
  883. after_place_node = function(pos, player)
  884. local name = player:get_player_name()
  885. local meta = minetest.get_meta(pos)
  886. meta:set_string("owner", name)
  887. meta:set_string("infotext", S("Schematic Creator").."\n"..S("(owned by @1)", name))
  888. meta:set_string("prob_list", minetest.serialize({}))
  889. meta:set_string("slices", minetest.serialize({}))
  890. local node = minetest.get_node(pos)
  891. local dir = minetest.facedir_to_dir(node.param2)
  892. meta:set_int("x_size", 1)
  893. meta:set_int("y_size", 1)
  894. meta:set_int("z_size", 1)
  895. -- Don't take item from itemstack
  896. return true
  897. end,
  898. can_dig = function(pos, player)
  899. local name = player:get_player_name()
  900. local meta = minetest.get_meta(pos)
  901. if meta:get_string("owner") == name or
  902. minetest.check_player_privs(player, "schematic_override") == true then
  903. return true
  904. end
  905. return false
  906. end,
  907. on_rightclick = function(pos, node, player)
  908. local meta = minetest.get_meta(pos)
  909. local name = player:get_player_name()
  910. if meta:get_string("owner") == name or
  911. minetest.check_player_privs(player, "schematic_override") == true then
  912. -- Get player attribute
  913. local tab = player:get_attribute("schemedit:tab")
  914. if not forms[tab] or not tab then
  915. tab = "main"
  916. end
  917. schemedit.show_formspec(pos, player, tab, true)
  918. end
  919. end,
  920. after_destruct = function(pos)
  921. schemedit.unmark(pos)
  922. end,
  923. -- No support for Minetest Game's screwdriver
  924. on_rotate = false,
  925. })
  926. minetest.register_tool("schemedit:probtool", {
  927. description = S("Schematic Node Probability Tool"),
  928. _doc_items_longdesc =
  929. 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"..
  930. S("It allows you to set two things:").."\n"..
  931. S("1) Set probability: Chance for any particular node to be actually placed (default: always placed)").."\n"..
  932. S("2) Enable force placement: These nodes replace node other than air and ignore when placed in a schematic (default: off)"),
  933. _doc_items_usagehelp = "\n"..
  934. S("BASIC USAGE:").."\n"..
  935. 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"..
  936. 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"..
  937. S("NODE HUD:").."\n"..
  938. 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"..
  939. S("To disable the node HUD, unselect the tool or hit “place” while not pointing anything.").."\n\n"..
  940. S("UPDATING THE NODE HUD:").."\n"..
  941. 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."),
  942. wield_image = "schemedit_probtool.png",
  943. inventory_image = "schemedit_probtool.png",
  944. liquids_pointable = true,
  945. groups = { disable_repair = 1 },
  946. on_use = function(itemstack, user, pointed_thing)
  947. local uname = user:get_player_name()
  948. if uname and not check_priv(uname) then
  949. return
  950. end
  951. local ctrl = user:get_player_control()
  952. -- Simple use
  953. if not ctrl.sneak then
  954. -- Open dialog to change the probability to apply to nodes
  955. schemedit.show_formspec(user:get_pos(), user, "probtool", true)
  956. -- Use + sneak
  957. else
  958. -- Display the probability and force_place values for nodes.
  959. -- If a schematic creator was punched, only enable display for all nodes
  960. -- within the creator's region.
  961. local use_creator_region = false
  962. if pointed_thing and pointed_thing.type == "node" and pointed_thing.under then
  963. local punchpos = pointed_thing.under
  964. local node = minetest.get_node(punchpos)
  965. if node.name == "schemedit:creator" then
  966. local pos1, pos2 = schemedit.size(punchpos)
  967. pos1, pos2 = schemedit.sort_pos(pos1, pos2)
  968. schemedit.display_node_probs_region(user, pos1, pos2)
  969. return
  970. end
  971. end
  972. -- Otherwise, just display the region close to the player
  973. schemedit.display_node_probs_region(user)
  974. end
  975. end,
  976. on_secondary_use = function(itemstack, user, pointed_thing)
  977. local uname = user:get_player_name()
  978. if uname and not check_priv(uname) then
  979. return
  980. end
  981. schemedit.clear_displayed_node_probs(user)
  982. end,
  983. -- Set note probability and force_place and enable node probability display
  984. on_place = function(itemstack, placer, pointed_thing)
  985. local pname = placer:get_player_name()
  986. if pname and not check_priv(pname) then
  987. return
  988. end
  989. -- Use pointed node's on_rightclick function first, if present
  990. local node = minetest.get_node(pointed_thing.under)
  991. if placer and not placer:get_player_control().sneak then
  992. if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
  993. return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
  994. end
  995. end
  996. -- This sets the node probability of pointed node to the
  997. -- currently used probability stored in the tool.
  998. local pos = pointed_thing.under
  999. local node = minetest.get_node(pos)
  1000. -- Schematic void are ignored, they always have probability 0
  1001. if node.name == "schemedit:void" then
  1002. return itemstack
  1003. end
  1004. local nmeta = minetest.get_meta(pos)
  1005. local imeta = itemstack:get_meta()
  1006. local prob = tonumber(imeta:get_string("schemedit_prob"))
  1007. local force_place = imeta:get_string("schemedit_force_place")
  1008. if not prob or prob == 255 then
  1009. nmeta:set_string("schemedit_prob", nil)
  1010. else
  1011. nmeta:set_string("schemedit_prob", prob)
  1012. end
  1013. if force_place == "true" then
  1014. nmeta:set_string("schemedit_force_place", "true")
  1015. else
  1016. nmeta:set_string("schemedit_force_place", nil)
  1017. end
  1018. -- Enable node probablity display
  1019. schemedit.display_node_probs_region(placer)
  1020. return itemstack
  1021. end,
  1022. })
  1023. minetest.register_node("schemedit:void", {
  1024. description = S("Schematic Void"),
  1025. _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."),
  1026. _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."),
  1027. tiles = { "schemedit_void.png" },
  1028. drawtype = "nodebox",
  1029. is_ground_content = false,
  1030. paramtype = "light",
  1031. walkable = false,
  1032. sunlight_propagates = true,
  1033. node_box = {
  1034. type = "fixed",
  1035. fixed = {
  1036. { -4/16, -4/16, -4/16, 4/16, 4/16, 4/16 },
  1037. },
  1038. },
  1039. groups = { dig_immediate = 3},
  1040. })
  1041. -- [entity] Visible schematic border
  1042. minetest.register_entity("schemedit:display", {
  1043. visual = "upright_sprite",
  1044. textures = {"schemedit_border.png"},
  1045. visual_size = {x=10, y=10},
  1046. pointable = false,
  1047. physical = false,
  1048. static_save = false,
  1049. glow = minetest.LIGHT_MAX,
  1050. on_step = function(self, dtime)
  1051. if not self.id then
  1052. self.object:remove()
  1053. elseif not schemedit.markers[self.id] then
  1054. self.object:remove()
  1055. end
  1056. end,
  1057. on_activate = function(self)
  1058. self.object:set_armor_groups({immortal = 1})
  1059. end,
  1060. })
  1061. minetest.register_lbm({
  1062. label = "Reset schematic creator border entities",
  1063. name = "schemedit:reset_border",
  1064. nodenames = "schemedit:creator",
  1065. run_at_every_load = true,
  1066. action = function(pos, node)
  1067. local meta = minetest.get_meta(pos)
  1068. meta:set_string("schem_border", "false")
  1069. end,
  1070. })
  1071. local function add_suffix(schem)
  1072. -- Automatically add file name suffix if omitted
  1073. local schem_full, schem_lua
  1074. if string.sub(schem, string.len(schem)-3, string.len(schem)) == ".mts" then
  1075. schem_full = schem
  1076. schem_lua = string.sub(schem, 1, -5) .. ".lua"
  1077. else
  1078. schem_full = schem .. ".mts"
  1079. schem_lua = schem .. ".lua"
  1080. end
  1081. return schem_full, schem_lua
  1082. end
  1083. -- [chatcommand] Place schematic
  1084. minetest.register_chatcommand("placeschem", {
  1085. description = S("Place schematic at the position specified or the current player position (loaded from @1)", export_path_trunc),
  1086. privs = {server = true},
  1087. params = S("<schematic name>[.mts] [<x> <y> <z>]"),
  1088. func = function(name, param)
  1089. local schem, p = string.match(param, "^([^ ]+) *(.*)$")
  1090. local pos = minetest.string_to_pos(p)
  1091. if not schem then
  1092. return false, S("No schematic file specified.")
  1093. end
  1094. if not pos then
  1095. pos = minetest.get_player_by_name(name):get_pos()
  1096. end
  1097. local schem_full, schem_lua = add_suffix(schem)
  1098. local success = false
  1099. local schem_path = export_path_full .. DIR_DELIM .. schem_full
  1100. if minetest.read_schematic then
  1101. -- We don't call minetest.place_schematic with the path name directly because
  1102. -- this would trigger the caching and we wouldn't get any updates to the schematic
  1103. -- files when we reload. minetest.read_schematic circumvents that.
  1104. local schematic = minetest.read_schematic(schem_path, {})
  1105. if schematic then
  1106. success = minetest.place_schematic(pos, schematic, "random", nil, false)
  1107. end
  1108. else
  1109. -- Legacy support for Minetest versions that do not have minetest.read_schematic
  1110. success = minetest.place_schematic(schem_path, schematic, "random", nil, false)
  1111. end
  1112. if success == nil then
  1113. return false, S("Schematic file could not be loaded!")
  1114. else
  1115. return true
  1116. end
  1117. end,
  1118. })
  1119. if can_import then
  1120. -- [chatcommand] Convert MTS schematic file to .lua file
  1121. minetest.register_chatcommand("mts2lua", {
  1122. description = S("Convert .mts schematic file to .lua file (loaded from @1)", export_path_trunc),
  1123. privs = {server = true},
  1124. params = S("<schematic name>[.mts] [comments]"),
  1125. func = function(name, param)
  1126. local schem, comments_str = string.match(param, "^([^ ]+) *(.*)$")
  1127. if not schem then
  1128. return false, S("No schematic file specified.")
  1129. end
  1130. local comments = comments_str == "comments"
  1131. -- Automatically add file name suffix if omitted
  1132. local schem_full, schem_lua = add_suffix(schem)
  1133. local schem_path = export_path_full .. DIR_DELIM .. schem_full
  1134. local schematic = minetest.read_schematic(schem_path, {})
  1135. if schematic then
  1136. local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
  1137. local lua_path = export_path_full .. DIR_DELIM .. schem_lua
  1138. local file = io.open(lua_path, "w")
  1139. if file and str then
  1140. file:write(str)
  1141. file:flush()
  1142. file:close()
  1143. return true, S("Exported schematic to @1", lua_path)
  1144. else
  1145. return false, S("Failed!")
  1146. end
  1147. end
  1148. end,
  1149. })
  1150. end