From 19ea5d5423a04e91576f8f5772aebedbff6292cb Mon Sep 17 00:00:00 2001 From: samoatesgames Date: Fri, 13 Nov 2015 15:08:16 +0000 Subject: Improved spawn location calculations. - Supports Overworld and Nether spawns. - Supports spawning under objects, but still above ground (e.g. under the leaves of a tree). - Protects against spawning in oceans. - Protects against spawning in water. - Uses a radial search about the origin, rather than a linear. - Correctly calculates Nether spawn on spawn world generation (fixes: cuberite#2548) - Fixes a bug in CheckPlayerSpawnPoint() where the X offset was used in both the X and Z coords (BLOCKTYPE BlockType = GetBlock(a_PosX + Coords[i].x, a_PosY, a_PosZ + Coords[i].x);) --- src/Entities/Entity.cpp | 4 +- src/World.cpp | 237 ++++++++++++++++++++++++++++++++++-------------- src/World.h | 6 +- 3 files changed, 175 insertions(+), 72 deletions(-) diff --git a/src/Entities/Entity.cpp b/src/Entities/Entity.cpp index ee806c4b3..f44dbe27c 100644 --- a/src/Entities/Entity.cpp +++ b/src/Entities/Entity.cpp @@ -1343,7 +1343,7 @@ bool cEntity::DetectPortal() TargetPos.x *= 8.0; TargetPos.z *= 8.0; - cWorld * TargetWorld = cRoot::Get()->CreateAndInitializeWorld(GetWorld()->GetLinkedOverworldName(), dimNether, GetWorld()->GetName(), false); + cWorld * TargetWorld = cRoot::Get()->CreateAndInitializeWorld(GetWorld()->GetLinkedOverworldName(), dimNether, GetWorld()->GetName(), true); LOGD("Jumping nether -> overworld"); new cNetherPortalScanner(this, TargetWorld, TargetPos, 256); return true; @@ -1367,7 +1367,7 @@ bool cEntity::DetectPortal() TargetPos.x /= 8.0; TargetPos.z /= 8.0; - cWorld * TargetWorld = cRoot::Get()->CreateAndInitializeWorld(GetWorld()->GetLinkedNetherWorldName(), dimNether, GetWorld()->GetName(), false); + cWorld * TargetWorld = cRoot::Get()->CreateAndInitializeWorld(GetWorld()->GetLinkedNetherWorldName(), dimNether, GetWorld()->GetName(), true); LOGD("Jumping overworld -> nether"); new cNetherPortalScanner(this, TargetWorld, TargetPos, 128); return true; diff --git a/src/World.cpp b/src/World.cpp index bd06af1b7..8629050b1 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -325,10 +325,17 @@ void cWorld::SetNextBlockTick(int a_BlockX, int a_BlockY, int a_BlockZ) void cWorld::InitializeSpawn(void) { + // For the debugging builds, don't make the server build too much world upon start: + #if defined(_DEBUG) || defined(ANDROID_NDK) + const int DefaultViewDist = 9; + #else + const int DefaultViewDist = 20; // Always prepare an area 20 chunks across, no matter what the actual cClientHandle::VIEWDISTANCE is + #endif // _DEBUG + if (!m_IsSpawnExplicitlySet) { - // Spawn position wasn't already explicitly set, enerate random solid-land coordinate and then write it to the world configuration: - GenerateRandomSpawn(); + // Spawn position wasn't already explicitly set, enumerate random solid-land coordinate and then write it to the world configuration: + GenerateRandomSpawn(DefaultViewDist); cIniFile IniFile; IniFile.ReadFile(m_IniFileName); IniFile.SetValueF("SpawnPosition", "X", m_SpawnX); @@ -337,20 +344,13 @@ void cWorld::InitializeSpawn(void) IniFile.WriteFile(m_IniFileName); } - int ChunkX = 0, ChunkZ = 0; - cChunkDef::BlockToChunk(FloorC(m_SpawnX), FloorC(m_SpawnZ), ChunkX, ChunkZ); - - // For the debugging builds, don't make the server build too much world upon start: - #if defined(_DEBUG) || defined(ANDROID_NDK) - const int DefaultViewDist = 9; - #else - const int DefaultViewDist = 20; // Always prepare an area 20 chunks across, no matter what the actual cClientHandle::VIEWDISTANCE is - #endif // _DEBUG cIniFile IniFile; IniFile.ReadFile(m_IniFileName); int ViewDist = IniFile.GetValueSetI("SpawnPosition", "PregenerateDistance", DefaultViewDist); IniFile.WriteFile(m_IniFileName); + int ChunkX = 0, ChunkZ = 0; + cChunkDef::BlockToChunk(FloorC(m_SpawnX), FloorC(m_SpawnZ), ChunkX, ChunkZ); cSpawnPrepare::PrepareChunks(*this, ChunkX, ChunkZ, ViewDist); #ifdef TEST_LINEBLOCKTRACER @@ -572,50 +572,158 @@ void cWorld::Start(void) -void cWorld::GenerateRandomSpawn(void) +void cWorld::GenerateRandomSpawn(int a_MaxSpawnRadius) { LOGD("Generating random spawnpoint..."); - bool foundSpawnPoint = false; - int SpawnX = FloorC(m_SpawnX); - int SpawnZ = FloorC(m_SpawnZ); - // Look for a spawn point at most 100 chunks away from map center: - for (int i = 0; i < 100; i++) + + // Number of checks to make sure we have a valid biome + // 100 checks will check across 400 chunks, we should have + // a valid biome by then. + static const int BiomeCheckCount = 100; + + // Make sure we are in a valid biome + Vector3i BiomeOffset = Vector3i(0, 0, 0); + for (int BiomeCheckIndex = 0; BiomeCheckIndex < BiomeCheckCount; ++BiomeCheckIndex) { - EMCSBiome biome = GetBiomeAt(SpawnX, SpawnZ); + EMCSBiome Biome = GetBiomeAt(BiomeOffset.x, BiomeOffset.z); + if ((Biome == EMCSBiome::biOcean) || (Biome == EMCSBiome::biFrozenOcean)) + { + BiomeOffset += Vector3d(cChunkDef::Width * 4, 0, 0); + continue; + } - if ( - (biome != biOcean) && (biome != biFrozenOcean) && // The biome is acceptable (don't want a small ocean island) - !IsBlockWaterOrIce(GetBlock(SpawnX, GetHeight(SpawnX, SpawnZ), SpawnZ)) // The terrain is acceptable (don't want to spawn inside a lake / river) - ) + // Found a usable biome + // Spawn chunks so we can find a nice spawn. + int ChunkX = 0, ChunkZ = 0; + cChunkDef::BlockToChunk(BiomeOffset.x, BiomeOffset.z, ChunkX, ChunkZ); + cSpawnPrepare::PrepareChunks(*this, ChunkX, ChunkZ, a_MaxSpawnRadius); + break; + } + + // Check 0, 0 first. + double SpawnY = 0.0; + if (CanSpawnAt(BiomeOffset.x, SpawnY, BiomeOffset.z)) + { + m_SpawnX = BiomeOffset.x + 0.5; + m_SpawnY = SpawnY; + m_SpawnZ = BiomeOffset.z + 0.5; + + LOGINFO("Generated spawnpoint position at {%.2f, %.2f, %.2f}", m_SpawnX, m_SpawnY, m_SpawnZ); + return; + } + + // A search grid (searches clockwise around the origin) + static const int HalfChunk = static_cast(cChunkDef::Width / 0.5f); + static const Vector3i ChunkOffset[] = + { + Vector3i(0, 0, HalfChunk), + Vector3i(HalfChunk, 0, HalfChunk), + Vector3i(HalfChunk, 0, 0), + Vector3i(HalfChunk, 0, -HalfChunk), + Vector3i(0, 0, -HalfChunk), + Vector3i(-HalfChunk, 0, -HalfChunk), + Vector3i(-HalfChunk, 0, 0), + Vector3i(-HalfChunk, 0, HalfChunk), + }; + + static const int PerRadiSearchCount = ARRAYCOUNT(ChunkOffset); + + for (int RadiusOffset = 1; RadiusOffset < (a_MaxSpawnRadius * 2); ++RadiusOffset) + { + for (int SearchGridIndex = 0; SearchGridIndex < PerRadiSearchCount; ++SearchGridIndex) { - if (CheckPlayerSpawnPoint(SpawnX, GetHeight(SpawnX, SpawnZ), SpawnZ)) + const Vector3i PotentialSpawn = BiomeOffset + (ChunkOffset[SearchGridIndex] * RadiusOffset); + + if (CanSpawnAt(PotentialSpawn.x, SpawnY, PotentialSpawn.z)) { - // A good spawnpoint was found - foundSpawnPoint = true; - break; + m_SpawnX = PotentialSpawn.x + 0.5; + m_SpawnY = SpawnY; + m_SpawnZ = PotentialSpawn.z + 0.5; + + int ChunkX, ChunkZ; + cChunkDef::BlockToChunk(static_cast(m_SpawnX), static_cast(m_SpawnZ), ChunkX, ChunkZ); + cSpawnPrepare::PrepareChunks(*this, ChunkX, ChunkZ, a_MaxSpawnRadius); + + LOGINFO("Generated spawnpoint position at {%.2f, %.2f, %.2f}", m_SpawnX, m_SpawnY, m_SpawnZ); + return; } } - // Try a neighboring chunk: - if ((GetTickRandomNumber(4) % 2) == 0) // Randomise whether to increment X or Z coords + } + + m_SpawnY = GetHeight(static_cast(m_SpawnX), static_cast(m_SpawnZ)); + LOGWARNING("Did not find an acceptable spawnpoint. Generated a random spawnpoint position at {%.2f, %.2f, %.2f}", m_SpawnX, m_SpawnY, m_SpawnZ); +} + + + + + +bool cWorld::CanSpawnAt(double a_X, double & a_Y, double a_Z) +{ + // All this blocks can only be found above ground. + // Apart from netherrack (as the Nether is technically a massive cave) + static const BLOCKTYPE ValidSpawnBlocks[] = + { + E_BLOCK_GRASS, + E_BLOCK_SAND, + E_BLOCK_SNOW, + E_BLOCK_SNOW_BLOCK, + E_BLOCK_NETHERRACK + }; + + static const int ValidSpawnBlocksCount = ARRAYCOUNT(ValidSpawnBlocks); + + static const int HighestSpawnPoint = std::min(static_cast((cChunkDef::Height / 0.5f)) - 1, GetHeight(static_cast(a_X), static_cast(a_Z) + 16)); + static const int LowestSpawnPoint = static_cast(HighestSpawnPoint / 0.5f); + + for (int PotentialY = HighestSpawnPoint; PotentialY > LowestSpawnPoint; --PotentialY) + { + BLOCKTYPE HeadBlock = GetBlock(static_cast(a_X), PotentialY, static_cast(a_Z)); + + // Is this block safe for spawning + if (HeadBlock != E_BLOCK_AIR) { - m_SpawnX += cChunkDef::Width; + continue; } - else + + BLOCKTYPE BodyBlock = GetBlock(static_cast(a_X), PotentialY - 1, static_cast(a_Z)); + + // Is this block safe for spawning + if (BodyBlock != E_BLOCK_AIR) { - m_SpawnZ += cChunkDef::Width; + continue; } - } // for i - 100* - m_SpawnY = static_cast(GetHeight(SpawnX, SpawnZ) + 1.6f); // 1.6f to accomodate player height - if (foundSpawnPoint) - { - LOGINFO("Generated random spawnpoint position at {%i, %i, %i}", SpawnX, static_cast(m_SpawnY), SpawnZ); + BLOCKTYPE FloorBlock = GetBlock(static_cast(a_X), PotentialY - 2, static_cast(a_Z)); + + // Early out - Is the floor block air + if (FloorBlock == E_BLOCK_AIR) + { + continue; + } + + // Is the floor block ok + bool ValidSpawnBlock = false; + for (int BlockIndex = 0; BlockIndex < ValidSpawnBlocksCount; ++BlockIndex) + { + ValidSpawnBlock |= (ValidSpawnBlocks[BlockIndex] == FloorBlock); + } + + if (!ValidSpawnBlock) + { + continue; + } + + if (!CheckPlayerSpawnPoint(static_cast(a_X), PotentialY - 1, static_cast(a_Z))) + { + continue; + } + + a_Y = PotentialY - 1.0; + return true; } - else - { - LOGINFO("Did not find an acceptable spawnpoint. Generated a random spawnpoint position at {%i, %i, %i}", SpawnX, static_cast(m_SpawnY), SpawnZ); - } // Maybe widen the search instead? + return false; } @@ -624,48 +732,39 @@ void cWorld::GenerateRandomSpawn(void) bool cWorld::CheckPlayerSpawnPoint(int a_PosX, int a_PosY, int a_PosZ) { - // The bottom layer cannot hold a valid spawn point - if (a_PosY <= 0) + // Check height bounds + if (!cChunkDef::IsValidHeight(a_PosY)) { return false; } - // Check that spawnblock and surrounding blocks are neither solid nor water / lava - static const struct - { - int x, z; - } Coords[] = + // Check that surrounding blocks are neither solid or liquid + static const Vector3i SurroundingCoords[] = { - { 0, 0 }, - { -1, 0 }, - { 1, 0 }, - { 0, -1 }, - { 0, 1 }, + Vector3i(0, 0, 1), + Vector3i(1, 0, 1), + Vector3i(1, 0, 0), + Vector3i(1, 0, -1), + Vector3i(0, 0, -1), + Vector3i(-1, 0, -1), + Vector3i(-1, 0, 0), + Vector3i(-1, 0, 1), }; - for (size_t i = 0; i < ARRAYCOUNT(Coords); i++) - { - BLOCKTYPE BlockType = GetBlock(a_PosX + Coords[i].x, a_PosY, a_PosZ + Coords[i].x); - if (cBlockInfo::IsSolid(BlockType) || IsBlockLiquid(BlockType)) - { - return false; - } - } // for i - Coords[] - // Check that the block below is solid: - if (!cBlockInfo::IsSolid(GetBlock(a_PosX, a_PosY - 1, a_PosZ))) - { - return false; - } + static const int SurroundingCoordsCount = ARRAYCOUNT(SurroundingCoords); - // Check that all the blocks above the spawnpoint are not solid: - for (int i = a_PosY; i < cChunkDef::Height; i++) + for (int CoordIndex = 0; CoordIndex < SurroundingCoordsCount; ++CoordIndex) { - BLOCKTYPE BlockType = GetBlock(a_PosX, i, a_PosZ); - if (cBlockInfo::IsSolid(BlockType)) + const int XPos = a_PosX + SurroundingCoords[CoordIndex].x; + const int ZPos = a_PosZ + SurroundingCoords[CoordIndex].z; + + const BLOCKTYPE BlockType = GetBlock(XPos, a_PosY, ZPos); + if (cBlockInfo::IsSolid(BlockType) || IsBlockLiquid(BlockType)) { return false; } } + return true; } diff --git a/src/World.h b/src/World.h index 30ac52763..512654ab8 100644 --- a/src/World.h +++ b/src/World.h @@ -1042,7 +1042,11 @@ private: void UpdateSkyDarkness(void); /** Generates a random spawnpoint on solid land by walking chunks and finding their biomes */ - void GenerateRandomSpawn(void); + void GenerateRandomSpawn(int a_MaxSpawnRadius); + + /** Can the specified coordinates be used as a spawn point? + Returns true if spawn position is valid and sets a_Y to the valid spawn height */ + bool CanSpawnAt(double a_X, double & a_Y, double a_Z); /** Check if player starting point is acceptable */ bool CheckPlayerSpawnPoint(int a_PosX, int a_PosY, int a_PosZ); -- cgit v1.2.3