A highly configurable mod providing item magnet and in-world node drops
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

425 lines
12KB

  1. local load_time_start = minetest.get_us_time()
  2. -- Functions which can be overridden by mods
  3. item_drop = {
  4. -- This function is executed before picking up an item or making it fly to
  5. -- the player. If it does not return true, the item is ignored.
  6. -- It is also executed before collecting the item after it flew to
  7. -- the player and did not reach him/her for magnet_time seconds.
  8. can_pickup = function(entity, player)
  9. if entity.item_drop_picked then
  10. -- Ignore items where picking has already failed
  11. return false
  12. end
  13. return true
  14. end,
  15. -- before_collect and after_collect are executed before and after an item
  16. -- is collected by a player
  17. before_collect = function(entity, pos, player)
  18. end,
  19. after_collect = function(entity, pos, player)
  20. entity.item_drop_picked = true
  21. end,
  22. }
  23. local function legacy_setting_getbool(name_new, name_old, default)
  24. local v = minetest.settings:get_bool(name_new)
  25. if v == nil then
  26. v = minetest.settings:get_bool(name_new)
  27. end
  28. if default then
  29. return v ~= false
  30. end
  31. return v
  32. end
  33. local function legacy_setting_getnumber(name_new, name_old, default)
  34. return tonumber(minetest.settings:get(name_new))
  35. or tonumber(minetest.settings:get(name_old))
  36. or default
  37. end
  38. if legacy_setting_getbool("item_drop.enable_item_pickup",
  39. "enable_item_pickup", true) then
  40. local pickup_gain = legacy_setting_getnumber("item_drop.pickup_sound_gain",
  41. "item_pickup_gain", 0.2)
  42. local pickup_particle =
  43. minetest.settings:get_bool("item_drop.pickup_particle", true)
  44. local pickup_radius = legacy_setting_getnumber("item_drop.pickup_radius",
  45. "item_pickup_radius", 0.75)
  46. local magnet_radius = tonumber(
  47. minetest.settings:get("item_drop.magnet_radius")) or -1
  48. local magnet_time = tonumber(
  49. minetest.settings:get("item_drop.magnet_time")) or 5.0
  50. local pickup_age = tonumber(
  51. minetest.settings:get("item_drop.pickup_age")) or 0.5
  52. local key_triggered = legacy_setting_getbool("item_drop.enable_pickup_key",
  53. "enable_item_pickup_key", true)
  54. local key_invert = minetest.settings:get_bool(
  55. "item_drop.pickup_keyinvert") ~= false
  56. local keytype
  57. if key_triggered then
  58. keytype = minetest.settings:get("item_drop.pickup_keytype") or
  59. minetest.settings:get("item_pickup_keytype") or "Use"
  60. -- disable pickup age if picking is explicitly enabled by the player
  61. if not key_invert then
  62. pickup_age = math.min(pickup_age, 0)
  63. end
  64. end
  65. local mouse_pickup = minetest.settings:get_bool(
  66. "item_drop.mouse_pickup") ~= false
  67. if not mouse_pickup then
  68. minetest.registered_entities["__builtin:item"].pointable = false
  69. end
  70. local magnet_mode = magnet_radius > pickup_radius
  71. local zero_velocity_mode = pickup_age == -1
  72. if magnet_mode
  73. and zero_velocity_mode then
  74. error"zero velocity mode can't be used together with magnet mode"
  75. end
  76. -- tells whether an inventorycube should be shown as pickup_particle or not
  77. -- for known drawtypes
  78. local inventorycube_drawtypes = {
  79. normal = true,
  80. allfaces = true,
  81. allfaces_optional = true,
  82. glasslike = true,
  83. glasslike_framed = true,
  84. glasslike_framed_optional = true,
  85. liquid = true,
  86. flowingliquid = true,
  87. }
  88. -- adds the item to the inventory and removes the object
  89. local function collect_item(ent, pos, player)
  90. item_drop.before_collect(ent, pos, player)
  91. minetest.sound_play("item_drop_pickup", {
  92. pos = pos,
  93. gain = pickup_gain,
  94. })
  95. if pickup_particle then
  96. local item = minetest.registered_nodes[
  97. ent.itemstring:gsub("(.*)%s.*$", "%1")]
  98. local image
  99. if item and item.tiles and item.tiles[1] then
  100. if inventorycube_drawtypes[item.drawtype] then
  101. local tiles = item.tiles
  102. local top = tiles[1]
  103. if type(top) == "table" then
  104. top = top.name
  105. end
  106. local left = tiles[3] or top
  107. if type(left) == "table" then
  108. left = left.name
  109. end
  110. local right = tiles[5] or left
  111. if type(right) == "table" then
  112. right = right.name
  113. end
  114. image = minetest.inventorycube(top, left, right)
  115. else
  116. image = item.inventory_image or item.tiles[1]
  117. end
  118. minetest.add_particle({
  119. pos = {x = pos.x, y = pos.y + 1.5, z = pos.z},
  120. velocity = {x = 0, y = 1, z = 0},
  121. acceleration = {x = 0, y = -4, z = 0},
  122. expirationtime = 0.2,
  123. size = 3,--math.random() + 0.5,
  124. vertical = false,
  125. texture = image,
  126. })
  127. end
  128. end
  129. ent:on_punch(player)
  130. item_drop.after_collect(ent, pos, player)
  131. end
  132. -- opt_get_ent gets the object's luaentity if it can be collected
  133. local opt_get_ent
  134. if zero_velocity_mode then
  135. function opt_get_ent(object)
  136. if object:is_player()
  137. or not vector.equals(object:get_velocity(), {x=0, y=0, z=0}) then
  138. return
  139. end
  140. local ent = object:get_luaentity()
  141. if not ent
  142. or ent.name ~= "__builtin:item"
  143. or ent.itemstring == "" then
  144. return
  145. end
  146. return ent
  147. end
  148. else
  149. function opt_get_ent(object)
  150. if object:is_player() then
  151. return
  152. end
  153. local ent = object:get_luaentity()
  154. if not ent
  155. or ent.name ~= "__builtin:item"
  156. or (ent.dropped_by and ent.age < pickup_age)
  157. or ent.itemstring == "" then
  158. return
  159. end
  160. return ent
  161. end
  162. end
  163. local afterflight
  164. if magnet_mode then
  165. -- take item or reset velocity after flying a second
  166. function afterflight(object, inv, player)
  167. -- TODO: test what happens if player left the game
  168. local ent = opt_get_ent(object)
  169. if not ent then
  170. return
  171. end
  172. local item = ItemStack(ent.itemstring)
  173. if inv
  174. and inv:room_for_item("main", item)
  175. and item_drop.can_pickup(ent, player) then
  176. collect_item(ent, object:get_pos(), player)
  177. else
  178. -- the acceleration will be reset by the object's on_step
  179. object:set_velocity({x=0,y=0,z=0})
  180. ent.is_magnet_item = false
  181. end
  182. end
  183. -- disable velocity and acceleration changes of items flying to players
  184. minetest.after(0, function()
  185. local ObjectRef
  186. local blocked_methods = {"set_acceleration", "set_velocity",
  187. "setacceleration", "setvelocity"}
  188. local itemdef = minetest.registered_entities["__builtin:item"]
  189. local old_on_step = itemdef.on_step
  190. local function do_nothing() end
  191. function itemdef.on_step(self, ...)
  192. if not self.is_magnet_item then
  193. return old_on_step(self, ...)
  194. end
  195. ObjectRef = ObjectRef or getmetatable(self.object)
  196. local old_funcs = {}
  197. for i = 1, #blocked_methods do
  198. local method = blocked_methods[i]
  199. old_funcs[method] = ObjectRef[method]
  200. ObjectRef[method] = do_nothing
  201. end
  202. old_on_step(self, ...)
  203. for i = 1, #blocked_methods do
  204. local method = blocked_methods[i]
  205. ObjectRef[method] = old_funcs[method]
  206. end
  207. end
  208. end)
  209. end
  210. -- set keytype to the key name if possible
  211. if keytype == "Use" then
  212. keytype = "aux1"
  213. elseif keytype == "Sneak" then
  214. keytype = "sneak"
  215. elseif keytype == "LeftAndRight" then -- LeftAndRight combination
  216. keytype = 0
  217. elseif keytype == "SneakAndRMB" then -- SneakAndRMB combination
  218. keytype = 1
  219. end
  220. -- tests if the player has the keys pressed to enable item picking
  221. local function has_keys_pressed(player)
  222. if not key_triggered then
  223. return true
  224. end
  225. local control = player:get_player_control()
  226. local keys_pressed
  227. if keytype == 0 then -- LeftAndRight combination
  228. keys_pressed = control.left and control.right
  229. elseif keytype == 1 then -- SneakAndRMB combination
  230. keys_pressed = control.sneak and control.RMB
  231. else
  232. keys_pressed = control[keytype]
  233. end
  234. return keys_pressed ~= key_invert
  235. end
  236. local function is_inside_map(pos)
  237. local bound = 31000
  238. return -bound < pos.x and pos.x < bound
  239. and -bound < pos.y and pos.y < bound
  240. and -bound < pos.z and pos.z < bound
  241. end
  242. -- called for each player to possibly collect an item, returns true if so
  243. local function pickupfunc(player)
  244. if not has_keys_pressed(player)
  245. or not minetest.get_player_privs(player:get_player_name()).interact
  246. or player:get_hp() <= 0 then
  247. return
  248. end
  249. local pos = player:get_pos()
  250. if not is_inside_map(pos) then
  251. -- get_objects_inside_radius crashes for too far positions
  252. return
  253. end
  254. pos.y = pos.y+0.5
  255. local inv = player:get_inventory()
  256. local objectlist = minetest.get_objects_inside_radius(pos,
  257. magnet_mode and magnet_radius or pickup_radius)
  258. for i = 1,#objectlist do
  259. local object = objectlist[i]
  260. local ent = opt_get_ent(object)
  261. if ent
  262. and item_drop.can_pickup(ent, player) then
  263. local item = ItemStack(ent.itemstring)
  264. if inv:room_for_item("main", item) then
  265. local flying_item
  266. local pos2
  267. if magnet_mode then
  268. pos2 = object:get_pos()
  269. flying_item = vector.distance(pos, pos2) > pickup_radius
  270. end
  271. if not flying_item then
  272. -- The item is near enough to pick it
  273. collect_item(ent, pos, player)
  274. -- Collect one item at a time to avoid the loud pop
  275. return true
  276. end
  277. -- The item is not too far a way but near enough to be
  278. -- magnetised, make it fly to the player
  279. local vel = vector.multiply(vector.subtract(pos, pos2), 3)
  280. vel.y = vel.y + 0.6
  281. object:set_velocity(vel)
  282. if not ent.is_magnet_item then
  283. ent.object:set_acceleration({x=0, y=0, z=0})
  284. ent.is_magnet_item = true
  285. minetest.after(magnet_time, afterflight,
  286. object, inv, player)
  287. end
  288. end
  289. end
  290. end
  291. end
  292. local function pickup_step()
  293. local got_item
  294. local players = minetest.get_connected_players()
  295. for i = 1,#players do
  296. got_item = got_item or pickupfunc(players[i])
  297. end
  298. -- lower step if takeable item(s) were found
  299. local time
  300. if got_item then
  301. time = 0.02
  302. else
  303. time = 0.2
  304. end
  305. minetest.after(time, pickup_step)
  306. end
  307. minetest.after(3.0, pickup_step)
  308. end
  309. if legacy_setting_getbool("item_drop.enable_item_drop", "enable_item_drop", true)
  310. and not minetest.settings:get_bool("creative_mode") then
  311. -- Workaround to test if an item metadata (ItemStackMetaRef) is empty
  312. local function itemmeta_is_empty(meta)
  313. local t = meta:to_table()
  314. for k, v in pairs(t) do
  315. if k ~= "fields" then
  316. return false
  317. end
  318. assert(type(v) == "table")
  319. if next(v) ~= nil then
  320. return false
  321. end
  322. end
  323. return true
  324. end
  325. -- Tests if the item has special information such as metadata
  326. local function can_split_item(item)
  327. return item:get_wear() == 0 and itemmeta_is_empty(item:get_meta())
  328. end
  329. local function spawn_items(pos, items_to_spawn)
  330. for i = 1,#items_to_spawn do
  331. local obj = minetest.add_item(pos, items_to_spawn[i])
  332. if not obj then
  333. error("Couldn't spawn item " .. name .. ", drops: "
  334. .. dump(drops))
  335. end
  336. local vel = obj:get_velocity()
  337. local x = math.random(-5, 4)
  338. if x >= 0 then
  339. x = x+1
  340. end
  341. vel.x = 1 / x
  342. local z = math.random(-5, 4)
  343. if z >= 0 then
  344. z = z+1
  345. end
  346. vel.z = 1 / z
  347. obj:set_velocity(vel)
  348. end
  349. end
  350. local old_handle_node_drops = minetest.handle_node_drops
  351. function minetest.handle_node_drops(pos, drops, player)
  352. if player.is_fake_player then
  353. -- Node Breaker or similar machines should receive items in the
  354. -- inventory
  355. return old_handle_node_drops(pos, drops, player)
  356. end
  357. for i = 1,#drops do
  358. local item = drops[i]
  359. if type(item) == "string" then
  360. -- The string is not necessarily only the item name,
  361. -- so always convert it to ItemStack
  362. item = ItemStack(item)
  363. end
  364. local count = item:get_count()
  365. local name = item:get_name()
  366. -- Sometimes nothing should be dropped
  367. if name == ""
  368. or not minetest.registered_items[name] then
  369. count = 0
  370. end
  371. if count > 0 then
  372. -- Split items if possible
  373. local items_to_spawn = {item}
  374. if can_split_item(item) then
  375. for i = 1,count do
  376. items_to_spawn[i] = name
  377. end
  378. end
  379. spawn_items(pos, items_to_spawn)
  380. end
  381. end
  382. end
  383. end
  384. local time = (minetest.get_us_time() - load_time_start) / 1000000
  385. local msg = "[item_drop] loaded after ca. " .. time .. " seconds."
  386. if time > 0.01 then
  387. print(msg)
  388. else
  389. minetest.log("info", msg)
  390. end