diff --git a/docs/README.md b/docs/README.md
index 9160eba96..d321c684f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -17,7 +17,7 @@ First time using Node.js? You may want to start with the [tutorial](tutorial.md)
 
 ## Features
 
- * Supports Minecraft 1.8 to 1.20.1 (1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19 and 1.20)
+ * Supports Minecraft 1.8 to 1.20.2 (1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19 and 1.20)
  * Entity knowledge and tracking.
  * Block knowledge. You can query the world around you. Milliseconds to find any block.
  * Physics and movement - handle all bounding boxes
diff --git a/examples/anvil_saver/.npmrc b/examples/anvil_saver/.npmrc
new file mode 100644
index 000000000..4c1bf7793
--- /dev/null
+++ b/examples/anvil_saver/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+package-lock=false
diff --git a/examples/pathfinder/.npmrc b/examples/pathfinder/.npmrc
new file mode 100644
index 000000000..4c1bf7793
--- /dev/null
+++ b/examples/pathfinder/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+package-lock=false
diff --git a/examples/pathfinder/package.json b/examples/pathfinder/package.json
index e94536838..a0a435fe9 100644
--- a/examples/pathfinder/package.json
+++ b/examples/pathfinder/package.json
@@ -3,7 +3,7 @@
   "version": "0.0.0",
   "private": true,
   "dependencies": {
-    "mineflayer-pathfinder": "^1.6.1",
+    "mineflayer-pathfinder": "^2.4.5",
     "mineflayer": "file:../../"
   },
   "description": "A mineflayer example"
diff --git a/examples/place_end_crystal/.npmrc b/examples/place_end_crystal/.npmrc
new file mode 100644
index 000000000..4c1bf7793
--- /dev/null
+++ b/examples/place_end_crystal/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+package-lock=false
diff --git a/examples/screenshot-with-node-canvas-webgl/.npmrc b/examples/screenshot-with-node-canvas-webgl/.npmrc
new file mode 100644
index 000000000..4c1bf7793
--- /dev/null
+++ b/examples/screenshot-with-node-canvas-webgl/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+package-lock=false
diff --git a/examples/viewer/.npmrc b/examples/viewer/.npmrc
new file mode 100644
index 000000000..4c1bf7793
--- /dev/null
+++ b/examples/viewer/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+package-lock=false
diff --git a/lib/plugins/blocks.js b/lib/plugins/blocks.js
index 0a8f5fc13..d8c0b41d0 100644
--- a/lib/plugins/blocks.js
+++ b/lib/plugins/blocks.js
@@ -268,6 +268,30 @@ function inject (bot, { version, storageBuilder, hideErrors }) {
     }
   })
 
