123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- -- Localize for performance.
- local vector_distance = vector.distance
- local vector_round = vector.round
- local math_floor = math.floor
- local math_random = math.random
- local math_min = math.min
- local math_max = math.max
- local cid_data = {}
- minetest.register_on_mods_loaded(function()
- for name, def in pairs(minetest.registered_nodes) do
- cid_data[minetest.get_content_id(name)] = {
- name = name,
- drops = def.drops,
- flammable = def.groups.flammable,
- on_blast = def.on_blast,
- on_destruct = def.on_destruct,
- after_destruct = def.after_destruct,
- }
- end
- end)
- -- loss probabilities array (one in X will be lost)
- local stack_loss_prob = {}
- stack_loss_prob["default:cobble"] = 4
- stack_loss_prob["rackstone:redrack"] = 4
- stack_loss_prob["default:ice"] = 4
- local function rand_pos(center, pos, radius)
- pos.x = center.x + math_random(-radius, radius)
- pos.z = center.z + math_random(-radius, radius)
-
- -- Keep picking random positions until a position inside the sphere is chosen.
- -- This gives us a uniform (flattened) spherical distribution.
- while vector_distance(center, pos) >= radius do
- pos.x = center.x + math_random(-radius, radius)
- pos.z = center.z + math_random(-radius, radius)
- end
- end
- local function eject_drops(drops, pos, radius)
- local drop_pos = vector.new(pos)
- for name, total in pairs(drops) do
- local trash = false
- -- Nothing is lost unless the player loses it.
- if stack_loss_prob[name] ~= nil and math_random(1, stack_loss_prob[name]) == 1 then
- trash = true
- end
- if not trash then
- local count = total
- local item = ItemStack(name)
- while count > 0 do
- local take = math_max(1, math_min(radius * radius, count, item:get_stack_max()))
- rand_pos(pos, drop_pos, radius*0.9)
- local dropitem = ItemStack(name)
- dropitem:set_count(take)
- local obj = minetest.add_item(drop_pos, dropitem)
- if obj then
- obj:get_luaentity().collect = true
- obj:setacceleration({x = 0, y = -10, z = 0})
- obj:setvelocity({x = math_random(-3, 3), y = math_random(0, 10), z = math_random(-3, 3)})
- droplift.invoke(obj, math_random(3, 10))
- end
- count = count - take
- end
- end
- end
- end
- local function add_drop(drops, item)
- item = ItemStack(item)
- local name = item:get_name()
-
- local drop = drops[name]
- if drop == nil then
- drops[name] = item:get_count()
- else
- -- This is causing stacks to get clamped to stack_max, which causes stuff to be lost.
- --drop:set_count(drop:get_count() + item:get_count())
- drops[name] = drops[name] + item:get_count()
- end
- end
- local function destroy(drops, npos, cid, c_air, c_fire, on_blast_queue, on_destruct_queue, on_after_destruct_queue, fire_locations, ignore_protection, ignore_on_blast)
- -- This, right here, is probably what slows TNT code down the most.
- -- Perhaps we can avoid the issue by not allowing TNT to be placed within
- -- a hundred meters of a city block?
- -- Must also consider: explosions caused by mobs, arrows, other code ...
- -- Idea: TNT blasts ignore protection, but TNT can only be placed away from
- -- cityblocks. Explosions from mobs and arrows respect protection as usual.
- if not ignore_protection then
- if minetest.test_protection(npos, "") then
- return cid
- end
- end
- local def = cid_data[cid]
- if not def then
- return c_air
- end
-
- if def.on_destruct then
- -- Queue on_destruct callbacks only if ignoring on_blast.
- if ignore_on_blast or not def.on_blast then
- on_destruct_queue[#on_destruct_queue+1] = {
- pos = vector.new(npos),
- on_destruct = def.on_destruct,
- }
- end
- end
- if def.after_destruct then
- -- Queue after_destruct callbacks only if ignoring on_blast.
- if ignore_on_blast or not def.on_blast then
- on_after_destruct_queue[#on_after_destruct_queue+1] = {
- pos = vector.new(npos),
- after_destruct = def.after_destruct,
- oldnode = minetest.get_node(npos),
- }
- end
- end
- if not ignore_on_blast and def.on_blast then
- on_blast_queue[#on_blast_queue + 1] = {
- pos = vector.new(npos),
- on_blast = def.on_blast,
- }
- return cid
- elseif def.flammable then
- fire_locations[#fire_locations+1] = vector.new(npos)
- return c_fire
- else
- local node_drops = minetest.get_node_drops(def.name, "")
- for _, item in ipairs(node_drops) do
- add_drop(drops, item)
- end
- return c_air
- end
- end
- local function calc_velocity(pos1, pos2, old_vel, power)
- -- Avoid errors caused by a vector of zero length
- if vector.equals(pos1, pos2) then
- return old_vel
- end
- local vel = vector.direction(pos1, pos2)
- vel = vector.normalize(vel)
- vel = vector.multiply(vel, power)
- -- Divide by distance
- local dist = vector_distance(pos1, pos2)
- dist = math_max(dist, 1)
- vel = vector.divide(vel, dist)
- -- Add old velocity
- vel = vector.add(vel, old_vel)
- -- randomize it a bit
- vel = vector.add(vel, {
- x = math_random() - 0.5,
- y = math_random() - 0.5,
- z = math_random() - 0.5,
- })
- -- Limit to terminal velocity
- dist = vector.length(vel)
- if dist > 250 then
- vel = vector.divide(vel, dist / 250)
- end
- return vel
- end
- local function entity_physics(pos, radius, drops, boomdef)
- local objs = minetest.get_objects_inside_radius(pos, radius)
- for _, obj in pairs(objs) do
- local obj_pos = obj:get_pos()
- local dist = math_max(1, vector_distance(pos, obj_pos))
- -- Calculate damage to be applied to player or mob.
- local damage = (8 / dist) * radius
- if obj:is_player() then
- local pname = obj:get_player_name()
- -- Admin is exempt from TNT blasts.
- if not gdac.player_is_admin(obj) then
- -- Damage player. For reasons having to do with bone placement, this
- -- needs to happen before any knockback effects. And knockback effects
- -- should only be applied if the player does not actually die.
- if obj:get_hp() > 0 then
- local dg = {
- fleshy = damage,
- }
- local hitter = obj
- -- Hack used to signal to the city-block handler what happened.
- -- This is ugly as Sin, but what else can I do? (Note: it will not
- -- work to try to use the actual player responsible as the hitter,
- -- because the city-block code enforces a range limit which arrows can
- -- exceed.)
- if boomdef.name and boomdef.name ~= "" and boomdef.from_arrow then
- -- But only if the target to be punched is NOT the target that fired
- -- the weapon. This prevents an issue in the city-block code, which
- -- cannot differentiate between someone killing another and someone
- -- killing themselves. In other words, this causes the city-block
- -- code to skip its arrow-handling routine if the player fired the
- -- arrow at their own feet. If we did not do this check, then a very
- -- clever player could suicide using a ranged TNT weapon, log off
- -- before the city-block code runs, and thus trick the city-block
- -- code into blaming someone else entirely for the death caused!
- if pname ~= boomdef.name then
- dg.from_arrow = 0
- -- If the player that launched this TNT is online, we can also
- -- provide the city-block handler with their actual player ref.
- -- This lets the city-block code skip finding the nearest player,
- -- which can be inaccurate if several players are near each other.
- local pref = minetest.get_player_by_name(boomdef.name)
- if pref then
- hitter = pref
- end
- end
- end
- obj:punch(hitter, 1.0, {
- full_punch_interval = 1.0,
- max_drop_level = 0,
- damage_groups = dg,
- }, nil)
- if obj:get_hp() <= 0 then
- if player_labels.query_nametag_onoff(pname) == true and not cloaking.is_cloaked(pname) then
- minetest.chat_send_all("# Server: <" .. rename.gpn(pname) .. "> exploded.")
- else
- minetest.chat_send_all("# Server: Someone exploded.")
- end
- end
- end
- -- Do knockback only if player didn't die.
- if obj:get_hp() > 0 then
- local dir = vector.normalize(vector.subtract(obj_pos, pos))
- local moveoff = vector.multiply(dir, 2 / dist * radius)
- moveoff = vector.multiply(moveoff, 3)
- obj:add_player_velocity(moveoff)
- end
- end
- else
- local do_damage = true
- local do_knockback = true
- local entity_drops = {}
- local luaobj = obj:get_luaentity()
- -- Ignore mobs of the same type as the one that launched the TNT boom.
- local ignore = false
- if boomdef.mob and luaobj.mob and boomdef.mob == luaobj.name then
- ignore = true
- end
- if not ignore then
- local objdef = minetest.registered_entities[luaobj.name]
- if objdef and objdef.on_blast then
- do_damage, do_knockback, entity_drops = objdef.on_blast(luaobj, damage)
- end
- if do_knockback then
- local obj_vel = obj:getvelocity()
- obj:setvelocity(calc_velocity(pos, obj_pos,
- obj_vel, radius * 10))
- end
- if do_damage then
- if not obj:get_armor_groups().immortal then
- obj:punch(obj, 1.0, {
- full_punch_interval = 1.0,
- damage_groups = {fleshy = damage},
- }, nil)
- end
- end
- for _, item in ipairs(entity_drops) do
- add_drop(drops, item)
- end
- end
- end
- end
- end
- local function add_effects(pos, radius, drops)
- minetest.add_particle({
- pos = pos,
- velocity = vector.new(),
- acceleration = vector.new(),
- expirationtime = 0.4,
- size = radius * 10,
- collisiondetection = false,
- vertical = false,
- texture = "tnt_boom.png",
- })
- minetest.add_particlespawner({
- amount = 64,
- time = 0.5,
- minpos = vector.subtract(pos, radius / 2),
- maxpos = vector.add(pos, radius / 2),
- minvel = {x = -10, y = -10, z = -10},
- maxvel = {x = 10, y = 10, z = 10},
- minacc = vector.new(),
- maxacc = vector.new(),
- minexptime = 1,
- maxexptime = 2.5,
- minsize = radius * 3,
- maxsize = radius * 5,
- texture = "tnt_smoke.png",
- })
-
- -- we just dropped some items. Look at the items entities and pick
- -- one of them to use as texture
- local texture = "tnt_blast.png" --fallback texture
- local most = 0
- for name, count in pairs(drops) do
- --local count = stack:get_count()
- if count > most then
- most = count
- local def = minetest.registered_nodes[name]
- if def and def.tiles and def.tiles[1] then
- if type(def.tiles[1]) == "string" then
- texture = def.tiles[1]
- end
- end
- end
- end
- minetest.add_particlespawner({
- amount = 64,
- time = 0.1,
- minpos = vector.subtract(pos, radius / 2),
- maxpos = vector.add(pos, radius / 2),
- minvel = {x = -3, y = 0, z = -3},
- maxvel = {x = 3, y = 5, z = 3},
- minacc = {x = 0, y = -10, z = 0},
- maxacc = {x = 0, y = -10, z = 0},
- minexptime = 0.8,
- maxexptime = 2.0,
- minsize = radius * 0.66,
- maxsize = radius * 2,
- texture = texture,
- collisiondetection = true,
- })
- end
- -- Quickly check for protection in an area.
- local function check_protection(pos, radius)
- -- How much beyond the radius to check for protections.
- local e = 10
- local minp = vector.new(pos.x-(radius+e), pos.y-(radius+e), pos.z-(radius+e))
- local maxp = vector.new(pos.x+(radius+e), pos.y+(radius+e), pos.z+(radius+e))
- -- Step size, to avoid checking every single node.
- -- This assumes protections cannot be smaller than this size.
- local ss = 5
- local check = minetest.test_protection
- for x=minp.x, maxp.x, ss do
- for y=minp.y, maxp.y, ss do
- for z=minp.z, maxp.z, ss do
- if check({x=x, y=y, z=z}, "") then
- -- Protections are present.
- return true
- end
- end
- end
- end
- -- Nothing in the area is protected.
- return false
- end
- local function tnt_explode(pos, radius, ignore_protection, ignore_on_blast)
- pos = vector_round(pos)
- -- scan for adjacent TNT nodes first, and enlarge the explosion
- local vm1 = VoxelManip()
- local p1 = vector.subtract(pos, 3)
- local p2 = vector.add(pos, 3)
- local minp, maxp = vm1:read_from_map(p1, p2)
- local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
- local data = vm1:get_data()
- local count = 0
- local c_tnt = minetest.get_content_id("tnt:tnt")
- local c_tnt_burning = minetest.get_content_id("tnt:tnt_burning")
- local c_tnt_boom = minetest.get_content_id("tnt:boom")
- local c_air = minetest.get_content_id("air")
- for z = pos.z - 3, pos.z + 3 do
- for y = pos.y - 3, pos.y + 3 do
- for x = pos.x - 3, pos.x + 3 do
- local vi = a:index(x, y, z)
- local cid = data[vi]
- if cid == c_tnt or cid == c_tnt_boom or cid == c_tnt_burning then
- count = count + 1
- data[vi] = c_air
- end
- end
- end
- end
-
- -- The variable 'count' may be 0 if the bomb exploded in a protected area. In
- -- which case no "tnt boom" flash (node) will have been created. Clamping
- -- 'count' to a minimum of 1 fixes the problem.
- -- [MustTest]
- if count < 1 then
- count = 1
- end
-
- -- Clamp to avoid massive explosions.
- if count > 64 then count = 64 end
- vm1:set_data(data)
- vm1:write_to_map()
- -- recalculate new radius
- radius = math_floor(radius * math.pow(count, 0.60))
- -- If no protections are present, we can optimize by skipping the protection
- -- check for individual nodes. If we have a small radius, then don't bother.
- if radius > 8 then
- if not check_protection(pos, radius) then
- ignore_protection = true
- end
- end
- -- perform the explosion
- local vm = VoxelManip()
- local pr = PseudoRandom(os.time())
- p1 = vector.subtract(pos, radius)
- p2 = vector.add(pos, radius)
- minp, maxp = vm:read_from_map(p1, p2)
- a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
- data = vm:get_data()
- local drops = {}
- local on_blast_queue = {}
- local on_destruct_queue = {}
- local on_after_destruct_queue = {}
- local fire_locations = {}
- local c_fire = minetest.get_content_id("fire:basic_flame")
-
- for z = -radius, radius do
- for y = -radius, radius do
- local vi = a:index(pos.x + (-radius), pos.y + y, pos.z + z)
- for x = -radius, radius do
- local r = vector.length(vector.new(x, y, z))
- local r2 = radius
-
- -- Roughen the walls a bit.
- if pr:next(0, 6) == 0 then
- r2 = radius - 0.8
- end
-
- if r <= r2 then
- local cid = data[vi]
- local p = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
- if cid ~= c_air then
- data[vi] = destroy(drops, p, cid, c_air, c_fire,
- on_blast_queue, on_destruct_queue, on_after_destruct_queue,
- fire_locations, ignore_protection, ignore_on_blast)
- end
- end
-
- vi = vi + 1
- end
- end
- end
-
- -- Call on_destruct callbacks.
- for k, v in ipairs(on_destruct_queue) do
- v.on_destruct(v.pos)
- end
- vm:set_data(data)
- vm:write_to_map()
- vm:update_map()
- vm:update_liquids()
- -- Check unstable nodes for everything within blast effect.
- local minr = {x=pos.x-(radius+2), y=pos.y-(radius+2), z=pos.z-(radius+2)}
- local maxr = {x=pos.x+(radius+2), y=pos.y+(radius+2), z=pos.z+(radius+2)}
- for z=minr.z, maxr.z do
- for x=minr.x, maxr.x do
- for y=minr.y, maxr.y do
- local p = {x=x, y=y, z=z}
- local d = vector_distance(pos, p)
- if d < radius+2 and d > radius-2 then
- -- Check for nodes with 'falling_node' in groups.
- minetest.check_single_for_falling(p)
- -- Now check using additional falling node logic.
- instability.check_unsupported_single(p)
- end
- end
- end
- end
- -- Execute after-destruct callbacks.
- for k, v in ipairs(on_after_destruct_queue) do
- v.after_destruct(v.pos, v.oldnode)
- end
- for _, queued_data in ipairs(on_blast_queue) do
- local dist = math_max(1, vector_distance(queued_data.pos, pos))
- local intensity = (radius * radius) / (dist * dist)
- local node_drops = queued_data.on_blast(queued_data.pos, intensity)
- if node_drops then
- for _, item in ipairs(node_drops) do
- add_drop(drops, item)
- end
- end
- end
-
- -- Initialize flames.
- local fdef = minetest.registered_nodes["fire:basic_flame"]
- if fdef and fdef.on_construct then
- for k, v in ipairs(fire_locations) do
- fdef.on_construct(v)
- end
- end
- return drops, radius
- end
- --[[
- {
- radius,
- ignore_protection,
- ignore_on_blast,
- damage_radius,
- disable_drops,
- name, -- Name to use when testing protection. Defaults to "".
- }
- --]]
- function tnt.boom(pos, def)
- pos = vector_round(pos)
- -- The TNT code crashes sometimes, for no particular reason?
- local func = function()
- tnt.boom_impl(pos, def)
- end
- pcall(func)
- end
- -- Not to be called externally.
- function tnt.boom_impl(pos, def)
- if def.make_sound == nil or def.make_sound == true then
- minetest.sound_play("tnt_explode", {pos = pos, gain = 1.5, max_hear_distance = 2*64}, true)
- end
- -- Make sure TNT never somehow gets keyed to the admin!
- if def.name and gdac.player_is_admin(def.name) then
- def.name = nil
- end
-
- if not minetest.test_protection(pos, "") then
- local node = minetest.get_node(pos)
- -- Never destroy death boxes.
- if node.name ~= "bones:bones" then
- minetest.set_node(pos, {name = "tnt:boom"})
- end
- end
-
- local drops, radius = tnt_explode(pos, def.radius, def.ignore_protection, def.ignore_on_blast, def.name or "")
- -- append entity drops
- local damage_radius = (radius / def.radius) * def.damage_radius
- entity_physics(pos, damage_radius, drops, def)
- if not def.disable_drops then
- eject_drops(drops, pos, radius)
- end
- add_effects(pos, radius, drops)
-
- minetest.log("action", "A TNT explosion occurred at " .. minetest.pos_to_string(pos) ..
- " with radius " .. radius)
- end
|