#include "Globals.h" #include "BlockInfo.h" #include "Explodinator.h" #include "Blocks/BlockHandler.h" #include "Blocks/ChunkInterface.h" #include "Chunk.h" #include "ClientHandle.h" #include "Entities/FallingBlock.h" #include "Physics/Tracers/LineBlockTracer.h" #include "Simulator/SandSimulator.h" namespace Explodinator { static const auto StepUnit = 0.3f; static const auto KnockbackFactor = 25U; static const auto StepAttenuation = 0.225f; static const auto TraceCubeSideLength = 16U; static const auto BoundingBoxStepUnit = 0.5; /** Returns how much of an explosion Destruction Lazor's (tm) intensity the given block attenuates. Values are scaled as 0.3 * (0.3 + Wiki) since some compilers miss the constant folding optimisation. Wiki values are https://minecraft.gamepedia.com/Explosion#Blast_resistance as of 2021-02-06. */ static constexpr float GetExplosionAbsorption(const BLOCKTYPE Block) { switch (Block) { case E_BLOCK_BEDROCK: case E_BLOCK_COMMAND_BLOCK: case E_BLOCK_END_GATEWAY: case E_BLOCK_END_PORTAL: case E_BLOCK_END_PORTAL_FRAME: return 1080000.09f; case E_BLOCK_ANVIL: case E_BLOCK_ENCHANTMENT_TABLE: case E_BLOCK_OBSIDIAN: return 360.09f; case E_BLOCK_ENDER_CHEST: return 180.09f; case E_BLOCK_LAVA: case E_BLOCK_STATIONARY_LAVA: case E_BLOCK_WATER: case E_BLOCK_STATIONARY_WATER: return 30.09f; case E_BLOCK_DRAGON_EGG: case E_BLOCK_END_STONE: case E_BLOCK_END_BRICKS: return 2.79f; case E_BLOCK_STONE: case E_BLOCK_BLOCK_OF_COAL: case E_BLOCK_DIAMOND_BLOCK: case E_BLOCK_EMERALD_BLOCK: case E_BLOCK_GOLD_BLOCK: case E_BLOCK_IRON_BLOCK: case E_BLOCK_BLOCK_OF_REDSTONE: case E_BLOCK_BRICK: case E_BLOCK_BRICK_STAIRS: case E_BLOCK_COBBLESTONE: case E_BLOCK_COBBLESTONE_STAIRS: case E_BLOCK_IRON_BARS: case E_BLOCK_JUKEBOX: case E_BLOCK_MOSSY_COBBLESTONE: case E_BLOCK_NETHER_BRICK: case E_BLOCK_NETHER_BRICK_FENCE: case E_BLOCK_NETHER_BRICK_STAIRS: case E_BLOCK_PRISMARINE_BLOCK: case E_BLOCK_STONE_BRICKS: case E_BLOCK_STONE_BRICK_STAIRS: case E_BLOCK_COBBLESTONE_WALL: return 1.89f; case E_BLOCK_IRON_DOOR: case E_BLOCK_IRON_TRAPDOOR: case E_BLOCK_MOB_SPAWNER: return 1.59f; case E_BLOCK_HOPPER: return 1.53f; case E_BLOCK_TERRACOTTA: return 1.35f; case E_BLOCK_COBWEB: return 1.29f; case E_BLOCK_DISPENSER: case E_BLOCK_DROPPER: case E_BLOCK_FURNACE: case E_BLOCK_OBSERVER: return 1.14f; case E_BLOCK_BEACON: case E_BLOCK_COAL_ORE: case E_BLOCK_COCOA_POD: case E_BLOCK_DIAMOND_ORE: case E_BLOCK_EMERALD_ORE: case E_BLOCK_GOLD_ORE: case E_BLOCK_IRON_ORE: case E_BLOCK_LAPIS_BLOCK: case E_BLOCK_LAPIS_ORE: case E_BLOCK_NETHER_QUARTZ_ORE: case E_BLOCK_PLANKS: case E_BLOCK_REDSTONE_ORE: case E_BLOCK_FENCE: case E_BLOCK_FENCE_GATE: case E_BLOCK_WOODEN_DOOR: case E_BLOCK_WOODEN_SLAB: case E_BLOCK_WOODEN_STAIRS: case E_BLOCK_TRAPDOOR: return 0.99f; case E_BLOCK_CHEST: case E_BLOCK_WORKBENCH: case E_BLOCK_TRAPPED_CHEST: return 0.84f; case E_BLOCK_BONE_BLOCK: case E_BLOCK_CAULDRON: case E_BLOCK_LOG: return 0.69f; // nIcE case E_BLOCK_CONCRETE: return 0.63f; case E_BLOCK_BOOKCASE: return 0.54f; case E_BLOCK_STANDING_BANNER: case E_BLOCK_WALL_BANNER: case E_BLOCK_JACK_O_LANTERN: case E_BLOCK_MELON: case E_BLOCK_HEAD: case E_BLOCK_NETHER_WART_BLOCK: case E_BLOCK_PUMPKIN: case E_BLOCK_SIGN_POST: case E_BLOCK_WALLSIGN: return 0.39f; case E_BLOCK_QUARTZ_BLOCK: case E_BLOCK_QUARTZ_STAIRS: case E_BLOCK_RED_SANDSTONE: case E_BLOCK_RED_SANDSTONE_STAIRS: case E_BLOCK_SANDSTONE: case E_BLOCK_SANDSTONE_STAIRS: case E_BLOCK_WOOL: return 0.33f; case E_BLOCK_SILVERFISH_EGG: return 0.315f; case E_BLOCK_ACTIVATOR_RAIL: case E_BLOCK_DETECTOR_RAIL: case E_BLOCK_POWERED_RAIL: case E_BLOCK_RAIL: return 0.3f; case E_BLOCK_GRASS_PATH: case E_BLOCK_CLAY: case E_BLOCK_FARMLAND: case E_BLOCK_GRASS: case E_BLOCK_GRAVEL: case E_BLOCK_SPONGE: return 0.27f; case E_BLOCK_BREWING_STAND: case E_BLOCK_STONE_BUTTON: case E_BLOCK_WOODEN_BUTTON: case E_BLOCK_CAKE: case E_BLOCK_CONCRETE_POWDER: case E_BLOCK_DIRT: case E_BLOCK_FROSTED_ICE: case E_BLOCK_HAY_BALE: case E_BLOCK_ICE: return 0.24f; default: return 0.09f; } } /** Calculates the approximate percentage of an Entity's bounding box that is exposed to an explosion centred at Position. */ static float CalculateEntityExposure(const cChunk & a_Chunk, const cEntity & a_Entity, const Vector3f a_Position, const int a_SquareRadius) { class LineOfSightCallbacks final : public BlockTracerCallbacks { virtual bool OnNextBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, eBlockFace a_EntryFace) override { return a_BlockType != E_BLOCK_AIR; } } Callback; const Vector3d Position = a_Position; unsigned Unobstructed = 0, Total = 0; const auto Box = a_Entity.GetBoundingBox(); for (double X = Box.GetMinX(); X < Box.GetMaxX(); X += BoundingBoxStepUnit) { for (double Y = Box.GetMinY(); Y < Box.GetMaxY(); Y += BoundingBoxStepUnit) { for (double Z = Box.GetMinZ(); Z < Box.GetMaxZ(); Z += BoundingBoxStepUnit) { const Vector3d Destination{X, Y, Z}; if ((Destination - Position).SqrLength() > a_SquareRadius) { // Don't bother with points outside our designated area-of-effect // This is, surprisingly, a massive amount of work saved (~3m to detonate a sphere of 37k TNT before, ~1m after): continue; } if (LineBlockTracer::Trace(a_Chunk, Callback, Position, Destination)) { Unobstructed++; } Total++; } } } return (Total == 0) ? 0 : (static_cast(Unobstructed) / Total); } /** Applies distance-based damage and knockback to all entities within the explosion's effect range. */ static void DamageEntities(const cChunk & a_Chunk, const Vector3f a_Position, const int a_Power) { const auto Radius = a_Power * 2; const auto SquareRadius = Radius * Radius; a_Chunk.GetWorld()->ForEachEntityInBox({ a_Position, Radius * 2.f }, [&a_Chunk, a_Position, a_Power, Radius, SquareRadius](cEntity & Entity) { // Percentage of rays unobstructed. const auto Exposure = CalculateEntityExposure(a_Chunk, Entity, a_Position, SquareRadius); const auto Direction = Entity.GetPosition() - a_Position; const auto Impact = (1 - (static_cast(Direction.Length()) / Radius)) * Exposure; // Don't apply damage to other TNT entities and falling blocks, they should be invincible: if (!Entity.IsTNT() && !Entity.IsFallingBlock()) { const auto Damage = (Impact * Impact + Impact) * 7 * a_Power + 1; Entity.TakeDamage(dtExplosion, nullptr, FloorC(Damage), 0); } // Impact reduced by armour, expensive call so only apply to Pawns: if (Entity.IsPawn()) { const auto ReducedImpact = Impact - Impact * Entity.GetEnchantmentBlastKnockbackReduction(); Entity.AddSpeed(Direction.NormalizeCopy() * KnockbackFactor * ReducedImpact); } else { Entity.AddSpeed(Direction.NormalizeCopy() * KnockbackFactor * Impact); } // Continue iteration: return false; }); } /** Returns true if block should always drop when exploded. Currently missing conduits from 1.13 */ static bool BlockAlwaysDrops(const BLOCKTYPE a_Block) { if (IsBlockShulkerBox(a_Block)) { return true; } switch (a_Block) { case E_BLOCK_DRAGON_EGG: case E_BLOCK_BEACON: case E_BLOCK_HEAD: return true; } return false; } /** Sets the block at the given position, updating surroundings. */ static void SetBlock(cChunk & a_Chunk, const Vector3i a_AbsolutePosition, const Vector3i a_RelativePosition, const BLOCKTYPE a_CurrentBlock, const NIBBLETYPE a_CurrentMeta, const BLOCKTYPE a_NewBlock, const cEntity * const a_ExplodingEntity) { // SetBlock wakes up all simulators for the area, so that water and lava flows and sand falls into the blasted holes // It also is responsible for calling cBlockHandler::OnNeighborChanged to pop off blocks that fail CanBeAt // An explicit call to cBlockHandler::OnBroken handles the destruction of multiblock structures // References at (FS #391, GH #4418): a_Chunk.SetBlock(a_RelativePosition, a_NewBlock, 0); auto & World = *a_Chunk.GetWorld(); cChunkInterface Interface(World.GetChunkMap()); cBlockHandler::For(a_CurrentBlock).OnBroken(Interface, World, a_AbsolutePosition, a_CurrentBlock, a_CurrentMeta, a_ExplodingEntity); } /** Work out what should happen when an explosion destroys the given block. Tasks include lighting TNT, dropping pickups, setting fire and flinging shrapnel according to Minecraft rules. OK, _mostly_ Minecraft rules. */ static void DestroyBlock(MTRand & a_Random, cChunk & a_Chunk, const Vector3i a_AbsolutePosition, const Vector3i a_RelativePosition, const BLOCKTYPE a_CurrentBlock, const NIBBLETYPE a_CurrentMeta, const cBoundingBox a_ExplosionBounds, const int a_Power, const bool a_Fiery, const cEntity * const a_ExplodingEntity) { auto & World = *a_Chunk.GetWorld(); if (a_CurrentBlock == E_BLOCK_TNT) // If the block is TNT we should set it off { // Random fuse between 10 to 30 game ticks. const int FuseTime = a_Random.RandInt(10, 30); // Activate the TNT, with initial velocity and no fuse sound: World.SpawnPrimedTNT(Vector3d(0.5, 0, 0.5) + a_AbsolutePosition, FuseTime, 1, false); } else if ((a_ExplodingEntity != nullptr) && (a_ExplodingEntity->IsTNT() || BlockAlwaysDrops(a_CurrentBlock) || a_Random.RandBool(1.f / a_Power))) // For TNT explosions, destroying a block that always drops, or if RandBool, drop pickups { for (auto & Item : cBlockHandler::For(a_CurrentBlock).ConvertToPickups(a_CurrentMeta)) { World.SpawnItemPickup(Vector3d(0.5, 0, 0.5) + a_AbsolutePosition, std::move(Item), Vector3d(), a_ExplosionBounds); } } else if (a_Fiery && a_Random.RandBool(1 / 3.0)) // 33% chance of starting fires if it can start fires { const auto Below = a_AbsolutePosition.addedY(-1); if ((Below.y >= 0) && cBlockInfo::FullyOccupiesVoxel(a_Chunk.GetBlock(Below))) { // Start a fire: SetBlock(a_Chunk, a_AbsolutePosition, a_RelativePosition, a_CurrentBlock, a_CurrentMeta, E_BLOCK_FIRE, a_ExplodingEntity); return; } } else if (const auto Shrapnel = World.GetTNTShrapnelLevel(); (Shrapnel > slNone) && a_Random.RandBool(0)) // Currently 0% chance of flinging stuff around { // If the block is shrapnel-able, make a falling block entity out of it: if ( ((Shrapnel == slAll) && cBlockInfo::FullyOccupiesVoxel(a_CurrentBlock)) || ((Shrapnel == slGravityAffectedOnly) && cSandSimulator::IsAllowedBlock(a_CurrentBlock)) ) { auto FallingBlock = std::make_unique(Vector3d(0.5, 0, 0.5) + a_AbsolutePosition, a_CurrentBlock, a_CurrentMeta); // TODO: correct velocity FallingBlock->SetSpeedY(40); FallingBlock->Initialize(std::move(FallingBlock), World); } } SetBlock(a_Chunk, a_AbsolutePosition, a_RelativePosition, a_CurrentBlock, a_CurrentMeta, E_BLOCK_AIR, a_ExplodingEntity); } /** Returns a random intensity for an Explosion Lazor (tm) as a function of the explosion's power. */ static float RandomIntensity(MTRand & a_Random, const int a_Power) { return a_Power * (0.7f + a_Random.RandReal(0.6f)); } /** Traces the path taken by one Explosion Lazor (tm) with given direction and random intensity, that will destroy blocks until it is exhausted. */ static void DestructionTrace(MTRand & a_Random, cChunk * a_Chunk, Vector3f a_Origin, const Vector3f a_Direction, const cBoundingBox a_ExplosionBounds, const int a_Power, const bool a_Fiery, const cEntity * const a_ExplodingEntity) { // The current position the ray is at. auto Checkpoint = a_Origin; auto Position = Checkpoint.Floor(); auto Intensity = RandomIntensity(a_Random, a_Power); // The displacement that the ray in one iteration step should travel. const auto Step = a_Direction.NormalizeCopy() * StepUnit; // Loop until intensity runs out: while (Intensity > 0) { if (!cChunkDef::IsValidHeight(Position)) { break; } Vector3i RelativePosition; if (!a_Chunk->GetChunkAndRelByAbsolute(Position, &a_Chunk, RelativePosition)) { break; } BLOCKTYPE CurrentBlock; NIBBLETYPE CurrentMeta; a_Chunk->GetBlockTypeMeta(RelativePosition, CurrentBlock, CurrentMeta); Intensity -= GetExplosionAbsorption(CurrentBlock); if (Intensity <= 0) { // The ray is exhausted: break; } if (CurrentBlock != E_BLOCK_AIR) { DestroyBlock(a_Random, *a_Chunk, Position, RelativePosition, CurrentBlock, CurrentMeta, a_ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); } // Increment the simulation, weaken the ray: Checkpoint += Step; Intensity -= StepAttenuation; for (int i = 0; i != 2; i++) { const auto PreviousPosition = Position; Position = Checkpoint.Floor(); if (Position != PreviousPosition) { break; } Checkpoint += Step; Intensity -= StepAttenuation + GetExplosionAbsorption(E_BLOCK_AIR); } } } /** Sends out Explosion Lazors (tm) originating from the given position that destroy blocks. */ static void DamageBlocks(cChunk & a_Chunk, const Vector3f a_Position, const int a_Power, const bool a_Fiery, const cEntity * const a_ExplodingEntity) { // Oh boy... Better hope you have a hot cache, 'cos this little manoeuvre's gonna cost us 1352 raytraces in one tick... auto & Random = GetRandomProvider(); const int HalfSide = TraceCubeSideLength / 2; const cBoundingBox ExplosionBounds(a_Position, TraceCubeSideLength); // The following loops implement the tracing algorithm described in http://minecraft.gamepedia.com/Explosion // Trace rays from the explosion centre to all points in a square of area TraceCubeSideLength * TraceCubeSideLength // for the top and bottom sides: for (float OffsetX = -HalfSide; OffsetX < HalfSide; OffsetX++) { for (float OffsetZ = -HalfSide; OffsetZ < HalfSide; OffsetZ++) { DestructionTrace(Random, &a_Chunk, a_Position, Vector3f(OffsetX, +HalfSide, OffsetZ), ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); DestructionTrace(Random, &a_Chunk, a_Position, Vector3f(OffsetX, -HalfSide, OffsetZ), ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); } } // Left and right sides, avoid duplicates at top and bottom edges: for (float OffsetX = -HalfSide; OffsetX < HalfSide; OffsetX++) { for (float OffsetY = -HalfSide + 1; OffsetY < HalfSide - 1; OffsetY++) { DestructionTrace(Random, &a_Chunk, a_Position, Vector3f(OffsetX, OffsetY, +HalfSide), ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); DestructionTrace(Random, &a_Chunk, a_Position, Vector3f(OffsetX, OffsetY, -HalfSide), ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); } } // Front and back sides, avoid all edges: for (float OffsetZ = -HalfSide + 1; OffsetZ < HalfSide - 1; OffsetZ++) { for (float OffsetY = -HalfSide + 1; OffsetY < HalfSide - 1; OffsetY++) { DestructionTrace(Random, &a_Chunk, a_Position, Vector3f(+HalfSide, OffsetY, OffsetZ), ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); DestructionTrace(Random, &a_Chunk, a_Position, Vector3f(-HalfSide, OffsetY, OffsetZ), ExplosionBounds, a_Power, a_Fiery, a_ExplodingEntity); } } } /** Sends an explosion packet to all clients in the given chunk. */ static void LagTheClient(cChunk & a_Chunk, const Vector3f a_Position, const int a_Power) { for (const auto Client : a_Chunk.GetAllClients()) { Client->SendExplosion(a_Position, static_cast(a_Power)); } } void Kaboom(cWorld & a_World, const Vector3f a_Position, const int a_Power, const bool a_Fiery, const cEntity * const a_ExplodingEntity) { a_World.DoWithChunkAt(a_Position.Floor(), [a_Position, a_Power, a_Fiery, a_ExplodingEntity](cChunk & a_Chunk) { LagTheClient(a_Chunk, a_Position, a_Power); DamageEntities(a_Chunk, a_Position, a_Power); DamageBlocks(a_Chunk, a_Position, a_Power, a_Fiery, a_ExplodingEntity); return false; }); } }