+  // Chunk batches are used by the server to throttle the chunks per tick for players based on their connection speed.
+  let chunkBatchStartTime = 0
+  // The Vanilla client uses nano seconds with its weighted average starting at 2000000 converted to milliseconds that is 2
+  let weightedAverage = 2
+  // This is used for keeping track of the weight of the old average when updating it.
+  let oldSampleWeight = 1
+
+  bot._client.on('chunk_batch_start', (packet) => {
+    // Get the time the chunk batch is starting.
+    chunkBatchStartTime = Date.now()
+  })
+
+  bot._client.on('chunk_batch_finished', (packet) => {
+    const milliPerChunk = (Date.now() - chunkBatchStartTime) / packet.batchSize
+    // Prevents the MilliPerChunk from being hugely different then the average, Vanilla uses 3 as a constant here.
+    const clampedMilliPerChunk = Math.min(Math.max(milliPerChunk, weightedAverage / 3.0), weightedAverage * 3.0)
+    weightedAverage = ((weightedAverage * oldSampleWeight) + clampedMilliPerChunk) / (oldSampleWeight + 1)
+    // 49 is used in Vanilla client to limit it to 50 samples
+    oldSampleWeight = Math.min(49, oldSampleWeight + 1)
+    bot._client.write('chunk_batch_received', {
+      // Vanilla uses 7000000 as a constant here, since we are using milliseconds that is now 7. Not sure why they pick this constant to convert from nano seconds per chunk to chunks per tick.
+      chunksPerTick: 7 / weightedAverage
+    })
+  })
   bot._client.on('map_chunk', (packet) => {
     addColumn({
       x: packet.x,
diff --git a/lib/plugins/entities.js b/lib/plugins/entities.js
index 8ee6f6742..135219521 100644
--- a/lib/plugins/entities.js
+++ b/lib/plugins/entities.js
@@ -26,7 +26,7 @@ const entityStatusEvents = {
 }
 
 function inject (bot) {
-  const { mobs, entitiesArray } = bot.registry
+  const { mobs } = bot.registry
   const Entity = require('prismarine-entity')(bot.version)
   const Item = require('prismarine-item')(bot.version)
   const ChatMessage = require('prismarine-chat')(bot.registry)
@@ -129,33 +129,6 @@ function inject (bot) {
     if (eventName) bot.emit(eventName, entity)
   })
 
-  bot._client.on('named_entity_spawn', (packet) => {
-    // in case player_info packet was not sent before named_entity_spawn : ignore named_entity_spawn (see #213)
-    if (packet.playerUUID in bot.uuidToUsername) {
-      // spawn named entity
-      const entity = fetchEntity(packet.entityId)
-      entity.type = 'player'
-      entity.name = 'player'
-      entity.username = bot.uuidToUsername[packet.playerUUID]
-      entity.uuid = packet.playerUUID
-      entity.dataBlobs = packet.data
-      if (bot.supportFeature('fixedPointPosition')) {
-        entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32)
-      } else if (bot.supportFeature('doublePosition')) {
-        entity.position.set(packet.x, packet.y, packet.z)
-      }
-      entity.yaw = conv.fromNotchianYawByte(packet.yaw)
-      entity.pitch = conv.fromNotchianPitchByte(packet.pitch)
-      entity.height = NAMED_ENTITY_HEIGHT
-      entity.width = NAMED_ENTITY_WIDTH
-      entity.metadata = parseMetadata(packet.metadata, entity.metadata)
-      if (bot.players[entity.username] !== undefined && !bot.players[entity.username].entity) {
-        bot.players[entity.username].entity = entity
-      }
-      bot.emit('entitySpawn', entity)
-    }
-  })
-
   bot.on('entityCrouch', (entity) => {
     entity.height = CROUCH_HEIGHT
   })
@@ -171,10 +144,11 @@ function inject (bot) {
     bot.emit('playerCollect', collector, collected)
   })
 
+  // What is internalId?
+  const entityDataByInternalId = Object.fromEntries(bot.registry.entitiesArray.map((e) => [e.internalId, e]))
+
   function setEntityData (entity, type, entityData) {
-    if (entityData === undefined) {
-      entityData = entitiesArray.find(entity => entity.internalId === type)
-    }
+    entityData ??= entityDataByInternalId[type]
     if (entityData) {
       entity.type = entityData.type || 'object'
       entity.displayName = entityData.displayName
@@ -193,24 +167,57 @@ function inject (bot) {
     }
   }
 
-  // spawn object/vehicle on versions < 1.19, on versions > 1.19 handles all non-player entities
-  bot._client.on('spawn_entity', (packet) => {
-    const entity = fetchEntity(packet.entityId)
-    const entityData = bot.registry.entities[packet.type]
-    setEntityData(entity, packet.type, entityData)
-
+  function updateEntityPos (entity, pos) {
     if (bot.supportFeature('fixedPointPosition')) {
-      entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32)
+      entity.position.set(pos.x / 32, pos.y / 32, pos.z / 32)
     } else if (bot.supportFeature('doublePosition')) {
-      entity.position.set(packet.x, packet.y, packet.z)
-    } else if (bot.supportFeature('consolidatedEntitySpawnPacket')) {
-      entity.headPitch = conv.fromNotchianPitchByte(packet.headPitch)
+      entity.position.set(pos.x, pos.y, pos.z)
     }
+    entity.yaw = conv.fromNotchianYawByte(pos.yaw)
+    entity.pitch = conv.fromNotchianPitchByte(pos.pitch)
+  }
 
-    entity.uuid = packet.objectUUID
-    entity.yaw = conv.fromNotchianYawByte(packet.yaw)
-    entity.pitch = conv.fromNotchianPitchByte(packet.pitch)
-    entity.objectData = packet.objectData
+  function addNewPlayer (entityId, uuid, pos) {
+    const entity = fetchEntity(entityId)
+    entity.type = 'player'
+    entity.name = 'player'
+    entity.username = bot.uuidToUsername[uuid]
+    entity.uuid = uuid
+    updateEntityPos(entity, pos)
+    entity.height = NAMED_ENTITY_HEIGHT
+    entity.width = NAMED_ENTITY_WIDTH
+    if (bot.players[entity.username] !== undefined && !bot.players[entity.username].entity) {
+      bot.players[entity.username].entity = entity
+    }
+    return entity
+  }
+
+  function addNewNonPlayer (entityId, uuid, entityType, pos) {
+    const entity = fetchEntity(entityId)
+    const entityData = bot.registry.entities[entityType]
+    setEntityData(entity, entityType, entityData)
+    updateEntityPos(entity, pos)
+    return entity
+  }
+
+  bot._client.on('named_entity_spawn', (packet) => {
+    // in case player_info packet was not sent before named_entity_spawn : ignore named_entity_spawn (see #213)
+    if (packet.playerUUID in bot.uuidToUsername) {
+      // spawn named entity
+      const entity = addNewPlayer(packet.entityId, packet.playerUUID, packet, packet.metadata)
+      entity.dataBlobs = packet.data // this field doesn't appear to be listed on any version
+      entity.metadata = parseMetadata(packet.metadata, entity.metadata) // 1.8
+      bot.emit('entitySpawn', entity)
+    }
+  })
+
+  // spawn object/vehicle on versions < 1.19, on versions > 1.19 handles all non-player entities
+  // on versions >= 1.20.2, this also handles player entities
+  bot._client.on('spawn_entity', (packet) => {
+    const entityData = entityDataByInternalId[packet.type]
+    const entity = entityData?.type === 'player'
+      ? addNewPlayer(packet.entityId, packet.objectUUID, packet)
+      : addNewNonPlayer(packet.entityId, packet.objectUUID, packet.type, packet)
     bot.emit('entitySpawn', entity)
   })
 
diff --git a/lib/plugins/game.js b/lib/plugins/game.js
index 218e2f5ca..4006d145f 100644
--- a/lib/plugins/game.js
+++ b/lib/plugins/game.js
@@ -71,6 +71,11 @@ function inject (bot, options) {
   const brandChannel = getBrandCustomChannelName()
   bot._client.registerChannel(brandChannel, ['string', []])
 
+  // 1.20.2
+  bot._client.on('registry_data', (packet) => {
+    bot.registry.loadDimensionCodec(packet.codec)
+  })
+
   bot._client.on('login', (packet) => {
     handleRespawnPacketData(packet)
 
diff --git a/lib/version.js b/lib/version.js
index 6d056eb96..059ef3dea 100644
--- a/lib/version.js
+++ b/lib/version.js
@@ -1,6 +1,8 @@
-const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20.1']
+const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20.1', '1.20.2']
 module.exports = {
+
   testedVersions,
   latestSupportedVersion: testedVersions[testedVersions.length - 1],
   oldestSupportedVersion: testedVersions[0]
+
 }
diff --git a/package.json b/package.json
index f148771ab..b8eb0618f 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
   },
   "license": "MIT",
   "dependencies": {
-    "minecraft-data": "^3.44.0",
+    "minecraft-data": "^3.56.0",
     "minecraft-protocol": "^1.44.0",
     "prismarine-biome": "^1.1.1",
     "prismarine-block": "^1.17.0",
diff --git a/test/internalTest.js b/test/internalTest.js
index 913b8e35b..25b44e774 100644
--- a/test/internalTest.js
+++ b/test/internalTest.js
@@ -104,7 +104,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
         assert.strictEqual(message, 'hello')
         bot.chat('hi')
       })
-      server.on('login', (client) => {
+      server.on('playerJoin', (client) => {
         client.write('login', bot.test.generateLoginPacket())
         const message = hasSignedChat
           ? JSON.stringify({ text: 'hello' })
@@ -180,7 +180,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
       // Versions prior to 1.11 have capital first letter
       const entities = bot.registry.entitiesByName
       const creeperId = entities.creeper ? entities.creeper.id : entities.Creeper.id
-      server.on('login', (client) => {
+      server.on('playerJoin', (client) => {
         client.write(bot.registry.supportFeature('consolidatedEntitySpawnPacket') ? 'spawn_entity' : 'spawn_entity_living', {
           entityId: 8, // random
           entityUUID: '00112233-4455-6677-8899-aabbccddeeff',
@@ -215,7 +215,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
         assert.strictEqual(bot.blockAt(pos).type, goldId)
         done()
       })
-      server.on('login', (client) => {
+      server.on('playerJoin', (client) => {
         client.write('login', bot.test.generateLoginPacket())
         const chunk = bot.test.buildChunk()
         chunk.setBlockType(pos, goldId)
@@ -238,7 +238,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
           flags: 0,
           teleportId: 0
         }
-        server.on('login', async (client) => {
+        server.on('playerJoin', async (client) => {
           await client.write('login', bot.test.generateLoginPacket())
           await client.write('position', basePosition)
           client.on('packet', (data, meta) => {
@@ -261,7 +261,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
         })
       })
       it('absolute position & relative position (velocity)', (done) => {
-        server.on('login', async (client) => {
+        server.on('playerJoin', async (client) => {
           await client.write('login', bot.test.generateLoginPacket())
           const chunk = await bot.test.buildChunk()
 
@@ -383,7 +383,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
             assert.strictEqual(bot.entity.onGround, false)
           }
         })
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           client.write('login', bot.test.generateLoginPacket())
           const chunk = bot.test.buildChunk()
 
@@ -451,7 +451,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
           flags: 0,
           teleportId: 0
         }
-        server.on('login', async (client) => {
+        server.on('playerJoin', async (client) => {
           bot.once('respawn', () => {
             assert.ok(bot.world.getColumn(0, 0) !== undefined)
             bot.once('respawn', () => {
@@ -482,7 +482,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
 
     describe('game', () => {
       it('responds to ping / transaction packets', (done) => { // only on 1.17
-        server.on('login', async (client) => {
+        server.on('playerJoin', async (client) => {
           if (bot.supportFeature('transactionPacketExists')) {
             const transactionPacket = { windowId: 0, action: 42, accepted: false }
             client.once('transaction', (data, meta) => {
@@ -506,7 +506,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
     describe('entities', () => {
       it('entity id changes on login', (done) => {
         const loginPacket = bot.test.generateLoginPacket()
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           if (bot.supportFeature('usesLoginPacket')) {
             loginPacket.entityId = 0 // Default login packet in minecraft-data 1.16.5 is 1, so set it to 0
           }
@@ -524,7 +524,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
       })
 
       it('player displayName', (done) => {
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           bot.on('entitySpawn', (entity) => {
             const player = bot.players[entity.username]
             assert.strictEqual(entity.username, player.displayName.toString())
@@ -632,17 +632,35 @@ for (const supportedVersion of mineflayer.testedVersions) {
             })
           }
 
-          client.write('named_entity_spawn', {
-            entityId: 56,
-            playerUUID: '1-2-3-4',
-            x: 1,
-            y: 2,
-            z: 3,
-            yaw: 0,
-            pitch: 0,
-            currentItem: -1,
-            metadata: []
-          })
+          if (bot.registry.supportFeature('unifiedPlayerAndEntitySpawnPacket')) {
+            client.write('spawn_entity', {
+              entityId: 56,
+              objectUUID: '1-2-3-4',
+              type: bot.registry.entitiesByName.player.internalId,
+              x: 1,
+              y: 2,
+              z: 3,
+              pitch: 0,
+              yaw: 0,
+              headPitch: 0,
+              objectData: 1,
+              velocityX: 0,
+              velocityY: 0,
+              velocityZ: 0
+            })
+          } else {
+            client.write('named_entity_spawn', {
+              entityId: 56,
+              playerUUID: '1-2-3-4',
+              x: 1,
+              y: 2,
+              z: 3,
+              yaw: 0,
+              pitch: 0,
+              currentItem: -1,
+              metadata: []
+            })
+          }
         })
       })
 
@@ -663,7 +681,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
           assert.strictEqual(bot.players[entity.username], undefined)
           done()
         })
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           serverClient = client
 
           if (registry.supportFeature('playerInfoActionIsBitfield')) {
@@ -697,22 +715,40 @@ for (const supportedVersion of mineflayer.testedVersions) {
             })
           }
 
-          client.write('named_entity_spawn', {
-            entityId: 56,
-            playerUUID: '1-2-3-4',
-            x: 1,
-            y: 2,
-            z: 3,
-            yaw: 0,
-            pitch: 0,
-            currentItem: -1,
-            metadata: []
-          })
+          if (bot.registry.supportFeature('unifiedPlayerAndEntitySpawnPacket')) {
+            client.write('spawn_entity', {
+              entityId: 56,
+              objectUUID: '1-2-3-4',
+              type: bot.registry.entitiesByName.player.internalId,
+              x: 1,
+              y: 2,
+              z: 3,
+              pitch: 0,
+              yaw: 0,
+              headPitch: 0,
+              objectData: 1,
+              velocityX: 0,
+              velocityY: 0,
+              velocityZ: 0
+            })
+          } else {
+            client.write('named_entity_spawn', {
+              entityId: 56,
+              playerUUID: '1-2-3-4',
+              x: 1,
+              y: 2,
+              z: 3,
+              yaw: 0,
+              pitch: 0,
+              currentItem: -1,
+              metadata: []
+            })
+          }
         })
       })
 
       it('metadata', (done) => {
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           bot.on('entitySpawn', (entity) => {
             assert.strictEqual(entity.displayName, 'Creeper')
 
@@ -763,7 +799,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
           itemCount: 5
         }
 
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           bot.on('itemDrop', (entity) => {
             const slotPosition = metadataPacket.metadata[0].key
 
@@ -870,7 +906,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
         done()
       })
 
-      server.once('login', (client) => {
+      server.once('playerJoin', (client) => {
         bot.time.timeOfDay = 18000
         const loginPacket = bot.test.generateLoginPacket()
         client.write('login', loginPacket)
@@ -952,7 +988,7 @@ for (const supportedVersion of mineflayer.testedVersions) {
           })
         })
 
-        server.on('login', (client) => {
+        server.on('playerJoin', (client) => {
           client.write('playerlist_header', {
             header: JSON.stringify({ text: '', extra: [{ text: HEADER, color: 'yellow' }] }),
             footer: JSON.stringify({ text: '', extra: [{ text: FOOTER, color: 'yellow' }] })