diff --git a/gradle.properties b/gradle.properties
index 82366e11..40ece3d5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,7 +2,7 @@ kotlin.code.style=official
 org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
 
 kotlinVersion=1.9.0
-kommonsVersion=2.1.5
+kommonsVersion=2.1.8
 
 ffiVersion=2.2.13
 lwjglVersion=3.3.0
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index 633247b8..4b03bfe1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -83,7 +83,8 @@ fun main() {
 			//item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0))
 		}
 
-		client.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())
+		//client.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())
+		client.connectToRemoteServer(InetSocketAddress("127.0.0.1", 21025), UUID.randomUUID())
 		//client2.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())
 		server.channels.createChannel(InetSocketAddress(21060))
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt
index 545d01f3..d1dd17a0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt
@@ -24,6 +24,8 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid:
 		sendAndFlush(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION))
 	}
 
+	var connectionID: Int = -1
+
 	override fun inGame() {
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt
index 51805abc..43f5c84b 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt
@@ -2,10 +2,10 @@ package ru.dbotthepony.kstarbound.client.network.packets
 
 import ru.dbotthepony.kommons.io.readCollection
 import ru.dbotthepony.kommons.io.writeCollection
+import ru.dbotthepony.kommons.io.writeStruct2i
 import ru.dbotthepony.kstarbound.client.ClientConnection
 import ru.dbotthepony.kstarbound.network.IClientPacket
 import ru.dbotthepony.kstarbound.io.readChunkPos
-import ru.dbotthepony.kstarbound.io.writeVec2i
 import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
 import ru.dbotthepony.kstarbound.world.Chunk
 import ru.dbotthepony.kstarbound.world.ChunkPos
@@ -25,7 +25,7 @@ class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClie
 	})
 
 	override fun write(stream: DataOutputStream, isLegacy: Boolean) {
-		stream.writeVec2i(pos)
+		stream.writeStruct2i(pos)
 		stream.writeCollection(data) { it.write(stream) }
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt
index 05ae5a50..90a75e5f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt
@@ -1,9 +1,9 @@
 package ru.dbotthepony.kstarbound.client.network.packets
 
+import ru.dbotthepony.kommons.io.writeStruct2i
 import ru.dbotthepony.kstarbound.client.ClientConnection
 import ru.dbotthepony.kstarbound.network.IClientPacket
 import ru.dbotthepony.kstarbound.io.readChunkPos
-import ru.dbotthepony.kstarbound.io.writeVec2i
 import ru.dbotthepony.kstarbound.world.ChunkPos
 import java.io.DataInputStream
 import java.io.DataOutputStream
@@ -12,7 +12,7 @@ class ForgetChunkPacket(val pos: ChunkPos) : IClientPacket {
 	constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos())
 
 	override fun write(stream: DataOutputStream, isLegacy: Boolean) {
-		stream.writeVec2i(pos)
+		stream.writeStruct2i(pos)
 	}
 
 	override fun play(connection: ClientConnection) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt
index 06e7e8e1..6ea60274 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt
@@ -1,8 +1,8 @@
 package ru.dbotthepony.kstarbound.defs
 
+import ru.dbotthepony.kommons.io.readVector2i
 import ru.dbotthepony.kommons.io.writeStruct2i
 import ru.dbotthepony.kommons.vector.Vector2i
-import ru.dbotthepony.kstarbound.io.readVec2i
 import ru.dbotthepony.kstarbound.json.builder.JsonFactory
 import java.io.DataInputStream
 import java.io.DataOutputStream
@@ -19,8 +19,8 @@ data class CelestialBaseInformation(
 		stream.readInt(),
 		stream.readInt(),
 		stream.readInt(),
-		stream.readVec2i(),
-		stream.readVec2i(),
+		stream.readVector2i(),
+		stream.readVector2i(),
 	)
 
 	fun write(stream: DataOutputStream, isLegacy: Boolean) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ShipUpgrades.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ShipUpgrades.kt
index 47bcfc51..f2741c4a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ShipUpgrades.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ShipUpgrades.kt
@@ -19,6 +19,15 @@ data class ShipUpgrades(
 	val shipSpeed: Int = 0,
 	val capabilities: ImmutableSet<String> = ImmutableSet.of()
 ) {
+	constructor(stream: DataInputStream, isLegacy: Boolean) : this(
+		stream.readInt(),
+		stream.readInt(),
+		stream.readInt(),
+		if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(),
+		stream.readInt(),
+		ImmutableSet.copyOf(stream.readCollection { readBinaryString() })
+	)
+
 	fun apply(upgrades: ShipUpgrades): ShipUpgrades {
 		return ShipUpgrades(
 			shipLevel = shipLevel.coerceAtLeast(upgrades.shipLevel),
@@ -43,17 +52,4 @@ data class ShipUpgrades(
 		stream.writeInt(shipSpeed)
 		stream.writeCollection(capabilities) { writeBinaryString(it) }
 	}
-
-	companion object {
-		fun read(stream: DataInputStream, isLegacy: Boolean): ShipUpgrades {
-			return ShipUpgrades(
-				stream.readInt(),
-				stream.readInt(),
-				stream.readInt(),
-				if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(),
-				stream.readInt(),
-				ImmutableSet.copyOf(stream.readCollection { readBinaryString() })
-			)
-		}
-	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt
index 073601eb..3cc37131 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt
@@ -37,24 +37,6 @@ fun InputStream.readHeader(header: String) {
 	}
 }
 
-fun OutputStream.writeVec2i(value: IStruct2i) {
-	writeSignedVarInt(value.component1())
-	writeSignedVarInt(value.component2())
-}
-
-fun OutputStream.writeVec2d(value: IStruct2d) {
-	writeDouble(value.component1())
-	writeDouble(value.component2())
-}
-
-fun InputStream.readVec2i(): Vector2i {
-	return Vector2i(readSignedVarInt(), readSignedVarInt())
-}
-
-fun InputStream.readVec2d(): Vector2d {
-	return Vector2d(readDouble(), readDouble())
-}
-
 fun InputStream.readChunkPos(): ChunkPos {
 	return ChunkPos(readSignedVarInt(), readSignedVarInt())
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt
index 116b9f91..95941354 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt
@@ -24,7 +24,7 @@ import java.util.LinkedList
  */
 fun DataInputStream.readJsonElement(): JsonElement {
 	return when (val id = read()) {
-		BinaryJsonReader.TYPE_NULL -> JsonNull.INSTANCE
+		BinaryJsonReader.TYPE_INVALID, BinaryJsonReader.TYPE_NULL -> JsonNull.INSTANCE
 		BinaryJsonReader.TYPE_DOUBLE -> JsonPrimitive(readDouble())
 		BinaryJsonReader.TYPE_BOOLEAN -> InternedJsonElementAdapter.of(readBoolean())
 		BinaryJsonReader.TYPE_INT -> JsonPrimitive(readSignedVarLong())
@@ -388,6 +388,7 @@ class BinaryJsonReader(private val stream: DataInputStream) : JsonReader(unreada
 	}
 
 	companion object {
+		const val TYPE_INVALID = 0x00
 		const val TYPE_NULL = 0x01
 		const val TYPE_DOUBLE = 0x02
 		const val TYPE_BOOLEAN = 0x03
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt
index df49c653..f7bcd292 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt
@@ -35,7 +35,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
 	private val legacySerializer = PacketRegistry.LEGACY.Serializer(side)
 
 	fun setupLegacy() {
-		LOGGER.info("Handshake successful from ${channel.remoteAddress()}, channel is using legacy protocol")
+		LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol")
 
 		if (type == ConnectionType.MEMORY) {
 			channel.pipeline().remove(handshakeValidator)
@@ -49,7 +49,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
 	}
 
 	fun setupNative() {
-		LOGGER.info("Handshake successful from ${channel.remoteAddress()}, channel is using native protocol")
+		LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol")
 
 		if (type == ConnectionType.MEMORY) {
 			channel.pipeline().remove(handshakeValidator)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt
index 7f5122c3..371102ee 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt
@@ -7,12 +7,15 @@ import io.netty.channel.ChannelDuplexHandler
 import io.netty.channel.ChannelHandlerContext
 import io.netty.channel.ChannelPromise
 import it.unimi.dsi.fastutil.bytes.ByteArrayList
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
 import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
 import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
 import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kommons.io.readSignedVarInt
+import ru.dbotthepony.kommons.io.readVarInt
 import ru.dbotthepony.kommons.io.writeSignedVarInt
+import ru.dbotthepony.kommons.io.writeVarInt
 import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
 import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
 import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
@@ -25,6 +28,7 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePa
 import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
 import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket
 import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
+import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
 import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket
 import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket
 import java.io.BufferedInputStream
@@ -37,6 +41,7 @@ import kotlin.reflect.KClass
 
 class PacketRegistry(val isLegacy: Boolean) {
 	private val packets = ArrayList<Type<*>?>()
+	private val missingNames = Int2ObjectArrayMap<String>()
 	private val clazz2Type = Reference2ObjectOpenHashMap<KClass<*>, Type<*>>()
 
 	private data class Type<T : IPacket>(val id: Int, val type: KClass<T>, val factory: IPacketReader<T>, val direction: PacketDirection)
@@ -67,6 +72,11 @@ class PacketRegistry(val isLegacy: Boolean) {
 		}
 	}
 
+	private fun skip(name: String) {
+		missingNames[packets.size] = name
+		packets.add(null)
+	}
+
 	inner class Serializer(val side: ConnectionSide) : ChannelDuplexHandler() {
 		private val backlog = ByteArrayList()
 		private var discardBytes = 0
@@ -121,12 +131,19 @@ class PacketRegistry(val isLegacy: Boolean) {
 						val type = packets.getOrNull(packetType)
 
 						if (type == null) {
-							LOGGER.error("Unknown packet type $packetType! Discarding ${dataLength.absoluteValue} bytes")
+							val name = missingNames[packetType]
+
+							if (name != null)
+								LOGGER.error("Unknown packet type $packetType ($name)! Discarding ${dataLength.absoluteValue} bytes")
+							else
+								LOGGER.error("Unknown packet type $packetType! Discarding ${dataLength.absoluteValue} bytes")
+
 							discardBytes = dataLength.absoluteValue
 						} else if (!type.direction.acceptedOn(side)) {
 							LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes")
 							discardBytes = dataLength.absoluteValue
 						} else {
+							LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue)
 							readingType = type
 							readableBytes = dataLength.absoluteValue
 							isCompressed = dataLength < 0
@@ -159,6 +176,7 @@ class PacketRegistry(val isLegacy: Boolean) {
 				stream2.writeByte(type.id)
 				stream2.writeSignedVarInt(stream.length)
 				stream2.write(stream.array, 0, stream.length)
+				LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length)
 				ctx.write(buff, promise)
 			}
 		}
@@ -213,92 +231,100 @@ class PacketRegistry(val isLegacy: Boolean) {
 			HANDSHAKE.add(::ProtocolRequestPacket)
 			HANDSHAKE.add(::ProtocolResponsePacket)
 
-			LEGACY.skip() // ProtocolRequest
-			LEGACY.skip() // ProtocolResponse
+			// legacy protocol handshake looks like this
+			// --> ProtocolRequest
+			// <-- ProtocolResponse
+			// --> ClientConnect
+			// <-- HandshakeChallenge *
+			// --> HandshakeResponse *
+			// <-- ConnectSuccess / ConnectFailure
+
+			LEGACY.skip("ProtocolRequest")
+			LEGACY.skip("ProtocolResponse")
 
 			// Packets sent universe server -> universe client
 			LEGACY.add(::ServerDisconnectPacket) // ServerDisconnect
 			LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess
-			LEGACY.skip() // ConnectFailure
+			LEGACY.skip("ConnectFailure")
 			LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge
-			LEGACY.skip() // ChatReceive
-			LEGACY.skip() // UniverseTimeUpdate
-			LEGACY.skip() // CelestialResponse
-			LEGACY.skip() // PlayerWarpResult
-			LEGACY.skip() // PlanetTypeUpdate
-			LEGACY.skip() // Pause
-			LEGACY.skip() // ServerInfo
+			LEGACY.skip("ChatReceive")
+			LEGACY.skip("UniverseTimeUpdate")
+			LEGACY.skip("CelestialResponse")
+			LEGACY.skip("PlayerWarpResult")
+			LEGACY.skip("PlanetTypeUpdate")
+			LEGACY.skip("Pause")
+			LEGACY.skip("ServerInfo")
 
 			// Packets sent universe client -> universe server
 			LEGACY.add(::ClientConnectPacket) // ClientConnect
-			LEGACY.skip() // ClientDisconnectRequest
+			LEGACY.skip("ClientDisconnectRequest")
 			LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse
-			LEGACY.skip() // PlayerWarp
-			LEGACY.skip() // FlyShip
-			LEGACY.skip() // ChatSend
-			LEGACY.skip() // CelestialRequest
+			LEGACY.skip("PlayerWarp")
+			LEGACY.skip("FlyShip")
+			LEGACY.skip("ChatSend")
+			LEGACY.skip("CelestialRequest")
 
 			// Packets sent bidirectionally between the universe client and the universe
 			// server
-			LEGACY.skip() // ClientContextUpdate
+			LEGACY.skip("ClientContextUpdate")
 
 			// Packets sent world server -> world client
-			LEGACY.skip() // WorldStart
-			LEGACY.skip() // WorldStop
-			LEGACY.skip() // WorldLayoutUpdate
-			LEGACY.skip() // WorldParametersUpdate
-			LEGACY.skip() // CentralStructureUpdate
-			LEGACY.skip() // TileArrayUpdate
-			LEGACY.skip() // TileUpdate
-			LEGACY.skip() // TileLiquidUpdate
-			LEGACY.skip() // TileDamageUpdate
-			LEGACY.skip() // TileModificationFailure
-			LEGACY.skip() // GiveItem
-			LEGACY.skip() // EnvironmentUpdate
-			LEGACY.skip() // UpdateTileProtection
-			LEGACY.skip() // SetDungeonGravity
-			LEGACY.skip() // SetDungeonBreathable
-			LEGACY.skip() // SetPlayerStart
-			LEGACY.skip() // FindUniqueEntityResponse
-			LEGACY.skip() // Pong
+			LEGACY.add(::WorldStartPacket) // WorldStart
+			LEGACY.skip("WorldStop")
+			LEGACY.skip("WorldLayoutUpdate")
+			LEGACY.skip("WorldParametersUpdate")
+			LEGACY.skip("CentralStructureUpdate")
+			LEGACY.skip("TileArrayUpdate")
+			LEGACY.skip("TileUpdate")
+			LEGACY.skip("TileLiquidUpdate")
+			LEGACY.skip("TileDamageUpdate")
+			LEGACY.skip("TileModificationFailure")
+			LEGACY.skip("GiveItem")
+			LEGACY.skip("EnvironmentUpdate")
+			LEGACY.skip("UpdateTileProtection")
+			LEGACY.skip("SetDungeonGravity")
+			LEGACY.skip("SetDungeonBreathable")
+			LEGACY.skip("SetPlayerStart")
+			LEGACY.skip("FindUniqueEntityResponse")
+			LEGACY.skip("Pong")
 
 			// Packets sent world client -> world server
-			LEGACY.skip() // ModifyTileList
-			LEGACY.skip() // DamageTileGroup
-			LEGACY.skip() // CollectLiquid
-			LEGACY.skip() // RequestDrop
-			LEGACY.skip() // SpawnEntity
-			LEGACY.skip() // ConnectWire
-			LEGACY.skip() // DisconnectAllWires
-			LEGACY.skip() // WorldClientStateUpdate
-			LEGACY.skip() // FindUniqueEntity
-			LEGACY.skip() // WorldStartAcknowledge
-			LEGACY.skip() // Ping
+			LEGACY.skip("ModifyTileList")
+			LEGACY.skip("DamageTileGroup")
+			LEGACY.skip("CollectLiquid")
+			LEGACY.skip("RequestDrop")
+			LEGACY.skip("SpawnEntity")
+			LEGACY.skip("ConnectWire")
+			LEGACY.skip("DisconnectAllWires")
+			LEGACY.skip("WorldClientStateUpdate")
+			LEGACY.skip("FindUniqueEntity")
+			LEGACY.skip("WorldStartAcknowledge")
+			LEGACY.skip("Ping")
 
 			// Packets sent bidirectionally between world client and world server
-			LEGACY.skip() // EntityCreate
-			LEGACY.skip() // EntityUpdateSet
-			LEGACY.skip() // EntityDestroy
-			LEGACY.skip() // EntityInteract
-			LEGACY.skip() // EntityInteractResult
-			LEGACY.skip() // HitRequest
-			LEGACY.skip() // DamageRequest
-			LEGACY.skip() // DamageNotification
-			LEGACY.skip() // EntityMessage
-			LEGACY.skip() // EntityMessageResponse
-			LEGACY.skip() // UpdateWorldProperties
-			LEGACY.skip() // StepUpdate
+			LEGACY.skip("EntityCreate")
+			LEGACY.skip("EntityUpdateSet")
+			LEGACY.skip("EntityDestroy")
+			LEGACY.skip("EntityInteract")
+			LEGACY.skip("EntityInteractResult")
+			LEGACY.skip("HitRequest")
+			LEGACY.skip("DamageRequest")
+			LEGACY.skip("DamageNotification")
+			LEGACY.skip("EntityMessage")
+			LEGACY.skip("EntityMessageResponse")
+			LEGACY.skip("UpdateWorldProperties")
+			LEGACY.skip("StepUpdate")
 
 			// Packets sent system server -> system client
-			LEGACY.skip() // SystemWorldStart
-			LEGACY.skip() // SystemWorldUpdate
-			LEGACY.skip() // SystemObjectCreate
-			LEGACY.skip() // SystemObjectDestroy
-			LEGACY.skip() // SystemShipCreate
-			LEGACY.skip() // SystemShipDestroy
+			LEGACY.skip("SystemWorldStart")
+			LEGACY.skip("SystemWorldUpdate")
+			LEGACY.skip("SystemObjectCreate")
+			LEGACY.skip("SystemObjectDestroy")
+			LEGACY.skip("SystemShipCreate")
+			LEGACY.skip("SystemShipDestroy")
 
 			// Packets sent system client -> system server
-			LEGACY.skip() // SystemObjectSpawn
+			LEGACY.skip("SystemObjectSpawn")
 		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt
index 9c3710bb..d6b418d5 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt
@@ -29,9 +29,9 @@ data class ProtocolResponsePacket(val allowed: Boolean) : IClientPacket {
 						playerSpecies = "hylotl",
 						shipChunks = HashMap(),
 						shipUpgrades = ShipUpgrades(),
-						introComplete = false,
+						introComplete = true,
 						account = ""
-				)
+					)
 				)
 			} else {
 				connection.setupNative()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt
index 1d8678a6..5f4f7761 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt
@@ -1,7 +1,9 @@
 package ru.dbotthepony.kstarbound.network.packets.clientbound
 
 import ru.dbotthepony.kommons.io.readUUID
+import ru.dbotthepony.kommons.io.readVarInt
 import ru.dbotthepony.kommons.io.writeUUID
+import ru.dbotthepony.kommons.io.writeVarInt
 import ru.dbotthepony.kstarbound.client.ClientConnection
 import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
 import ru.dbotthepony.kstarbound.network.IClientPacket
@@ -11,18 +13,18 @@ import java.util.UUID
 
 class ConnectSuccessPacket(val connectionID: Int, val serverUUID: UUID, val celestialInformation: CelestialBaseInformation) : IClientPacket {
 	constructor(stream: DataInputStream, isLegacy: Boolean) : this(
-		stream.readUnsignedShort(),
+		stream.readVarInt(),
 		stream.readUUID(),
 		CelestialBaseInformation(stream, isLegacy)
 	)
 
 	override fun write(stream: DataOutputStream, isLegacy: Boolean) {
-		stream.writeShort(connectionID)
+		stream.writeVarInt(connectionID)
 		stream.writeUUID(serverUUID)
 		celestialInformation.write(stream, isLegacy)
 	}
 
 	override fun play(connection: ClientConnection) {
-		TODO("Not yet implemented")
+		connection.connectionID = connectionID
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt
new file mode 100644
index 00000000..6a50fc41
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt
@@ -0,0 +1,77 @@
+package ru.dbotthepony.kstarbound.network.packets.clientbound
+
+import com.google.gson.JsonElement
+import it.unimi.dsi.fastutil.ints.Int2BooleanAVLTreeMap
+import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
+import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
+import ru.dbotthepony.kommons.io.readByteArray
+import ru.dbotthepony.kommons.io.readCollection
+import ru.dbotthepony.kommons.io.readMap
+import ru.dbotthepony.kommons.io.readVector2d
+import ru.dbotthepony.kommons.io.readVector2f
+import ru.dbotthepony.kommons.io.writeByteArray
+import ru.dbotthepony.kommons.io.writeCollection
+import ru.dbotthepony.kommons.io.writeMap
+import ru.dbotthepony.kommons.io.writeStruct2d
+import ru.dbotthepony.kommons.io.writeStruct2f
+import ru.dbotthepony.kommons.vector.Vector2d
+import ru.dbotthepony.kstarbound.client.ClientConnection
+import ru.dbotthepony.kstarbound.json.readJsonElement
+import ru.dbotthepony.kstarbound.json.writeJsonElement
+import ru.dbotthepony.kstarbound.network.IClientPacket
+import java.io.DataInputStream
+import java.io.DataOutputStream
+
+class WorldStartPacket(
+	val templateData: JsonElement, val skyData: ByteArray, val weatherData: ByteArray,
+	val playerStart: Vector2d, val playerRespawn: Vector2d, val respawnInWorld: Boolean,
+	val dungeonGravity: Map<Int, Vector2d>, val dungeonBreathable: Map<Int, Boolean>,
+	val protectedDungeonIDs: Set<Int>, val worldProperties: JsonElement, val connectionID: Int,
+	val localInterpolationMode: Boolean,
+) : IClientPacket {
+	constructor(stream: DataInputStream, isLegacy: Boolean) : this(
+		stream.readJsonElement(),
+		stream.readByteArray(),
+		stream.readByteArray(),
+		if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(),
+		if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(),
+		stream.readBoolean(),
+		if (isLegacy) stream.readMap({ readUnsignedShort() }, { Vector2d(0.0, readFloat().toDouble()) }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readVector2d() }, { Int2ObjectAVLTreeMap() }),
+		if (isLegacy) stream.readMap({ readUnsignedShort() }, { readBoolean() }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readBoolean() }, { Int2BooleanAVLTreeMap() }),
+		if (isLegacy) stream.readCollection({ readUnsignedShort() }, { IntAVLTreeSet() }) else stream.readCollection({ readInt() }, { IntAVLTreeSet() }),
+		stream.readJsonElement(),
+		stream.readUnsignedShort(),
+		stream.readBoolean()
+	)
+
+	override fun write(stream: DataOutputStream, isLegacy: Boolean) {
+		stream.writeJsonElement(templateData)
+		stream.writeByteArray(skyData)
+		stream.writeByteArray(weatherData)
+
+		if (isLegacy) {
+			stream.writeStruct2f(playerStart.toFloatVector())
+			stream.writeStruct2f(playerRespawn.toFloatVector())
+			stream.writeBoolean(respawnInWorld)
+			stream.writeMap(dungeonGravity, { writeShort(it) }, { writeFloat(it.y.toFloat()) })
+			stream.writeMap(dungeonBreathable, { writeShort(it) }, { writeBoolean(it) })
+			stream.writeCollection(protectedDungeonIDs) { writeShort(it) }
+		} else {
+			stream.writeStruct2d(playerStart)
+			stream.writeStruct2d(playerRespawn)
+			stream.writeBoolean(respawnInWorld)
+			stream.writeMap(dungeonGravity, { writeInt(it) }, { writeStruct2d(it) })
+			stream.writeMap(dungeonBreathable, { writeInt(it) }, { writeBoolean(it) })
+			stream.writeCollection(protectedDungeonIDs) { writeInt(it) }
+		}
+
+		stream.writeJsonElement(worldProperties)
+		stream.writeShort(connectionID)
+		stream.writeBoolean(localInterpolationMode)
+	}
+
+	override fun play(connection: ClientConnection) {
+		TODO("Not yet implemented")
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt
index d41df676..2866c09d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt
@@ -39,7 +39,7 @@ data class ClientConnectPacket(
 		stream.readBinaryString(),
 		stream.readBinaryString(),
 		stream.readMap(InputStream::readByteKey, { readKOptional { readByteArray() } }, { HashMap(it) }),
-		ShipUpgrades.read(stream, isLegacy),
+		ShipUpgrades(stream, isLegacy),
 		stream.readBoolean(),
 		stream.readBinaryString()
 	)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/packets/TrackedPositionPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/packets/TrackedPositionPacket.kt
index 35c899d7..5efad3cd 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/packets/TrackedPositionPacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/network/packets/TrackedPositionPacket.kt
@@ -1,18 +1,18 @@
 package ru.dbotthepony.kstarbound.server.network.packets
 
+import ru.dbotthepony.kommons.io.readVector2d
+import ru.dbotthepony.kommons.io.writeStruct2d
 import ru.dbotthepony.kommons.vector.Vector2d
 import ru.dbotthepony.kstarbound.network.IServerPacket
 import ru.dbotthepony.kstarbound.server.ServerConnection
-import ru.dbotthepony.kstarbound.io.readVec2d
-import ru.dbotthepony.kstarbound.io.writeVec2d
 import java.io.DataInputStream
 import java.io.DataOutputStream
 
 data class TrackedPositionPacket(val pos: Vector2d) : IServerPacket {
-	constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVec2d())
+	constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector2d())
 
 	override fun write(stream: DataOutputStream, isLegacy: Boolean) {
-		stream.writeVec2d(pos)
+		stream.writeStruct2d(pos)
 	}
 
 	override fun play(connection: ServerConnection) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt
index 3a96ca4d..8d09e95e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt
@@ -1,23 +1,23 @@
 package ru.dbotthepony.kstarbound.world
 
+import ru.dbotthepony.kommons.io.readVector2i
+import ru.dbotthepony.kommons.io.writeStruct2i
 import ru.dbotthepony.kommons.util.IStruct2d
 import ru.dbotthepony.kommons.util.IStruct2f
 import ru.dbotthepony.kommons.util.IStruct2i
 import ru.dbotthepony.kommons.vector.Vector2d
 import ru.dbotthepony.kommons.vector.Vector2i
-import ru.dbotthepony.kstarbound.io.readVec2i
-import ru.dbotthepony.kstarbound.io.writeVec2i
 import java.io.DataInputStream
 import java.io.DataOutputStream
 
 data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Boolean) {
-	constructor(buff: DataInputStream) : this(buff.readVec2i(), buff.readBoolean(), buff.readBoolean())
+	constructor(buff: DataInputStream) : this(buff.readVector2i(), buff.readBoolean(), buff.readBoolean())
 
 	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)
 
 	fun write(buff: DataOutputStream) {
-		buff.writeVec2i(size)
+		buff.writeStruct2i(size)
 		buff.writeBoolean(loopX)
 		buff.writeBoolean(loopY)
 	}