// LineBlockTracer.cpp // Implements the cLineBlockTracer class representing a cBlockTracer that traces along a straight line between two points #include "Globals.h" #include "LineBlockTracer.h" #include "BlockInfo.h" #include "World.h" #include "Chunk.h" #include "BoundingBox.h" cLineBlockTracer::cLineBlockTracer(cWorld & a_World, cCallbacks & a_Callbacks) : Super(a_World, a_Callbacks), m_Start(), m_End(), m_Diff(), m_Dir(), m_Current(), m_CurrentFace(BLOCK_FACE_NONE) { } bool cLineBlockTracer::Trace(cWorld & a_World, cBlockTracer::cCallbacks & a_Callbacks, const Vector3d a_Start, const Vector3d a_End) { cLineBlockTracer Tracer(a_World, a_Callbacks); return Tracer.Trace(a_Start, a_End); } bool cLineBlockTracer::LineOfSightTrace(cWorld & a_World, const Vector3d & a_Start, const Vector3d & a_End, int a_Sight) { static class LineOfSightCallbacks: public cLineBlockTracer::cCallbacks { bool m_IsAirOpaque; bool m_IsWaterOpaque; bool m_IsLavaOpaque; public: LineOfSightCallbacks(bool a_IsAirOpaque, bool a_IsWaterOpaque, bool a_IsLavaOpaque): m_IsAirOpaque(a_IsAirOpaque), m_IsWaterOpaque(a_IsWaterOpaque), m_IsLavaOpaque(a_IsLavaOpaque) {} virtual bool OnNextBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, eBlockFace a_EntryFace) override { switch (a_BlockType) { case E_BLOCK_AIR: return m_IsAirOpaque; case E_BLOCK_LAVA: return m_IsLavaOpaque; case E_BLOCK_STATIONARY_LAVA: return m_IsLavaOpaque; case E_BLOCK_STATIONARY_WATER: return m_IsWaterOpaque; case E_BLOCK_WATER: return m_IsWaterOpaque; default: return true; } } } callbacks( (a_Sight & losAir) == 0, (a_Sight & losWater) == 0, (a_Sight & losLava) == 0 ); return Trace(a_World, callbacks, a_Start, a_End); } bool cLineBlockTracer::FirstSolidHitTrace( cWorld & a_World, const Vector3d & a_Start, const Vector3d & a_End, Vector3d & a_HitCoords, Vector3i & a_HitBlockCoords, eBlockFace & a_HitBlockFace ) { class cSolidHitCallbacks: public cCallbacks { public: cSolidHitCallbacks(const Vector3d & a_CBStart, const Vector3d & a_CBEnd, Vector3d & a_CBHitCoords, Vector3i & a_CBHitBlockCoords, eBlockFace & a_CBHitBlockFace): m_Start(a_CBStart), m_End(a_CBEnd), m_HitCoords(a_CBHitCoords), m_HitBlockCoords(a_CBHitBlockCoords), m_HitBlockFace(a_CBHitBlockFace) { } virtual bool OnNextBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, eBlockFace a_EntryFace) override { if (!cBlockInfo::IsSolid(a_BlockType)) { return false; } // We hit a solid block, calculate the exact hit coords and abort trace: m_HitBlockCoords = a_BlockPos; m_HitBlockFace = a_EntryFace; cBoundingBox bb(a_BlockPos, a_BlockPos + Vector3i(1, 1, 1)); // Bounding box of the block hit double LineCoeff = 0; // Used to calculate where along the line an intersection with the bounding box occurs eBlockFace Face; // Face hit if (!bb.CalcLineIntersection(m_Start, m_End, LineCoeff, Face)) { // Math rounding errors have caused the calculation to miss the block completely, assume immediate hit LineCoeff = 0; } m_HitCoords = m_Start + (m_End - m_Start) * LineCoeff; // Point where projectile goes into the hit block return true; } protected: const Vector3d & m_Start; const Vector3d & m_End; Vector3d & m_HitCoords; Vector3i & m_HitBlockCoords; eBlockFace & m_HitBlockFace; } callbacks(a_Start, a_End, a_HitCoords, a_HitBlockCoords, a_HitBlockFace); return !Trace(a_World, callbacks, a_Start, a_End); } bool cLineBlockTracer::Trace(const Vector3d a_Start, const Vector3d a_End) { // Initialize the member veriables: m_Start = a_Start; m_End = a_End; m_Dir.x = (m_Start.x < m_End.x) ? 1 : -1; m_Dir.y = (m_Start.y < m_End.y) ? 1 : -1; m_Dir.z = (m_Start.z < m_End.z) ? 1 : -1; m_CurrentFace = BLOCK_FACE_NONE; // Check the start coords, adjust into the world: if (m_Start.y < 0) { if (m_End.y < 0) { // Nothing to trace m_Callbacks->OnNoMoreHits(); return true; } FixStartBelowWorld(); m_Callbacks->OnIntoWorld(m_Start); } else if (m_Start.y >= cChunkDef::Height) { if (m_End.y >= cChunkDef::Height) { m_Callbacks->OnNoMoreHits(); return true; } FixStartAboveWorld(); m_Callbacks->OnIntoWorld(m_Start); } m_Current = m_Start.Floor(); m_Diff = m_End - m_Start; // The actual trace is handled with ChunkMapCS locked by calling our ChunkCallback for the specified chunk int BlockX = FloorC(m_Start.x); int BlockZ = FloorC(m_Start.z); int ChunkX, ChunkZ; cChunkDef::BlockToChunk(BlockX, BlockZ, ChunkX, ChunkZ); return m_World->DoWithChunk(ChunkX, ChunkZ, [this](cChunk & a_Chunk) { return ChunkCallback(&a_Chunk); }); } void cLineBlockTracer::FixStartAboveWorld(void) { // We must set the start Y to less than cChunkDef::Height so that it is considered inside the world later on // Therefore we use an EPS-offset from the height, as small as reasonably possible. const double Height = static_cast(cChunkDef::Height) - 0.00001; CalcXZIntersection(Height, m_Start.x, m_Start.z); m_Start.y = Height; } void cLineBlockTracer::FixStartBelowWorld(void) { CalcXZIntersection(0, m_Start.x, m_Start.z); m_Start.y = 0; } void cLineBlockTracer::CalcXZIntersection(double a_Y, double & a_IntersectX, double & a_IntersectZ) { double Ratio = (m_Start.y - a_Y) / (m_Start.y - m_End.y); a_IntersectX = m_Start.x + (m_End.x - m_Start.x) * Ratio; a_IntersectZ = m_Start.z + (m_End.z - m_Start.z) * Ratio; } bool cLineBlockTracer::MoveToNextBlock(void) { // Find out which of the current block's walls gets hit by the path: static const double EPS = 0.00001; enum { dirNONE, dirX, dirY, dirZ, } Direction = dirNONE; // Calculate the next YZ wall hit: double Coeff = 1; if (std::abs(m_Diff.x) > EPS) { double DestX = (m_Dir.x > 0) ? (m_Current.x + 1) : m_Current.x; double CoeffX = (DestX - m_Start.x) / m_Diff.x; if (CoeffX <= 1) // We need to include equality for the last block in the trace { Coeff = CoeffX; Direction = dirX; } } // If the next XZ wall hit is closer, use it instead: if (std::abs(m_Diff.y) > EPS) { double DestY = (m_Dir.y > 0) ? (m_Current.y + 1) : m_Current.y; double CoeffY = (DestY - m_Start.y) / m_Diff.y; if (CoeffY <= Coeff) // We need to include equality for the last block in the trace { Coeff = CoeffY; Direction = dirY; } } // If the next XY wall hit is closer, use it instead: if (std::abs(m_Diff.z) > EPS) { double DestZ = (m_Dir.z > 0) ? (m_Current.z + 1) : m_Current.z; double CoeffZ = (DestZ - m_Start.z) / m_Diff.z; if (CoeffZ <= Coeff) // We need to include equality for the last block in the trace { Direction = dirZ; } } // Based on the wall hit, adjust the current coords switch (Direction) { case dirX: m_Current.x += m_Dir.x; m_CurrentFace = (m_Dir.x > 0) ? BLOCK_FACE_XM : BLOCK_FACE_XP; break; case dirY: m_Current.y += m_Dir.y; m_CurrentFace = (m_Dir.y > 0) ? BLOCK_FACE_YM : BLOCK_FACE_YP; break; case dirZ: m_Current.z += m_Dir.z; m_CurrentFace = (m_Dir.z > 0) ? BLOCK_FACE_ZM : BLOCK_FACE_ZP; break; case dirNONE: return false; } return true; } bool cLineBlockTracer::ChunkCallback(cChunk * a_Chunk) { ASSERT((m_Current.y >= 0) && (m_Current.y < cChunkDef::Height)); // This should be provided by FixStartAboveWorld() / FixStartBelowWorld() // This is the actual line tracing loop. for (;;) { // Report the current block through the callbacks: if (a_Chunk == nullptr) { m_Callbacks->OnNoChunk(); return false; } // Move to next block if (!MoveToNextBlock()) { // We've reached the end m_Callbacks->OnNoMoreHits(); return true; } if ((m_Current.y < 0) || (m_Current.y >= cChunkDef::Height)) { // We've gone out of the world, that's the end of this trace double IntersectX, IntersectZ; CalcXZIntersection(m_Current.y, IntersectX, IntersectZ); if (m_Callbacks->OnOutOfWorld({IntersectX, double(m_Current.y), IntersectZ})) { // The callback terminated the trace return false; } m_Callbacks->OnNoMoreHits(); return true; } // Update the current chunk a_Chunk = a_Chunk->GetNeighborChunk(m_Current.x, m_Current.z); if (a_Chunk == nullptr) { m_Callbacks->OnNoChunk(); return false; } if (a_Chunk->IsValid()) { BLOCKTYPE BlockType; NIBBLETYPE BlockMeta; int RelX = m_Current.x - a_Chunk->GetPosX() * cChunkDef::Width; int RelZ = m_Current.z - a_Chunk->GetPosZ() * cChunkDef::Width; a_Chunk->GetBlockTypeMeta(RelX, m_Current.y, RelZ, BlockType, BlockMeta); if (m_Callbacks->OnNextBlock(m_Current, BlockType, BlockMeta, m_CurrentFace)) { // The callback terminated the trace return false; } } else if (m_Callbacks->OnNextBlockNoData(m_Current, m_CurrentFace)) { // The callback terminated the trace return false; } } }