diff --git a/README.md b/README.md
index 98f697a8..817b35c9 100644
--- a/README.md
+++ b/README.md
@@ -44,4 +44,4 @@ It's recommended to also install `pm2` for process auto-restart on crashes and s
 0. Clone, setup Node.js (at least v18.6.0)
 1. Install dependencies: `npm install` or `pnpm install`
 2. Run `npm run dev`, `npm run start` for without watch or `npm run watch` for watch mode (for prismarine-web-client)
-2.1. If using [Bun](https://bun.sh) (recommended) instead: `bun --watch src/app.js` or `bun --hot src/app.js` (preview)
+2.1. If using [Bun](https://bun.sh) (experimental) instead: `bun --watch src/app.js` or `bun --hot src/app.js` (preview)
diff --git a/package.json b/package.json
index 74cc39a2..b45cbd49 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
     "colors": "1.4.0",
     "diamond-square": "^1.2.0",
     "emit-then": "^2.0.0",
-    "@tootallnate/once": "^3.0.0",
+    "@tootallnate/once": "2.0.0",
     "exit-hook": "^2.2.1",
     "flatmap": "^0.0.3",
     "long": "^5.1.0",
@@ -77,7 +77,7 @@
     "mineflayer": "^4.0.0",
     "mocha": "^10.0.0",
     "pkg": "^5.8.1",
-    "typescript": "^5.3.3",
+    "typescript": "^5.4.5",
     "vitest": "^0.34.6"
   },
   "pnpm": {
diff --git a/src/index.ts b/src/index.ts
index 7f7d980d..d42b3e01 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -47,6 +47,7 @@ export { supportedVersions }
 
 class MCServer extends EventEmitter {
   pluginsReady = false
+  private abortController = new AbortController()
   constructor() {
     modules.initPlugins()
     super()
@@ -54,8 +55,13 @@ class MCServer extends EventEmitter {
 
   connect (options: Options) {
     const server = this as unknown as Server
+    server.abortSignal = this.abortController.signal
+    server.cleanupFunctions = [
+      () => this.abortController.abort()
+    ]
     const mcData = require('minecraft-data')(options.version)
     server.mcData = mcData
+    if (mcData === null) throw new Error(`Version ${options.version} is not supported as it doesn't have the data.`)
     const version = mcData.version
     if (!supportedVersions.some(v => v.includes(version.majorVersion))) {
       console.warn(`Version ${version.minecraftVersion} is not supported.`)
@@ -92,13 +98,20 @@ class MCServer extends EventEmitter {
   }
 }
 
+type Cleanup = () => void
+type MaybePromise<T> = T | Promise<T>
+
 declare global {
   interface Server {
     commands: Command
     pluginsReady: boolean
     _server: ProtocolServer
     supportFeature: IndexedData['supportFeature']
+    abortSignal: AbortSignal
+    cleanupFunctions: Cleanup[]
   }
+
+  type ServerModule = (server: Server, options: Options) => MaybePromise<void | Cleanup>
 }
 
 export * as Behavior from './lib/behavior'
diff --git a/src/lib/modules/chat.ts b/src/lib/modules/chat.ts
index bc953a27..dd2050ca 100644
--- a/src/lib/modules/chat.ts
+++ b/src/lib/modules/chat.ts
@@ -173,35 +173,35 @@ declare global {
     "broadcast": (message: any, opt?: { whitelist?: any; blacklist?: any[]; system?: boolean }) => void
     /** @internal */
     "color": {
-      black: string;
-      dark_blue: string;
-      dark_green: string;
-      dark_cyan: string;
-      dark_red: string;
-      purple: string;
-      dark_purple: string;
-      gold: string;
-      gray: string;
-      grey: string;
-      dark_gray: string;
-      dark_grey: string;
-      blue: string;
-      green: string;
-      aqua: string;
-      cyan: string;
-      red: string;
-      pink: string;
-      light_purple: string;
-      yellow: string;
-      white: string;
-      random: string;
-      obfuscated: string;
-      bold: string;
-      strikethrough: string;
-      underlined: string;
-      underline: string;
-      italic: string;
-      italics: string;
+      black: string
+      dark_blue: string
+      dark_green: string
+      dark_cyan: string
+      dark_red: string
+      purple: string
+      dark_purple: string
+      gold: string
+      gray: string
+      grey: string
+      dark_gray: string
+      dark_grey: string
+      blue: string
+      green: string
+      aqua: string
+      cyan: string
+      red: string
+      pink: string
+      light_purple: string
+      yellow: string
+      white: string
+      random: string
+      obfuscated: string
+      bold: string
+      strikethrough: string
+      underlined: string
+      underline: string
+      italic: string
+      italics: string
       reset: string
     }
     /** @internal */
diff --git a/src/lib/modules/login.ts b/src/lib/modules/login.ts
index c89def0f..91c8fc97 100644
--- a/src/lib/modules/login.ts
+++ b/src/lib/modules/login.ts
@@ -68,7 +68,7 @@ export const player = async function (player: Player, serv: Server, settings: Op
     await player.findSpawnPoint()
 
     player.setLoadingStatus('Reading player data')
-    playerData = await playerDat.read(player.uuid, player.spawnPoint, settings.worldFolder)
+    playerData = await playerDat.read(player.uuid, player.spawnPoint, settings.worldFolder ?? false)
     Object.keys(playerData.player).forEach(k => { player[k] = playerData.player[k] })
 
     serv.players.push(player)
@@ -309,8 +309,7 @@ export const player = async function (player: Player, serv: Server, settings: Op
     player.sendRestMap()
     sendChunkWhenMove()
 
-    if (playerData.new) {
-      // skip unnecessary fs operation
+    if (playerData.new) { // otherwise we skip unnecessary fs operation
       player.save()
     }
   }
diff --git a/src/lib/modules/logout.ts b/src/lib/modules/logout.ts
index b692d7a3..db724fc0 100644
--- a/src/lib/modules/logout.ts
+++ b/src/lib/modules/logout.ts
@@ -2,6 +2,7 @@ import once from '@tootallnate/once'
 
 export const server = function (serv: Server) {
   serv.quit = async (reason = 'Server closed') => {
+    serv.cleanupFunctions.forEach(fn => fn())
     await Promise.all(serv.players.map((player) => {
       player.kick(reason)
       return once(player, 'disconnected')
@@ -41,7 +42,9 @@ export const player = function (player: Player, serv: Server, { worldFolder }: O
 }
 declare global {
   interface Server {
-    /** @internal */
+    /**
+     * Stop and dispose the server
+     */
     "quit": (reason?: string) => Promise<void>
   }
   interface Player {
diff --git a/src/lib/modules/world.ts b/src/lib/modules/world.ts
index 03fab3dd..253d7d71 100644
--- a/src/lib/modules/world.ts
+++ b/src/lib/modules/world.ts
@@ -18,7 +18,7 @@ import { generateSpiralMatrix } from '../../utils'
 const fsStat = promisify(fs.stat)
 const fsMkdir = promisify(fs.mkdir)
 
-export const server = async function (serv: Server, options: Options) {
+export const server: ServerModule = async function (serv, options) {
   const { version, worldSaveVersion: _worldSaveVersion, worldFolder, generation = { name: 'diamond_square', options: { worldHeight: 80 } } } = options
   generation.options.worldHeight = serv.supportFeature('tallWorld') ? 384 : 256
   generation.options.minY = serv.supportFeature('tallWorld') ? -64 : 0
@@ -74,6 +74,12 @@ export const server = async function (serv: Server, options: Options) {
   serv.netherworld = new World(generations.nether(generationOptions)) as CustomWorld
   // serv.endworld = new World(generations["end"]({}));
 
+  serv.worlds = {
+    'overworld': serv.overworld,
+    'nether': serv.netherworld
+    // 'end': serv.endworld
+  }
+
   serv.dimensionNames = {
     '-1': 'minecraft:nether',
     0: 'minecraft:overworld'
@@ -264,6 +270,13 @@ export const server = async function (serv: Server, options: Options) {
       ctx.player!.world.blockEntityData[key] = blockEntities[key]
     },
   })
+
+  return () => {
+    for (const world of Object.values(serv.worlds)) {
+      // world.throwOnReadWrite = true
+      world.stopSaving()
+    }
+  }
 }
 
 export const player = function (player: Player, serv: Server, settings: Options) {
@@ -276,6 +289,7 @@ export const player = function (player: Player, serv: Server, settings: Options)
   })
 
   player.save = async () => {
+    if (!settings.worldFolder) return
     return await playerDat.save(player, settings.worldFolder, serv.supportFeature('attributeSnakeCase'), serv.supportFeature('theFlattening'))
   }
 
@@ -499,6 +513,7 @@ declare global {
     spawnPoint?: Vec3
     /** Parsed level.dat of the loaded world (only if worldFolder is specificed) */
     levelData?: LevelDatFull
+    worlds: Record<string, CustomWorld>
     /** Contains the overworld world. This is where the default spawn point is */
     "overworld": CustomWorld
     /** Contains the nether world. This **WILL** be used when a player travels through a portal if they are in the overworld! */
diff --git a/src/lib/playerDat.js b/src/lib/playerDat.js
index 68f6e41e..622fc0a4 100644
--- a/src/lib/playerDat.js
+++ b/src/lib/playerDat.js
@@ -16,7 +16,22 @@ const playerDefaults = {
   heldItemSlot: 0
 }
 
+/**
+ * @param {string} uuid
+ * @param {Vec3} spawnPoint
+ * @param {string | false} worldFolder
+ */
 async function read (uuid, spawnPoint, worldFolder) {
+  const newPlayerData = () => {
+    return {
+      player: { ...playerDefaults, ...{ position: spawnPoint.clone() } },
+      inventory: [],
+      new: true
+    }
+  }
+
+  if (!worldFolder) return newPlayerData()
+
   try {
     const playerDataFile = await promises.readFile(`${worldFolder}/playerdata/${uuid}.dat`)
     /** @type {any} */
@@ -41,11 +56,7 @@ async function read (uuid, spawnPoint, worldFolder) {
       new: false
     }
   } catch (e) {
-    return {
-      player: { ...playerDefaults, ...{ position: spawnPoint.clone() } },
-      inventory: [],
-      new: true
-    }
+    return newPlayerData()
   }
 }
 
@@ -86,6 +97,12 @@ function playerInventoryToNBT (playerInventory, theFlattening) {
   return nbtInventory
 }
 
+/**
+ * @param {Player} player
+ * @param {string | undefined} worldFolder
+ * @param {boolean} snakeCase
+ * @param {boolean} theFlattening
+ */
 async function save (player, worldFolder, snakeCase, theFlattening) {
   if (worldFolder === undefined) {
     return
diff --git a/src/lib/requireindex.js b/src/lib/requireindex.js
index bd7c24c7..3a82e35a 100644
--- a/src/lib/requireindex.js
+++ b/src/lib/requireindex.js
@@ -43,7 +43,7 @@ module.exports = function (dir, basenames) {
       const stats = fs.statSync(filepath)
 
       // Don't require non-javascript files (.txt, .md, etc.)
-      if (stats.isFile() && !(['.js', '.node', '.json', '.ts', '.tsx'].includes(ext))) {
+      if (['.d.ts'].some(ext => filename.endsWith(ext)) || (stats.isFile() && !(['.js', '.node', '.json', '.ts', '.tsx'].includes(ext)))) {
         return
       }
 
diff --git a/src/modules.ts b/src/modules.ts
index 29f4f88b..2e55c6d0 100644
--- a/src/modules.ts
+++ b/src/modules.ts
@@ -65,7 +65,7 @@ declare global {
     logging?: boolean
     gameMode?: number
     difficulty?: number
-    worldFolder?: string
+    worldFolder?: string | false
     generation?: {
       name: string
       options: {
@@ -91,5 +91,9 @@ declare global {
     "max-entities": number
     noConsoleOutput?: boolean
     savingInterval?: number | false
+    /**
+     * @jsOnly
+     */
+    oldServerInstance?: Server
   }
 }