diff --git a/game/client/build.gradle.kts b/game/client/build.gradle.kts
index b850ec707..75d30edb7 100644
--- a/game/client/build.gradle.kts
+++ b/game/client/build.gradle.kts
@@ -34,7 +34,13 @@ repositories {
 }
 
 dependencies {
+    // <test dependencies>
+    testImplementation(platform("org.junit:junit-bom:5.10.3"))
+    testImplementation("org.junit.jupiter:junit-jupiter")
+    // </test dependencies>
+
     implementation(project(":game:core"))?.let { include(it) }
+    implementation(project(":util:seed-viewer"))?.let { include(it) }
     runtimeOnly(project(path = ":util:datagen", configuration = "output"))?.let { include(it) }
 
     // <core dependencies>
@@ -55,8 +61,6 @@ dependencies {
     implementation("com.github.JnCrMx:discord-game-sdk4j:v0.5.5")?.let { include(it) }
 
     implementation("objects:client:43db9b498cb67058d2e12d394e6507722e71bb45") // Minecraft client jar
-
-    implementation("com.formdev:flatlaf:3.1.1")?.let { include(it) } // Dark theme used by SeedViewer
 }
 
 java {
@@ -124,6 +128,15 @@ tasks.withType(JavaCompile::class.java) {
         options.compilerArgs.add("8")
     }
 }
+
+tasks.withType(Jar::class.java) {
+    dependsOn(tasks.test)
+}
+
+tasks.test {
+    useJUnitPlatform()
+}
+
 //endregion
 
 sourceSets.main {
diff --git a/game/client/src/main/java/net/minecraft/client/Minecraft.java b/game/client/src/main/java/net/minecraft/client/Minecraft.java
index 550c8cf1f..ff7e67aad 100644
--- a/game/client/src/main/java/net/minecraft/client/Minecraft.java
+++ b/game/client/src/main/java/net/minecraft/client/Minecraft.java
@@ -276,30 +276,16 @@ public class Minecraft
     
     public Minecraft(GameWindow gameWindow)
     {
-    	this.gameWindow = gameWindow;
+        this();
+        this.gameWindow = gameWindow;
         this.resolution = new ScaledResolution(this.gameWindow);
-        Global.accessor = this;
+
         LoggedPrintStream.wrapStreams();
-        hasCrashed = false;
-        timer = new Timer(Global.TICKS_PER_SECOND);
-        session = null;
-        hideQuitButton = false;
-        isGamePaused = false;
-        currentScreen = null;
+
         loadingScreen = new LoadingScreenRenderer(this);
-        ticksRan = 0;
         guiToasts = new GuiElementToastsHud(this);
-        skipRenderWorld = false;
-        objectMouseOver = null;
         sndManager = new SoundEngine();
-        running = true;
-        debug = "";
-        isTakingScreenshot = false;
-        inGameHasFocus = false;
-        mouseTicksRan = 0;
-        isRaining = false;
-        systemTime = System.currentTimeMillis();
-        joinPlayerCounter = 0;
+
         new Thread("Timer hack thread") {
             {
                 this.setDaemon(true);
@@ -316,6 +302,26 @@ public class Minecraft
                 }
             }
         };
+    }
+    public Minecraft() {
+        Global.accessor = this;
+        hasCrashed = false;
+        timer = new Timer(Global.TICKS_PER_SECOND);
+        session = null;
+        hideQuitButton = false;
+        isGamePaused = false;
+        currentScreen = null;
+        ticksRan = 0;
+        skipRenderWorld = false;
+        objectMouseOver = null;
+        running = true;
+        debug = "";
+        isTakingScreenshot = false;
+        inGameHasFocus = false;
+        mouseTicksRan = 0;
+        isRaining = false;
+        systemTime = System.currentTimeMillis();
+        joinPlayerCounter = 0;
         INSTANCE = this;
     }
 
diff --git a/game/client/src/main/java/net/minecraft/client/entity/particle/ParticleDispatcher.java b/game/client/src/main/java/net/minecraft/client/entity/particle/ParticleDispatcher.java
index d87d72efc..f899c04a5 100644
--- a/game/client/src/main/java/net/minecraft/client/entity/particle/ParticleDispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/entity/particle/ParticleDispatcher.java
@@ -77,14 +77,5 @@ public final class ParticleDispatcher extends Dispatcher<String, ParticleLambda>
             return new ParticleFluidDrip(world, x, y, z, color, TextureRegistry.getTexture("minecraft:block/water_flowing"));
         });
         addDispatch("dripLava", (world, x, y, z, motionX, motionY, motionZ, data) -> new ParticleFluidDrip(world, x, y, z, 0xFFFFFF, TextureRegistry.getTexture("minecraft:block/lava_flowing")).setFullBright());
-
-        if (Global.ASSERTIONS_ENABLED) {
-            for (String s : INSTANCE.getParticleIds()) {
-                assert ArgumentTypeParticleId.particleIds.contains(s) : "ArgumentTypeParticleId is missing id " + s + "!";
-            }
-            for (String s : ArgumentTypeParticleId.particleIds) {
-                assert dispatches.containsKey(s) : "ArgumentTypeParticleId defined particle " + s + " which doesn't have an assigned particle lambda!";
-            }
-        }
     }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/MapItemRenderer.java b/game/client/src/main/java/net/minecraft/client/render/MapItemRenderer.java
index dad99d663..bb083313d 100644
--- a/game/client/src/main/java/net/minecraft/client/render/MapItemRenderer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/MapItemRenderer.java
@@ -20,22 +20,34 @@ public class MapItemRenderer
     public static final int IMAGE_HEIGHT = 128;
     public static final int IMAGE_AREA = 128 * 128;
 
+    private final int[] mapImageData;
+    private TextureBuffered mapTexture;
+    private final GameSettings gameSettings;
+    private final TextureManager textureManager;
+    private Font font;
+
+
     public MapItemRenderer(Font font, GameSettings gamesettings, TextureManager textureManager)
     {
         mapImageData = new int[IMAGE_AREA];
         gameSettings = gamesettings;
+        this.textureManager = textureManager;
         this.font = font;
-        mapTexture = textureManager.loadBufferedTexture(new BufferedImage(128, 128, 2));
-        for(int i = 0; i < IMAGE_AREA; i++)
-        {
-            mapImageData[i] = 0;
-        }
-
     }
 
