init.lua 33 KB

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