From bb363dc06fdfd32d45d8c9fe570a87a459e80a04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Sun, 22 Oct 2023 22:22:39 +0200
Subject: [PATCH] Added void-world for selection Added loading indicators Added
 speed modifiers Code cleanup

---
 .idea/gradle.xml                              |   5 +-
 .idea/misc.xml                                |   1 -
 build.gradle                                  |  26 ++--
 .../eu/mhsl/craftattack/worldmuseum/Main.java |  30 ++---
 .../worldmuseum/commands/SpawnCommand.java    |  16 ++-
 .../worldmuseum/commands/TeleportCommand.java |  15 ++-
 .../worldmuseum/handler/SignHandler.java      |   1 -
 .../worldmuseum/inventory/WorldSelector.java  |  31 +++++
 .../worldmuseum/items/ActionItem.java         |  32 +++++
 .../worldmuseum/items/ItemManager.java        |  14 --
 .../craftattack/worldmuseum/items/Items.java  |  67 ++++++++++
 .../listener/BlockBreakListener.java          |   5 +-
 .../listener/BlockPlaceListener.java          |  17 ++-
 .../worldmuseum/listener/ChunkUnloading.java  |   2 +-
 .../listener/InventoryClickListener.java      |  19 +--
 .../worldmuseum/listener/ItemUseListener.java |  29 +----
 .../listener/MovementListener.java            |  13 --
 .../worldmuseum/util/ChangeWorld.java         |  23 ----
 .../craftattack/worldmuseum/util/Config.java  |   6 +-
 .../worldmuseum/util/MuseumPlayer.java        |  98 +++++++-------
 .../worldmuseum/util/TablistUpdateTask.java   |   4 +-
 .../worldmuseum/worlds/PlayerMovable.java     |   7 +
 .../worldmuseum/worlds/VoidWorld.java         |  47 +++++++
 .../craftattack/worldmuseum/worlds/World.java | 120 +++++++++++-------
 .../worldmuseum/worlds/WorldManager.java      |  24 ++--
 25 files changed, 399 insertions(+), 253 deletions(-)
 create mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/inventory/WorldSelector.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/items/ActionItem.java
 delete mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/items/ItemManager.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/items/Items.java
 delete mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/listener/MovementListener.java
 delete mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/util/ChangeWorld.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/PlayerMovable.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/VoidWorld.java

diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d28243b..70217ed 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,8 +4,11 @@
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
+        <option name="delegatedBuild" value="true" />
+        <option name="testRunner" value="GRADLE" />
+        <option name="distributionType" value="DEFAULT_WRAPPED" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
-        <option name="gradleHome" value="/usr/share/java/gradle" />
+        <option name="gradleJvm" value="17" />
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 775445d..9f230de 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="ExternalStorageConfigurationManager" enabled="true" />
   <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
diff --git a/build.gradle b/build.gradle
index adb2947..36c5a57 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,8 +3,7 @@ plugins {
     id "com.github.johnrengelman.shadow" version "7.1.0"
 }
 