+    public void preRender() {
+        if (mapTexture == null) {
+            mapTexture = textureManager.loadBufferedTexture(new BufferedImage(128, 128, 2));
+            for(int i = 0; i < IMAGE_AREA; i++)
+            {
+                mapImageData[i] = 0;
+            }
+        }
+    }
 
     public void renderMap(TextureManager textureManager, ItemMapSavedData data, float brightness)
     {
+        preRender();
         for(int i = 0; i < IMAGE_AREA; i++)
         {
             int colorIndex = data.colors[i];
@@ -176,9 +188,4 @@ public class MapItemRenderer
         //field_28160_d.drawString("X: " + (int)entityplayer.posX + " Z: " + (int)entityplayer.posZ, j, k, 0xff000000);
         GL11.glPopMatrix();
     }
-
-    private final int[] mapImageData;
-    private final TextureBuffered mapTexture;
-    private final GameSettings gameSettings;
-    private Font font;
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/worldtype/WorldTypeFXDispatcher.java b/game/client/src/main/java/net/minecraft/client/render/worldtype/WorldTypeFXDispatcher.java
index d4fd88b6e..42e8ea491 100644
--- a/game/client/src/main/java/net/minecraft/client/render/worldtype/WorldTypeFXDispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/render/worldtype/WorldTypeFXDispatcher.java
@@ -64,10 +64,6 @@ public class WorldTypeFXDispatcher extends Dispatcher<WorldType, WorldTypeFX> {
         addDispatch(new WorldTypeFXParadise(WorldTypes.PARADISE_DEFAULT)
             .setCloudHeight(8f)
             .setHasGround(false));
-
-        for (WorldType worldType : Registries.WORLD_TYPES) {
-            assert dispatches.containsKey(worldType) : "WorldType " + worldType.getLanguageKey() + " is missing a WorldTypeFX!";
-        }
     }
 
     public void addDispatch(WorldTypeFX worldTypeFX) {
diff --git a/game/client/src/main/java/net/minecraft/client/util/dispatch/Dispatcher.java b/game/client/src/main/java/net/minecraft/client/util/dispatch/Dispatcher.java
index a0e3003ba..9503d913b 100644
--- a/game/client/src/main/java/net/minecraft/client/util/dispatch/Dispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/util/dispatch/Dispatcher.java
@@ -30,6 +30,15 @@ public abstract class Dispatcher<T, U> {
         return dispatches.getOrDefault(key, getDefault());
     }
 
+    /**
+     * Checks dispatcher for entry.
+     * @param key The key to look up.
+     * @return True if the dispatcher has an entry for the key.
+     */
+    public boolean hasDispatch(T key) {
+        return dispatches.containsKey(key);
+    }
+
     /**
      * Returns the default value for the dispatcher.
      */
diff --git a/game/client/src/main/java/net/minecraft/client/util/helper/Colors.java b/game/client/src/main/java/net/minecraft/client/util/helper/Colors.java
index 3a8edcdd2..baac5a316 100644
--- a/game/client/src/main/java/net/minecraft/client/util/helper/Colors.java
+++ b/game/client/src/main/java/net/minecraft/client/util/helper/Colors.java
@@ -3,6 +3,7 @@ package net.minecraft.client.util.helper;
 import java.awt.Graphics;
 import java.awt.image.BufferedImage;
 import java.io.InputStream;
+import java.util.Arrays;
 
 import javax.imageio.ImageIO;
 
@@ -21,6 +22,17 @@ public abstract class Colors {
 	public static final Color[] allRedstoneColors = new Color[16];
 	public static final Color[] allPortalColors = new Color[16];
 
+	public static void loadDummyColors() {
+		Color white = new Color().setRGB(255, 255, 255);
+		Arrays.fill(allChatColors, white);
+		Arrays.fill(allPlankColors, white);
+		Arrays.fill(allLampColors, white);
+		Arrays.fill(allFlagColors, white);
+		Arrays.fill(allSignColors, white);
+		Arrays.fill(allRedstoneColors, white);
+		Arrays.fill(allPortalColors, white);
+	}
+
 	public static void loadColors() {
 		fillColorArray("/assets/minecraft/textures/misc/colors_chat.png", allChatColors);
 		fillColorArray("/assets/minecraft/textures/misc/colors_planks.png", allPlankColors);
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/DummyMinecraft.java b/game/client/src/main/java/org/useless/seedviewer/bta/DummyMinecraft.java
deleted file mode 100644
index 20be08c87..000000000
--- a/game/client/src/main/java/org/useless/seedviewer/bta/DummyMinecraft.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.useless.seedviewer.bta;
-
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.world.chunk.provider.ChunkProviderStatic;
-import net.minecraft.core.MinecraftAccessor;
-import net.minecraft.core.entity.SkinVariantList;
-import net.minecraft.core.world.World;
-import net.minecraft.core.world.chunk.IChunkLoader;
-import net.minecraft.core.world.chunk.provider.IChunkProvider;
-
-import java.io.File;
-
-public class DummyMinecraft implements MinecraftAccessor {
-    public File getMinecraftDir() {
-        return Minecraft.getAppDir("minecraft-bta");
-    }
-
-    public IChunkProvider createChunkProvider(World world, IChunkLoader chunkLoader) {
-        return new ChunkProviderStatic(world, chunkLoader, world.getWorldType().createChunkGenerator(world));
-    }
-
-    public int getAutosaveTimer() {
-        return 0;
-    }
-
-    public SkinVariantList getSkinVariantList() {
-        return null;
-    }
-
-    public String getMinecraftVersion() {
-        return null;
-    }
-}
diff --git a/game/client/src/test/java/TestBlockModelDispatcher.java b/game/client/src/test/java/TestBlockModelDispatcher.java
new file mode 100644
index 000000000..9130ef2aa
--- /dev/null
+++ b/game/client/src/test/java/TestBlockModelDispatcher.java
@@ -0,0 +1,17 @@
+import net.minecraft.client.render.block.model.BlockModelDispatcher;
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.Blocks;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class TestBlockModelDispatcher {
+    @Test
+    @DisplayName("Verify block models")
+    public void veryBlockModels() {
+        Blocks.init();
+        for (Block<?> block : Blocks.blockMap.values()) {
+            Assertions.assertTrue(BlockModelDispatcher.getInstance().hasDispatch(block), () -> "Block '" + block.namespaceId() + "' is not assigned a block model!");
+        }
+    }
+}
diff --git a/game/client/src/test/java/TestItemModelDispatcher.java b/game/client/src/test/java/TestItemModelDispatcher.java
new file mode 100644
index 000000000..c437ee87e
--- /dev/null
+++ b/game/client/src/test/java/TestItemModelDispatcher.java
@@ -0,0 +1,38 @@
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.option.GameSettings;
+import net.minecraft.client.render.Font;
+import net.minecraft.client.render.TextureManager;
+import net.minecraft.client.render.item.model.ItemModelDispatcher;
+import net.minecraft.client.render.texturepack.TexturePackList;
+import net.minecraft.client.util.helper.Colors;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.item.Item;
+import net.minecraft.core.item.Items;
+import net.minecraft.core.util.collection.NamespaceID;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+public class TestItemModelDispatcher {
+    @Test
+    @DisplayName("Verify item models")
+    public void verifyItemsModels() {
+        Blocks.init();
+        Items.init();
+
+        Colors.loadDummyColors();
+
+        // Map model requires mc font reference
+        Minecraft mc = new Minecraft();
+        mc.gameSettings = new GameSettings(mc, mc.getMinecraftDir());
+        mc.texturePackList = new TexturePackList(mc, mc.getMinecraftDir());
+        mc.textureManager = new TextureManager(mc, mc.texturePackList, mc.gameSettings);
+        mc.font = new Font(mc, mc.gameSettings, mc.textureManager);
+
+        for (Map.Entry<NamespaceID, Item> e : Item.itemsMap.entrySet()) {
+            Assertions.assertTrue(ItemModelDispatcher.getInstance().hasDispatch(e.getValue()), () -> "Item '" + e.getKey() + "' is not assigned a item model!");
+        }
+    }
+}
diff --git a/game/client/src/test/java/TestParticleDispatcher.java b/game/client/src/test/java/TestParticleDispatcher.java
new file mode 100644
index 000000000..5e4c803c6
--- /dev/null
+++ b/game/client/src/test/java/TestParticleDispatcher.java
@@ -0,0 +1,18 @@
+import net.minecraft.client.entity.particle.ParticleDispatcher;
+import net.minecraft.core.net.command.arguments.ArgumentTypeParticleId;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class TestParticleDispatcher {
+    @Test
+    @DisplayName("Verify particle ids")
+    public void verifyIds() {
+        for (String s : ParticleDispatcher.getInstance().getParticleIds()) {
+            Assertions.assertTrue(ArgumentTypeParticleId.particleIds.contains(s), () -> "ArgumentTypeParticleId is missing id " + s + "!");
+        }
+        for (String s : ArgumentTypeParticleId.particleIds) {
+            Assertions.assertTrue(ParticleDispatcher.getInstance().hasDispatch(s), () -> "ArgumentTypeParticleId defined particle " + s + " which doesn't have an assigned particle lambda!");
+        }
+    }
+}
diff --git a/game/client/src/test/java/TestWorldTypeFXDispatcher.java b/game/client/src/test/java/TestWorldTypeFXDispatcher.java
new file mode 100644
index 000000000..2ed50710d
--- /dev/null
+++ b/game/client/src/test/java/TestWorldTypeFXDispatcher.java
@@ -0,0 +1,26 @@
+import net.minecraft.client.render.worldtype.WorldTypeFXDispatcher;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.data.registry.Registries;
+import net.minecraft.core.item.Items;
+import net.minecraft.core.world.biome.Biomes;
+import net.minecraft.core.world.type.WorldType;
+import net.minecraft.core.world.type.WorldTypes;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class TestWorldTypeFXDispatcher {
+    @Test
+    @DisplayName("Verify WorldTypeFX")
+    public void verifyWorldTypeFX() {
+        Blocks.init();
+        Items.init();
+        new Registries();
+        Biomes.init();
+        WorldTypes.init();
+
+        for (WorldType worldType : Registries.WORLD_TYPES) {
+            Assertions.assertTrue(WorldTypeFXDispatcher.getInstance().hasDispatch(worldType), () -> "WorldType " + worldType.getLanguageKey() + " is missing a WorldTypeFX!");
+        }
+    }
+}
diff --git a/game/core/build.gradle.kts b/game/core/build.gradle.kts
index c07796b5a..a206ea0dc 100644
--- a/game/core/build.gradle.kts
+++ b/game/core/build.gradle.kts
@@ -8,6 +8,7 @@ group = providers.gradleProperty("bta.group").get()
 version = providers.gradleProperty("bta.version").get()
 val texturePackFormat = providers.gradleProperty("bta.texturePackFormat").get()
 val buildChannel = providers.gradleProperty("bta.buildChannel").get()
+val mpProtocolVersion = providers.gradleProperty("bta.mpProtocolVersion").get()
 
 run {
     val file = File("version.txt")
@@ -23,6 +24,11 @@ repositories {
 }
 
 dependencies {
+    // <test dependencies>
+    testImplementation(platform("org.junit:junit-bom:5.10.3"))
+    testImplementation("org.junit.jupiter:junit-jupiter")
+    // </test dependencies>
+
     // <core dependencies>
     implementation(libs.jetbrains.annotations)
     implementation(libs.gson)
@@ -54,6 +60,7 @@ val versionGenerator by tasks.registering {
                         public static final long BUILD_TIME = ${Date().time}L;
                         public static final int TEXTURE_PACK_FORMAT = $texturePackFormat;
                         public static final BuildChannel BUILD_CHANNEL = $buildChannel;
+                        public static final int MP_PROTOCOL_VERSION = $mpProtocolVersion;
                     }
                 """.trimIndent()
         )
@@ -68,6 +75,15 @@ tasks.withType(JavaCompile::class.java) {
         options.compilerArgs.add("8")
     }
 }
+
+tasks.withType(Jar::class.java) {
+    dependsOn(tasks.test)
+}
+
+tasks.test {
+    useJUnitPlatform()
+}
+
 //endregion
 
 sourceSets {
diff --git a/game/core/src/main/java/net/minecraft/core/Global.java b/game/core/src/main/java/net/minecraft/core/Global.java
index b443ee094..967f056e9 100644
--- a/game/core/src/main/java/net/minecraft/core/Global.java
+++ b/game/core/src/main/java/net/minecraft/core/Global.java
@@ -29,9 +29,9 @@ public class Global
 	// 2 byte value
 	// 1st nibble: Major version
 	// 2nd nibble: Minor version
-	// 3rd nibble: 0 if release, 1 if prerelease
+	// 3rd nibble: 0 if release, 1 if prerelease, 2 if nightly release
 	// 4th nibble: Prerelease version or patch version
 	// For example, 7.1_01 would be encoded as 0x7101
 	// 7.1 prerelease 1 would be encoded as 0x7111
-	public static final int MP_PROTOCOL_VERSION = 0x7200;
+	public static final int MP_PROTOCOL_VERSION = Version.MP_PROTOCOL_VERSION;
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/Block.java b/game/core/src/main/java/net/minecraft/core/block/Block.java
index 04ba1e808..c39207287 100644
--- a/game/core/src/main/java/net/minecraft/core/block/Block.java
+++ b/game/core/src/main/java/net/minecraft/core/block/Block.java
@@ -54,7 +54,7 @@ public final class Block<T extends BlockLogic>
     public float blockParticleGravity;
     public float friction;
     public int emission;
-    public int lightBlock;
+    public Integer lightBlock = null;
     public boolean immovable = false;
     public boolean isEntityTile = false;
     public boolean disabledNeighborMetaNotify = false;
@@ -246,7 +246,7 @@ public final class Block<T extends BlockLogic>
 
     public MaterialColor getMaterialColor(){
         if (overrideColor != null) return overrideColor;
-        return getMaterialColor();
+        return logic.getMaterialColor();
     }
 
     @Override
@@ -620,6 +620,6 @@ public final class Block<T extends BlockLogic>
     }
 
     public static boolean hasLogicClass(@Nullable Block<?> block, Class<?> logicClass) {
-        return block != null && block.getLogic().getClass().isAssignableFrom(logicClass);
+        return block != null && logicClass.isAssignableFrom(block.getLogic().getClass());
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFire.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFire.java
index 03e988a02..a50e5da09 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFire.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFire.java
@@ -53,6 +53,8 @@ public class BlockLogicFire extends BlockLogic {
 		setFlammable(Blocks.LOG_CHERRY, FLAME_HARD, BURN_HARD);
 		setFlammable(Blocks.LOG_OAK_MOSSY, FLAME_HARD, BURN_HARD);
 		setFlammable(Blocks.LOG_EUCALYPTUS, FLAME_HARD, BURN_HARD);
+		setFlammable(Blocks.LOG_THORN, FLAME_HARD, BURN_HARD);
+		setFlammable(Blocks.LOG_PALM, FLAME_HARD, BURN_HARD);
 		setFlammable(Blocks.LEAVES_OAK, FLAME_EASY, BURN_EASY);
 		setFlammable(Blocks.LEAVES_PINE, FLAME_EASY, BURN_EASY);
 		setFlammable(Blocks.LEAVES_BIRCH, FLAME_EASY, BURN_EASY);
@@ -62,6 +64,8 @@ public class BlockLogicFire extends BlockLogic {
 		setFlammable(Blocks.LEAVES_SHRUB, FLAME_EASY, BURN_EASY);
 		setFlammable(Blocks.LEAVES_EUCALYPTUS, FLAME_EASY, BURN_EASY);
 		setFlammable(Blocks.LEAVES_CACAO, FLAME_EASY, BURN_EASY);
+		setFlammable(Blocks.LEAVES_THORN, FLAME_EASY, BURN_EASY);
+		setFlammable(Blocks.LEAVES_PALM, FLAME_EASY, BURN_EASY);
 		setFlammable(Blocks.BOOKSHELF_PLANKS_OAK, FLAME_EASY, BURN_MEDIUM);
 		setFlammable(Blocks.TNT, FLAME_MEDIUM, BURN_INSTANT);
 		setFlammable(Blocks.WOOL, FLAME_EASY, BURN_EASY);
diff --git a/game/core/src/main/java/net/minecraft/core/block/Blocks.java b/game/core/src/main/java/net/minecraft/core/block/Blocks.java
index d0c56f5d2..f43785b0a 100644
--- a/game/core/src/main/java/net/minecraft/core/block/Blocks.java
+++ b/game/core/src/main/java/net/minecraft/core/block/Blocks.java
@@ -1193,16 +1193,21 @@ public final class Blocks {
 
     private static void cacheBlock(Block<?> container) {
         Blocks.solid[container.id()] = container.isSolidRender();
-        Blocks.lightBlock[container.id()] = (container.isSolidRender() || container.blocksLight()) ? 255 : 0;
+        if (container.lightBlock == null) {
+            container.lightBlock = (container.isSolidRender() || container.blocksLight()) ? 255 : 0;
+        }
+        Blocks.lightBlock[container.id()] = container.lightBlock;
         Blocks.translucent[container.id()] = !container.getMaterial().blocksLight();
         Blocks.isEntityTile[container.id()] = container.isEntityTile;
         Blocks.lightEmission[container.id()] = container.emission;
-        Blocks.lightBlock[container.id()] = container.lightBlock;
         Blocks.neighborNotifyOnMetadataChangeDisabled[container.id()] = container.disabledNeighborMetaNotify;
         Blocks.shouldTick[container.id()] = container.shouldTick;
     }
 
+    private static boolean hasInit = false;
     public static void init(){
+        if (hasInit) return;
+        hasInit = true;
         resetCaches();
         for (Block<?> b : blocksList) {
             if (b == null) continue;
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemMap.java b/game/core/src/main/java/net/minecraft/core/item/ItemMap.java
index 422979c86..c9ccc12be 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemMap.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemMap.java
@@ -185,8 +185,6 @@ public class ItemMap extends Item implements IComplexItem
 				}
 
 				blockCounts.clear();
-				Chunk chunk = world.getChunkFromBlockCoords(blockX, blockZ);
-				
 				
 				int blockInChunkX = blockX & 0xf;
 				int blockInChunkZ = blockZ & 0xf;
@@ -207,6 +205,7 @@ public class ItemMap extends Item implements IComplexItem
 					}
 					pixelHeight = 100D;
 				} else {
+					Chunk chunk = world.getChunkFromBlockCoords(blockX, blockZ);
 					//Determine Color (Not Nether)
 					for (int _x = 0; _x < blocksPerPixel; _x++) {
 						for (int _z = 0; _z < blocksPerPixel; _z++) {
@@ -217,7 +216,7 @@ public class ItemMap extends Item implements IComplexItem
 								do {
 									runLoop = false;
 									id = chunk.getBlockID(_x + blockInChunkX, height, _z + blockInChunkZ);
-									if(id <= 0 || (height > 0 && Blocks.blocksList[id].getMaterial().color == MaterialColor.none)) {
+									if(id <= 0 || (height > 0 && Blocks.blocksList[id].getMaterialColor() == MaterialColor.none)) {
 										runLoop = true;
 										height--;
 									}
@@ -269,7 +268,7 @@ public class ItemMap extends Item implements IComplexItem
 				
 				int colorIndex = 0;
 				if(id > 0) {
-					MaterialColor mapcolor = Blocks.blocksList[id].getMaterial().color;
+					MaterialColor mapcolor = Blocks.blocksList[id].getMaterialColor();
 					if(mapcolor == MaterialColor.water) {
 						double d3 = (double) waterDepth * 0.1D + (double) (x + z & 1) * 0.2D;
 						brightness = 1;
diff --git a/game/core/src/main/java/net/minecraft/core/item/Items.java b/game/core/src/main/java/net/minecraft/core/item/Items.java
index aa7b83735..9de4c78d8 100644
--- a/game/core/src/main/java/net/minecraft/core/item/Items.java
+++ b/game/core/src/main/java/net/minecraft/core/item/Items.java
@@ -189,7 +189,11 @@ public final class Items {
     public static Item WAND_MONSTER_SPAWNER;
     public static Item DOOR_STEEL;
     public static Item PAINTBRUSH;
+
+    private static boolean hasInit = false;
     public static void init(){
+        if (hasInit) return;
+        hasInit = true;
         setupItems();
         initStats();
     }
diff --git a/game/core/src/main/java/net/minecraft/core/world/World.java b/game/core/src/main/java/net/minecraft/core/world/World.java
index 5c6241087..438515f37 100644
--- a/game/core/src/main/java/net/minecraft/core/world/World.java
+++ b/game/core/src/main/java/net/minecraft/core/world/World.java
@@ -113,7 +113,7 @@ public class World implements WorldSource {
     public boolean isNewWorld = false;
     public Dimension dimension;
     public WorldType worldType;
-    protected IChunkProvider chunkProvider;
+    public IChunkProvider chunkProvider;
     public LevelStorage saveHandler;
     protected LevelData levelData;
     public DimensionData dimensionData;
diff --git a/game/core/src/test/java/TestBlock.java b/game/core/src/test/java/TestBlock.java
new file mode 100644
index 000000000..de9b7c0c8
--- /dev/null
+++ b/game/core/src/test/java/TestBlock.java
@@ -0,0 +1,29 @@
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.BlockLogic;
+import net.minecraft.core.block.BlockLogicLeavesBase;
+import net.minecraft.core.block.BlockLogicLeavesCacao;
+import net.minecraft.core.block.BlockLogicStone;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.IPainted;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestBlock {
+    @Test
+    public void testHasLogicMethod() {
+        Blocks.init();
+
+        Assertions.assertTrue(Block.hasLogicClass(Blocks.STONE, BlockLogic.class));
+        Assertions.assertTrue(Block.hasLogicClass(Blocks.STONE, BlockLogicStone.class));
+
+        Assertions.assertFalse(Block.hasLogicClass(null, BlockLogic.class));
+        Assertions.assertFalse(Block.hasLogicClass(null, BlockLogicStone.class));
+
+        Assertions.assertTrue(Block.hasLogicClass(Blocks.LEAVES_THORN, BlockLogic.class));
+        Assertions.assertTrue(Block.hasLogicClass(Blocks.LEAVES_THORN, BlockLogicLeavesBase.class));
+        Assertions.assertFalse(Block.hasLogicClass(Blocks.LEAVES_THORN, BlockLogicLeavesCacao.class));
+
+        Assertions.assertTrue(Block.hasLogicClass(Blocks.PLANKS_OAK_PAINTED, IPainted.class));
+        Assertions.assertFalse(Block.hasLogicClass(Blocks.BASALT, IPainted.class));
+    }
+}
diff --git a/game/core/src/test/java/TestFlammables.java b/game/core/src/test/java/TestFlammables.java
new file mode 100644
index 000000000..c662cff05
--- /dev/null
+++ b/game/core/src/test/java/TestFlammables.java
@@ -0,0 +1,39 @@
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.BlockLogicFire;
+import net.minecraft.core.block.BlockLogicLeavesBase;
+import net.minecraft.core.block.BlockLogicLog;
+import net.minecraft.core.block.BlockLogicMoss;
+import net.minecraft.core.block.BlockLogicSaplingBase;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.crafting.LookupFuelFurnace;
+import net.minecraft.core.item.Items;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class TestFlammables {
+    @Test
+    @DisplayName("Test flammable assignments")
+    public void testFlameAssignments() {
+        Blocks.init();
+        Items.init();
+
+        // Mostly common sense checks to make sure that we don't forget to assign flammability to new leaves for the 5th time!
+        for (Block<?> block : Blocks.blockMap.values()) {
+            boolean shouldCombust =
+                Block.hasLogicClass(block, BlockLogicLog.class) ||
+                Block.hasLogicClass(block, BlockLogicLeavesBase.class) ||
+                Block.hasLogicClass(block, BlockLogicMoss.class);
+            boolean shouldSmelt =
+                Block.hasLogicClass(block, BlockLogicLog.class) ||
+                Block.hasLogicClass(block, BlockLogicSaplingBase.class);
+
+            if (shouldCombust) {
+                Assertions.assertTrue(BlockLogicFire.canBurn(block.id()), () -> "Block '" + block.namespaceId() + "' of class '" + block.getLogic().getClass() + "' should probably be flammable!");
+            }
+            if (shouldSmelt) {
+                Assertions.assertTrue(LookupFuelFurnace.instance.getFuelYield(block.id()) > 0, () -> "Block '" + block.namespaceId() + "' or class '" + block.getLogic().getClass() + "' should probably be valid furnace fuel!");
+            }
+        }
+    }
+}
diff --git a/gradle.properties b/gradle.properties
index 2d3b5a02e..b7ac11372 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,14 @@
 bta.group = net.minecraft
 bta.version = 7.3
 bta.texturePackFormat = 3
-bta.buildChannel = BuildChannel.DEVELOPMENT
\ No newline at end of file
+bta.buildChannel = BuildChannel.DEVELOPMENT
+
+# Protocol version scheme:
+# 2 byte value
+# 1st nibble: Major version
+# 2nd nibble: Minor version
+# 3rd nibble: 0 if release, 1 if prerelease, 2 if nightly/dev release
+# 4th nibble: Prerelease version or patch version
+# For example, 7.1_01 would be encoded as 0x7101
+# 7.1 prerelease 1 would be encoded as 0x7111
+bta.mpProtocolVersion = 0x7320
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 39b025406..54db8e0a2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -4,3 +4,5 @@ include("game:client")
 include("game:server")
 include("util:stellar")
 include("util:datagen")
+include("util:seed-viewer")
+findProject(":util:seed-viewer")?.name = "seed-viewer"
diff --git a/util/seed-viewer/.gitignore b/util/seed-viewer/.gitignore
new file mode 100644
index 000000000..2959db291
--- /dev/null
+++ b/util/seed-viewer/.gitignore
@@ -0,0 +1,2 @@
+build
+logs
\ No newline at end of file
diff --git a/util/seed-viewer/build.gradle.kts b/util/seed-viewer/build.gradle.kts
new file mode 100644
index 000000000..d901f9c79
--- /dev/null
+++ b/util/seed-viewer/build.gradle.kts
@@ -0,0 +1,98 @@
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import java.util.*
+
+plugins {
+    id("java")
+    alias(libs.plugins.shadow)
+}
+
+group = "org.useless"
+version = "1.1.2"
+
+val include: Configuration by configurations.creating
+
+repositories {
+    mavenCentral()
+    maven("https://libraries.minecraft.net/")
+}
+
+dependencies {
+    testImplementation(platform("org.junit:junit-bom:5.10.0"))
+    testImplementation("org.junit.jupiter:junit-jupiter")
+
+    implementation(project(":game:core"))?.let { include(it) }
+
+    implementation(libs.jetbrains.annotations)?.let { include(it) }
+    implementation(libs.bundles.log4j)?.let { include(it) }
+
+    implementation("com.formdev:flatlaf:3.1.1")?.let { include(it) } // Dark theme
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+//region Tasks
+val versionGenerator by tasks.registering {
+    group = "bta.generator"
+    val versionDir = project.layout.buildDirectory.dir("generated/sources/version/java")
+    val outputFile = versionDir.get().file("org/useless/seedviewer/Version.java").asFile
+    outputs.dir(versionDir)
+
+    doLast {
+        outputFile.parentFile.mkdirs()
+        outputFile.writeText(
+            """
+                    package org.useless.seedviewer;
+                    
+                    final class Version {
+                        public static final String VERSION = "$version";
+                        public static final long BUILD_TIME = ${Date().time}L;
+                    }
+                """.trimIndent()
+        )
+    }
+}
+
+val buildJar by tasks.registering(ShadowJar::class) {
+    dependsOn(tasks["test"])
+    group = "useless"
+    configurations = listOf(include)
+
+    from(sourceSets["main"].output.classesDirs)
+    from(sourceSets["main"].resources)
+    archiveFileName.set("SeedViewer-$version.jar")
+    manifest {
+        attributes["Main-Class"] = "org.useless.seedviewer.SeedViewerMain"
+        attributes["Implementation-Version"] = version
+    }
+}
+
+val run by tasks.registering(JavaExec::class) {
+    group = "useless"
+
+    mainClass.set("org.useless.seedviewer.SeedViewerMain")
+    classpath += sourceSets["main"].runtimeClasspath
+    jvmArgs = if (arrayOf("Mac OS X", "Darwin").any { System.getProperty("os.name").startsWith(it) }) {
+        listOf("-XstartOnFirstThread", "-Djava.awt.headless=true", "-ea")
+    } else {
+        listOf("-ea")
+    }
+}
+
+tasks.test {
+    useJUnitPlatform()
+}
+
+tasks.withType(Jar::class.java) {
+    dependsOn(tasks.test)
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir(versionGenerator)
+        }
+    }
+}
\ No newline at end of file
diff --git a/game/client/src/main/java/org/useless/seedviewer/SeedViewerMain.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/SeedViewerMain.java
similarity index 97%
rename from game/client/src/main/java/org/useless/seedviewer/SeedViewerMain.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/SeedViewerMain.java
index a88e811f8..67e4b6967 100644
--- a/game/client/src/main/java/org/useless/seedviewer/SeedViewerMain.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/SeedViewerMain.java
@@ -3,14 +3,14 @@ package org.useless.seedviewer;
 import com.formdev.flatlaf.FlatDarculaLaf;
 import com.mojang.logging.LogUtils;
 import org.jetbrains.annotations.NotNull;
-import org.slf4j.Logger;
 import org.useless.seedviewer.gui.SeedViewer;
+import org.slf4j.Logger;
 
 import javax.swing.*;
 import java.util.Properties;
 
 public class SeedViewerMain {
-    public static final String VERSION = "1.1.1";
+    public static final String VERSION = Version.VERSION;
     public static final Logger LOGGER = LogUtils.getLogger();
 
     public static void main(String[] args) {
diff --git a/game/client/src/main/java/org/useless/seedviewer/TestChunkProvider.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/TestChunkProvider.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/TestChunkProvider.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/TestChunkProvider.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/BTABiome.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTABiome.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/bta/BTABiome.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTABiome.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/BTAChunkProvider.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAChunkProvider.java
similarity index 97%
rename from game/client/src/main/java/org/useless/seedviewer/bta/BTAChunkProvider.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAChunkProvider.java
index eddf95ff2..c6f5cc2a5 100644
--- a/game/client/src/main/java/org/useless/seedviewer/bta/BTAChunkProvider.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAChunkProvider.java
@@ -11,11 +11,11 @@ import net.minecraft.core.world.biome.provider.BiomeProvider;
 import net.minecraft.core.world.biome.provider.BiomeProviderOverworld;
 import net.minecraft.core.world.save.LevelData;
 import net.minecraft.core.world.type.WorldTypes;
-import org.slf4j.Logger;
 import org.useless.seedviewer.bta.worldloader.ChunkLoader;
 import org.useless.seedviewer.collections.ChunkLocation;
 import org.useless.seedviewer.data.Chunk;
 import org.useless.seedviewer.gui.ChunkProvider;
+import org.slf4j.Logger;
 
 import java.io.File;
 import java.io.IOException;
@@ -53,7 +53,7 @@ public class BTAChunkProvider implements ChunkProvider {
             try {
                 net.minecraft.core.world.chunk.Chunk c = chunkLoader.loadChunk(location);
                 if (c != null) {
-                    return new BTAComplexChunk(c);
+                    return new BTAComplexChunk(c, biomeProvider);
                 }
             } catch (IOException e) {
                 LOGGER.error("Failed to load chunk data for chunk {}, {}", location.x, location.z, e);
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/BTAComplexChunk.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAComplexChunk.java
similarity index 77%
rename from game/client/src/main/java/org/useless/seedviewer/bta/BTAComplexChunk.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAComplexChunk.java
index 1ae877411..e4fb3beb7 100644
--- a/game/client/src/main/java/org/useless/seedviewer/bta/BTAComplexChunk.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAComplexChunk.java
@@ -4,6 +4,7 @@ import net.minecraft.core.block.Block;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.MaterialColor;
+import net.minecraft.core.world.biome.provider.BiomeProvider;
 import org.jetbrains.annotations.NotNull;
 import org.useless.seedviewer.collections.ChunkPos2D;
 import org.useless.seedviewer.collections.ChunkPos3D;
@@ -12,12 +13,16 @@ import org.useless.seedviewer.data.Chunk;
 
 public class BTAComplexChunk implements Chunk {
     private final net.minecraft.core.world.chunk.Chunk chunk;
-    public BTAComplexChunk(@NotNull net.minecraft.core.world.chunk.Chunk chunk) {
+    private final BiomeProvider provider;
+    public BTAComplexChunk(@NotNull net.minecraft.core.world.chunk.Chunk chunk, BiomeProvider biomeProvider) {
         this.chunk = chunk;
+        this.provider = biomeProvider;
     }
     @Override
     public Biome getBiome(ChunkPos3D pos) {
-        return new BTABiome(chunk.getBlockBiome(pos.x, pos.y, pos.z));
+        net.minecraft.core.world.biome.Biome b = chunk.getBlockBiome(pos.x, pos.y, pos.z);
+        if (b != null) return new BTABiome(b);;
+        return new BTABiome(provider.getBiome(chunk.xPosition * Chunk.CHUNK_SIZE_X + pos.x, pos.y, chunk.zPosition * Chunk.CHUNK_SIZE_Z + pos.z));
     }
 
     @Override
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/BTASimpleChunk.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTASimpleChunk.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/bta/BTASimpleChunk.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTASimpleChunk.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/BTAWorld.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAWorld.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/bta/BTAWorld.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/BTAWorld.java
diff --git a/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/DummyMinecraft.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/DummyMinecraft.java
new file mode 100644
index 000000000..a1eb4446b
--- /dev/null
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/DummyMinecraft.java
@@ -0,0 +1,106 @@
+package org.useless.seedviewer.bta;
+
+import net.minecraft.core.MinecraftAccessor;
+import net.minecraft.core.entity.SkinVariantList;
+import net.minecraft.core.world.World;
+import net.minecraft.core.world.chunk.IChunkLoader;
+import net.minecraft.core.world.chunk.provider.IChunkProvider;
+import org.useless.seedviewer.bta.chunk.provider.ChunkProviderSeedViewer;
+
+import java.io.File;
+
+public class DummyMinecraft implements MinecraftAccessor {
+    public File getMinecraftDir() {
+        return getAppDir("minecraft-bta");
+    }
+
+    public IChunkProvider createChunkProvider(World world, IChunkLoader chunkLoader) {
+        return new ChunkProviderSeedViewer(world, chunkLoader, world.getWorldType().createChunkGenerator(world));
+    }
+
+    public int getAutosaveTimer() {
+        return 0;
+    }
+
+    public SkinVariantList getSkinVariantList() {
+        return null;
+    }
+
+    public String getMinecraftVersion() {
+        return null;
+    }
+
+    private static File getAppDir(String s)
+    {
+        String homeDirPath = System.getProperty("user.home", ".");
+        File appDir;
+
+        switch(getOs())
+        {
+            case linux: // Linux
+            case solaris: // Solaris
+                appDir = new File(homeDirPath, '.' + s + '/');
+                break;
+            case windows: // Windows
+                String s2 = System.getenv("APPDATA");
+                if(s2 != null)
+                {
+                    appDir = new File(s2, "." + s + '/');
+                }
+                else
+                {
+                    appDir = new File(homeDirPath, '.' + s + '/');
+                }
+                break;
+            case macos: // macOS
+                appDir = new File(homeDirPath, "Library/Application Support/" + s);
+                break;
+            default:
+                appDir = new File(homeDirPath, s + '/');
+                break;
+        }
+
+        if(!appDir.exists() && !appDir.mkdirs())
+        {
+            throw new RuntimeException("The working directory could not be created: " + appDir);
+        }
+        else
+        {
+            return appDir;
+        }
+    }
+
+    private static EnumOS getOs()
+    {
+        String s = System.getProperty("os.name").toLowerCase();
+        if(s.contains("win"))
+        {
+            return EnumOS.windows;
+        }
+        else if(s.contains("mac"))
+        {
+            return EnumOS.macos;
+        }
+        else if(s.contains("solaris") || s.contains("sunos"))
+        {
+            return EnumOS.solaris;
+        }
+        else if(s.contains("linux") || s.contains("unix"))
+        {
+            return EnumOS.linux;
+        }
+        else
+        {
+            return EnumOS.unknown;
+        }
+    }
+
+    private enum EnumOS
+    {
+        linux,
+        solaris,
+        windows,
+        macos,
+        unknown
+    }
+}
diff --git a/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/chunk/provider/ChunkProviderSeedViewer.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/chunk/provider/ChunkProviderSeedViewer.java
new file mode 100644
index 000000000..c46feaf47
--- /dev/null
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/chunk/provider/ChunkProviderSeedViewer.java
@@ -0,0 +1,257 @@
+package org.useless.seedviewer.bta.chunk.provider;
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.core.world.ProgressListener;
+import net.minecraft.core.world.World;
+import net.minecraft.core.world.chunk.Chunk;
+import net.minecraft.core.world.chunk.EmptyChunk;
+import net.minecraft.core.world.chunk.IChunkLoader;
+import net.minecraft.core.world.chunk.provider.IChunkProvider;
+import net.minecraft.core.world.generate.chunk.ChunkGenerator;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class ChunkProviderSeedViewer
+    implements IChunkProvider
+{
+    public static final int CHUNK_RADIUS = 64; // Radius should *always* be a power of 2!
+    public static final int CHUNK_MASK = CHUNK_RADIUS * 2 - 1;
+
+    private static final Logger LOGGER = LogUtils.getLogger();
+    private final Chunk blankChunk;
+    public ChunkGenerator chunkGenerator;
+    private final IChunkLoader chunkLoader;
+    private final Chunk[] chunks;
+    private final World worldObj;
+    int lastQueriedChunkXPos;
+    int lastQueriedChunkZPos;
+    private Chunk lastQueriedChunk;
+    private int curChunkX;
+    private int curChunkY;
+
+
+    public ChunkProviderSeedViewer(World world, IChunkLoader ichunkloader, ChunkGenerator chunkGenerator)
+    {
+        chunks = new Chunk[CHUNK_RADIUS * 2 * CHUNK_RADIUS * 2];
+        lastQueriedChunkXPos = Integer.MIN_VALUE;
+        lastQueriedChunkZPos = Integer.MIN_VALUE;
+        blankChunk = new EmptyChunk(world, 0, 0);
+        worldObj = world;
+        chunkLoader = ichunkloader;
+        this.chunkGenerator = chunkGenerator;
+
+    }
+
+    @Override
+    public void setCurrentChunkOver(int chunkX, int chunkZ)
+    {
+        curChunkX = chunkX;
+        curChunkY = chunkZ;
+    }
+
+    public boolean canChunkExist(int chunkX, int chunkZ)
+    {
+        byte byte0 = CHUNK_RADIUS - 1;
+        return chunkX >= curChunkX - byte0 && chunkZ >= curChunkY - byte0 && chunkX <= curChunkX + byte0 && chunkZ <= curChunkY + byte0;
+    }
+
+    @Override
+    public void regenerateChunk(int chunkX, int chunkZ)
+    {
+        int k = chunkX & CHUNK_MASK;
+        int l = chunkZ & CHUNK_MASK;
+        int i1 = k + l * CHUNK_RADIUS * 2;
+        Chunk newChunk;
+        if (chunkGenerator != null)
+        {
+            newChunk = chunkGenerator.generate(chunkX, chunkZ);
+            newChunk.fixMissingBlocks();
+        }
+        else
+        {
+            return;
+        }
+        chunks[i1] = newChunk;
+        if(!chunks[i1].isTerrainPopulated && isChunkLoaded(chunkX + 1, chunkZ + 1) && isChunkLoaded(chunkX, chunkZ + 1) && isChunkLoaded(chunkX + 1, chunkZ))
+        {
+            populate(this, chunkX, chunkZ);
+        }
+        if(isChunkLoaded(chunkX - 1, chunkZ) && !provideChunk(chunkX - 1, chunkZ).isTerrainPopulated && isChunkLoaded(chunkX - 1, chunkZ + 1) && isChunkLoaded(chunkX, chunkZ + 1) && isChunkLoaded(chunkX - 1, chunkZ))
+        {
+            populate(this, chunkX - 1, chunkZ);
+        }
+        if(isChunkLoaded(chunkX, chunkZ - 1) && !provideChunk(chunkX, chunkZ - 1).isTerrainPopulated && isChunkLoaded(chunkX + 1, chunkZ - 1) && isChunkLoaded(chunkX, chunkZ - 1) && isChunkLoaded(chunkX + 1, chunkZ))
+        {
+            populate(this, chunkX, chunkZ - 1);
+        }
+        if(isChunkLoaded(chunkX - 1, chunkZ - 1) && !provideChunk(chunkX - 1, chunkZ - 1).isTerrainPopulated && isChunkLoaded(chunkX - 1, chunkZ - 1) && isChunkLoaded(chunkX, chunkZ - 1) && isChunkLoaded(chunkX - 1, chunkZ))
+        {
+            populate(this, chunkX - 1, chunkZ - 1);
+        }
+    }
+
+    @Override
+    public boolean isChunkLoaded(int chunkX, int chunkZ)
+    {
+        if(!canChunkExist(chunkX, chunkZ))
+        {
+            return false;
+        }
+        if(chunkX == lastQueriedChunkXPos && chunkZ == lastQueriedChunkZPos && lastQueriedChunk != null)
+        {
+            return true;
+        } else
+        {
+            int k = chunkX & CHUNK_MASK;
+            int l = chunkZ & CHUNK_MASK;
+            int chunkIndex = k + l * CHUNK_RADIUS * 2;
+            return chunks[chunkIndex] == blankChunk || (chunks[chunkIndex] != null && chunks[chunkIndex].isAtLocation(chunkX, chunkZ));
+        }
+    }
+
+    @Override
+    public Chunk prepareChunk(int chunkX, int chunkZ)
+    {
+        return provideChunk(chunkX, chunkZ);
+    }
+
+    @Override
+    public Chunk provideChunk(int chunkX, int chunkZ)
+    {
+        if(chunkX == lastQueriedChunkXPos && chunkZ == lastQueriedChunkZPos && lastQueriedChunk != null)
+        {
+            return lastQueriedChunk;
+        }
+        if(!worldObj.findingSpawnPoint && !canChunkExist(chunkX, chunkZ))
+        {
+            return blankChunk;
+        }
+        int k = chunkX & CHUNK_MASK;
+        int l = chunkZ & CHUNK_MASK;
+        int i1 = k + l * CHUNK_RADIUS * 2;
+        if(!isChunkLoaded(chunkX, chunkZ))
+        {
+            if(chunks[i1] != null)
+            {
+                saveChunk(chunks[i1]);
+                chunks[i1].onUnload();
+            }
+            Chunk chunk = loadChunk(chunkX, chunkZ);
+            if(chunk == null)
+            {
+                if (chunkGenerator != null)
+                {
+                    chunk = chunkGenerator.generate(chunkX, chunkZ);
+                    chunk.fixMissingBlocks();
+                }
+                else
+                {
+                    chunk = blankChunk;
+                }
+            }
+            chunks[i1] = chunk;
+            if(chunks[i1] != null)
+            {
+                chunks[i1].onLoad();
+            }
+            if(!chunks[i1].isTerrainPopulated && isChunkLoaded(chunkX + 1, chunkZ + 1) && isChunkLoaded(chunkX, chunkZ + 1) && isChunkLoaded(chunkX + 1, chunkZ))
+            {
+                populate(this, chunkX, chunkZ);
+            }
+            if(isChunkLoaded(chunkX - 1, chunkZ) && !provideChunk(chunkX - 1, chunkZ).isTerrainPopulated && isChunkLoaded(chunkX - 1, chunkZ + 1) && isChunkLoaded(chunkX, chunkZ + 1) && isChunkLoaded(chunkX - 1, chunkZ))
+            {
+                populate(this, chunkX - 1, chunkZ);
+            }
+            if(isChunkLoaded(chunkX, chunkZ - 1) && !provideChunk(chunkX, chunkZ - 1).isTerrainPopulated && isChunkLoaded(chunkX + 1, chunkZ - 1) && isChunkLoaded(chunkX, chunkZ - 1) && isChunkLoaded(chunkX + 1, chunkZ))
+            {
+                populate(this, chunkX, chunkZ - 1);
+            }
+            if(isChunkLoaded(chunkX - 1, chunkZ - 1) && !provideChunk(chunkX - 1, chunkZ - 1).isTerrainPopulated && isChunkLoaded(chunkX - 1, chunkZ - 1) && isChunkLoaded(chunkX, chunkZ - 1) && isChunkLoaded(chunkX - 1, chunkZ))
+            {
+                populate(this, chunkX - 1, chunkZ - 1);
+            }
+            if (worldObj.getCurrentWeather() != null)
+            {
+                worldObj.getCurrentWeather().doChunkLoadEffect(worldObj, chunk);
+            }
+        }
+        lastQueriedChunkXPos = chunkX;
+        lastQueriedChunkZPos = chunkZ;
+        lastQueriedChunk = chunks[i1];
+        return chunks[i1];
+    }
+
+    private Chunk loadChunk(int chunkX, int chunkZ)
+    {
+        if(chunkLoader == null)
+        {
+            return blankChunk;
+        }
+        try
+        {
+            Chunk chunk = chunkLoader.loadChunk(worldObj, chunkX, chunkZ);
+            if(chunk != null)
+            {
+                chunk.lastSaveTime = worldObj.getWorldTime();
+            }
+            return chunk;
+        }
+        catch(Exception exception)
+        {
+            LOGGER.error("Error loading chunk at X:{}, Z:{}", chunkX, chunkZ, exception);
+        }
+        return blankChunk;
+    }
+
+    private void saveChunk(Chunk chunk)
+    {
+
+    }
+
+    @Override
+    public void populate(IChunkProvider chunkProvider, int chunkX, int chunkZ)
+    {
+
+    }
+
+    @Override
+    public boolean saveChunks(boolean saveImmediately, ProgressListener progressUpdate)
+    {
+        return false;
+    }
+
+    @Override
+    public boolean tick()
+    {
+        return false;
+    }
+
+    @Override
+    public void unloadAllChunks()
+    {
+        for (Chunk chunk : chunks) {
+            if (chunk != null) chunk.onUnload();
+        }
+        Arrays.fill(chunks, null);
+        chunkGenerator = null;
+    	System.gc();
+    }
+
+    @Override
+    public boolean canSave()
+    {
+        return false;
+    }
+
+    @Override
+    public String getInfoString()
+    {
+        return "ChunkCache: " + chunks.length;
+    }
+
+    public IChunkLoader getChunkLoader() {
+        return chunkLoader;
+    }
+}
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/worldloader/ChunkLoader.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/ChunkLoader.java
similarity index 70%
rename from game/client/src/main/java/org/useless/seedviewer/bta/worldloader/ChunkLoader.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/ChunkLoader.java
index 1060f5454..5712fabc2 100644
--- a/game/client/src/main/java/org/useless/seedviewer/bta/worldloader/ChunkLoader.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/ChunkLoader.java
@@ -3,16 +3,20 @@ package org.useless.seedviewer.bta.worldloader;
 import com.mojang.logging.LogUtils;
 import com.mojang.nbt.CompoundTag;
 import com.mojang.nbt.NbtIo;
+import net.minecraft.core.world.ProgressListener;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.chunk.Chunk;
 import net.minecraft.core.world.chunk.ChunkLoaderLegacy;
+import net.minecraft.core.world.chunk.EmptyChunk;
+import net.minecraft.core.world.chunk.provider.IChunkProvider;
 import net.minecraft.core.world.chunk.reader.ChunkReader;
 import net.minecraft.core.world.chunk.reader.ChunkReaderLegacy;
 import net.minecraft.core.world.chunk.reader.ChunkReaderVersion1;
 import net.minecraft.core.world.chunk.reader.ChunkReaderVersion2;
 import net.minecraft.core.world.save.mcregion.RegionFileCache;
-import org.slf4j.Logger;
+import net.minecraft.core.world.type.WorldTypes;
 import org.useless.seedviewer.collections.ChunkLocation;
+import org.slf4j.Logger;
 
 import java.io.DataInputStream;
 import java.io.File;
@@ -20,12 +24,19 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Map;
 
-public class ChunkLoader {
+public class ChunkLoader implements IChunkProvider {
     private static final Logger LOGGER = LogUtils.getLogger();
-    private static final World dummyWorld = new World();
+    private final World dummyWorld;
+    private final Chunk empty;
+
+
     private final File dimensionDir;
     public ChunkLoader(File worldFolder, int dimension) {
         this.dimensionDir = new File(worldFolder, "dimensions/" + dimension);
+        dummyWorld = new World();
+        dummyWorld.worldType = WorldTypes.OVERWORLD_EXTENDED;
+        dummyWorld.chunkProvider = this;
+        empty = new EmptyChunk(dummyWorld, 0, 0);
     }
 
     public boolean chunkExists(ChunkLocation location) {
@@ -66,7 +77,7 @@ public class ChunkLoader {
         return loadChunkFromCompound(tag.getCompound("Level"));
     }
 
-    public static Chunk loadChunkFromCompound(CompoundTag tag) {
+    public Chunk loadChunkFromCompound(CompoundTag tag) {
         int version = tag.getIntegerOrDefault("Version", -1);
         ChunkReader reader = getChunkReaderByVersion(tag, version);
         int x = reader.getX();
@@ -111,4 +122,62 @@ public class ChunkLoader {
         }
     }
 
+    @Override
+    public boolean isChunkLoaded(int chunkX, int chunkZ) {
+        return false;
+    }
+
+    @Override
+    public Chunk provideChunk(int chunkX, int chunkZ) {
+        try {
+            return loadChunk(new ChunkLocation(chunkX, chunkZ));
+        } catch (IOException e) {
+            return empty;
+        }
+    }
+
+    @Override
+    public Chunk prepareChunk(int chunkX, int chunkZ) {
+        return provideChunk(chunkX, chunkZ);
+    }
+
+    @Override
+    public void regenerateChunk(int chunkX, int chunkZ) {
+
+    }
+
+    @Override
+    public void populate(IChunkProvider chunkProvider, int chunkX, int chunkZ) {
+
+    }
+
+    @Override
+    public boolean saveChunks(boolean saveImmediately, ProgressListener progressUpdate) {
+        return false;
+    }
+
+    @Override
+    public boolean tick() {
+        return false;
+    }
+
+    @Override
+    public boolean canSave() {
+        return false;
+    }
+
+    @Override
+    public String getInfoString() {
+        return "";
+    }
+
+    @Override
+    public void unloadAllChunks() {
+
+    }
+
+    @Override
+    public void setCurrentChunkOver(int chunkX, int chunkZ) {
+
+    }
 }
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/worldloader/DimensionDataLoader.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/DimensionDataLoader.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/bta/worldloader/DimensionDataLoader.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/DimensionDataLoader.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/bta/worldloader/LevelDataLoader.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/LevelDataLoader.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/bta/worldloader/LevelDataLoader.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/bta/worldloader/LevelDataLoader.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/BlockPos2D.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/BlockPos2D.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/BlockPos2D.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/BlockPos2D.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/BlockPos3D.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/BlockPos3D.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/BlockPos3D.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/BlockPos3D.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/ChunkLocation.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ChunkLocation.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/ChunkLocation.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ChunkLocation.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/ChunkPos2D.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ChunkPos2D.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/ChunkPos2D.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ChunkPos2D.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/ChunkPos3D.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ChunkPos3D.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/ChunkPos3D.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ChunkPos3D.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/NamespaceID.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/NamespaceID.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/NamespaceID.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/NamespaceID.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/collections/ObjectWrapper.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ObjectWrapper.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/collections/ObjectWrapper.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/collections/ObjectWrapper.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/data/Biome.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/data/Biome.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/data/Biome.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/data/Biome.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/data/Chunk.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/data/Chunk.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/data/Chunk.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/data/Chunk.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/data/World.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/data/World.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/data/World.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/data/World.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/gui/ChunkProvider.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/ChunkProvider.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/gui/ChunkProvider.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/gui/ChunkProvider.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/gui/ChunkView.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/ChunkView.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/gui/ChunkView.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/gui/ChunkView.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/gui/SeedViewer.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/SeedViewer.java
similarity index 99%
rename from game/client/src/main/java/org/useless/seedviewer/gui/SeedViewer.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/gui/SeedViewer.java
index 9ebbd4b00..895be3f55 100644
--- a/game/client/src/main/java/org/useless/seedviewer/gui/SeedViewer.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/SeedViewer.java
@@ -9,6 +9,7 @@ import org.useless.seedviewer.gui.components.InfoPanel;
 import org.useless.seedviewer.gui.components.InputPanel;
 import org.useless.seedviewer.gui.components.Viewport;
 
+import javax.imageio.ImageIO;
 import javax.swing.*;
 import java.awt.*;
 import java.awt.event.ComponentAdapter;
@@ -36,6 +37,7 @@ public class SeedViewer extends JFrame {
     public final InfoPanel infoPanel;
 
     public SeedViewer(Properties properties) {
+        ImageIO.setUseCache(false);
         LOGGER.info("Starting Seed Viewer!");
         this.launchProperties = properties;
 
diff --git a/game/client/src/main/java/org/useless/seedviewer/gui/components/InfoPanel.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/InfoPanel.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/gui/components/InfoPanel.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/InfoPanel.java
diff --git a/game/client/src/main/java/org/useless/seedviewer/gui/components/InputPanel.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/InputPanel.java
similarity index 100%
rename from game/client/src/main/java/org/useless/seedviewer/gui/components/InputPanel.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/InputPanel.java
index ab3c9e65a..7d461c6cd 100644
--- a/game/client/src/main/java/org/useless/seedviewer/gui/components/InputPanel.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/InputPanel.java
@@ -2,8 +2,8 @@ package org.useless.seedviewer.gui.components;
 
 import com.mojang.logging.LogUtils;
 import net.minecraft.core.Global;
-import org.slf4j.Logger;
 import org.useless.seedviewer.gui.SeedViewer;
+import org.slf4j.Logger;
 
 import javax.imageio.ImageIO;
 import javax.swing.*;
diff --git a/game/client/src/main/java/org/useless/seedviewer/gui/components/Viewport.java b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/Viewport.java
similarity index 99%
rename from game/client/src/main/java/org/useless/seedviewer/gui/components/Viewport.java
rename to util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/Viewport.java
index 8860674c8..5647806fc 100644
--- a/game/client/src/main/java/org/useless/seedviewer/gui/components/Viewport.java
+++ b/util/seed-viewer/src/main/java/org/useless/seedviewer/gui/components/Viewport.java
@@ -3,9 +3,6 @@ package org.useless.seedviewer.gui.components;
 import com.mojang.logging.LogUtils;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-import org.useless.seedviewer.TestChunkProvider;
-import org.useless.seedviewer.bta.BTAChunkProvider;
 import org.useless.seedviewer.bta.BTAWorld;
 import org.useless.seedviewer.collections.ChunkLocation;
 import org.useless.seedviewer.collections.ChunkPos2D;
@@ -17,6 +14,9 @@ import org.useless.seedviewer.data.World;
 import org.useless.seedviewer.gui.ChunkProvider;
 import org.useless.seedviewer.gui.ChunkView;
 import org.useless.seedviewer.gui.SeedViewer;
+import org.slf4j.Logger;
+import org.useless.seedviewer.TestChunkProvider;
+import org.useless.seedviewer.bta.BTAChunkProvider;
 
 import javax.imageio.ImageIO;
 import javax.swing.*;
@@ -169,7 +169,7 @@ public class Viewport extends JLabel {
 
     public synchronized void addMissingViewers(Rectangle viewportBounds) {
         ChunkLocation topLeftLocation =
-            new ChunkLocation(viewportBounds.x/Chunk.CHUNK_SIZE_X,
+            new ChunkLocation(viewportBounds.x/ Chunk.CHUNK_SIZE_X,
                 viewportBounds.y/Chunk.CHUNK_SIZE_Z);
         int chunksX = viewportBounds.width/Chunk.CHUNK_SIZE_X;
         int chunksZ = viewportBounds.height/Chunk.CHUNK_SIZE_Z;
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/1024.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/1024.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/1024.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/1024.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/128.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/128.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/128.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/128.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/16.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/16.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/16.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/16.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/256.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/256.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/256.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/256.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/32.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/32.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/32.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/32.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/512.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/512.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/512.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/512.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/icons/64.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/64.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/icons/64.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/icons/64.png
diff --git a/game/client/src/main/resources/assets/seedviewer/textures/slime_vignette.png b/util/seed-viewer/src/main/resources/assets/seedviewer/textures/slime_vignette.png
similarity index 100%
rename from game/client/src/main/resources/assets/seedviewer/textures/slime_vignette.png
rename to util/seed-viewer/src/main/resources/assets/seedviewer/textures/slime_vignette.png
diff --git a/util/seed-viewer/src/test/java/SeedViewerTest.java b/util/seed-viewer/src/test/java/SeedViewerTest.java
new file mode 100644
index 000000000..cd25fefb4
--- /dev/null
+++ b/util/seed-viewer/src/test/java/SeedViewerTest.java
@@ -0,0 +1,46 @@
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.useless.seedviewer.collections.ChunkLocation;
+import org.useless.seedviewer.gui.SeedViewer;
+
+public class SeedViewerTest {
+
+    @Test
+    @DisplayName("Test for Slime Chunks")
+    void slimeChunkTest() {
+        {
+            long seed = 100;
+            Assertions.assertAll(
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(3, -4))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(8, -7))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(15, 0))));
+            Assertions.assertAll(
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(11, -5))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(12, -8))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(12, -11))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(0, -17))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(-4, -11))));
+        }
+        {
+            long seed = 10000;
+            Assertions.assertAll(
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(2, 0))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(6, -4))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(0, -6))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(5, 3))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(7, 7))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(6, 7))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(3, 11))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(8, 13))),
+                () -> Assertions.assertTrue(SeedViewer.isSlimeChunk(seed, new ChunkLocation(10, 3))));
+            Assertions.assertAll(
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(10, -1))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(12, -4))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(8, -10))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(5, -14))),
+                () -> Assertions.assertFalse(SeedViewer.isSlimeChunk(seed, new ChunkLocation(0, -11))));
+        }
+
+    }
+}
diff --git a/util/seed-viewer/src/test/java/collections/BlockPosTest.java b/util/seed-viewer/src/test/java/collections/BlockPosTest.java
new file mode 100644
index 000000000..8b101eec9
--- /dev/null
+++ b/util/seed-viewer/src/test/java/collections/BlockPosTest.java
@@ -0,0 +1,72 @@
+package collections;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.useless.seedviewer.collections.BlockPos2D;
+import org.useless.seedviewer.collections.BlockPos3D;
+
+public class BlockPosTest {
+    @Test
+    @DisplayName("Test BlockPos2D Creation")
+    void blockPos2DCreate() {
+        // Test valid inputs
+        final short range = 8192;
+        final short targetAttempts = 128;
+        final byte scalar = range/targetAttempts;
+        for (int x = -range; x <= range; x += scalar) {
+            for (int z = -range; z <= range; z += scalar) {
+                BlockPos2D blockPos2D_1 = new BlockPos2D(x, z);
+                Assertions.assertEquals(blockPos2D_1.x, x);
+                Assertions.assertEquals(blockPos2D_1.z, z);
+
+                BlockPos2D blockPos2D_2 = new BlockPos2D(x, z);
+                Assertions.assertEquals(blockPos2D_2.x, x);
+                Assertions.assertEquals(blockPos2D_2.z, z);
+
+                Assertions.assertEquals(blockPos2D_1.x, blockPos2D_2.x);
+                Assertions.assertEquals(blockPos2D_1.z, blockPos2D_2.z);
+                Assertions.assertEquals(blockPos2D_1, blockPos2D_2);
+
+                BlockPos2D blockPos2D_3 = new BlockPos2D(x + 1, z + 17);
+                BlockPos3D blockPos3D_3 = new BlockPos3D(x + 1, 128, z + 17);
+                Assertions.assertNotEquals(blockPos2D_3, blockPos2D_1);
+                Assertions.assertNotEquals(blockPos3D_3, blockPos2D_1);
+            }
+        }
+    }
+
+    @Test
+    @DisplayName("Test BlockPos3D Creation")
+    void blockPos3DCreate() {
+        // Test valid inputs
+        final short range = 8192;
+        final short targetAttempts = 128;
+        final byte scalar = range/targetAttempts;
+        for (int x = -range; x <= range; x += scalar) {
+            for (int y = -range; y < range; y += scalar) {
+                for (int z = -range; z <= range; z += scalar) {
+                    BlockPos3D blockPos3D_1 = new BlockPos3D(x, y, z);
+                    Assertions.assertEquals(blockPos3D_1.x, x);
+                    Assertions.assertEquals(blockPos3D_1.y, y);
+                    Assertions.assertEquals(blockPos3D_1.z, z);
+
+                    BlockPos3D blockPos3D_2 = new BlockPos3D(x, y, z);
+                    Assertions.assertEquals(blockPos3D_2.x, x);
+                    Assertions.assertEquals(blockPos3D_2.y, y);
+                    Assertions.assertEquals(blockPos3D_2.z, z);
+
+                    Assertions.assertEquals(blockPos3D_1.x, blockPos3D_2.x);
+                    Assertions.assertEquals(blockPos3D_1.y, blockPos3D_2.y);
+                    Assertions.assertEquals(blockPos3D_1.z, blockPos3D_2.z);
+                    Assertions.assertEquals(blockPos3D_1, blockPos3D_2);
+
+                    BlockPos2D blockPos2D_3 = new BlockPos2D(x + 1, z + 17);
+                    BlockPos3D blockPos3D_3 = new BlockPos3D(x + 1, y - 2, z + 17);
+                    Assertions.assertNotEquals(blockPos3D_3, blockPos3D_1);
+                    Assertions.assertNotEquals(blockPos2D_3, blockPos3D_1);
+                }
+            }
+        }
+    }
+}
diff --git a/util/seed-viewer/src/test/java/collections/ChunkLocationTest.java b/util/seed-viewer/src/test/java/collections/ChunkLocationTest.java
new file mode 100644
index 000000000..3f4cc6f07
--- /dev/null
+++ b/util/seed-viewer/src/test/java/collections/ChunkLocationTest.java
@@ -0,0 +1,35 @@
+package collections;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.useless.seedviewer.collections.ChunkLocation;
+
+public class ChunkLocationTest {
+    @Test
+    @DisplayName("Test ChunkLocation Creation")
+    void blockPos2DCreate() {
+        // Test valid inputs
+        final short range = 8192;
+        final short targetAttempts = 128;
+        final byte scalar = range/targetAttempts;
+        for (int x = -range; x <= range; x += scalar) {
+            for (int z = -range; z <= range; z += scalar) {
+                ChunkLocation chunkLocation_1 = new ChunkLocation(x, z);
+                Assertions.assertEquals(chunkLocation_1.x, x);
+                Assertions.assertEquals(chunkLocation_1.z, z);
+
+                ChunkLocation chunkLocation_2 = new ChunkLocation(x, z);
+                Assertions.assertEquals(chunkLocation_2.x, x);
+                Assertions.assertEquals(chunkLocation_2.z, z);
+
+                Assertions.assertEquals(chunkLocation_1.x, chunkLocation_2.x);
+                Assertions.assertEquals(chunkLocation_1.z, chunkLocation_2.z);
+                Assertions.assertEquals(chunkLocation_1, chunkLocation_2);
+
+                ChunkLocation chunkLocation_3 = new ChunkLocation(x + 1, z + 17);
+                Assertions.assertNotEquals(chunkLocation_3, chunkLocation_1);
+            }
+        }
+    }
+}
diff --git a/util/seed-viewer/src/test/java/collections/ChunkPosTest.java b/util/seed-viewer/src/test/java/collections/ChunkPosTest.java
new file mode 100644
index 000000000..206d4f19b
--- /dev/null
+++ b/util/seed-viewer/src/test/java/collections/ChunkPosTest.java
@@ -0,0 +1,81 @@
+package collections;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.useless.seedviewer.collections.ChunkPos2D;
+import org.useless.seedviewer.collections.ChunkPos3D;
+import org.useless.seedviewer.data.Chunk;
+
+public class ChunkPosTest {
+    @Test
+    @DisplayName("Test Valid ChunkPos2D")
+    void validChunkPos2D() {
+        for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
+            for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
+                ChunkPos2D chunkPos2D_1 = new ChunkPos2D(x, z);
+                Assertions.assertEquals(chunkPos2D_1.x, x);
+                Assertions.assertEquals(chunkPos2D_1.z, z);
+
+                ChunkPos2D chunkPos2D_2 = new ChunkPos2D(x, z);
+                Assertions.assertEquals(chunkPos2D_2.x, x);
+                Assertions.assertEquals(chunkPos2D_2.z, z);
+
+                Assertions.assertEquals(chunkPos2D_1.x, chunkPos2D_2.x);
+                Assertions.assertEquals(chunkPos2D_1.z, chunkPos2D_2.z);
+                Assertions.assertEquals(chunkPos2D_1, chunkPos2D_2);
+            }
+        }
+    }
+
+    @Test
+    @DisplayName("Test Invalid ChunkPos2D")
+    void invalidChunkPos2D() {
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos2D(-10, -10));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos2D(0, -10));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos2D(-10, 0));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos2D(20, 0));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos2D(0, 20));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos2D(20, 20));
+    }
+
+    @Test
+    @DisplayName("Test Valid ChunkPos3D")
+    void validChunkPos3D() {
+        for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
+            for (int y = 0; y < Chunk.CHUNK_SIZE_Y; y++) {
+                for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
+                    ChunkPos3D chunkPos3D_1 = new ChunkPos3D(x, y, z);
+                    Assertions.assertEquals(chunkPos3D_1.x, x);
+                    Assertions.assertEquals(chunkPos3D_1.y, y);
+                    Assertions.assertEquals(chunkPos3D_1.z, z);
+
+                    ChunkPos3D chunkPos3D_2 = new ChunkPos3D(x, y, z);
+                    Assertions.assertEquals(chunkPos3D_2.x, x);
+                    Assertions.assertEquals(chunkPos3D_2.y, y);
+                    Assertions.assertEquals(chunkPos3D_2.z, z);
+
+                    Assertions.assertEquals(chunkPos3D_1.x, chunkPos3D_2.x);
+                    Assertions.assertEquals(chunkPos3D_1.y, chunkPos3D_2.y);
+                    Assertions.assertEquals(chunkPos3D_1.z, chunkPos3D_2.z);
+                    Assertions.assertEquals(chunkPos3D_1, chunkPos3D_2);
+                }
+            }
+        }
+    }
+
+    @Test
+    @DisplayName("Test Invalid ChunkPos3D")
+    void invalidChunkPos3D() {
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(-10, -10, -10));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(0, -10, -10));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(0, 0, -10));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(-10, 0, -10));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(-10, 0, 0));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(20, 20, 20));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(0, 20, 20));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(0, 0, 20));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(20, 0, 20));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new ChunkPos3D(20, 0, 0));
+    }
+}
diff --git a/util/seed-viewer/src/test/java/collections/NamespaceTest.java b/util/seed-viewer/src/test/java/collections/NamespaceTest.java
new file mode 100644
index 000000000..e5b06e98d
--- /dev/null
+++ b/util/seed-viewer/src/test/java/collections/NamespaceTest.java
@@ -0,0 +1,86 @@
+package collections;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.useless.seedviewer.collections.NamespaceID;
+
+public class NamespaceTest {
+    @Test
+    @DisplayName("Test Namespace 2String Constructor")
+    void namespaceCreation1() {
+        // Valid creation
+        NamespaceID namespaceID_1 = new NamespaceID("minecraft", "value1");
+        Assertions.assertEquals(namespaceID_1.namespace, "minecraft");
+        Assertions.assertEquals(namespaceID_1.value, "value1");
+        Assertions.assertEquals(namespaceID_1.toString(), "minecraft:value1");
+
+        NamespaceID namespaceID_2 = new NamespaceID("minecraft", "value1");
+        Assertions.assertEquals(namespaceID_2.namespace, "minecraft");
+        Assertions.assertEquals(namespaceID_2.value, "value1");
+        Assertions.assertEquals(namespaceID_2.toString(), "minecraft:value1");
+
+        Assertions.assertEquals(namespaceID_1.namespace, namespaceID_2.namespace);
+        Assertions.assertEquals(namespaceID_1.value, namespaceID_2.value);
+        Assertions.assertEquals(namespaceID_1, namespaceID_2);
+        Assertions.assertEquals(namespaceID_1.toString(), namespaceID_2.toString());
+
+        NamespaceID namespaceID_3 = new NamespaceID("bta", "value2");
+        Assertions.assertEquals(namespaceID_3.namespace, "bta");
+        Assertions.assertEquals(namespaceID_3.value, "value2");
+        Assertions.assertEquals(namespaceID_3.toString(), "bta:value2");
+
+        Assertions.assertNotEquals(namespaceID_3.namespace, "minecraft");
+        Assertions.assertNotEquals(namespaceID_3.value, "value1");
+        Assertions.assertNotEquals(namespaceID_3.toString(), "minecraft:value1");
+
+        Assertions.assertNotEquals(namespaceID_3, namespaceID_1);
+        Assertions.assertNotEquals(namespaceID_3.value, namespaceID_1.value);
+        Assertions.assertNotEquals(namespaceID_3.namespace, namespaceID_1.namespace);
+        Assertions.assertNotEquals(namespaceID_3.toString(), namespaceID_1.toString());
+
+        Assertions.assertNotEquals(namespaceID_3, namespaceID_2);
+        Assertions.assertNotEquals(namespaceID_3.value, namespaceID_2.value);
+        Assertions.assertNotEquals(namespaceID_3.namespace, namespaceID_2.namespace);
+        Assertions.assertNotEquals(namespaceID_3.toString(), namespaceID_2.toString());
+
+        // Invalid creation
+        Assertions.assertThrowsExactly(NullPointerException.class, () -> new NamespaceID(null, null));
+        Assertions.assertThrowsExactly(NullPointerException.class, () -> new NamespaceID("minecraft", null));
+        Assertions.assertThrowsExactly(NullPointerException.class, () -> new NamespaceID(null, "value1"));
+
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft:", "value1"));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft", "value1:"));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft:", "value1:"));
+    }
+
+    @Test
+    @DisplayName("Test Namespace 1String Constructor")
+    void namespaceCreation2() {
+        String formatted_1 = "minecraft:value1";
+        NamespaceID namespaceID_1 = new NamespaceID(formatted_1);
+        Assertions.assertEquals(namespaceID_1.namespace, "minecraft");
+        Assertions.assertEquals(namespaceID_1.value, "value1");
+        Assertions.assertEquals(namespaceID_1.toString(), formatted_1);
+        Assertions.assertEquals(namespaceID_1.namespace + ":" + namespaceID_1.value, formatted_1);
+
+        NamespaceID namespaceID_2 = new NamespaceID(formatted_1);
+        Assertions.assertEquals(namespaceID_2.namespace, "minecraft");
+        Assertions.assertEquals(namespaceID_2.value, "value1");
+        Assertions.assertEquals(namespaceID_2.toString(), formatted_1);
+        Assertions.assertEquals(namespaceID_2.namespace + ":" + namespaceID_2.value, formatted_1);
+
+        Assertions.assertEquals(namespaceID_1.namespace, namespaceID_2.namespace);
+        Assertions.assertEquals(namespaceID_1.value, namespaceID_2.value);
+        Assertions.assertEquals(namespaceID_1, namespaceID_2);
+        Assertions.assertEquals(namespaceID_1.toString(), namespaceID_2.toString());
+
+        Assertions.assertThrowsExactly(NullPointerException.class, () -> new NamespaceID(null));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft::value1"));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft:value1:"));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID(":value1"));
+        Assertions.assertDoesNotThrow(() -> new NamespaceID("minecraft:value"));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft value"));
+        Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> new NamespaceID("minecraft|value"));
+    }
+}
