Fix load-crash if _magnet_timer of item entity is uninizialized
[MineClone/MineClone2.git] / mods / ENTITIES / mcl_item_entity / init.lua
blobf3e02a0c3c9bf4fc2ac4b229a879c5b76d3e6faa
1 --basic settings
2 local item_drop_settings = {} --settings table
3 item_drop_settings.age = 1.0 --how old a dropped item (_insta_collect==false) has to be before collecting
4 item_drop_settings.radius_magnet = 2.0 --radius of item magnet. MUST BE LARGER THAN radius_collect!
5 item_drop_settings.radius_collect = 0.2 --radius of collection
6 item_drop_settings.player_collect_height = 1.0 --added to their pos y value
7 item_drop_settings.collection_safety = false --do this to prevent items from flying away on laggy servers
8 item_drop_settings.random_item_velocity = true --this sets random item velocity if velocity is 0
9 item_drop_settings.drop_single_item = false --if true, the drop control drops 1 item instead of the entire stack, and sneak+drop drops the stack
10 -- drop_single_item is disabled by default because it is annoying to throw away items from the intentory screen
12 item_drop_settings.magnet_time = 0.75 -- how many seconds an item follows the player before giving up
14 local get_gravity = function()
15 return tonumber(minetest.settings:get("movement_gravity")) or 9.81
16 end
18 local check_pickup_achievements = function(object, player)
19 local itemname = ItemStack(object:get_luaentity().itemstring):get_name()
20 if minetest.get_item_group(itemname, "tree") ~= 0 then
21 awards.unlock(player:get_player_name(), "mcl:mineWood")
22 elseif itemname == "mcl_mobitems:blaze_rod" then
23 awards.unlock(player:get_player_name(), "mcl:blazeRod")
24 elseif itemname == "mcl_mobitems:leather" then
25 awards.unlock(player:get_player_name(), "mcl:killCow")
26 elseif itemname == "mcl_core:diamond" then
27 awards.unlock(player:get_player_name(), "mcl:diamonds")
28 end
29 end
31 local enable_physics = function(object, luaentity, ignore_check)
32 if luaentity.physical_state == false or ignore_check == true then
33 luaentity.physical_state = true
34 object:set_properties({
35 physical = true
37 object:set_velocity({x=0,y=0,z=0})
38 object:set_acceleration({x=0,y=-get_gravity(),z=0})
39 end
40 end
42 local disable_physics = function(object, luaentity, ignore_check, reset_movement)
43 if luaentity.physical_state == true or ignore_check == true then
44 luaentity.physical_state = false
45 object:set_properties({
46 physical = false
48 if reset_movement ~= false then
49 object:set_velocity({x=0,y=0,z=0})
50 object:set_acceleration({x=0,y=0,z=0})
51 end
52 end
53 end
55 minetest.register_globalstep(function(dtime)
56 for _,player in ipairs(minetest.get_connected_players()) do
57 if player:get_hp() > 0 or not minetest.settings:get_bool("enable_damage") then
58 local pos = player:getpos()
59 local inv = player:get_inventory()
60 local checkpos = {x=pos.x,y=pos.y + item_drop_settings.player_collect_height,z=pos.z}
62 --magnet and collection
63 for _,object in ipairs(minetest.get_objects_inside_radius(checkpos, item_drop_settings.radius_magnet)) do
64 if not object:is_player() and object:get_luaentity() and object:get_luaentity().name == "__builtin:item" and object:get_luaentity()._magnet_timer and (object:get_luaentity()._insta_collect or (object:get_luaentity().age > item_drop_settings.age)) then
65 object:get_luaentity()._magnet_timer = object:get_luaentity()._magnet_timer + dtime
66 local collected = false
67 if object:get_luaentity()._magnet_timer >= 0 and object:get_luaentity()._magnet_timer < item_drop_settings.magnet_time and inv and inv:room_for_item("main", ItemStack(object:get_luaentity().itemstring)) then
69 -- Collection
70 if vector.distance(checkpos, object:getpos()) <= item_drop_settings.radius_collect and not object:get_luaentity()._removed then
71 -- Ignore if itemstring is not set yet
72 if object:get_luaentity().itemstring ~= "" then
73 inv:add_item("main", ItemStack(object:get_luaentity().itemstring))
74 minetest.sound_play("item_drop_pickup", {
75 pos = pos,
76 max_hear_distance = 16,
77 gain = 1.0,
79 check_pickup_achievements(object, player)
82 -- Destroy entity
83 -- This just prevents this section to be run again because object:remove() doesn't remove the item immediately.
84 object:get_luaentity()._removed = true
85 object:remove()
86 collected = true
87 end
89 -- Magnet
90 else
92 object:get_luaentity()._magnet_active = true
93 object:get_luaentity()._collector_timer = 0
95 -- Move object to player
96 disable_physics(object, object:get_luaentity())
98 local opos = object:getpos()
99 local vec = vector.subtract(checkpos, opos)
100 vec = vector.add(opos, vector.divide(vec, 2))
101 object:moveto(vec)
104 --fix eternally falling items
105 minetest.after(0, function(object)
106 local lua = object:get_luaentity()
107 if lua then
108 object:setacceleration({x=0, y=0, z=0})
110 end, object)
113 --this is a safety to prevent items flying away on laggy servers
114 if item_drop_settings.collection_safety == true then
115 if object:get_luaentity().init ~= true then
116 object:get_luaentity().init = true
117 minetest.after(1, function(args)
118 local player = args[1]
119 local object = args[2]
120 local lua = object:get_luaentity()
121 if player == nil or not player:is_player() or object == nil or lua == nil or lua.itemstring == nil then
122 return
124 if inv:room_for_item("main", ItemStack(object:get_luaentity().itemstring)) then
125 inv:add_item("main", ItemStack(object:get_luaentity().itemstring))
126 if not object:get_luaentity()._removed then
127 minetest.sound_play("item_drop_pickup", {
128 pos = pos,
129 max_hear_distance = 16,
130 gain = 1.0,
133 check_pickup_achievements(object, player)
134 object:get_luaentity()._removed = true
135 object:remove()
136 else
137 enable_physics(object, object:get_luaentity())
139 end, {player, object})
145 if not collected then
146 if object:get_luaentity()._magnet_timer > 1 then
147 object:get_luaentity()._magnet_timer = -item_drop_settings.magnet_time
148 object:get_luaentity()._magnet_active = false
149 elseif object:get_luaentity()._magnet_timer < 0 then
150 object:get_luaentity()._magnet_timer = object:get_luaentity()._magnet_timer + dtime
159 end)
161 local minigroups = { "shearsy", "swordy", "shearsy_wool", "swordy_cobweb" }
162 local basegroups = { "pickaxey", "axey", "shovely" }
163 local materials = { "wood", "gold", "stone", "iron", "diamond" }
165 -- Checks if the given node would drop its useful drop if dug by a tool
166 -- with the given tool capabilities. Returns true if it will yield its useful
167 -- drop, false otherwise.
168 local check_can_drop = function(node_name, tool_capabilities)
169 local handy = minetest.get_item_group(node_name, "handy")
170 local dig_immediate = minetest.get_item_group(node_name, "dig_immediate")
171 if handy == 1 or dig_immediate == 2 or dig_immediate == 3 then
172 return true
173 else
174 local toolgroupcaps
175 if tool_capabilities then
176 toolgroupcaps = tool_capabilities.groupcaps
177 else
178 return false
181 -- Compare node groups with tool capabilities
182 for m=1, #minigroups do
183 local minigroup = minigroups[m]
184 local g = minetest.get_item_group(node_name, minigroup)
185 if g ~= 0 then
186 local plus = minigroup .. "_dig"
187 if toolgroupcaps[plus] then
188 return true
192 for b=1, #basegroups do
193 local basegroup = basegroups[b]
194 local g = minetest.get_item_group(node_name, basegroup)
195 if g ~= 0 then
196 for m=g, #materials do
197 local plus = basegroup .. "_dig_"..materials[m]
198 if toolgroupcaps[plus] then
199 return true
205 return false
209 function minetest.handle_node_drops(pos, drops, digger)
210 -- NOTE: This function override allows digger to be nil.
211 -- This means there is no digger. This is a special case which allows this function to be called
212 -- by hand. Creative Mode is intentionally ignored in this case.
214 local doTileDrops = minetest.settings:get_bool("mcl_doTileDrops") or true
215 if (digger ~= nil and minetest.settings:get_bool("creative_mode")) or doTileDrops == false then
216 return
219 -- Check if node will yield its useful drop by the digger's tool
220 local dug_node = minetest.get_node(pos)
221 local toolcaps
222 if digger ~= nil then
223 local tool = digger:get_wielded_item()
224 toolcaps = tool:get_tool_capabilities()
226 if not check_can_drop(dug_node.name, toolcaps) then
227 return
231 --[[ Special node drops when dug by shears by reading _mcl_shears_drop
232 from the node definition.
233 Definition of _mcl_shears_drop:
234 * true: Drop itself when dug by shears
235 * table: Drop every itemstring in this table when dub by shears
237 local nodedef = minetest.registered_nodes[dug_node.name]
238 if toolcaps ~= nil and toolcaps.groupcaps and toolcaps.groupcaps.shearsy_dig and nodedef._mcl_shears_drop then
239 if nodedef._mcl_shears_drop == true then
240 drops = { dug_node.name }
241 else
242 drops = nodedef._mcl_shears_drop
246 for _,item in ipairs(drops) do
247 local count, name
248 if type(item) == "string" then
249 count = 1
250 name = item
251 else
252 count = item:get_count()
253 name = item:get_name()
255 for i=1,count do
256 local obj = core.add_item(pos, name)
257 if obj ~= nil then
258 local x = math.random(1, 5)
259 if math.random(1,2) == 1 then
260 x = -x
262 local z = math.random(1, 5)
263 if math.random(1,2) == 1 then
264 z = -z
266 obj:setvelocity({x=1/x, y=obj:getvelocity().y, z=1/z})
272 -- Drop single items by default
273 function minetest.item_drop(itemstack, dropper, pos)
274 if dropper and dropper:is_player() then
275 local v = dropper:get_look_dir()
276 local p = {x=pos.x, y=pos.y+1.2, z=pos.z}
277 local cs = itemstack:get_count()
278 if dropper:get_player_control().sneak then
279 cs = 1
281 local item = itemstack:take_item(cs)
282 local obj = core.add_item(p, item)
283 if obj then
284 v.x = v.x*4
285 v.y = v.y*4 + 2
286 v.z = v.z*4
287 obj:setvelocity(v)
288 -- Force collection delay
289 obj:get_luaentity()._insta_collect = false
290 return itemstack
295 --modify builtin:item
297 local time_to_live = tonumber(core.setting_get("item_entity_ttl"))
298 if not time_to_live then
299 time_to_live = 300
302 core.register_entity(":__builtin:item", {
303 initial_properties = {
304 hp_max = 1,
305 physical = true,
306 collide_with_objects = false,
307 collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},
308 visual = "wielditem",
309 visual_size = {x = 0.4, y = 0.4},
310 textures = {""},
311 spritediv = {x = 1, y = 1},
312 initial_sprite_basepos = {x = 0, y = 0},
313 is_visible = false,
314 infotext = "",
317 -- Itemstring of dropped item. The empty string is used when the item is not yet initialized yet.
318 -- The itemstring MUST be set immediately to a non-empty string after creating the entity.
319 -- The hand is NOT permitted as dropped item. ;-)
320 -- Item entities will be deleted if they still have an empty itemstring on their first on_step tick.
321 itemstring = '',
323 -- If true, item will fall
324 physical_state = true,
326 -- If item entity is currently flowing in water
327 _flowing = false,
329 -- Number of seconds this item entity has existed so far
330 age = 0,
332 set_item = function(self, itemstring)
333 self.itemstring = itemstring
334 if self.itemstring == "" then
335 -- item not yet known
336 return
338 local stack = ItemStack(itemstring)
339 local count = stack:get_count()
340 local max_count = stack:get_stack_max()
341 if count > max_count then
342 count = max_count
343 self.itemstring = stack:get_name().." "..max_count
345 local s = 0.2 + 0.1 * (count / max_count)
346 local c = s
347 local itemtable = stack:to_table()
348 local itemname = nil
349 local description = ""
350 if itemtable then
351 itemname = stack:to_table().name
353 local item_texture = nil
354 local item_type = ""
355 if core.registered_items[itemname] then
356 item_texture = core.registered_items[itemname].inventory_image
357 item_type = core.registered_items[itemname].type
358 description = core.registered_items[itemname].description
360 local prop = {
361 is_visible = true,
362 visual = "wielditem",
363 textures = {itemname},
364 visual_size = {x = s, y = s},
365 collisionbox = {-c, -c, -c, c, c, c},
366 automatic_rotate = math.pi * 0.5,
367 infotext = description,
369 self.object:set_properties(prop)
370 if item_drop_settings.random_item_velocity == true then
371 minetest.after(0, function(self)
372 if not self or not self.object or not self.object:get_luaentity() then
373 return
375 local vel = self.object:getvelocity()
376 if vel and vel.x == 0 and vel.z == 0 then
377 local x = math.random(1, 5)
378 if math.random(1,2) == 1 then
379 x = -x
381 local z = math.random(1, 5)
382 if math.random(1,2) == 1 then
383 z = -z
385 local y = math.random(2,4)
386 self.object:setvelocity({x=1/x, y=y, z=1/z})
388 end, self)
391 end,
393 get_staticdata = function(self)
394 return core.serialize({
395 itemstring = self.itemstring,
396 always_collect = self.always_collect,
397 age = self.age,
398 _insta_collect = self._insta_collect,
399 _flowing = self._flowing,
400 _removed = self._removed,
402 end,
404 on_activate = function(self, staticdata, dtime_s)
405 if string.sub(staticdata, 1, string.len("return")) == "return" then
406 local data = core.deserialize(staticdata)
407 if data and type(data) == "table" then
408 self.itemstring = data.itemstring
409 self.always_collect = data.always_collect
410 if data.age then
411 self.age = data.age + dtime_s
412 else
413 self.age = dtime_s
415 --remember collection data
416 -- If true, can collect item without delay
417 self._insta_collect = data._insta_collect
418 self._flowing = data._flowing
419 self._removed = data._removed
421 else
422 self.itemstring = staticdata
424 if self._removed then
425 self._removed = true
426 self.object:remove()
427 return
429 if self._insta_collect == nil then
430 -- Intentionally default, since delayed collection is rare
431 self._insta_collect = true
433 if self._flowing == nil then
434 self._flowing = false
436 self._magnet_timer = 0
437 self._magnet_active = false
438 -- How long ago the last possible collector was detected. nil = none in this session
439 self._collector_timer = nil
440 -- Used to apply additional force
441 self._force = nil
442 self._forcestart = nil
443 self._forcetimer = 0
445 self.object:set_armor_groups({immortal = 1})
446 self.object:setvelocity({x = 0, y = 2, z = 0})
447 self.object:setacceleration({x = 0, y = -get_gravity(), z = 0})
448 self:set_item(self.itemstring)
449 end,
451 try_merge_with = function(self, own_stack, object, entity)
452 if self.age == entity.age or entity._removed then
453 -- Can not merge with itself and remove entity
454 return false
457 local stack = ItemStack(entity.itemstring)
458 local name = stack:get_name()
459 if own_stack:get_name() ~= name or
460 own_stack:get_meta() ~= stack:get_meta() or
461 own_stack:get_wear() ~= stack:get_wear() or
462 own_stack:get_free_space() == 0 then
463 -- Can not merge different or full stack
464 return false
467 local count = own_stack:get_count()
468 local total_count = stack:get_count() + count
469 local max_count = stack:get_stack_max()
471 if total_count > max_count then
472 return false
474 -- Merge the remote stack into this one
476 local pos = object:get_pos()
477 pos.y = pos.y + ((total_count - count) / max_count) * 0.15
478 self.object:move_to(pos)
480 self.age = 0 -- Handle as new entity
481 own_stack:set_count(total_count)
482 self:set_item(own_stack:to_string())
484 entity._removed = true
485 object:remove()
486 return true
487 end,
489 on_step = function(self, dtime)
490 if self._removed then
491 return
493 self.age = self.age + dtime
494 if self._collector_timer ~= nil then
495 self._collector_timer = self._collector_timer + dtime
497 if time_to_live > 0 and self.age > time_to_live then
498 self._removed = true
499 self.object:remove()
500 return
502 -- Delete corrupted item entities. The itemstring MUST be non-empty on its first step,
503 -- otherwise there might have some data corruption.
504 if self.itemstring == "" then
505 minetest.log("warning", "Item entity with empty itemstring found at "..minetest.pos_to_string(self.object:getpos()).. "! Deleting it now.")
506 self._removed = true
507 self.object:remove()
510 local p = self.object:getpos()
511 local node = core.get_node_or_nil(p)
512 local in_unloaded = (node == nil)
514 -- If no collector was found for a long enough time, declare the magnet as disabled
515 if self._magnet_active and (self._collector_timer == nil or (self._collector_timer > item_drop_settings.magnet_time)) then
516 self._magnet_active = false
517 enable_physics(self.object, self)
518 return
520 if in_unloaded then
521 -- Don't infinetly fall into unloaded map
522 disable_physics(self.object, self)
523 return
526 -- Destroy item in lava or special nodes
527 local nn = node.name
528 local def = minetest.registered_nodes[nn]
529 if (def and def.groups and (def.groups.lava or def.groups.destroys_items == 1)) then
530 -- Special effect for lava
531 if def.groups.lava then
532 minetest.sound_play("builtin_item_lava", {pos = self.object:getpos(), gain = 0.5})
534 self._removed = true
535 self.object:remove()
536 return
539 -- Push item out when stuck inside solid opaque node
540 if def and def.walkable and def.groups and def.groups.opaque == 1 then
541 local shootdir
542 local cx = p.x % 1
543 local cz = p.z % 1
544 local order = {}
546 -- First prepare the order in which the 4 sides are to be checked.
547 -- 1st: closest
548 -- 2nd: other direction
549 -- 3rd and 4th: other axis
550 local cxcz = function(o, cw, one, zero)
551 if cw > 0 then
552 table.insert(o, { [one]=1, y=0, [zero]=0 })
553 table.insert(o, { [one]=-1, y=0, [zero]=0 })
554 else
555 table.insert(o, { [one]=-1, y=0, [zero]=0 })
556 table.insert(o, { [one]=1, y=0, [zero]=0 })
558 return o
560 if math.abs(cx) > math.abs(cz) then
561 order = cxcz(order, cx, "x", "z")
562 order = cxcz(order, cz, "z", "x")
563 else
564 order = cxcz(order, cz, "z", "x")
565 order = cxcz(order, cx, "x", "z")
568 -- Check which one of the 4 sides is free
569 for o=1, #order do
570 local nn = minetest.get_node(vector.add(p, order[o])).name
571 local def = minetest.registered_nodes[nn]
572 if def and def.walkable == false and nn ~= "ignore" then
573 shootdir = order[o]
574 break
577 -- If none of the 4 sides is free, shoot upwards
578 if shootdir == nil then
579 shootdir = { x=0, y=1, z=0 }
580 local nn = minetest.get_node(vector.add(p, shootdir)).name
581 if nn == "ignore" then
582 -- Do not push into ignore
583 return
587 -- Set new item moving speed accordingly
588 local newv = vector.multiply(shootdir, 3)
589 self.object:setacceleration({x = 0, y = 0, z = 0})
590 self.object:setvelocity(newv)
592 disable_physics(self.object, self, false, false)
594 if shootdir.y == 0 then
595 self._force = newv
596 p.x = math.floor(p.x)
597 p.y = math.floor(p.y)
598 p.z = math.floor(p.z)
599 self._forcestart = p
600 self._forcetimer = 1
602 return
605 -- This code is run after the entity got a push from above “push away” code.
606 -- It is responsible for making sure the entity is entirely outside the solid node
607 -- (with its full collision box), not just its center.
608 if self._forcetimer > 0 then
609 local cbox = self.object:get_properties().collisionbox
610 local ok = false
611 if self._force.x > 0 and (p.x > (self._forcestart.x + 0.5 + (cbox[4] - cbox[1])/2)) then ok = true
612 elseif self._force.x < 0 and (p.x < (self._forcestart.x + 0.5 - (cbox[4] - cbox[1])/2)) then ok = true
613 elseif self._force.z > 0 and (p.z > (self._forcestart.z + 0.5 + (cbox[6] - cbox[3])/2)) then ok = true
614 elseif self._force.z < 0 and (p.z < (self._forcestart.z + 0.5 - (cbox[6] - cbox[3])/2)) then ok = true end
615 -- Item was successfully forced out. No more pushing
616 if ok then
617 self._forcetimer = -1
618 self._force = nil
619 enable_physics(self.object, self)
620 else
621 self._forcetimer = self._forcetimer - dtime
623 return
624 elseif self._force then
625 self._force = nil
626 enable_physics(self.object, self)
627 return
630 -- Move item around on flowing liquids
631 if def and def.liquidtype == "flowing" then
633 --[[ Get flowing direction (function call from flowlib), if there's a liquid.
634 NOTE: According to Qwertymine, flowlib.quickflow is only reliable for liquids with a flowing distance of 7.
635 Luckily, this is exactly what we need if we only care about water, which has this flowing distance. ]]
636 local vec = flowlib.quick_flow(p, node)
637 -- Just to make sure we don't manipulate the speed for no reason
638 if vec.x ~= 0 or vec.y ~= 0 or vec.z ~= 0 then
639 -- Minecraft Wiki: Flowing speed is "about 1.39 meters per second"
640 local f = 1.39
641 -- Set new item moving speed into the direciton of the liquid
642 local newv = vector.multiply(vec, f)
643 self.object:setacceleration({x = 0, y = 0, z = 0})
644 self.object:setvelocity({x = newv.x, y = -0.22, z = newv.z})
646 self.physical_state = true
647 self._flowing = true
648 self.object:set_properties({
649 physical = true
651 return
653 elseif self._flowing == true then
654 -- Disable flowing physics if not on/in flowing liquid
655 self._flowing = false
656 enable_physics(self.object, self, true)
657 return
660 -- If node is not registered or node is walkably solid and resting on nodebox
661 local nn = minetest.get_node({x=p.x, y=p.y-0.5, z=p.z}).name
662 local v = self.object:getvelocity()
664 if not core.registered_nodes[nn] or core.registered_nodes[nn].walkable and v.y == 0 then
665 if self.physical_state then
666 local own_stack = ItemStack(self.object:get_luaentity().itemstring)
667 -- Merge with close entities of the same item
668 for _, object in ipairs(core.get_objects_inside_radius(p, 0.8)) do
669 local obj = object:get_luaentity()
670 if obj and obj.name == "__builtin:item"
671 and obj.physical_state == false then
672 if self:try_merge_with(own_stack, object, obj) then
673 return
677 disable_physics(self.object, self)
679 else
680 if self._magnet_active == false then
681 enable_physics(self.object, self)
684 end,
686 -- Note: on_punch intentionally left out. The player should *not* be able to collect items by punching
689 if minetest.settings:get_bool("log_mods") then
690 minetest.log("action", "mcl_item_entity loaded")