init.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. -- TODO:
  2. -- There's potential race conditions in here if two players have the board open
  3. -- and a culling happens or they otherwise diddle around with it. For now just
  4. -- make sure it doesn't crash
  5. local S = minetest.get_translator(minetest.get_current_modname())
  6. local bulletin_max = 7*8
  7. local culling_interval = 86400 -- one day in seconds
  8. local culling_min = bulletin_max - 12 -- won't cull if there are this many or fewer bulletins
  9. local bulletin_boards = {}
  10. bulletin_boards.player_state = {}
  11. bulletin_boards.board_def = {}
  12. local path = minetest.get_worldpath() .. "/bulletin_boards.lua"
  13. local f, e = loadfile(path);
  14. if f then
  15. bulletin_boards.global_boards = f()
  16. else
  17. bulletin_boards.global_boards = {}
  18. end
  19. local function save_boards()
  20. local file, e = io.open(path, "w");
  21. if not file then
  22. return error(e);
  23. end
  24. file:write(minetest.serialize(bulletin_boards.global_boards))
  25. file:close()
  26. end
  27. local max_text_size = 5000 -- half a book
  28. local max_title_size = 60
  29. local short_title_size = 12
  30. -- gets the bulletins currently on a board
  31. -- and other persisted data
  32. local function get_board(name)
  33. local board = bulletin_boards.global_boards[name]
  34. if board then
  35. return board
  36. end
  37. board = {}
  38. board.last_culled = minetest.get_gametime()
  39. bulletin_boards.global_boards[name] = board
  40. return board
  41. end
  42. -- for incrementing through the bulletins on a board
  43. local function find_next(board, start_index)
  44. local index = start_index + 1
  45. while index ~= start_index do
  46. if board[index] then
  47. return index
  48. end
  49. index = index + 1
  50. if index > bulletin_max then
  51. index = 1
  52. end
  53. end
  54. return index
  55. end
  56. local function find_prev(board, start_index)
  57. local index = start_index - 1
  58. while index ~= start_index do
  59. if board[index] then
  60. return index
  61. end
  62. index = index - 1
  63. if index < 1 then
  64. index = bulletin_max
  65. end
  66. end
  67. return index
  68. end
  69. -- Groups bulletins by count-per-player, then picks the oldest bulletin from the group with the highest count.
  70. -- eg, if A has 1 bulletin, B has 2 bulletins, and C has 2 bulletins, then this will pick the oldest
  71. -- bulletin from (B and C)'s bulletins. Returns index and timestamp, or nil if there's nothing.
  72. local function find_most_cullable(board_name)
  73. local board = get_board(board_name)
  74. local player_count = {}
  75. local max_count = 0
  76. local total = 0
  77. for i = 1, bulletin_max do
  78. local bulletin = board[i]
  79. if bulletin then
  80. total = total + 1
  81. local player_name = bulletin.owner
  82. local count = (player_count[player_name] or 0) + 1
  83. max_count = math.max(count, max_count)
  84. player_count[player_name] = count
  85. end
  86. end
  87. if total <= culling_min then
  88. return
  89. end
  90. local max_players = {}
  91. for player_name, count in pairs(player_count) do
  92. if count == max_count then
  93. max_players[player_name] = true
  94. end
  95. end
  96. local most_cullable_index
  97. local most_cullable_timestamp
  98. for i = 1, bulletin_max do
  99. local bulletin = board[i]
  100. if bulletin and max_players[bulletin.owner] then
  101. if bulletin.timestamp <= (most_cullable_timestamp or bulletin.timestamp) then
  102. most_cullable_timestamp = bulletin.timestamp
  103. most_cullable_index = i
  104. end
  105. end
  106. end
  107. return most_cullable_index, most_cullable_timestamp
  108. end
  109. -- safe way to get the description string of an item, in case it's not registered
  110. local function get_item_desc(stack)
  111. local stack_def = stack:get_definition()
  112. if stack_def then
  113. return stack_def.description
  114. end
  115. return stack:get_name()
  116. end
  117. -- shows the base board to a player
  118. local function show_board(player_name, board_name)
  119. local formspec = {}
  120. local board = get_board(board_name)
  121. local current_time = minetest.get_gametime()
  122. local intervals = (current_time - board.last_culled)/culling_interval
  123. local cull_count, remaining_cull_time = math.modf(intervals)
  124. while cull_count > 0 do
  125. local cull_index = find_most_cullable(board_name)
  126. if cull_index then
  127. board[cull_index] = nil
  128. cull_count = cull_count - 1
  129. else
  130. cull_count = 0
  131. end
  132. end
  133. board.last_culled = current_time - math.floor(culling_interval * remaining_cull_time)
  134. local def = bulletin_boards.board_def[board_name]
  135. local desc = minetest.formspec_escape(def.desc)
  136. local tip
  137. if def.cost then
  138. local stack = ItemStack(def.cost)
  139. tip = S("Post your bulletin here for the cost of @1 @2", stack:get_count(), get_item_desc(stack))
  140. desc = desc .. S(", Cost: @1 @2", stack:get_count(), get_item_desc(stack))
  141. else
  142. tip = S("Post your bulletin here")
  143. end
  144. formspec[#formspec+1] = "size[8,8.5]"
  145. .. "container[0,0]"
  146. .. "label[0.0,-0.25;"..desc.."]"
  147. .. "container_end[]"
  148. .. "container[0,0.5]"
  149. local i = 0
  150. for y = 0, 6 do
  151. for x = 0, 7 do
  152. i = i + 1
  153. local bulletin = board[i] or {}
  154. local short_title = bulletin.title or ""
  155. --Don't bother triming the title if the trailing dots would make it longer
  156. if #short_title > short_title_size + 3 then
  157. short_title = short_title:sub(1, short_title_size) .. "..."
  158. end
  159. local img = bulletin.icon or ""
  160. formspec[#formspec+1] =
  161. "image_button["..x..",".. y*1.2 ..";1,1;"..img..";button_"..i..";]"
  162. .."label["..x..","..y*1.2-0.35 ..";"..minetest.formspec_escape(short_title).."]"
  163. if bulletin.title and bulletin.owner and bulletin.timestamp then
  164. local days_ago = math.floor((current_time-bulletin.timestamp)/86400)
  165. formspec[#formspec+1] = "tooltip[button_"..i..";"
  166. ..S("@1\nPosted by @2\n@3 days ago", minetest.formspec_escape(bulletin.title), bulletin.owner, days_ago).."]"
  167. else
  168. formspec[#formspec+1] = "tooltip[button_"..i..";"..tip.."]"
  169. end
  170. end
  171. end
  172. formspec[#formspec+1] = "container_end[]"
  173. bulletin_boards.player_state[player_name] = {board=board_name}
  174. minetest.show_formspec(player_name, "bulletin_boards:board", table.concat(formspec))
  175. end
  176. -- shows a specific bulletin on a board
  177. local function show_bulletin(player, board_name, index)
  178. local board = get_board(board_name)
  179. local def = bulletin_boards.board_def[board_name]
  180. local icons = def.icons
  181. local bulletin = board[index] or {}
  182. local player_name = player:get_player_name()
  183. bulletin_boards.player_state[player_name] = {board=board_name, index=index}
  184. local tip
  185. local has_cost
  186. if def.cost then
  187. local stack = ItemStack(def.cost)
  188. local player_inventory = minetest.get_inventory({type="player", name=player_name})
  189. tip = S("Post bulletin with this icon at the cost of @1 @2", stack:get_count(), get_item_desc(stack))
  190. has_cost = player_inventory:contains_item("main", stack)
  191. else
  192. tip = S("Post bulletin with this icon")
  193. has_cost = true
  194. end
  195. local admin = minetest.check_player_privs(player, "server")
  196. local formspec = {"size[8,8]"
  197. .."button[0.2,0;1,1;prev;"..S("Prev").."]"
  198. .."button[6.65,0;1,1;next;"..S("Next").."]"}
  199. local esc = minetest.formspec_escape
  200. if ((bulletin.owner == nil or bulletin.owner == player_name) and has_cost) or admin then
  201. formspec[#formspec+1] =
  202. "field[1.5,0.75;5.5,0;title;"..S("Title:")..";"..esc(bulletin.title or "").."]"
  203. .."textarea[0.5,1.15;7.5,7;text;"..S("Contents:")..";"..esc(bulletin.text or "").."]"
  204. .."label[0.3,7;"..S("Post:").."]"
  205. for i, icon in ipairs(icons) do
  206. formspec[#formspec+1] = "image_button[".. i*0.75-0.5 ..",7.35;1,1;"..icon..";save_"..i..";]"
  207. .."tooltip[save_"..i..";"..tip.."]"
  208. end
  209. formspec[#formspec+1] = "image_button[".. (#icons+1)*0.75-0.25 ..",7.35;1,1;bulletin_boards_delete.png;delete;]"
  210. .."tooltip[delete;"..S("Delete this bulletin").."]"
  211. .."label["..(#icons+1)*0.75-0.25 ..",7;"..S("Delete:").."]"
  212. elseif bulletin.owner then
  213. formspec[#formspec+1] =
  214. "label[1.4,0.5;"..S("Posted by @1", bulletin.owner).."]"
  215. .."tablecolumns[color;text]"
  216. .."tableoptions[background=#00000000;highlight=#00000000;border=false]"
  217. .."table[1.35,0.25;5,0.5;title;#FFFF00,"..esc(bulletin.title or "").."]"
  218. .."textarea[0.5,1.5;7.5,7;;"..esc(bulletin.text or "")..";]"
  219. .."button[2.5,7.5;3,1;back;" .. S("Back to Board") .. "]"
  220. if bulletin.owner == player_name then
  221. formspec[#formspec+1] = "image_button[".. (#icons+1)*0.75-0.25 ..",7.35;1,1;bulletin_boards_delete.png;delete;]"
  222. .."tooltip[delete;"..S("Delete this bulletin").."]"
  223. .."label["..(#icons+1)*0.75-0.25 ..",7;"..S("Delete:").."]"
  224. end
  225. else
  226. return
  227. end
  228. minetest.show_formspec(player_name, "bulletin_boards:bulletin", table.concat(formspec))
  229. end
  230. -- interpret clicks on the base board
  231. minetest.register_on_player_receive_fields(function(player, formname, fields)
  232. if formname ~= "bulletin_boards:board" then return end
  233. local player_name = player:get_player_name()
  234. for field, state in pairs(fields) do
  235. if field:sub(1, #"button_") == "button_" then
  236. local i = tonumber(field:sub(#"button_"+1))
  237. local state = bulletin_boards.player_state[player_name]
  238. if state then
  239. show_bulletin(player, state.board, i)
  240. end
  241. return
  242. end
  243. end
  244. end)
  245. -- interpret clicks on the bulletin
  246. minetest.register_on_player_receive_fields(function(player, formname, fields)
  247. if formname ~= "bulletin_boards:bulletin" then return end
  248. local player_name = player:get_player_name()
  249. local state = bulletin_boards.player_state[player_name]
  250. if not state then return end
  251. local board = get_board(state.board)
  252. local def = bulletin_boards.board_def[state.board]
  253. if not board then return end
  254. -- no security needed on these actions
  255. if fields.back then
  256. bulletin_boards.player_state[player_name] = nil
  257. show_board(player_name, state.board)
  258. end
  259. if fields.prev then
  260. local next_index = find_prev(board, state.index)
  261. show_bulletin(player, state.board, next_index)
  262. return
  263. end
  264. if fields.next then
  265. local next_index = find_next(board, state.index)
  266. show_bulletin(player, state.board, next_index)
  267. return
  268. end
  269. if fields.quit then
  270. minetest.after(0.1, show_board, player_name, state.board)
  271. end
  272. -- check if the player's allowed to do the stuff after this
  273. local admin = minetest.check_player_privs(player, "server")
  274. local current_bulletin = board[state.index]
  275. if not admin and (current_bulletin and current_bulletin.owner ~= player_name) then
  276. -- someone's done something funny. Don't be accusatory, though - could be a race condition
  277. return
  278. end
  279. if fields.delete then
  280. board[state.index] = nil
  281. fields.title = ""
  282. save_boards()
  283. end
  284. local player_inventory = minetest.get_inventory({type="player", name=player_name})
  285. local has_cost = true
  286. if def.cost then
  287. has_cost = player_inventory:contains_item("main", def.cost)
  288. end
  289. if fields.text ~= "" and (has_cost or admin) then
  290. for field, _ in pairs(fields) do
  291. if field:sub(1, #"save_") == "save_" then
  292. local i = tonumber(field:sub(#"save_"+1))
  293. local bulletin = {}
  294. bulletin.owner = player_name
  295. bulletin.title = fields.title:sub(1, max_title_size)
  296. bulletin.text = fields.text:sub(1, max_text_size)
  297. bulletin.icon = def.icons[i]
  298. bulletin.timestamp = minetest.get_gametime()
  299. board[state.index] = bulletin
  300. if not admin and def.cost then
  301. player_inventory:remove_item("main", def.cost)
  302. end
  303. save_boards()
  304. break
  305. end
  306. end
  307. end
  308. bulletin_boards.player_state[player_name] = nil
  309. show_board(player_name, state.board)
  310. end)
  311. -- default icon set
  312. local base_icons = {
  313. "bulletin_boards_document_comment_above.png",
  314. "bulletin_boards_document_back.png",
  315. "bulletin_boards_document_next.png",
  316. "bulletin_boards_document_image.png",
  317. "bulletin_boards_document_signature.png",
  318. "bulletin_boards_to_do_list.png",
  319. "bulletin_boards_documents_email.png",
  320. "bulletin_boards_receipt_invoice.png",
  321. }
  322. -- generates a random jumble of icons to superimpose on a bulletin board texture
  323. -- rez is the "working" canvas size. 32-pixel icons get scattered on that canvas
  324. -- before it is scaled down to 16 pixels
  325. local function generate_random_board(rez, count, icons)
  326. icons = icons or base_icons
  327. local tex = {"([combine:"..rez.."x"..rez}
  328. for i = 1, count do
  329. tex[#tex+1] = ":"..math.random(1,rez-32)..","..math.random(1,rez-32)
  330. .."="..icons[math.random(1,#icons)]
  331. end
  332. tex[#tex+1] = "^[resize:16x16)"
  333. return table.concat(tex)
  334. end
  335. local function register_board(board_name, board_def)
  336. bulletin_boards.board_def[board_name] = board_def
  337. local background = board_def.background or "bulletin_boards_corkboard.png"
  338. local foreground = board_def.foreground or "bulletin_boards_frame.png"
  339. local tile = background.."^"..generate_random_board(98, 7, board_def.icons).."^"..foreground
  340. local bulletin_board_def = {
  341. description = board_def.desc,
  342. groups = {choppy=1},
  343. tiles = {tile},
  344. inventory_image = tile,
  345. paramtype = "light",
  346. paramtype2 = "wallmounted",
  347. sunlight_propagates = true,
  348. drawtype = "nodebox",
  349. node_box = {
  350. type = "wallmounted",
  351. wall_top = {-0.5, 0.4375, -0.5, 0.5, 0.5, 0.5},
  352. wall_bottom = {-0.5, -0.5, -0.5, 0.5, -0.4375, 0.5},
  353. wall_side = {-0.5, -0.5, -0.5, -0.4375, 0.5, 0.5},
  354. },
  355. on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
  356. local player_name = clicker:get_player_name()
  357. show_board(player_name, board_name)
  358. end,
  359. on_construct = function(pos)
  360. local meta = minetest.get_meta(pos)
  361. meta:set_string("infotext", board_def.desc or "")
  362. end,
  363. }
  364. minetest.register_node(board_name, bulletin_board_def)
  365. end
  366. if minetest.get_modpath("default") then
  367. register_board("bulletin_boards:bulletin_board_basic", {
  368. desc = S("Public Bulletin Board"),
  369. cost = "default:paper",
  370. icons = base_icons,
  371. })
  372. minetest.register_craft({
  373. output = "bulletin_boards:bulletin_board_basic",
  374. recipe = {
  375. {'group:wood', 'group:wood', 'group:wood'},
  376. {'group:wood', 'default:paper', 'group:wood'},
  377. {'group:wood', 'group:wood', 'group:wood'},
  378. },
  379. })
  380. register_board("bulletin_boards:bulletin_board_copper", {
  381. desc = S("Copper Board"),
  382. cost = "default:copper_ingot",
  383. foreground = "bulletin_boards_frame_copper.png",
  384. icons = base_icons,
  385. })
  386. minetest.register_craft({
  387. output = "bulletin_boards:bulletin_board_copper",
  388. recipe = {
  389. {"default:copper_ingot", "default:copper_ingot", "default:copper_ingot"},
  390. {"default:copper_ingot", 'default:paper', "default:copper_ingot"},
  391. {"default:copper_ingot", "default:copper_ingot", "default:copper_ingot"},
  392. },
  393. })
  394. end