diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index 0924a91a..9a9ce81c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -78,7 +78,7 @@ fun main() {
 					var reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater())))
 					reader.skipBytes(3)
 
-					val chunk = client.world!!.chunkMap.compute(chunkX - 2, chunkY)
+					val chunk = client.world!!.chunkMap.compute(chunkX, chunkY)
 
 					if (chunk != null) {
 						for (y in 0 .. 31) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
index e0771b53..b98b5b45 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
@@ -32,7 +32,7 @@ import java.util.concurrent.Future
 class ClientWorld(
 	val client: StarboundClient,
 	seed: Long,
-	size: Vector2i? = null,
+	size: Vector2i,
 	loopX: Boolean = false,
 	loopY: Boolean = false
 ) : World<ClientWorld, ClientChunk>(seed, size, loopX, loopY) {
@@ -49,13 +49,13 @@ class ClientWorld(
 	override val isClient: Boolean
 		get() = true
 
-	val renderRegionWidth = if (size == null) 16 else determineChunkSize(size.x)
-	val renderRegionHeight = if (size == null) 16 else determineChunkSize(size.y)
-	val renderRegionsX = if (size == null) 0 else size.x / renderRegionWidth
-	val renderRegionsY = if (size == null) 0 else size.y / renderRegionHeight
+	val renderRegionWidth = determineChunkSize(size.x)
+	val renderRegionHeight = determineChunkSize(size.y)
+	val renderRegionsX = size.x / renderRegionWidth
+	val renderRegionsY = size.y / renderRegionHeight
 
 	fun isValidRenderRegionX(value: Int): Boolean {
-		if (size == null || loopX) {
+		if (loopX) {
 			return true
 		} else {
 			return value in 0 .. renderRegionsX
@@ -63,7 +63,7 @@ class ClientWorld(
 	}
 
 	fun isValidRenderRegionY(value: Int): Boolean {
-		if (size == null || loopY) {
+		if (loopY) {
 			return true
 		} else {
 			return value in 0 .. renderRegionsY
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt
index b5d0af82..6e9c2b07 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt
@@ -37,23 +37,6 @@ abstract class CoordinateMapper {
 	open fun isValidCellIndex(value: Int): Boolean = inBoundsCell(value)
 	open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value)
 
-	object Infinite : CoordinateMapper() {
-		override val chunks: Int
-			get() = Int.MAX_VALUE
-
-		override fun cell(value: Int): Int = value
-		override fun cell(value: Double): Double = value
-		override fun cell(value: Float): Float = value
-		override fun chunk(value: Int): Int = value
-
-		override fun chunkFromCell(value: Int): Int {
-			return value shr CHUNK_SIZE_BITS
-		}
-
-		override fun inBoundsCell(value: Int) = true
-		override fun inBoundsChunk(value: Int) = true
-	}
-
 	class Wrapper(private val cells: Int) : CoordinateMapper() {
 		override val chunks = divideUp(cells, CHUNK_SIZE)
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
index cc89824f..5490c17a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
@@ -26,15 +26,14 @@ import java.util.concurrent.ForkJoinPool
 import java.util.concurrent.locks.ReentrantLock
 import java.util.random.RandomGenerator
 
-@Suppress("UNCHECKED_CAST")
 abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
 	val seed: Long,
-	val size: Vector2i?,
+	val size: Vector2i,
 	val loopX: Boolean,
 	val loopY: Boolean
 ) : ICellAccess {
-	val x: CoordinateMapper = if (size == null) CoordinateMapper.Infinite else if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x)
-	val y: CoordinateMapper = if (size == null) CoordinateMapper.Infinite else if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y)
+	val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x)
+	val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y)
 
 	// whenever provided cell position is within actual world borders, ignoring wrapping
 	fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y)
@@ -96,8 +95,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		}
 	}
 
-	// hash chunk map is around 30% slower than rectangular one
-	inner class HashChunkMap : ChunkMap() {
+	inner class SparseChunkMap : ChunkMap() {
 		private val map = Long2ObjectOpenHashMap<ChunkType>()
 
 		override fun getCell(x: Int, y: Int): AbstractCell {
@@ -107,13 +105,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 			return this[this@World.x.chunkFromCell(ix), this@World.y.chunkFromCell(iy)]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: AbstractCell.NULL
 		}
 
-		@Suppress("NAME_SHADOWING")
 		override fun get(x: Int, y: Int): ChunkType? {
-			if (!this@World.x.isValidChunkIndex(x) || !this@World.y.isValidChunkIndex(y)) return null
-
-			val x = this@World.x.chunk(x)
-			val y = this@World.y.chunk(y)
-
+			if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
 			return map[ChunkPos.toLong(x, y)]
 		}
 
@@ -137,10 +130,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 	}
 
 	inner class ArrayChunkMap : ChunkMap() {
-		val width = size!!.x
-		val height = size!!.y
-
-		private val map = Object2DArray.nulls<ChunkType>(divideUp(width, CHUNK_SIZE), divideUp(height, CHUNK_SIZE))
+		private val map = Object2DArray.nulls<ChunkType>(divideUp(size.x, CHUNK_SIZE), divideUp(size.y, CHUNK_SIZE))
 
 		private fun getRaw(x: Int, y: Int): ChunkType {
 			return map[x, y] ?: create(x, y).also { map[x, y] = it }
@@ -166,8 +156,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		}
 
 		override fun get(x: Int, y: Int): ChunkType? {
-			if (!this@World.x.isValidChunkIndex(x) || !this@World.y.isValidChunkIndex(y)) return null
-			return getRaw(this@World.x.chunk(x), this@World.y.chunk(y))
+			if (!this@World.x.inBoundsChunk(x) || !this@World.y.inBoundsChunk(y)) return null
+			return getRaw(x, y)
 		}
 
 		override fun remove(x: Int, y: Int) {
@@ -175,7 +165,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		}
 	}
 
-	val chunkMap: ChunkMap = if (size != null) ArrayChunkMap() else HashChunkMap()
+	val chunkMap: ChunkMap = if (size.x <= 32000 && size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
 
 	val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus")
 	var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)