init.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. -- Elemental mod for Minetest
  2. -- Copyright ©2019 Alex Yst <mailto:copyright@y.st>
  3. -- This program is free software; you can redistribute it and/or
  4. -- modify it under the terms of the GNU Lesser General Public
  5. -- License as published by the Free Software Foundation; either
  6. -- version 2.1 of the License, or (at your option) any later version.
  7. -- This software is distributed in the hope that it will be useful,
  8. -- but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  10. -- Lesser General Public License for more details.
  11. -- You should have received a copy of the GNU Lesser General Public
  12. -- License along with this program. If not, see
  13. -- <https://www.gnu.org./licenses/>.
  14. local storage = minetest.get_mod_storage()
  15. -- Conversion tables
  16. local to_element, to_index, palette_colour = dofile(minetest.get_modpath("elemental").."/elements.lua")
  17. -- We need to keep track of which crystal a player is using, so the
  18. -- deposit/withdrawal formspec can be used properly.
  19. local using_crystal = {}
  20. -- Storing integer metadata in Minetest is sort of interesting. From
  21. -- what I can tell, Lua's floats are cast into integers (probably by
  22. -- C), then truncated by C into 32 bits (signed). From there, they get
  23. -- cast into strings. The process might not be exactly as I describe
  24. -- here, as I'm not sure where in the code it is happening, and only
  25. -- have the data I've gotten through experimenting. Anyway, the exact
  26. -- details of how and why it happen aren't important. What's important
  27. -- is *what actually ends up done, and the above description gets close
  28. -- enough to understand the implications of the process. Essentially,
  29. -- we're working with 32-bit signed integers even though we're not
  30. -- allowed to use integers directly. That means that if we add too
  31. -- much, we're going to go negative,due to integer overflow. *Way*
  32. -- negative. To avoid that, we either can't rely on Minetest's integer
  33. -- storage mechanism or we have to put a cap on the input so it never
  34. -- exceed the maximum 32-bit signed integer and overflows.
  35. --
  36. -- We could roll our own integer representation. Liblevelup has code
  37. -- for that as far as its own data, proving that it's more than
  38. -- feasible. However, for now, I think staying within the bounds of
  39. -- what is basically a 3-bit unsigned integer - as we never want to go
  40. -- negative - is just fine as well. Honestly, that still allows you to
  41. -- store more than you could in 1024 chests in the worst case and more
  42. -- than you could store in 677867 chests in the common case. Even with
  43. -- putting a cap on the amount we can store, trying to actually exceed
  44. -- that maximum value is an unrealistic goal. If for some reason you
  45. -- you think you should be allowed to store more than this in each
  46. -- elemental crystal, by all means, get in touch and let me know. I
  47. -- have two higher caps we can work with if the need should arise, and
  48. -- if those don't work for you either, as I mentioned before, I have a
  49. -- way to remove the cap entirely. It'll just take a little more
  50. -- processing power from the server. But until someone complains about
  51. -- the cap, I'm going to assume no one needs to store more than 31 bits
  52. -- worth of material.
  53. local max_storage = 2^31-1
  54. -- This function drops a stack into a player's inventory. It's used to
  55. -- give players elemental crystals, to withdraw items from crystals,
  56. -- and to prevent deletion of items in case of error.
  57. --
  58. -- The implementation of this function might need work. This isn't
  59. -- exactly the semantically correct way to do this.
  60. local function give_to_player(player, item)
  61. minetest.handle_node_drops(player:get_pos(), {
  62. item,
  63. }, player)
  64. end
  65. local function send_formspec(player_name)
  66. local meta = minetest.get_meta(using_crystal[player_name])
  67. formspec = "\
  68. size[8,9]\
  69. label[0,0;"..(meta:get_string("infotext") or "").."]\
  70. label[2,1;Currently in storage: "..meta:get_int("count").."]\
  71. list[current_player;main;0,4.85;8,1;]\
  72. list[current_player;main;0,6.08;8,3;8]\
  73. listring[current_player;main]\
  74. listring[detached:elemental;input]\
  75. button[1,1.5;3,1;withdraw;Withdraw maximum]\
  76. button[1,2.5;3,1;withdraw;Withdraw stack]\
  77. button[1,3.5;3,1;withdraw;Withdraw count]\
  78. item_image[5,2;1,1;"..to_element[minetest.get_node(using_crystal[player_name]).param2].."]\
  79. field[4.3,4;3,1;count;Count:;1]"
  80. ..default.get_hotbar_bg(0,4.85)
  81. minetest.show_formspec(player_name, "elemental:0", formspec)
  82. end
  83. -- We can't always fit a full withdrawal into the player's inventory,
  84. -- so this function does the work of fitting what it can, and only
  85. -- removing from storage what was actually given to the player.
  86. local function withdraw(player, attempted_count)
  87. local player_name = player:get_player_name()
  88. if using_crystal[player_name] then
  89. local node = minetest.get_node_or_nil(using_crystal[player_name])
  90. if node then
  91. local element = to_element[node.param2]
  92. local item_name = minetest.registered_items.unknown.description
  93. if minetest.registered_items[element]
  94. and minetest.registered_items[element].description then
  95. item_name = minetest.registered_items[element].description
  96. end
  97. local meta = minetest.get_meta(using_crystal[player_name])
  98. local count
  99. -- A special value to take it all.
  100. if attempted_count == "all" then
  101. attempted_count = meta:get_int("count")
  102. count = attempted_count
  103. -- A special value to take a stack.
  104. elseif attempted_count == "stack" then
  105. local stack_max
  106. if minetest.registered_items[element] then
  107. stack_max = minetest.registered_items[element].stack_max
  108. else
  109. stack_max = minetest.registered_items.unknown.stack_max
  110. end
  111. attempted_count = stack_max
  112. -- You can't take a full stack unless you have a full stack in storage.
  113. count = math.min(stack_max, meta:get_int("count"))
  114. elseif attempted_count > 0 then
  115. -- You can't take more than the crystal has.
  116. count = math.min(attempted_count, meta:get_int("count"))
  117. else
  118. -- Withdrawing negative amounts isn't implemented. I wasn't sure if
  119. -- this special case needed to be checked for or if the code would just
  120. -- do nothing when given a negative number, so I ran a test to find
  121. -- out. As it turns out, allowing negative numbers will add to the
  122. -- amount stored in the crystal while not taking anything from the
  123. -- player. The player can simply mint their own items, and withdraw
  124. -- them when they're done. To avoid this, we simply disallow negative
  125. -- values.
  126. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], 'Negative withdrawals are not supported. Please use deposits instead. To deposit, hold "shift" and click on a stack of '..item_name..'s in your inventory.'))
  127. return
  128. end
  129. local stack = ItemStack(element)
  130. local withdrawn = 0
  131. local inv = player:get_inventory()
  132. while count ~= 0 do
  133. if count < 65535 then
  134. stack:set_count(count)
  135. withdrawn = withdrawn + count
  136. count = 0
  137. else
  138. stack:set_count(65535)
  139. count = count - 65535
  140. withdrawn = withdrawn + 65535
  141. end
  142. remainder = inv:add_item("main", stack)
  143. if remainder:get_count() ~= 0 then
  144. withdrawn = withdrawn - remainder:get_count()
  145. count = 0
  146. end
  147. end
  148. meta:set_int("count", meta:get_int("count") - withdrawn)
  149. send_formspec(player_name)
  150. if withdrawn == attempted_count then
  151. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "You withdrew "..withdrawn.." "..item_name.."s."))
  152. else
  153. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "You tried to withdraw "..attempted_count.." "..item_name.."s, but were only able to withdraw "..withdrawn.."."))
  154. end
  155. end
  156. end
  157. end
  158. local magic_input_inv = minetest.create_detached_inventory("elemental", {
  159. allow_put = function(inv, listname, index, stack, player)
  160. local player_name = player:get_player_name()
  161. if not using_crystal[player_name] then
  162. return 0
  163. end
  164. local node = minetest.get_node_or_nil(using_crystal[player_name])
  165. local meta = minetest.get_meta(using_crystal[player_name])
  166. local infotext = meta:get_string("infotext")
  167. if not node then
  168. minetest.chat_send_player(player_name, "The crystal you're trying to deposit into is out of range.")
  169. elseif not infotext then
  170. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "The crystal you're attempting to deposit into appears to be damaged, and is unable to accept items."))
  171. end
  172. local owner = infotext:split("'")[1]
  173. if owner ~= player_name then
  174. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "This crystal doesn't belong to you."))
  175. return 0
  176. end
  177. local item_name = minetest.registered_items.unknown.description
  178. local element = to_element[node.param2]
  179. if minetest.registered_items[element]
  180. and minetest.registered_items[element].description then
  181. item_name = minetest.registered_items[element].description
  182. end
  183. if stack:get_name() ~= element then
  184. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "Only "..item_name.."s can be deposited into this crystal."))
  185. return 0
  186. end
  187. local count = stack:get_count()
  188. if max_storage >= count + meta:get_int("count") then
  189. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "You deposited "..count.." "..item_name.."s."))
  190. return -1
  191. else
  192. minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "The crystal is too full to accept that many "..item_name.."s."))
  193. return 0
  194. end
  195. end,
  196. allow_take = function()
  197. return 0
  198. end,
  199. on_put = function(inv, listname, index, stack, player)
  200. local player_name = player:get_player_name()
  201. if not using_crystal[player_name] then
  202. give_to_player(player, stack)
  203. end
  204. local meta = minetest.get_meta(using_crystal[player_name])
  205. meta:set_int("count", meta:get_int("count") + stack:get_count())
  206. send_formspec(player_name)
  207. end,
  208. })
  209. magic_input_inv:set_size("input", 1)
  210. minetest.register_node("elemental:0", {
  211. stack_max = 65535,
  212. drawtype = "mesh",
  213. mesh = "elemental-pillar.obj",
  214. description = "Elemental Crystal",
  215. paramtype = "light",
  216. tiles = {
  217. {
  218. name = "default_mossycobble.png",
  219. color = "#fff",
  220. },
  221. "elemental-crystal.png",
  222. },
  223. light_source = LIGHT_MAX,
  224. paramtype2 = "color",
  225. palette = "elemental-palette.png",
  226. groups = {cracky = 3},
  227. can_dig = function(pos, player)
  228. local meta = minetest.get_meta(pos)
  229. local infotext = meta:get_string("infotext")
  230. if not player then
  231. -- Non-players that ask permission to dig the node are denied it. This
  232. -- doesn't protect against all non-player situations though, as most
  233. -- non-players won't ask permission first.
  234. return false
  235. elseif infotext then
  236. local owner = infotext:split("'")[1]
  237. return owner == player:get_player_name() and meta:get_int("count") == 0
  238. else
  239. -- If a mod corrupts the pillar and removes the owner information, we
  240. -- allow all players to remove the pillar. Otherwise, no one would be
  241. -- allowed to remove it, and it'd be stuck on the map permanently.
  242. return meta:get_int("count") == 0
  243. end
  244. end,
  245. selection_box = {
  246. type = "fixed",
  247. fixed = {
  248. {-0.125, -0.5, -0.125, 0.125, 0, 0.125},
  249. },
  250. },
  251. collision_box = {
  252. type = "fixed",
  253. fixed = {
  254. {-0.125, -0.5, -0.125, 0.125, 0, 0.125},
  255. },
  256. },
  257. after_place_node = function(pos, placer, itemstack, pointed_thing)
  258. local element = to_element[minetest.get_node(pos).param2]
  259. local infotext
  260. if minetest.registered_items[element] and minetest.registered_items[element].description then
  261. infotext = placer:get_player_name().."'s "..minetest.registered_items[element].description.." Elemental Crystal"
  262. else
  263. infotext = placer:get_player_name().."' Elemental Pillar"
  264. end
  265. local meta = minetest.get_meta(pos)
  266. meta:set_string("infotext", infotext)
  267. end,
  268. on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
  269. if clicker then
  270. local owner = minetest.get_meta(pos):get_string("infotext"):split("'")[1]
  271. using_crystal[clicker:get_player_name()] = pos
  272. send_formspec(clicker:get_player_name())
  273. end
  274. end,
  275. on_blast = function() end,
  276. after_dig_node = function(pos, oldnode, oldmetadata, digger)
  277. if oldmetadata.fields.infotext then
  278. using_crystal[oldmetadata.fields.infotext:split("'")[1]] = nil
  279. end
  280. end,
  281. })
  282. liblevelup.register.update_function(function(digger, drops)
  283. for _, drop in next, drops do
  284. local element = ItemStack(drop):get_name()
  285. if to_index[element] and to_index[element] < 26
  286. and minetest.registered_items[element] then
  287. local player_name = digger:get_player_name()
  288. local key = player_name..";"..to_index[element]
  289. local stat = liblevelup.get.drop_stat(player_name, element)
  290. local next_at = (storage:get_int(key)+1)*41*minetest.registered_items[element].stack_max
  291. if stat >= next_at then
  292. storage:set_int(key, storage:get_int(key)+1)
  293. give_to_player(digger, minetest.itemstring_with_palette("elemental:0", to_index[element]))
  294. end
  295. end
  296. end
  297. end)
  298. minetest.register_on_player_receive_fields(function(player, formname, fields)
  299. if formname == "elemental:0" and fields.withdraw then
  300. if fields.withdraw == "Withdraw maximum" then
  301. withdraw(player, "all")
  302. elseif fields.withdraw == "Withdraw stack" then
  303. withdraw(player, "stack")
  304. else
  305. -- only integers are allowed.
  306. local count = tonumber(fields.count)
  307. if count then
  308. withdraw(player, math.floor(count))
  309. end
  310. end
  311. end
  312. end)