init.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. --[[ Placeable Books by everamzah
  2. Copyright (C) 2016 James Stevenson
  3. LGPLv2.1+
  4. See LICENSE for more information ]]
  5. if not minetest.global_exists("books_placeable") then books_placeable = {} end
  6. books_placeable.modpath = minetest.get_modpath("books_placeable")
  7. -- Translation support
  8. local S = minetest.get_translator("books_placeable")
  9. local F = minetest.formspec_escape
  10. local lpp = 14 -- Lines per book's page
  11. local function copymeta(frommeta, tometa)
  12. local t = frommeta:to_table()
  13. -- Infotext can contain the whole text of the book, unencrypted.
  14. -- Do not leak it.
  15. t.fields.infotext = nil
  16. t.fields.description = nil
  17. tometa:from_table(t)
  18. end
  19. local function set_closed_infotext(nodemeta, itemmeta)
  20. local title = itemmeta:get_string("title")
  21. local owner = itemmeta:get_string("owner")
  22. owner = rename.gpn(owner)
  23. if title ~= "" and owner ~= "" then
  24. nodemeta:set_string("infotext", "\"" .. title .. "\"\nby <" .. owner .. ">")
  25. else
  26. nodemeta:set_string("infotext", "Book (Blank)")
  27. end
  28. -- Prevent leaks.
  29. nodemeta:set_string("description", "")
  30. end
  31. function books_placeable.set_closed_infotext(pos)
  32. local meta = minetest.get_meta(pos)
  33. set_closed_infotext(meta, meta)
  34. end
  35. local function set_open_infotext(meta)
  36. local iv = meta:get_string("iv")
  37. local enc = meta:get_string("text")
  38. -- We have to decrypt the text to show a few lines on mouse-over.
  39. if iv == "1" or #iv >= 16 then
  40. local dec
  41. if #iv >= 16 then
  42. dec = ossl.decrypt(iv, enc)
  43. else
  44. dec = ossl.decrypt(enc)
  45. end
  46. if dec then
  47. enc = dec
  48. end
  49. end
  50. meta:set_string("infotext", enc:sub(1, books.MAX_PREVIEW_LENGTH))
  51. end
  52. local function on_place(itemstack, placer, pointed_thing)
  53. if minetest.test_protection(pointed_thing.above, placer:get_player_name()) then
  54. return itemstack
  55. end
  56. -- Call 'on_rightclick' of pointed node.
  57. local pointed_on_rightclick = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name].on_rightclick
  58. if pointed_on_rightclick and not placer:get_player_control().sneak then
  59. return pointed_on_rightclick(pointed_thing.under, minetest.get_node(pointed_thing.under), placer, itemstack)
  60. end
  61. local data = itemstack:get_meta()
  62. local data_owner = data:get_string("owner")
  63. local stack = ItemStack({name = "books:book_closed"})
  64. if data and data_owner then
  65. copymeta(itemstack:get_meta(), stack:get_meta() )
  66. end
  67. local _, placed, pos = minetest.item_place_node(stack, placer, pointed_thing, nil)
  68. if placed then
  69. local meta = minetest.get_meta(pos)
  70. meta:mark_as_private({"text", "iv", "title", "owner", "page", "page_max", "description"})
  71. itemstack:take_item()
  72. end
  73. return itemstack
  74. end
  75. books_placeable.on_place = on_place
  76. local function after_place_node(pos, placer, itemstack, pointed_thing)
  77. local itemmeta = itemstack:get_meta()
  78. if itemmeta then
  79. local nodemeta = minetest.get_meta(pos)
  80. copymeta(itemmeta, nodemeta)
  81. nodemeta:mark_as_private({"text", "iv", "title", "owner", "page", "page_max", "description"})
  82. set_closed_infotext(nodemeta, itemmeta)
  83. end
  84. minetest.sound_play("book_slam", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  85. end
  86. books_placeable.after_place_node = after_place_node
  87. local function formspec_display(meta, player_name, pos, usertype)
  88. -- Courtesy of minetest_game/mods/default/craftitems.lua
  89. local title, text, owner, iv = "", "", player_name, ""
  90. local page, page_max, lines, string = 1, 1, {}, ""
  91. local datatable = meta:to_table().fields
  92. if datatable.owner then
  93. title = meta:get_string("title")
  94. text = meta:get_string("text")
  95. owner = meta:get_string("owner")
  96. iv = meta:get_string("iv")
  97. if #iv >= 16 then
  98. local dec = ossl.decrypt(iv, text)
  99. if dec then
  100. text = dec
  101. end
  102. elseif iv == "1" then
  103. local dec = ossl.decrypt(text)
  104. if dec then
  105. text = dec
  106. end
  107. end
  108. for str in (text .. "\n"):gmatch("([^\n]*)[\n]") do
  109. lines[#lines+1] = str
  110. end
  111. if datatable.page then
  112. page = datatable.page
  113. page_max = datatable.page_max
  114. for i = ((lpp * page) - lpp) + 1, lpp * page do
  115. if not lines[i] then break end
  116. string = string .. lines[i] .. "\n"
  117. end
  118. end
  119. end
  120. -- If player holds 'jump' control, they will get the user-facing formspec.
  121. local pref = minetest.get_player_by_name(player_name)
  122. if not pref then
  123. return
  124. end
  125. local control = pref:get_player_control()
  126. if usertype == "user" then
  127. control.jump = true
  128. end
  129. local formspec
  130. if owner == player_name and not control.jump then
  131. formspec = "size[8,8.3]" ..
  132. default.gui_bg ..
  133. default.gui_bg_img ..
  134. "field[0.5,1;7.5,0;title;"..F(S("Title:"))..";" ..
  135. F(title) .. "]" ..
  136. "textarea[0.5,1.5;7.5,7;text;"..F(S("Contents:"))..";" ..
  137. F(text) .. "]" ..
  138. "button_exit[2.5,7.5;3,1;save;"..F(S("Save")).."]"
  139. else
  140. formspec = "size[8,8.3]" ..
  141. default.gui_bg ..
  142. default.gui_bg_img ..
  143. "label[0.5,0.5;by <" .. rename.gpn(owner) .. ">]" ..
  144. "tablecolumns[color;text]" ..
  145. "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
  146. "table[0.4,0;7,0.5;title;#FFFF00," .. F(title) .. "]" ..
  147. "textarea[0.5,1.5;7.5,7;;" ..
  148. F(string ~= "" and string or text) .. ";]" ..
  149. "button[2.4,7.6;0.8,0.8;book_prev;<]" ..
  150. "label[3.2,7.7;"..F(S("Page @1 of @2", page, page_max)) .. "]" ..
  151. "button[4.9,7.6;0.8,0.8;book_next;>]"
  152. end
  153. local formname = "books:book_edit_"
  154. if control.jump then
  155. formname = "books:book_view_"
  156. end
  157. minetest.show_formspec(player_name,
  158. formname .. minetest.pos_to_string(pos), formspec)
  159. end
  160. books_placeable.formspec_display = formspec_display
  161. local function on_rightclick(pos, node, clicker, itemstack, pointed_thing)
  162. -- Safety check, get the REAL node instead of relying on function parameter.
  163. local node = minetest.get_node(pos)
  164. if node.name == "books:book_closed" then
  165. node.name = "books:book_open"
  166. minetest.swap_node(pos, node)
  167. local meta = minetest.get_meta(pos)
  168. set_open_infotext(meta)
  169. minetest.sound_play("book_open", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  170. elseif node.name == "books:book_open" then
  171. local player_name = clicker:get_player_name()
  172. local meta = minetest.get_meta(pos)
  173. formspec_display(meta, player_name, pos)
  174. end
  175. end
  176. books_placeable.on_rightclick = on_rightclick
  177. local function on_punch(pos, node, puncher, pointed_thing)
  178. -- Note: we must get the REAL node, because it might have dropped!
  179. local node = minetest.get_node(pos)
  180. if node.name == "books:book_open" then
  181. node.name = "books:book_closed"
  182. minetest.swap_node(pos, node)
  183. local meta = minetest.get_meta(pos)
  184. set_closed_infotext(meta, meta)
  185. minetest.sound_play("book_close", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  186. elseif node.name == "books:book_closed" then
  187. node.name = "books:book_open"
  188. minetest.swap_node(pos, node)
  189. local meta = minetest.get_meta(pos)
  190. set_open_infotext(meta)
  191. minetest.sound_play("book_open", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  192. end
  193. end
  194. books_placeable.on_punch = on_punch
  195. local function on_dig(pos, node, digger)
  196. if minetest.test_protection(pos, digger:get_player_name()) then
  197. return false
  198. end
  199. local nodemeta = minetest.get_meta(pos)
  200. if nodemeta:get_int("is_library_checkout") ~= 0 then
  201. local pname = nodemeta:get_string("checked_out_by")
  202. local title = nodemeta:get_string("title")
  203. if title == "" then
  204. title = "Untitled Book"
  205. end
  206. local pref = minetest.get_player_by_name(pname)
  207. if pref and vector.distance(pos, pref:get_pos()) < 32 then
  208. minetest.chat_send_player(pname, "# Server: \"" .. title .. "\" has been returned to the shelf.")
  209. end
  210. minetest.remove_node(pos)
  211. return true
  212. end
  213. local stack
  214. if nodemeta:get_string("owner") ~= "" then
  215. -- Manually set the stack description, because for security reasons the
  216. -- 'copymeta' func nils that.
  217. local owner = nodemeta:get_string("owner")
  218. local desc = nodemeta:get_string("title")
  219. -- Don't bother triming the title if the trailing dots would make it longer
  220. if #desc > books.SHORT_TITLE_SIZE + 3 then
  221. desc = desc:sub(1, books.SHORT_TITLE_SIZE) .. "..."
  222. end
  223. desc = "\"" .. desc .. "\" by <" .. rename.gpn(owner) .. ">"
  224. stack = ItemStack({name = "books:book_written"})
  225. copymeta(nodemeta, stack:get_meta() )
  226. stack:get_meta():set_string("description", desc)
  227. else
  228. stack = ItemStack({name = "books:book_blank"})
  229. end
  230. local adder = digger:get_inventory():add_item("main", stack)
  231. if adder then
  232. minetest.item_drop(adder, digger, digger:get_pos())
  233. end
  234. minetest.remove_node(pos)
  235. return true
  236. end
  237. books_placeable.on_dig = on_dig
  238. local function close_book(pos)
  239. local node = minetest.get_node(pos)
  240. local meta = minetest.get_meta(pos)
  241. if node.name == "books:book_open" then
  242. node.name = "books:book_closed"
  243. minetest.swap_node(pos, node)
  244. local meta = minetest.get_meta(pos)
  245. set_closed_infotext(meta, meta)
  246. minetest.sound_play("book_close", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  247. end
  248. end
  249. local function on_player_receive_fields(player, formname, fields)
  250. local formname2 = formname:sub(1, 16)
  251. if formname2 ~= "books:book_edit_" and formname2 ~= "books:book_view_" then
  252. return
  253. end
  254. if not player or not player:is_player() then
  255. return
  256. end
  257. local pname = player:get_player_name()
  258. if fields.save and fields.title ~= "" and fields.text ~= "" then
  259. local pos = minetest.string_to_pos(formname:sub(17))
  260. local node = minetest.get_node(pos)
  261. local meta = minetest.get_meta(pos)
  262. local text = fields.text:gsub("\r\n", "\n"):gsub("\r", "\n"):sub(1, books.MAX_TEXT_SIZE)
  263. local title = fields.title:sub(1, books.MAX_TITLE_SIZE)
  264. -- Security check. Player must be owner of book.
  265. local owner = meta:get_string("owner")
  266. if owner ~= "" and owner ~= pname then
  267. return
  268. end
  269. -- Book must be open.
  270. if node.name ~= "books:book_open" then
  271. return
  272. end
  273. if meta:get_int("is_library_checkout") ~= 0 then
  274. minetest.chat_send_player(pname, "# Server: Don't write on library property!")
  275. return
  276. end
  277. title = title:trim()
  278. text = text:trim()
  279. if title == "" then
  280. title = "Untitled"
  281. end
  282. local short_title = title
  283. -- Don't bother triming the title if the trailing dots would make it longer
  284. if #short_title > books.SHORT_TITLE_SIZE + 3 then
  285. short_title = short_title:sub(1, books.SHORT_TITLE_SIZE) .. "..."
  286. end
  287. local desc = "\"" .. short_title .. "\" by <" .. rename.gpn(pname) .. ">"
  288. -- Encrypt the text.
  289. local plaintext = text
  290. local iv = 0
  291. local enc = ossl.encrypt(text)
  292. if enc then
  293. text = enc
  294. iv = 1
  295. end
  296. meta:set_string("description", desc)
  297. meta:set_string("title", title)
  298. meta:set_string("text", text)
  299. meta:set_string("infotext", plaintext:sub(1, books.MAX_PREVIEW_LENGTH))
  300. meta:set_string("owner", pname)
  301. meta:set_int("page", 1)
  302. meta:set_int("page_max", math.ceil((text:gsub("[^\n]", ""):len() + 1) / lpp))
  303. meta:set_int("iv", iv)
  304. meta:mark_as_private({"text", "iv", "title", "owner", "page", "page_max", "description"})
  305. minetest.sound_play("book_write", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  306. minetest.after(1.5, function() close_book(pos) end)
  307. elseif fields.book_next or fields.book_prev then
  308. local pos = minetest.string_to_pos(formname:sub(17))
  309. local node = minetest.get_node(pos)
  310. local meta = minetest.get_meta(pos)
  311. if fields.book_next then
  312. meta:set_int("page", meta:get_int("page") + 1)
  313. if meta:get_int("page") > meta:get_int("page_max") then
  314. meta:set_int("page", 1)
  315. end
  316. minetest.sound_play("book_turn", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  317. elseif fields.book_prev then
  318. meta:set_int("page", meta:get_int("page") - 1)
  319. if meta:get_int("page") == 0 then
  320. meta:set_int("page", meta:get_int("page_max"))
  321. end
  322. minetest.sound_play("book_turn", {pos = pos, gain = 0.1, max_hear_distance = 16}, true)
  323. end
  324. formspec_display(meta, player:get_player_name(), pos, "user")
  325. elseif fields.quit then
  326. local pos = minetest.string_to_pos(formname:sub(17))
  327. minetest.after(0.5, function() close_book(pos) end)
  328. end
  329. end
  330. books_placeable.on_player_receive_fields = on_player_receive_fields
  331. -- This is technically a hack (not documented by the Minetest API), but we can
  332. -- chose what node name gets dropped inside this function, IN ADDITION to being
  333. -- able to set its metadata.
  334. function books_placeable.preserve_metadata(pos, oldnode, oldmeta, drops)
  335. if drops and drops[1] then
  336. -- Overwrite engine-borked 'oldmeta' with actual old meta.
  337. -- This is technically a hack. It relies on the fact that the engine has not
  338. -- deleted the old meta yet.
  339. local oldmeta = minetest.get_meta(pos):to_table()
  340. if oldmeta.fields.owner and oldmeta.fields.owner ~= "" then
  341. local stack = ItemStack("books:book_written")
  342. local newmeta = stack:get_meta()
  343. newmeta:from_table(oldmeta)
  344. drops[1] = stack
  345. else
  346. drops[1] = ItemStack("books:book_blank")
  347. end
  348. end
  349. end
  350. if not books_placeable.registered then
  351. minetest.override_item("books:book_blank", {
  352. on_place = function(...) return books_placeable.on_place(...) end,
  353. })
  354. minetest.override_item("books:book_written", {
  355. on_place = function(...) return books_placeable.on_place(...) end,
  356. })
  357. -- For books:book_open, books:book_written_open
  358. minetest.register_node(":books:book_open", {
  359. description = S("Book Open"),
  360. inventory_image = "default_book.png",
  361. tiles = {
  362. "books_book_open_top.png", -- Top
  363. "books_book_open_bottom.png", -- Bottom
  364. "books_book_open_side.png", -- Right
  365. "books_book_open_side.png", -- Left
  366. "books_book_open_front.png", -- Back
  367. "books_book_open_front.png" -- Front
  368. },
  369. drawtype = "nodebox",
  370. paramtype = "light",
  371. paramtype2 = "facedir",
  372. sunlight_propagates = true,
  373. walkable = false,
  374. node_box = {
  375. type = "fixed",
  376. fixed = {
  377. {-0.375, -0.47, -0.282, 0.375, -0.4125, 0.282}, -- Top
  378. {-0.4375, -0.5, -0.3125, 0.4375, -0.47, 0.3125},
  379. }
  380. },
  381. sounds = {
  382. dig = {name="default_silence", gain=1.0},
  383. },
  384. -- Can only have 1, single drop.
  385. -- Will be overridden inside 'preserve_metadata'!
  386. drop = 'books:book_open',
  387. -- Must use 'bigitem' group otherwise books cannot be closed by punching,
  388. -- because they would simply be dug instantly instead.
  389. groups = utility.dig_groups("bigitem", {attached_node = 3}),
  390. on_punch = function(...) return books_placeable.on_punch(...) end,
  391. on_rightclick = function(...) return books_placeable.on_rightclick(...) end,
  392. on_dig = function(...) return books_placeable.on_dig(...) end,
  393. preserve_metadata = function(...) return books_placeable.preserve_metadata(...) end,
  394. on_rotate = screwdriver.disallow,
  395. })
  396. -- For books:book_closed, books:book_written_closed.
  397. minetest.register_node(":books:book_closed", {
  398. description = S("Book Closed"),
  399. inventory_image = "default_book.png",
  400. tiles = {
  401. "books_book_closed_topbottom.png", -- Top
  402. "books_book_closed_topbottom.png", -- Bottom
  403. "books_book_closed_right.png", -- Right
  404. "books_book_closed_left.png", -- Left
  405. "books_book_closed_front.png^[transformFX", -- Back
  406. "books_book_closed_front.png" -- Front
  407. },
  408. drawtype = "nodebox",
  409. paramtype = "light",
  410. paramtype2 = "facedir",
  411. sunlight_propagates = true,
  412. walkable = false,
  413. node_box = {
  414. type = "fixed",
  415. fixed = {
  416. {-0.25, -0.5, -0.3125, 0.25, -0.35, 0.3125},
  417. }
  418. },
  419. sounds = {
  420. dig = {name="default_silence", gain=1.0},
  421. },
  422. -- Can only have 1, single drop.
  423. -- Will be overridden inside 'preserve_metadata'!
  424. drop = 'books:book_closed',
  425. groups = utility.dig_groups("bigitem", {attached_node = 3}),
  426. on_dig = function(...) return books_placeable.on_dig(...) end,
  427. on_rightclick = function(...) return books_placeable.on_rightclick(...) end,
  428. on_punch = function(...) return books_placeable.on_punch(...) end,
  429. after_place_node = function(...) return books_placeable.after_place_node(...) end,
  430. preserve_metadata = function(...) return books_placeable.preserve_metadata(...) end,
  431. on_rotate = screwdriver.disallow,
  432. })
  433. minetest.register_on_player_receive_fields(function(...)
  434. books_placeable.on_player_receive_fields(...) end)
  435. minetest.register_alias("default:book_closed", "books:book_closed")
  436. minetest.register_alias("default:book_open", "books:book_open")
  437. local c = "books_placeable:core"
  438. local f = books_placeable.modpath .. "/init.lua"
  439. reload.register_file(c, f, false)
  440. books_placeable.registered = true
  441. end