init.lua 14 KB

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