-group = '' +
-        ''
+group = 'eu.mhsl.craftattack.worldmuseum'
 version = '1.0'
 
 repositories {
@@ -13,13 +12,24 @@ repositories {
 }
 
 dependencies {
-
-    implementation 'com.github.Minestom:Minestom:8ad2c7701f'
+    implementation 'com.github.waxeria:Minestom:e0427a36f3'
+    implementation 'org.jctools:jctools-core:4.0.1'
 }
 
-jar {
-    manifest {
-        attributes 'Main-Class': 'eu.mhsl.craftattack.worldmuseum.Main',
-                "Multi-Release": true
+tasks {
+    jar {
+        manifest {
+            attributes 'Main-Class': 'eu.mhsl.craftattack.worldmuseum.Main'
+            attributes 'Multi-Release': true
+        }
+        duplicatesStrategy = 'exclude'
+        from configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
+    }
+    build {
+        dependsOn(shadowJar)
+    }
+    shadowJar {
+        mergeServiceFiles()
+        archiveClassifier.set("")
     }
 }
\ No newline at end of file
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/Main.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/Main.java
index 8590ec1..8f71d17 100755
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/Main.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/Main.java
@@ -1,7 +1,6 @@
 package eu.mhsl.craftattack.worldmuseum;
 
 import eu.mhsl.craftattack.worldmuseum.commands.GamemodeCommand;
-import eu.mhsl.craftattack.worldmuseum.items.ItemManager;
 import eu.mhsl.craftattack.worldmuseum.commands.SpawnCommand;
 import eu.mhsl.craftattack.worldmuseum.commands.TeleportCommand;
 import eu.mhsl.craftattack.worldmuseum.listener.*;
@@ -10,7 +9,7 @@ import eu.mhsl.craftattack.worldmuseum.util.TablistUpdateTask;
 import eu.mhsl.craftattack.worldmuseum.util.Config;
 import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
 import eu.mhsl.craftattack.worldmuseum.handler.SignHandler;
-import eu.mhsl.craftattack.worldmuseum.worlds.World;
+import eu.mhsl.craftattack.worldmuseum.worlds.VoidWorld;
 import eu.mhsl.craftattack.worldmuseum.worlds.WorldManager;
 import net.minestom.server.MinecraftServer;
 import net.minestom.server.entity.GameMode;
@@ -21,13 +20,13 @@ import net.minestom.server.event.player.*;
 import net.minestom.server.extras.bungee.BungeeCordProxy;
 import net.minestom.server.timer.TaskSchedule;
 
-import java.time.Duration;
+import java.io.IOException;
 
 public class Main {
     static int PORT = 25565;
     static final String IP = "0.0.0.0";
 
-    public static void main(String[] args) {
+    public static void main(String[] args) throws IOException {
         // Initialization
         System.setProperty("minestom.chunk-view-distance", "16");
         MinecraftServer minecraftServer = MinecraftServer.init();
@@ -45,14 +44,12 @@ public class Main {
         //load main config
         Config config = Config.getInstance();
         config.loadConfig();
-        World startworld = config.getStart_world();
         if (config.isBungeecordEnabled()) {
             BungeeCordProxy.enable();
             System.out.println("[Info] Bungeecord enabled");
         }
 
 
-        // Add an event callback to specify the spawning instance (and the spawn position)
         GlobalEventHandler globalEventHandler = MinecraftServer.getGlobalEventHandler();
 
         //listeners
@@ -60,7 +57,6 @@ public class Main {
         globalEventHandler.addListener(PlayerChunkUnloadEvent.class, new ChunkUnloading());
         globalEventHandler.addListener(InventoryPreClickEvent.class, new InventoryClickListener());
         globalEventHandler.addListener(PlayerBlockBreakEvent.class, new BlockBreakListener());
-        globalEventHandler.addListener(PlayerMoveEvent.class, new MovementListener());
         globalEventHandler.addListener(PlayerBlockPlaceEvent.class, new BlockPlaceListener());
         globalEventHandler.addListener(PlayerDisconnectEvent.class, new DisconnectListener());
 
@@ -78,18 +74,17 @@ public class Main {
 
         globalEventHandler.addListener(PlayerLoginEvent.class, event -> {
             final MuseumPlayer player = (MuseumPlayer) event.getPlayer();
-            player.setSyncCooldown(Duration.ofSeconds(3));
-            event.setSpawningInstance(startworld);
+            final VoidWorld voidWorld = new VoidWorld();
 
             player.setPermissionLevel(4);
-            player.setRespawnPoint(startworld.getSpawn());
-            player.setGameMode(GameMode.SURVIVAL);
-            MinecraftServer.getSchedulerManager().scheduleNextTick(() -> {
-                SkinCache.setSkin(player);
-                player.setAllowFlying(true);
-                player.getInventory().setItemStack(0, ItemManager.getCompassItem());
-                player.getInventory().setItemStack(8, ItemManager.getBedItem());
-            });
+            player.setRespawnPoint(voidWorld.getSpawn());
+            player.setGameMode(GameMode.SPECTATOR);
+            player.setAllowFlying(true);
+            SkinCache.setSkin(player);
+
+            event.setSpawningInstance(voidWorld);
+            MinecraftServer.getSchedulerManager().scheduleNextTick(() -> voidWorld.movePlayer(player));
+
             System.out.println("[Join] Player " + player.getUsername() +" joined the server.");
         });
 
@@ -98,6 +93,7 @@ public class Main {
         } catch (Exception e) {
             if (args.length != 0) System.out.println("Given port doesn't work.");
         }
+
         // Start the server on port default port 25565 if none is given
         System.out.println("[Info] Running on " + IP + ":" + PORT);
         minecraftServer.start(IP, PORT);
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/SpawnCommand.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/SpawnCommand.java
index 1395a9a..b44f4cf 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/SpawnCommand.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/SpawnCommand.java
@@ -1,17 +1,23 @@
 package eu.mhsl.craftattack.worldmuseum.commands;
 
+import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
 import eu.mhsl.craftattack.worldmuseum.worlds.World;
 import net.minestom.server.command.builder.Command;
-import net.minestom.server.entity.Player;
 
 public class SpawnCommand extends Command {
     public SpawnCommand() {
         super("spawn");
         setDefaultExecutor((sender, context) -> {
-            Player p = (Player) sender;
-            World world = (World) p.getInstance();
-            assert world != null;
-            p.teleport(world.getSpawn());
+            if(sender instanceof MuseumPlayer p) {
+                teleportToSpawn(p);
+            }
         });
     }
+
+    public static void teleportToSpawn(MuseumPlayer p) {
+        if(p.getInstance() instanceof World world) {
+            p.startLoading();
+            p.teleport(world.getSpawn()).thenRun(p::stopLoading);
+        }
+    }
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/TeleportCommand.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/TeleportCommand.java
index 86cca77..25e4a8b 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/TeleportCommand.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/commands/TeleportCommand.java
@@ -11,22 +11,23 @@ public class TeleportCommand extends Command {
     public TeleportCommand() {
         super("tp");
 
-        var cordinate = ArgumentType.RelativeBlockPosition("Koordinate");
+        var coordinate = ArgumentType.RelativeBlockPosition("cords");
         addSyntax(((sender, context) -> {
-            Pos pos = context.get(cordinate).fromSender(sender).asPosition();
+            Pos pos = context.get(coordinate).fromSender(sender).asPosition();
             Player player = (Player) sender;
             player.teleport(pos);
-        }), cordinate);
+        }), coordinate);
 
-        var playerArgument = ArgumentType.Entity("Spieler").onlyPlayers(true).singleEntity(true);
+        var playerArgument = ArgumentType.Entity("player").onlyPlayers(true).singleEntity(true);
         addSyntax(((sender, context) -> {
-            if (context.get(playerArgument).find(sender).size() == 0) return; //check if player is online
+            if (context.get(playerArgument).find(sender).isEmpty()) return; //check if player is online
             Player targetPlayer = (Player) context.get(playerArgument).find(sender).get(0);
             Player p = (Player) sender;
-            if (!Objects.equals(p.getInstance(), targetPlayer.getInstance()))
+            if (!Objects.equals(p.getInstance(), targetPlayer.getInstance())) {
                 p.setInstance(Objects.requireNonNull(targetPlayer.getInstance()), targetPlayer.getPosition());
-            else
+            } else {
                 p.teleport(targetPlayer.getPosition());
+            }
         }), playerArgument);
     }
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/handler/SignHandler.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/handler/SignHandler.java
index caa0a45..11f6824 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/handler/SignHandler.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/handler/SignHandler.java
@@ -11,7 +11,6 @@ import java.util.Collection;
 public class SignHandler implements BlockHandler {
     @Override
     public @NotNull Collection<Tag<?>> getBlockEntityTags() {
-
         return new ArrayList<>() {
             {
                 add(Tag.Byte("GlowingText"));
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/inventory/WorldSelector.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/inventory/WorldSelector.java
new file mode 100644
index 0000000..c90db1f
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/inventory/WorldSelector.java
@@ -0,0 +1,31 @@
+package eu.mhsl.craftattack.worldmuseum.inventory;
+
+import eu.mhsl.craftattack.worldmuseum.worlds.World;
+import eu.mhsl.craftattack.worldmuseum.worlds.WorldManager;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.minestom.server.inventory.Inventory;
+import net.minestom.server.inventory.InventoryType;
+
+import java.util.List;
+
+public class WorldSelector extends Inventory {
+    public WorldSelector() {
+        super(InventoryType.CHEST_1_ROW, Component.text("Weltenauswahl").color(NamedTextColor.DARK_GRAY));
+
+        List<World> worlds = WorldManager.getInstance().getWorlds();
+        for (int i = 0; i < worlds.size(); i++) {
+            World world = worlds.get(i);
+            setItemStack(i, world.getItem());
+        }
+
+        addInventoryCondition((player, slot, clickType, inventoryConditionResult) -> {
+            inventoryConditionResult.setCancel(true);
+
+            player.closeInventory();
+            try {
+                worlds.get(slot).movePlayer(player);
+            } catch (IndexOutOfBoundsException ignore) {}
+        });
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/items/ActionItem.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/items/ActionItem.java
new file mode 100644
index 0000000..1eea542
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/items/ActionItem.java
@@ -0,0 +1,32 @@
+package eu.mhsl.craftattack.worldmuseum.items;
+
+import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
+import net.minestom.server.entity.Player;
+import net.minestom.server.item.ItemStack;
+import net.minestom.server.tag.Tag;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class ActionItem {
+    public static Tag<UUID> tag = Tag.UUID("action");
+    public static Map<UUID, Consumer<MuseumPlayer>> actions = new HashMap<>();
+
+    private final ItemStack itemStack;
+
+    public ActionItem(Consumer<MuseumPlayer> action, ItemStack itemStack) {
+        UUID uuid = UUID.randomUUID();
+        this.itemStack = itemStack.withTag(tag, uuid);
+        actions.put(uuid, action);
+    }
+
+    public static void run(UUID uuid, Player p) {
+        actions.get(uuid).accept((MuseumPlayer) p);
+    }
+
+    public ItemStack getItemStack() {
+        return itemStack;
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/items/ItemManager.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/items/ItemManager.java
deleted file mode 100644
index 5c73862..0000000
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/items/ItemManager.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package eu.mhsl.craftattack.worldmuseum.items;
-
-import net.kyori.adventure.text.Component;
-import net.minestom.server.item.ItemStack;
-import net.minestom.server.item.Material;
-
-public class ItemManager {
-    public static ItemStack getBedItem() {
-        return ItemStack.builder(Material.RED_BED).displayName(Component.text("Spawn-Teleporter")).build();
-    }
-    public static ItemStack getCompassItem() {
-        return ItemStack.builder(Material.COMPASS).displayName(Component.text("World-changer")).build();
-    }
-}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/items/Items.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/items/Items.java
new file mode 100644
index 0000000..7152b54
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/items/Items.java
@@ -0,0 +1,67 @@
+package eu.mhsl.craftattack.worldmuseum.items;
+
+import eu.mhsl.craftattack.worldmuseum.commands.SpawnCommand;
+import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
+import eu.mhsl.craftattack.worldmuseum.worlds.VoidWorld;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.minestom.server.item.ItemStack;
+import net.minestom.server.item.Material;
+
+import java.util.function.Consumer;
+
+public enum Items {
+    HUB(
+            ItemStack
+                    .of(Material.IRON_DOOR)
+                    .withDisplayName(
+                            Component
+                                    .text("Zurück zur Überischt")
+                                    .color(NamedTextColor.GOLD)
+                    ),
+            player -> new VoidWorld().movePlayer(player)
+    ),
+
+    SPAWN(
+            ItemStack
+                    .of(Material.TARGET)
+                    .withDisplayName(
+                            Component
+                                    .text("Zum Spawn")
+                                    .color(NamedTextColor.GOLD)
+                    ),
+            SpawnCommand::teleportToSpawn
+    ),
+
+    MORE_SPEED(
+            ItemStack
+                    .of(Material.FEATHER)
+                    .withDisplayName(
+                            Component
+                                    .text("Schneller fliegen")
+                                    .color(NamedTextColor.AQUA)
+                    ),
+            player -> player.updateFlyingSpeed(0.05f)
+    ),
+
+    LESS_SPEED(
+            ItemStack
+                    .of(Material.ANVIL)
+                    .withDisplayName(
+                            Component
+                                    .text("Langsamer fliegen")
+                                    .color(NamedTextColor.AQUA)
+                    ),
+            player -> player.updateFlyingSpeed(-0.05f)
+    );
+
+
+    private final ActionItem item;
+    Items(ItemStack itemStack, Consumer<MuseumPlayer> action) {
+        item = new ActionItem(action, itemStack);
+    }
+
+    public ItemStack getItem() {
+        return item.getItemStack();
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockBreakListener.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockBreakListener.java
index 68fdb4d..d4ee114 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockBreakListener.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockBreakListener.java
@@ -3,6 +3,7 @@ package eu.mhsl.craftattack.worldmuseum.listener;
 import net.minestom.server.MinecraftServer;
 import net.minestom.server.coordinate.Point;
 import net.minestom.server.event.player.PlayerBlockBreakEvent;
+import net.minestom.server.instance.Instance;
 import net.minestom.server.instance.block.Block;
 import net.minestom.server.timer.TaskSchedule;
 import java.util.function.Consumer;
@@ -12,6 +13,8 @@ public class BlockBreakListener implements Consumer<PlayerBlockBreakEvent> {
     public void accept(PlayerBlockBreakEvent playerBlockBreakEvent) {
         Block block = playerBlockBreakEvent.getBlock();
         Point point = playerBlockBreakEvent.getBlockPosition();
-        MinecraftServer.getSchedulerManager().scheduleTask(() -> playerBlockBreakEvent.getInstance().setBlock(point, block), TaskSchedule.seconds(10), TaskSchedule.stop());
+        Instance instance = playerBlockBreakEvent.getInstance();
+
+        MinecraftServer.getSchedulerManager().scheduleTask(() -> instance.setBlock(point, block), TaskSchedule.seconds(10), TaskSchedule.stop());
     }
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockPlaceListener.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockPlaceListener.java
index ad017e6..86a725a 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockPlaceListener.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/BlockPlaceListener.java
@@ -1,14 +1,25 @@
 package eu.mhsl.craftattack.worldmuseum.listener;
 
-import eu.mhsl.craftattack.worldmuseum.worlds.World;
+import net.minestom.server.entity.Player;
 import net.minestom.server.event.player.PlayerBlockPlaceEvent;
-import java.util.Objects;
+import net.minestom.server.event.player.PlayerUseItemEvent;
+
 import java.util.function.Consumer;
 
 public class BlockPlaceListener implements Consumer<PlayerBlockPlaceEvent> {
     @Override
     public void accept(PlayerBlockPlaceEvent playerBlockPlaceEvent) {
-        playerBlockPlaceEvent.getPlayer().teleport(((World) Objects.requireNonNull(playerBlockPlaceEvent.getPlayer().getInstance())).getSpawn());
+        Player p = playerBlockPlaceEvent.getPlayer();
+
+        // Fire ItemUseListener for possible missed block-clicks
+        new ItemUseListener().accept(
+                new PlayerUseItemEvent(
+                        p,
+                        playerBlockPlaceEvent.getHand(),
+                        p.getInventory().getItemStack(p.getHeldSlot())
+                )
+        );
+
         playerBlockPlaceEvent.setCancelled(true);
     }
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ChunkUnloading.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ChunkUnloading.java
index 7afa63f..9ea48c3 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ChunkUnloading.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ChunkUnloading.java
@@ -10,7 +10,7 @@ public class ChunkUnloading implements Consumer<PlayerChunkUnloadEvent> {
         var chunk = e.getInstance().getChunk(e.getChunkX(), e.getChunkZ());
         if (chunk == null) return;
 
-        if (chunk.getViewers().size() == 0) {
+        if (chunk.getViewers().isEmpty()) {
             try {
                 e.getInstance().unloadChunk(e.getChunkX(), e.getChunkZ());
             } catch (NullPointerException ignored) {}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/InventoryClickListener.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/InventoryClickListener.java
index ee58547..ca73ac6 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/InventoryClickListener.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/InventoryClickListener.java
@@ -1,29 +1,12 @@
 package eu.mhsl.craftattack.worldmuseum.listener;
 
-import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
-import eu.mhsl.craftattack.worldmuseum.util.ChangeWorld;
-import eu.mhsl.craftattack.worldmuseum.worlds.World;
-import eu.mhsl.craftattack.worldmuseum.worlds.WorldManager;
 import net.minestom.server.event.inventory.InventoryPreClickEvent;
-import java.util.Objects;
+
 import java.util.function.Consumer;
 
 public class InventoryClickListener implements Consumer<InventoryPreClickEvent> {
     @Override
     public void accept(InventoryPreClickEvent inventoryPreClickEvent) {
-        MuseumPlayer p = (MuseumPlayer) inventoryPreClickEvent.getPlayer();
-        WorldManager worldManager = WorldManager.getInstance();
-
-        for (World w : worldManager.getWorlds()) {
-            if (!w.getItem().equals(inventoryPreClickEvent.getClickedItem())) continue;
-            if (Objects.equals(p.getInstance(), w)) {
-                p.closeInventory();
-                p.teleport(w.getSpawn());
-            } else {
-                p.closeInventory();
-                ChangeWorld.changeWorld(p, w);
-            }
-        }
         inventoryPreClickEvent.setCancelled(true);
     }
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ItemUseListener.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ItemUseListener.java
index 4754e1d..80432fe 100755
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ItemUseListener.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/ItemUseListener.java
@@ -1,37 +1,18 @@
 package eu.mhsl.craftattack.worldmuseum.listener;
 
-import eu.mhsl.craftattack.worldmuseum.items.ItemManager;
-import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
-import eu.mhsl.craftattack.worldmuseum.worlds.World;
-import net.kyori.adventure.text.Component;
+import eu.mhsl.craftattack.worldmuseum.items.ActionItem;
 import net.minestom.server.event.player.PlayerUseItemEvent;
-import net.minestom.server.item.ItemStack;
+
 import java.util.function.Consumer;
 
 public class ItemUseListener implements Consumer<PlayerUseItemEvent> {
 
     @Override
     public void accept(PlayerUseItemEvent playerUseItemEvent) {
-        MuseumPlayer p = (MuseumPlayer) playerUseItemEvent.getPlayer();
-        ItemStack usedItem = playerUseItemEvent.getItemStack();
-
-        if (usedItem.equals(ItemManager.getCompassItem())) compassUse(p);
-        else if (usedItem.equals(ItemManager.getBedItem())) bedUse(p);
+        if(playerUseItemEvent.getItemStack().hasTag(ActionItem.tag)) {
+            ActionItem.run(playerUseItemEvent.getItemStack().getTag(ActionItem.tag), playerUseItemEvent.getPlayer());
+        }
 
         playerUseItemEvent.setCancelled(true);
     }
-    private void compassUse(MuseumPlayer p) {
-        if (!p.isAllowCompassUsage()) {
-            long lastUsed = (System.currentTimeMillis() - p.getLastCompassUsage()) / 1000;
-            p.sendActionBar(Component.text("Du kannst den Kompass erst in " + (15 - lastUsed) + " Sekunden wieder benutzen."));
-            return;
-        }
-        p.openInventory(p.getCompassInventory());
-    }
-
-    private void bedUse(MuseumPlayer player) {
-        World world = (World) player.getInstance();
-        assert world != null;
-        player.teleport(world.getSpawn());
-    }
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/MovementListener.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/MovementListener.java
deleted file mode 100644
index f6e36b5..0000000
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/listener/MovementListener.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package eu.mhsl.craftattack.worldmuseum.listener;
-
-import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
-import net.minestom.server.event.player.PlayerMoveEvent;
-import java.util.function.Consumer;
-
-public class MovementListener implements Consumer<PlayerMoveEvent> {
-    @Override
-    public void accept(PlayerMoveEvent playerMoveEvent) {
-        MuseumPlayer player = (MuseumPlayer) playerMoveEvent.getPlayer();
-        if (!player.isAllowMovement()) playerMoveEvent.setCancelled(true);
-    }
-}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/ChangeWorld.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/ChangeWorld.java
deleted file mode 100644
index 48f6c31..0000000
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/ChangeWorld.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package eu.mhsl.craftattack.worldmuseum.util;
-
-import eu.mhsl.craftattack.worldmuseum.worlds.World;
-import net.minestom.server.potion.Potion;
-import net.minestom.server.potion.PotionEffect;
-public class ChangeWorld {
-    public static void changeWorld(MuseumPlayer player, World targetWorld) {
-        player.setAllowMovement(false);
-        World world = (World) player.getInstance();
-        player.setAllowCompassUsage(false);
-        player.addEffect(new Potion(PotionEffect.BLINDNESS, (byte) 0, 1000000));
-
-        player.setRespawnPoint(targetWorld.getSpawn());
-        player.setInstance(targetWorld, targetWorld.getSpawn()).thenRun(() -> {
-            player.setAllowMovement(true);
-            player.clearEffects();
-            player.update_lastCompassUsage();
-            player.setAllowCompassUsage(true);
-        });
-        assert world != null;
-        System.out.println("[Worldchange] Player " + player.getUsername() + " changed world from " + world.getName() + " to " + targetWorld.getName() +".");
-    }
-}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/Config.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/Config.java
index 5437aab..9288351 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/Config.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/Config.java
@@ -25,13 +25,9 @@ public class Config {
                 if (!w.getName().equals(config.get("start_world").getAsString())) continue;
                 this.start_world = w;
             }
-            if (start_world == null) {
-                System.out.println("no starting world set");
-                System.exit(0);
-            }
             this.bungeecordEnabled = config.get("bungeecord").getAsBoolean();
 
-        }catch (IOException e) {
+        } catch (IOException e) {
             e.printStackTrace();
         }
     }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/MuseumPlayer.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/MuseumPlayer.java
index 68b6e53..4955be8 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/MuseumPlayer.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/MuseumPlayer.java
@@ -1,70 +1,66 @@
 package eu.mhsl.craftattack.worldmuseum.util;
 
-import eu.mhsl.craftattack.worldmuseum.worlds.World;
-import eu.mhsl.craftattack.worldmuseum.worlds.WorldManager;
 import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.title.Title;
+import net.minestom.server.MinecraftServer;
 import net.minestom.server.entity.Player;
-import net.minestom.server.inventory.Inventory;
-import net.minestom.server.inventory.InventoryType;
-import net.minestom.server.item.Enchantment;
-import net.minestom.server.item.ItemHideFlag;
-import net.minestom.server.item.ItemStack;
+import net.minestom.server.event.player.PlayerMoveEvent;
 import net.minestom.server.network.player.PlayerConnection;
+import net.minestom.server.potion.Potion;
+import net.minestom.server.potion.PotionEffect;
+import net.minestom.server.timer.ExecutionType;
+import net.minestom.server.timer.TaskSchedule;
+import net.minestom.server.utils.MathUtils;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+
 import java.time.Duration;
 import java.util.UUID;
 
 public class MuseumPlayer extends Player {
-
-    private boolean allowMovement = true;
-    private long lastCompassUsage = 0;
-    private boolean allowCompassUsage = true;
+    private boolean isLoading = false;
 
     public MuseumPlayer(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection playerConnection) {
         super(uuid, username, playerConnection);
-    }
-    public void setSyncCooldown(@Nullable Duration cooldown) {
-        this.setCustomSynchronizationCooldown(cooldown);
+
+        eventNode().addListener(PlayerMoveEvent.class, playerMoveEvent -> playerMoveEvent.setCancelled(isLoading));
     }
 
-    public boolean isAllowMovement() {
-        return allowMovement;
-    }
-    public void update_lastCompassUsage() {
-        lastCompassUsage = System.currentTimeMillis();
-    }
-    public boolean isAllowCompassUsage() {
-        long UsageDifference = System.currentTimeMillis() - lastCompassUsage;
-        long difference = 15000; //in Milisec
-        return UsageDifference >= difference && allowCompassUsage;
-    }
-    public void setAllowMovement(boolean allowMovement) {
-        this.allowMovement = allowMovement;
-    }
-    public void setAllowCompassUsage(boolean allowCompassUsage) {
-        this.allowCompassUsage = allowCompassUsage;
-    }
-    public long getLastCompassUsage() {
-        return lastCompassUsage;
-    }
-    public Inventory getCompassInventory() {
-        Inventory inventory = new Inventory(InventoryType.CHEST_1_ROW, Component.text("World-changer"));
-        for (World w : WorldManager.getInstance().getWorlds()) {
-            if (!w.equals(this.getInstance())) {
-                inventory.addItemStack(w.getItem());
-                continue;
+    public void startLoading() {
+        isLoading = true;
+        MinecraftServer.getSchedulerManager().submitTask(() -> {
+            sendMessage("" + System.currentTimeMillis());
+            addEffect(new Potion(PotionEffect.BLINDNESS, (byte) 3, 600));
+            showTitle(Title.title(
+                    Component.text("Loading").decorate(TextDecoration.BOLD).color(NamedTextColor.GOLD),
+                    Component.text("...").color(NamedTextColor.YELLOW),
+                    Title.Times.times(Duration.ZERO, Duration.ofSeconds(30), Duration.ZERO)
+            ));
+
+            if(!isLoading) {
+                clearEffects();
+                clearTitle();
+                return TaskSchedule.stop();
             }
-            ItemStack itemStack = ItemStack.builder(w.getItem().material())
-                    .displayName(w.getItem().getDisplayName())
-                    .meta(metaBuilder ->
-                            metaBuilder
-                                .enchantment(Enchantment.MENDING, (short) 10)
-                                .hideFlag(ItemHideFlag.HIDE_ENCHANTS))
-                    .build();
-            inventory.addItemStack(itemStack);
-        }
-        return inventory;
+            return TaskSchedule.millis(100);
+        }, ExecutionType.ASYNC);
     }
 
+    public void stopLoading() {
+        isLoading = false;
+    }
+
+    public void updateFlyingSpeed(float speed) {
+        speed = MathUtils.clamp(getFlyingSpeed() + speed, 0.01f, 0.2f);
+        setFlyingSpeed(speed);
+        sendActionBar(
+                Component
+                        .text("Geschwindigkeit: ")
+                        .color(NamedTextColor.GOLD)
+                        .append(Component.text(Math.round(speed * 1000) + "%").color(NamedTextColor.WHITE))
+        );
+    }
+
+
 }
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/TablistUpdateTask.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/TablistUpdateTask.java
index 631a49d..e27dd13 100644
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/util/TablistUpdateTask.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/util/TablistUpdateTask.java
@@ -5,9 +5,7 @@ import net.kyori.adventure.text.format.NamedTextColor;
 import net.minestom.server.MinecraftServer;
 import net.minestom.server.adventure.audience.Audiences;
 import net.minestom.server.entity.Player;
-import net.minestom.server.monitoring.TickMonitor;
 import java.util.Collection;
-import java.util.concurrent.atomic.AtomicReference;
 
 public class TablistUpdateTask implements Runnable {
     @Override
@@ -18,7 +16,7 @@ public class TablistUpdateTask implements Runnable {
         final Component header =
                 Component.newline()
                         .append(Component.text("  Welten-Museum  ", NamedTextColor.GOLD))
-                        .append(Component.newline()).append(Component.text("Players: " + players.size()))
+                        .append(Component.newline()).append(Component.text("Spieler: " + players.size()))
                         .append(Component.newline());
 
         final Component footer =
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/PlayerMovable.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/PlayerMovable.java
new file mode 100644
index 0000000..dd95381
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/PlayerMovable.java
@@ -0,0 +1,7 @@
+package eu.mhsl.craftattack.worldmuseum.worlds;
+
+import net.minestom.server.entity.Player;
+
+public interface PlayerMovable {
+    void movePlayer(Player p);
+}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/VoidWorld.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/VoidWorld.java
new file mode 100644
index 0000000..128261d
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/VoidWorld.java
@@ -0,0 +1,47 @@
+package eu.mhsl.craftattack.worldmuseum.worlds;
+
+import eu.mhsl.craftattack.worldmuseum.inventory.WorldSelector;
+import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
+import net.minestom.server.MinecraftServer;
+import net.minestom.server.coordinate.Pos;
+import net.minestom.server.entity.Player;
+import net.minestom.server.event.inventory.InventoryCloseEvent;
+import net.minestom.server.event.player.PlayerMoveEvent;
+import net.minestom.server.instance.InstanceContainer;
+import net.minestom.server.instance.block.Block;
+import net.minestom.server.world.DimensionType;
+
+import java.util.UUID;
+
+public class VoidWorld extends InstanceContainer implements PlayerMovable {
+    public VoidWorld() {
+        super(UUID.randomUUID(), DimensionType.OVERWORLD);
+        MinecraftServer.getInstanceManager().registerInstance(this);
+        setBlock(getSpawn().sub(0, 1, 0), Block.BARRIER);
+
+        eventNode().addListener(InventoryCloseEvent.class, inventoryCloseEvent -> {
+            if(inventoryCloseEvent.getPlayer().getInstance() != this) return;
+            inventoryCloseEvent.setNewInventory(inventoryCloseEvent.getInventory());
+        });
+
+        eventNode().addListener(PlayerMoveEvent.class, playerMoveEvent -> playerMoveEvent.setCancelled(true));
+    }
+
+    public Pos getSpawn() {
+        return new Pos(0, 50, 0, 0, 0);
+    }
+
+    public void movePlayer(Player p) {
+        MuseumPlayer museumPlayer = (MuseumPlayer) p;
+        if(p.getInstance() != this) p.setInstance(this).thenRun(() -> {
+            p.teleport(getSpawn());
+            p.openInventory(new WorldSelector());
+            museumPlayer.stopLoading();
+        });
+
+        museumPlayer.startLoading();
+        p.getInventory().clear();
+        p.setRespawnPoint(getSpawn());
+        p.setAllowFlying(false);
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/World.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/World.java
index 0eef411..831980b 100755
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/World.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/World.java
@@ -1,78 +1,108 @@
 package eu.mhsl.craftattack.worldmuseum.worlds;
 
+import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
+import eu.mhsl.craftattack.worldmuseum.items.Items;
+import eu.mhsl.craftattack.worldmuseum.util.MuseumPlayer;
 import net.kyori.adventure.text.Component;
 import net.minestom.server.MinecraftServer;
 import net.minestom.server.coordinate.Pos;
+import net.minestom.server.entity.GameMode;
+import net.minestom.server.entity.Player;
 import net.minestom.server.instance.AnvilLoader;
 import net.minestom.server.instance.InstanceContainer;
 import net.minestom.server.item.ItemStack;
 import net.minestom.server.item.Material;
 import net.minestom.server.utils.NamespaceID;
 import net.minestom.server.world.DimensionType;
+
 import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 
-public class World extends InstanceContainer {
+public class World extends InstanceContainer implements PlayerMovable {
     private final static DimensionType dimension = DimensionType.builder(NamespaceID.from("mhsl.eu:worldview_by_olischma")).height(400).ambientLight(2.0f).build();
-    private final ItemStack item;
-    private String name = "default";
-    private Pos spawn = new Pos(0,0,0);
-    private boolean enabled = false;
+
+    private final boolean enabled;
+    private final String name;
+    private final Material material;
+    private final Pos spawn;
+    private final List<String> tags = new ArrayList<>();
+
 
     static {
         MinecraftServer.getDimensionTypeManager().addDimension(dimension);
     }
-    AnvilLoader anvilLoader;
 
-    public World(File file) {
+    public World(File file) throws IOException {
         super(UUID.randomUUID(), dimension, new AnvilLoader(file.getPath()));
-        this.anvilLoader = new AnvilLoader(file.getPath());
         MinecraftServer.getInstanceManager().registerInstance(this);
-        try {
-            File checkFile = new File(file.getPath() + "/config.json");
-            if (checkFile.createNewFile()) {
-                FileWriter fileWriter = new FileWriter(file.getPath() + "/config.json");
-                String defaultConfig = """
-                        {
-                          "enabled": false,
-                          "name": "DEFAULT_NAME",
-                          "material": "GRASS_BLOCK",
-                          "spawn": {
-                            "x": 0,
-                            "y": 0,
-                            "z": 0
-                          },
-                          "tags": [
-                            "RANDOM_TAG"
-                          ]
-                        }""";
-                fileWriter.write(defaultConfig);
-                fileWriter.close();
-            }
-        } catch (IOException e){
-            e.printStackTrace();
+
+        File configFile = new File(file.getPath() + "/config.json");
+        if (configFile.createNewFile()) {
+            writeDefaultConfig(file);
         }
 
-        Material material = Material.GRASS_BLOCK;
-        try {
-            JsonObject config = (JsonObject) JsonParser.parseReader(new FileReader(file.getPath() + "/config.json"));
-            this.name = config.get("name").getAsString();
-            JsonObject spawn = config.getAsJsonObject("spawn");
-            this.spawn = new Pos(spawn.get("x").getAsDouble(), spawn.get("y").getAsDouble(), spawn.get("z").getAsDouble());
-            material = Material.fromNamespaceId("minecraft:" + config.get("material").getAsString().toLowerCase());
-            this.enabled = config.get("enabled").getAsBoolean();
+        JsonObject config = (JsonObject) JsonParser.parseReader(new FileReader(file.getPath() + "/config.json"));
 
-        } catch (FileNotFoundException e) {
-            e.printStackTrace();
-        }
-        assert material != null;
-        item = ItemStack.builder(material).displayName(Component.text(name)).build();
+        this.enabled = config.get("enabled").getAsBoolean();
+        this.name = config.get("name").getAsString();
+        this.material = Material.fromNamespaceId("minecraft:" + config.get("material").getAsString().toLowerCase());
+
+        JsonObject spawn = config.getAsJsonObject("spawn");
+        this.spawn = new Pos(spawn.get("x").getAsDouble(), spawn.get("y").getAsDouble(), spawn.get("z").getAsDouble());
+
+        JsonArray tags = config.getAsJsonArray("tags");
+        tags.forEach(jsonElement -> this.tags.add(jsonElement.getAsString()));
+
+    }
+
+    @Override
+    public void movePlayer(Player player) {
+        MuseumPlayer p = (MuseumPlayer) player;
+
+        p.startLoading();
+        p.teleport(getSpawn());
+        p.setInstance(this).thenRun(() -> {
+            p.setRespawnPoint(getSpawn());
+            p.teleport(getSpawn());
+            p.setGameMode(GameMode.SURVIVAL);
+            p.setAllowFlying(true);
+
+            p.getInventory().setItemStack(3, Items.MORE_SPEED.getItem());
+            p.getInventory().setItemStack(4, Items.LESS_SPEED.getItem());
+
+            p.getInventory().setItemStack(7, Items.SPAWN.getItem());
+            p.getInventory().setItemStack(8, Items.HUB.getItem());
+
+            p.stopLoading();
+        });
+    }
+
+    private void writeDefaultConfig(File file) throws IOException {
+        FileWriter fileWriter = new FileWriter(file.getPath() + "/config.json");
+        String defaultConfig = """
+                {
+                  "enabled": false,
+                  "name": "DEFAULT_NAME",
+                  "material": "GRASS_BLOCK",
+                  "spawn": {
+                    "x": 0,
+                    "y": 0,
+                    "z": 0
+                  },
+                  "tags": [
+                    "RANDOM_TAG"
+                  ]
+                }""";
+        fileWriter.write(defaultConfig);
+        fileWriter.close();
     }
 
     public ItemStack getItem() {
-        return item;
+        return ItemStack.builder(material).displayName(Component.text(name)).build();
     }
 
     public String getName() {
diff --git a/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/WorldManager.java b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/WorldManager.java
index dc35ee7..ea0c089 100755
--- a/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/WorldManager.java
+++ b/src/main/java/eu/mhsl/craftattack/worldmuseum/worlds/WorldManager.java
@@ -1,7 +1,9 @@
 package eu.mhsl.craftattack.worldmuseum.worlds;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 public class WorldManager {
@@ -14,11 +16,11 @@ public class WorldManager {
         }
         return instance;
     }
-    public void loadWorlds() {
+    public void loadWorlds() throws IOException {
         File saveDirectory = new File("saves");
-        List<File> worldFolders = getFolderNames(saveDirectory);
-        for (File f: worldFolders
-        ) {
+        if(!saveDirectory.exists()) throw new IOException("Saves folder 'saves' could not be found!");
+        List<File> worldFolders = getFolders(saveDirectory);
+        for (File f : worldFolders) {
             World w = new World(f);
             if (w.isEnabled()) {
                 worlds.add(w);
@@ -26,16 +28,14 @@ public class WorldManager {
                 System.out.println("Es gibt eine nicht aktivierte Welt: " + f);
             }
         }
+
     }
 
-    private List<File> getFolderNames(File curDir) {
-        File[] fileList = curDir.listFiles();
-        List<File> returnFolders = new ArrayList<>();
-        assert fileList != null;
-        for (File f: fileList) {
-            if (f.isDirectory()) returnFolders.add(f);
-        }
-        return returnFolders;
+    private List<File> getFolders(File directory) {
+        File[] fileList = directory.listFiles();
+        if(fileList == null) return new ArrayList<>();
+
+        return Arrays.stream(fileList).filter(File::isDirectory).toList();
     }
 
     public List<World> getWorlds() {