From d2097ca7bbce103040b3ba911522e3a98ff298c0 Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Wed, 4 Dec 2024 20:34:17 +0000 Subject: [PATCH 1/7] Major refactor of world, dimension, and chunk all are now abstract --- .../llbit/chunky/chunk/biome/BiomeData2d.java | 5 +- .../chunky/chunk/biome/BiomeDataFactory.java | 5 +- .../chunk/biome/GenericQuartBiomeData3d.java | 3 +- .../chunky/chunk/biome/QuartBiomeData3d.java | 2 +- .../src/java/se/llbit/chunky/map/MapTile.java | 9 +- .../se/llbit/chunky/renderer/scene/Scene.java | 5 +- .../src/java/se/llbit/chunky/ui/ChunkMap.java | 8 - .../ui/controller/ChunkyFxController.java | 33 +- .../chunky/ui/render/tabs/GeneralTab.java | 19 +- .../src/java/se/llbit/chunky/world/Chunk.java | 442 ++---------------- .../se/llbit/chunky/world/CubicDimension.java | 6 +- .../java/se/llbit/chunky/world/Dimension.java | 168 ++----- .../se/llbit/chunky/world/EmptyDimension.java | 43 ++ .../se/llbit/chunky/world/EmptyWorld.java | 15 +- .../chunky/world/ImposterCubicChunk.java | 31 +- .../java/se/llbit/chunky/world/JavaChunk.java | 377 +++++++++++++++ .../se/llbit/chunky/world/JavaDimension.java | 122 +++++ .../java/se/llbit/chunky/world/JavaWorld.java | 374 +++++++++++++++ .../src/java/se/llbit/chunky/world/World.java | 302 +----------- .../llbit/chunky/world/region/MCRegion.java | 6 +- .../world/region/MCRegionChangeWatcher.java | 5 +- .../chunky/world/region/RegionParser.java | 4 +- 22 files changed, 1099 insertions(+), 885 deletions(-) create mode 100644 chunky/src/java/se/llbit/chunky/world/JavaChunk.java create mode 100644 chunky/src/java/se/llbit/chunky/world/JavaDimension.java create mode 100644 chunky/src/java/se/llbit/chunky/world/JavaWorld.java diff --git a/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeData2d.java b/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeData2d.java index 709b9efbdf..40c9675dab 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeData2d.java +++ b/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeData2d.java @@ -1,5 +1,6 @@ package se.llbit.chunky.chunk.biome; +import se.llbit.chunky.world.JavaChunk; import se.llbit.chunky.world.biome.BiomePalette; import se.llbit.chunky.world.biome.Biomes; import se.llbit.nbt.Tag; @@ -34,7 +35,7 @@ public void clear() { } public static void loadBiomeDataByteArray(Tag chunkData, BiomeData2d biomeData, BiomePalette biomePalette) { - byte[] data = chunkData.get(LEVEL_BIOMES).byteArray(); + byte[] data = chunkData.get(JavaChunk.LEVEL_BIOMES).byteArray(); int i = 0; for(int z = 0; z < Z_MAX; z++) { for(int x = 0; x < X_MAX; x++) { @@ -46,7 +47,7 @@ public static void loadBiomeDataByteArray(Tag chunkData, BiomeData2d biomeData, public static void loadBiomeDataIntArray(Tag chunkData, BiomeData2d biomeData, BiomePalette biomePalette) { // Since Minecraft 1.13, biome IDs are stored in an int vector with 256 entries (one for each XZ position). - int[] data = chunkData.get(LEVEL_BIOMES).intArray(); + int[] data = chunkData.get(JavaChunk.LEVEL_BIOMES).intArray(); int i = 0; for(int z = 0; z < Z_MAX; z++) { for(int x = 0; x < X_MAX; x++) { diff --git a/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java b/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java index 84bd5be37b..395eab9bb6 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java +++ b/chunky/src/java/se/llbit/chunky/chunk/biome/BiomeDataFactory.java @@ -1,6 +1,7 @@ package se.llbit.chunky.chunk.biome; import se.llbit.chunky.chunk.ChunkData; +import se.llbit.chunky.world.JavaChunk; import se.llbit.chunky.world.biome.Biome; import se.llbit.chunky.world.biome.BiomePalette; import se.llbit.chunky.world.biome.Biomes; @@ -19,7 +20,7 @@ public class BiomeDataFactory { //TODO: Ideally we would have registered factory impls with an isValidFor(Tag chunkTag), but this messy if chain works for now public static void loadBiomeData(ChunkData chunkData, Tag chunkTag, BiomePalette biomePalette, int yMin, int yMax) { BiomeData biomeData = chunkData.getBiomeData(); - Tag biomeTagsPre21w39a = chunkTag.get(LEVEL_BIOMES); + Tag biomeTagsPre21w39a = chunkTag.get(JavaChunk.LEVEL_BIOMES); if (!biomeTagsPre21w39a.isError()) { // pre21w39a tag exists if (biomeTagsPre21w39a.isByteArray(X_MAX * Z_MAX)) { if (!(biomeData instanceof BiomeData2d)) { @@ -43,7 +44,7 @@ public static void loadBiomeData(ChunkData chunkData, Tag chunkTag, BiomePalette chunkData.setBiomeData(biomeData); return; } - } else if (!chunkTag.get(SECTIONS_POST_21W39A).isError()) { // in 21w39a biome tags moved into the sections array + } else if (!chunkTag.get(JavaChunk.SECTIONS_POST_21W39A).isError()) { // in 21w39a biome tags moved into the sections array if(!(biomeData instanceof GenericQuartBiomeData3d)) { biomeData = new GenericQuartBiomeData3d(); } diff --git a/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java b/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java index e633d3a2a4..6c76a29a1a 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java +++ b/chunky/src/java/se/llbit/chunky/chunk/biome/GenericQuartBiomeData3d.java @@ -1,6 +1,7 @@ package se.llbit.chunky.chunk.biome; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import se.llbit.chunky.world.JavaChunk; import se.llbit.chunky.world.biome.Biome; import se.llbit.chunky.world.biome.BiomePalette; import se.llbit.chunky.world.biome.Biomes; @@ -60,7 +61,7 @@ public void clear() { } public static void loadBiomeDataPost21w39a(Tag chunkData, GenericQuartBiomeData3d biomeData, BiomePalette biomePalette, int yMin, int yMax) { - Tag sections = chunkData.get(SECTIONS_POST_21W39A); + Tag sections = chunkData.get(JavaChunk.SECTIONS_POST_21W39A); if (sections.isList()) { for (SpecificTag section : sections.asList()) { Tag yTag = section.get("Y"); diff --git a/chunky/src/java/se/llbit/chunky/chunk/biome/QuartBiomeData3d.java b/chunky/src/java/se/llbit/chunky/chunk/biome/QuartBiomeData3d.java index 778ece7e28..72b38c9a09 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/biome/QuartBiomeData3d.java +++ b/chunky/src/java/se/llbit/chunky/chunk/biome/QuartBiomeData3d.java @@ -6,7 +6,7 @@ import java.util.Arrays; -import static se.llbit.chunky.world.Chunk.LEVEL_BIOMES; +import static se.llbit.chunky.world.JavaChunk.LEVEL_BIOMES; /** * Implementation of a 3D biome grid where the smallest size of a biome is 4x4x4 blocks diff --git a/chunky/src/java/se/llbit/chunky/map/MapTile.java b/chunky/src/java/se/llbit/chunky/map/MapTile.java index 2f00a9991a..41309e05e4 100644 --- a/chunky/src/java/se/llbit/chunky/map/MapTile.java +++ b/chunky/src/java/se/llbit/chunky/map/MapTile.java @@ -18,7 +18,6 @@ import se.llbit.chunky.resources.BitmapImage; import se.llbit.chunky.world.*; -import se.llbit.chunky.world.region.Region; /** * A tile in the 2D world map or minimap. The tile contains either a chunk or a region. @@ -71,14 +70,14 @@ public void draw(MapBuffer buffer, WorldMapLoader mapLoader, ChunkView view, } } else { RegionPosition regionPos = new RegionPosition(pos.x, pos.z); // intentionally don't convert, this position represented a region already. + boolean isValid = mapLoader.getWorld().currentDimension().regionExistsWithinRange(regionPos, view.yMin, view.yMax); - Region region = mapLoader.getWorld().currentDimension().getRegionWithinRange(regionPos, view.yMin, view.yMax); + int pixelOffset = 0; for (int z = 0; z < 32; ++z) { for (int x = 0; x < 32; ++x) { - Chunk chunk = region.getChunk(x, z); - //Calculate the chunk position as empty chunks are (0, 0) - ChunkPosition pos = region.getPosition().asChunkPosition(x, z); + ChunkPosition pos = regionPos.asChunkPosition(x, z); + Chunk chunk = mapLoader.getWorld().currentDimension().getChunk(pos); pixels[pixelOffset] = chunk.biomeColor(); if (isValid && !(chunk instanceof EmptyRegionChunk) && selection.isSelected(pos)) { diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index e2770c1cac..e6cd3ce0c6 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -821,7 +821,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec } for (RegionPosition region : regions) { - dimension.getRegion(region).parse(yMin, yMax); + ((JavaDimension) dimension).getRegion(region).parse(yMin, yMax); } } @@ -1192,7 +1192,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec if (!chunkData.isEmpty()){ nonEmptyChunks.add(cp); - if (dimension.getChunk(cp).getVersion() == ChunkVersion.PRE_FLATTENING) { + Chunk chunk = dimension.getChunk(cp); + if (chunk instanceof JavaChunk javaChunk && javaChunk.getVersion() == ChunkVersion.PRE_FLATTENING) { legacyChunks.add(cp); } } diff --git a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java index a7603177fc..4a5f7b8730 100644 --- a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java +++ b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java @@ -19,22 +19,16 @@ import javafx.application.Platform; import javafx.geometry.Point2D; -import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.*; -import javafx.scene.control.Button; -import javafx.scene.control.Dialog; import javafx.scene.control.MenuItem; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; -import javafx.scene.layout.Border; -import javafx.scene.layout.GridPane; import javafx.stage.PopupWindow; import se.llbit.chunky.map.MapBuffer; import se.llbit.chunky.map.MapView; @@ -45,7 +39,6 @@ import se.llbit.chunky.renderer.scene.SceneManager; import se.llbit.chunky.ui.controller.ChunkyFxController; import se.llbit.chunky.ui.dialogs.SelectChunksInRadiusDialog; -import se.llbit.chunky.ui.elements.TextFieldLabelWrapper; import se.llbit.chunky.world.*; import se.llbit.chunky.world.Dimension; import se.llbit.chunky.world.listeners.ChunkUpdateListener; @@ -57,7 +50,6 @@ import java.io.File; import java.io.IOException; import java.util.Collection; -import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java index 7fd253d564..c24973f918 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java @@ -32,6 +32,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import it.unimi.dsi.fastutil.ints.IntIntPair; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; @@ -482,19 +483,15 @@ public File getSceneFile(String fileName) { } if (!reloaded) { ignoreYUpdate.set(true); - if (mapLoader.getWorld().getVersionId() >= World.VERSION_21W06A) { - yMin.setRange(-64, 320); - yMin.set(-64); - yMax.setRange(-64, 320); - yMax.set(320); - mapView.setYMinMax(-64, 320); - } else { - yMin.setRange(0, 256); - yMin.set(0); - yMax.setRange(0, 256); - yMax.set(256); - mapView.setYMinMax(0, 256); - } + IntIntPair heightRange = mapLoader.getWorld().currentDimension().heightRange(); + int min = heightRange.firstInt(); + int max = heightRange.secondInt(); + yMin.setRange(min, max); + yMin.set(min); + yMax.setRange(min, max); + yMax.set(max); + mapView.setYMinMax(min, max); + yMin.getStyleClass().removeAll("invalid"); yMax.getStyleClass().removeAll("invalid"); ignoreYUpdate.set(false); @@ -680,13 +677,9 @@ public File getSceneFile(String fileName) { mapOverlay.setOnKeyReleased(map::onKeyReleased); mapLoader.loadWorld(PersistentSettings.getLastWorld()); - if (mapLoader.getWorld().getVersionId() >= World.VERSION_21W06A) { - mapView.setYMin(-64); - mapView.setYMax(320); - } else { - mapView.setYMin(0); - mapView.setYMax(256); - } + IntIntPair heightRange = mapLoader.getWorld().currentDimension().heightRange(); + mapView.setYMin(heightRange.firstInt()); + mapView.setYMax(heightRange.secondInt()); canvas = new RenderCanvasFx(this, chunky.getSceneManager().getScene(), chunky.getRenderController().getRenderManager()); diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java index b46ac8e1ea..2ea6cad703 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/GeneralTab.java @@ -17,6 +17,7 @@ */ package se.llbit.chunky.ui.render.tabs; +import it.unimi.dsi.fastutil.ints.IntIntPair; import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; @@ -506,16 +507,14 @@ private void updateCanvasCrop() { } private void updateYClipSlidersRanges(World world) { - if (world != null && world.getVersionId() >= World.VERSION_21W06A) { - yMin.setRange(-64, 320); - yMin.set(-64); - yMax.setRange(-64, 320); - yMax.set(320); - } else { - yMin.setRange(0, 256); - yMin.set(0); - yMax.setRange(0, 256); - yMax.set(256); + if (world != null) { + IntIntPair heightRange = world.currentDimension().heightRange(); + int min = heightRange.firstInt(); + int max = heightRange.secondInt(); + yMin.setRange(min, max); + yMin.set(min); + yMax.setRange(min, max); + yMax.set(max); } } } diff --git a/chunky/src/java/se/llbit/chunky/world/Chunk.java b/chunky/src/java/se/llbit/chunky/world/Chunk.java index 092955ba8a..1da9f93051 100644 --- a/chunky/src/java/se/llbit/chunky/world/Chunk.java +++ b/chunky/src/java/se/llbit/chunky/world/Chunk.java @@ -17,35 +17,17 @@ package se.llbit.chunky.world; import se.llbit.chunky.block.minecraft.Air; -import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; import se.llbit.chunky.block.Block; import se.llbit.chunky.block.minecraft.Lava; import se.llbit.chunky.block.minecraft.Water; -import se.llbit.chunky.block.legacy.LegacyBlocks; import se.llbit.chunky.chunk.BlockPalette; import se.llbit.chunky.chunk.ChunkData; import se.llbit.chunky.chunk.ChunkLoadingException; -import se.llbit.chunky.chunk.EmptyChunkData; -import se.llbit.chunky.chunk.biome.BiomeDataFactory; import se.llbit.chunky.map.*; -import se.llbit.chunky.world.biome.ArrayBiomePalette; import se.llbit.chunky.world.biome.BiomePalette; -import se.llbit.chunky.world.region.MCRegion; -import se.llbit.chunky.world.region.Region; -import se.llbit.log.Log; -import se.llbit.math.QuickMath; -import se.llbit.nbt.*; -import se.llbit.util.BitBuffer; import se.llbit.util.Mutable; import se.llbit.util.annotation.NotNull; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static se.llbit.util.NbtUtil.getTagFromNames; -import static se.llbit.util.NbtUtil.tagFromMap; - /** * This class represents a loaded or not-yet-loaded chunk in the world. *

@@ -53,19 +35,7 @@ * * @author Jesper Öqvist (jesper@llbit.se) */ -public class Chunk { - - public static final String DATAVERSION = ".DataVersion"; - public static final String LEVEL_HEIGHTMAP = ".Level.HeightMap"; - public static final String LEVEL_SECTIONS = ".Level.Sections"; - public static final String LEVEL_BIOMES = ".Level.Biomes"; - public static final String LEVEL_ENTITIES = ".Level.Entities"; - public static final String ENTITIES_POST_20W45A = ".Entities"; - public static final String LEVEL_TILEENTITIES = ".Level.TileEntities"; - public static final String BLOCK_ENTITIES_POST_21W43A = ".block_entities"; - - public static final String SECTIONS_POST_21W39A = ".sections"; - public static final String BIOMES_POST_21W39A = ".biomes"; +public abstract class Chunk { /** Chunk width. */ public static final int X_MAX = 16; @@ -76,18 +46,12 @@ public class Chunk { public static final int Z_MAX = 16; public static final int SECTION_Y_MAX = 16; - public static final int SECTION_BYTES = X_MAX * SECTION_Y_MAX * Z_MAX; - public static final int SECTION_HALF_NIBBLES = SECTION_BYTES / 2; - private static final int CHUNK_BYTES = X_MAX * Y_MAX * Z_MAX; - - public static final int DATAVERSION_20W17A = 2529; - public static final int DATAVERSION_20W45A = 2681; // entities moved into separate region files protected final ChunkPosition position; protected volatile AbstractLayer surface = IconLayer.UNKNOWN; protected volatile AbstractLayer biomes = IconLayer.UNKNOWN; - private final Dimension dimension; + protected final Dimension dimension; protected int dataTimestamp = 0; protected int surfaceTimestamp = 0; @@ -112,27 +76,6 @@ public int biomeColor() { return biomes.getAvgColor(); } - /** - * @param request fresh request set - * @return loaded data, or null if something went wrong - */ - private Map getChunkTags(Set request) throws ChunkLoadingException { - MCRegion region = (MCRegion) dimension.getRegion(position.getRegionPosition()); - Mutable timestamp = new Mutable<>(dataTimestamp); - Map chunkTags = region.getChunkTags(this.position, request, timestamp); - this.dataTimestamp = timestamp.get(); - return chunkTags; - } - - /** - * @param request fresh request set - * @return loaded data, or null if something went wrong - */ - private Map getEntityTags(Set request) throws ChunkLoadingException { - MCRegion region = (MCRegion) dimension.getRegion(position.getRegionPosition()); - return region.getEntityTags(this.position, request); - } - /** * Reset the rendered layers in this chunk. */ @@ -152,246 +95,7 @@ public ChunkPosition getPosition() { * layer, surface and cave maps. * @return whether the input chunkdata was modified */ - public synchronized boolean loadChunk(@NotNull Mutable chunkData, int yMin, int yMax) { - if (!shouldReloadChunk()) { - return false; - } - - Set request = new HashSet<>(); - request.add(Chunk.DATAVERSION); - request.add(Chunk.LEVEL_SECTIONS); - request.add(Chunk.SECTIONS_POST_21W39A); - request.add(Chunk.LEVEL_BIOMES); - request.add(Chunk.BIOMES_POST_21W39A); - request.add(Chunk.LEVEL_HEIGHTMAP); - - Map dataMap; - try { - dataMap = getChunkTags(request); - } catch (ChunkLoadingException e) { // we don't want to crash the map view if a chunk fails to load, so we warn the user - Log.warn(String.format("Failed to load chunk %s", position), e); - return false; - } - // TODO: improve error handling here. - if (dataMap == null) { - return false; - } - Tag data = tagFromMap(dataMap); - - surfaceTimestamp = dataTimestamp; - version = chunkVersion(data); - IntIntImmutablePair chunkBounds = inclusiveChunkBounds(data); - chunkData.set(this.dimension.createChunkData(chunkData.get(), chunkBounds.leftInt(), chunkBounds.rightInt())); - loadSurface(data, chunkData.get(), yMin, yMax); - biomesTimestamp = dataTimestamp; - - dimension.chunkUpdated(position); - return true; - } - - private void loadSurface(@NotNull Tag data, ChunkData chunkData, int yMin, int yMax) { - if (data == null) { - surface = IconLayer.CORRUPT; - return; - } - - Heightmap heightmap = dimension.getHeightmap(); - Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); - if (sections.isList()) { - if (version == ChunkVersion.PRE_FLATTENING || version == ChunkVersion.POST_FLATTENING) { - BiomePalette biomePalette = new ArrayBiomePalette(); - BiomeDataFactory.loadBiomeData(chunkData, data, biomePalette, yMin, yMax); - biomes = new BiomeLayer(chunkData, biomePalette); - - BlockPalette palette = new BlockPalette(); - palette.unsynchronize(); //only this RegionParser will use this palette - loadBlockData(data, chunkData, palette, yMin, yMax); - - int[] heightmapData = extractHeightmapData(data, chunkData); - updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - - surface = new SurfaceLayer(dimension.getDimensionId(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); - queueTopography(); - } - } else { - surface = IconLayer.CORRUPT; - } - } - - private int[] extractHeightmapData(@NotNull Tag data, ChunkData chunkData) { - Tag heightmapTag = data.get(LEVEL_HEIGHTMAP); - if (heightmapTag.isIntArray(X_MAX * Z_MAX)) { - return heightmapTag.intArray(); - } else { - int[] fallback = new int[X_MAX * Z_MAX]; - for (int i = 0; i < fallback.length; ++i) { - fallback[i] = chunkData.maxY(); - } - return fallback; - } - } - - /** Detect Minecraft version that generated the chunk. */ - private static ChunkVersion chunkVersion(@NotNull Tag data) { - Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); - if (sections.isList()) { - for (SpecificTag section : sections.asList()) { - if (!section.get("Palette").isList()) { - if (section.get("Blocks").isByteArray(SECTION_BYTES)) { - return ChunkVersion.PRE_FLATTENING; - } - } - } - return ChunkVersion.POST_FLATTENING; - } - return ChunkVersion.UNKNOWN; - } - - private static void loadBlockData(@NotNull Tag data, @NotNull ChunkData chunkData, - BlockPalette blockPalette, int minY, int maxY) { - - Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); - if (sections.isList()) { - for (SpecificTag section : sections.asList()) { - Tag yTag = section.get("Y"); - int sectionY = yTag.byteValue(); - int sectionMinBlockY = sectionY << 4; - - if(sectionY < minY >> 4 || sectionY-1 > (maxY >> 4)+1) - continue; //skip parsing sections that are outside requested bounds - - Tag blockPaletteTag = getTagFromNames(section, "Palette", "block_states\\palette"); - if (blockPaletteTag.isList()) { - ListTag localBlockPalette = blockPaletteTag.asList(); - // Bits per block: - int bpb = 4; - if (localBlockPalette.size() > 16) { - bpb = QuickMath.log2(QuickMath.nextPow2(localBlockPalette.size())); - } - - int dataSize = (4096 * bpb) / 64; - Tag blockStates = getTagFromNames(section, "BlockStates", "block_states\\data"); - - if (blockStates.isLongArray(dataSize)) { - // since 20w17a, block states are aligned to 64-bit boundaries, so there are 64 % bpb - // unused bits per block state; if so, the array is longer than the expected data size - boolean isAligned = data.get(DATAVERSION).intValue() >= DATAVERSION_20W17A; - if (isAligned) { - // entries are 64-bit-padded, re-calculate the bits per block - // this is the dataSize calculation from above reverted, we know the actual data size - bpb = blockStates.longArray().length / 64; - } - - int[] subpalette = new int[localBlockPalette.size()]; - int paletteIndex = 0; - for (Tag item : localBlockPalette.asList()) { - subpalette[paletteIndex] = blockPalette.put(item); - paletteIndex += 1; - } - BitBuffer buffer = new BitBuffer(blockStates.longArray(), bpb, isAligned); - for (int y = 0; y < SECTION_Y_MAX; y++) { - int blockY = sectionMinBlockY + y; - for (int z = 0; z < Z_MAX; z++) { - for(int x = 0; x < X_MAX; x++) { - int b0 = buffer.read(); - if (b0 < subpalette.length) { - chunkData.setBlockAt(x, blockY, z, subpalette[b0]); - } - } - } - } - } else { - // Single block palette - if (localBlockPalette.size() == 1) { - // Check it is not air block - int block = blockPalette.put(localBlockPalette.get(0)); - if (block != blockPalette.airId) { - // Set the entire section - for (int y = 0; y < SECTION_Y_MAX; y++) { - int blockY = sectionMinBlockY + y; - for (int z = 0; z < Z_MAX; z++) { - for(int x = 0; x < X_MAX; x++) { - chunkData.setBlockAt(x, blockY, z, block); - } - } - } - } - } - } - } else { - int yOffset = sectionY & 0xFF; - - Tag dataTag = section.get("Data"); - byte[] blockDataBytes = new byte[(Chunk.X_MAX * Chunk.Y_MAX * Chunk.Z_MAX) / 2]; - if (dataTag.isByteArray(SECTION_HALF_NIBBLES)) { - System.arraycopy(dataTag.byteArray(), 0, blockDataBytes, SECTION_HALF_NIBBLES * yOffset, - SECTION_HALF_NIBBLES); - } - - Tag blocksTag = section.get("Blocks"); - if (blocksTag.isByteArray(SECTION_BYTES)) { - byte[] blocksBytes = new byte[Chunk.X_MAX * Chunk.Y_MAX * Chunk.Z_MAX]; - System.arraycopy(blocksTag.byteArray(), 0, blocksBytes, SECTION_BYTES * yOffset, - SECTION_BYTES); - - int offset = SECTION_BYTES * yOffset; - for (int y = 0; y < SECTION_Y_MAX; y++) { - int blockY = sectionMinBlockY + y; - for (int z = 0; z < Z_MAX; z++) { - for (int x = 0; x < X_MAX; x++) { - chunkData.setBlockAt(x, blockY, z, blockPalette.put( - LegacyBlocks.getTag(offset, blocksBytes, blockDataBytes))); - offset += 1; - } - } - } - } - } - } - } - } - - /** - * Load heightmap information from a chunk heightmap array - * and insert into a quadtree. - */ - protected static void updateHeightmap(Heightmap heightmap, ChunkPosition pos, ChunkData chunkData, - int[] chunkHeightmap, BlockPalette palette, int yMax) { - for (int x = 0; x < 16; ++x) { - for (int z = 0; z < 16; ++z) { - int y = Math.max(chunkData.minY()+1, Math.min(chunkHeightmap[z * 16 + x] - 1, yMax)); - for (; y > chunkData.minY()+1; --y) { - Block block = palette.get(chunkData.getBlockAt(x, y, z)); - if (block != Air.INSTANCE && !block.isWater()) - break; - } - heightmap.set(y, pos.x * 16 + x, pos.z * 16 + z); - } - } - } - - protected boolean shouldReloadChunk() { - int timestamp = Integer.MAX_VALUE; - timestamp = Math.min(timestamp, surfaceTimestamp); - timestamp = Math.min(timestamp, biomesTimestamp); - if (timestamp == 0) { - return true; - } - Region region = dimension.getRegion(position.getRegionPosition()); - return region.chunkChangedSince(position, timestamp); - } - - protected void queueTopography() { - for (int x = -1; x <= 1; ++x) { - for (int z = -1; z <= 1; ++z) { - ChunkPosition pos = new ChunkPosition(position.x + x, position.z + z); - Chunk chunk = dimension.getChunk(pos); - if (!chunk.isEmpty()) { - dimension.chunkTopographyUpdated(chunk); - } - } - } - } + public abstract boolean loadChunk(@NotNull Mutable chunkData, int yMin, int yMax); public static int waterLevelAt(ChunkData chunkData, BlockPalette palette, int cx, int cy, int cz, int baseLevel) { @@ -445,97 +149,7 @@ public synchronized void renderTopography() { * @param maxY The requested maximum Y to be loaded into the chunkData object. The chunk implementation does NOT have to respect it * @throws ChunkLoadingException If there is an issue loading the chunk, and it should be aborted */ - public synchronized void getChunkData(@NotNull Mutable reuseChunkData, BlockPalette palette, BiomePalette biomePalette, int minY, int maxY) throws ChunkLoadingException { - Set request = new HashSet<>(); - request.add(DATAVERSION); - request.add(LEVEL_SECTIONS); - request.add(SECTIONS_POST_21W39A); - request.add(LEVEL_BIOMES); - request.add(BIOMES_POST_21W39A); - request.add(LEVEL_ENTITIES); - request.add(LEVEL_TILEENTITIES); - request.add(BLOCK_ENTITIES_POST_21W43A); - Map dataMap = getChunkTags(request); - // TODO: improve error handling here. - if (dataMap == null) { - throw new ChunkLoadingException(String.format("Got null data for chunk %s", this.position)); - } - Tag data = tagFromMap(dataMap); - - int dataVersion = data.get(DATAVERSION).intValue(); - - IntIntImmutablePair chunkBounds = inclusiveChunkBounds(data); - - if(reuseChunkData.get() == null || reuseChunkData.get() instanceof EmptyChunkData) { - reuseChunkData.set(dimension.createChunkData(reuseChunkData.get(), chunkBounds.leftInt(), chunkBounds.rightInt())); - } else { - reuseChunkData.get().clear(); - } - ChunkData chunkData = reuseChunkData.get(); //unwrap mutable, for ease of use - - version = chunkVersion(data); - Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); - Tag entitiesTag = data.get(LEVEL_ENTITIES); - Tag tileEntitiesTag = getTagFromNames(data, LEVEL_TILEENTITIES, BLOCK_ENTITIES_POST_21W43A); - - BiomeDataFactory.loadBiomeData(chunkData, data, biomePalette, minY, maxY); - if (sections.isList()) { - loadBlockData(data, chunkData, palette, minY, maxY); - - if (entitiesTag.isList()) { - for (SpecificTag tag : (ListTag) entitiesTag) { - if (tag.isCompoundTag()) - chunkData.addEntity((CompoundTag) tag); - } - } - - if (tileEntitiesTag.isList()) { - for (SpecificTag tag : (ListTag) tileEntitiesTag) { - if (tag.isCompoundTag()) - chunkData.addTileEntity((CompoundTag) tag); - } - } - } - - // post 20w45A entities - if (dataVersion >= DATAVERSION_20W45A) { - Set entitiesRequest = new HashSet<>(); - entitiesRequest.add(ENTITIES_POST_20W45A); - - Map entitiesMap = getEntityTags(entitiesRequest); - if (entitiesMap != null) { - entitiesTag = entitiesMap.get(".Entities"); - if (entitiesTag.isList()) { - for (SpecificTag tag : (ListTag) entitiesTag) { - if (tag.isCompoundTag()) - chunkData.addEntity((CompoundTag) tag); - } - } - } - } - } - - /** - * @return The min and max blockY for a given section array - */ - private IntIntImmutablePair inclusiveChunkBounds(Tag chunkData) { - Tag sections = getTagFromNames(chunkData, LEVEL_SECTIONS, SECTIONS_POST_21W39A); - int minSectionY = Integer.MAX_VALUE; - int maxSectionY = Integer.MIN_VALUE; - if (sections.isList()) { - for (SpecificTag section : sections.asList()) { - byte sectionY = (byte) section.get("Y").byteValue(); - if (sectionY < minSectionY) { - minSectionY = sectionY; - } - if (sectionY > maxSectionY) { - maxSectionY = sectionY; - } - } - } - - return new IntIntImmutablePair(minSectionY << 4, (maxSectionY << 4) + 15); - } + public abstract void getChunkData(@NotNull Mutable reuseChunkData, BlockPalette palette, BiomePalette biomePalette, int minY, int maxY) throws ChunkLoadingException; /** * @return Integer index into a chunk YXZ array @@ -551,10 +165,6 @@ public static int chunkXZIndex(int x, int z) { return x + Chunk.X_MAX * z; } - @Override public String toString() { - return "Chunk: " + position.toString(); - } - public String biomeAt(int blockX, int blockZ) { if (biomes instanceof BiomeLayer) { BiomeLayer biomeLayer = (BiomeLayer) biomes; @@ -565,9 +175,47 @@ public String biomeAt(int blockX, int blockZ) { } /** - * @return The version of this chunk. + * Load heightmap information from a chunk heightmap array + * and insert into a quadtree. */ - public ChunkVersion getVersion() { - return version; + protected static void updateHeightmap(Heightmap heightmap, ChunkPosition pos, ChunkData chunkData, + int[] chunkHeightmap, BlockPalette palette, int yMax) { + for (int x = 0; x < 16; ++x) { + for (int z = 0; z < 16; ++z) { + int y = Math.max(chunkData.minY()+1, Math.min(chunkHeightmap[z * 16 + x] - 1, yMax)); + for (; y > chunkData.minY()+1; --y) { + Block block = palette.get(chunkData.getBlockAt(x, y, z)); + if (block != Air.INSTANCE && !block.isWater()) + break; + } + heightmap.set(y, pos.x * 16 + x, pos.z * 16 + z); + } + } + } + + protected boolean shouldReloadChunk() { + int timestamp = Integer.MAX_VALUE; + timestamp = Math.min(timestamp, surfaceTimestamp); + timestamp = Math.min(timestamp, biomesTimestamp); + if (timestamp == 0) { + return true; + } + return dimension.chunkChangedSince(position, timestamp); + } + + protected void queueTopography() { + for (int x = -1; x <= 1; ++x) { + for (int z = -1; z <= 1; ++z) { + ChunkPosition pos = new ChunkPosition(position.x + x, position.z + z); + Chunk chunk = dimension.getChunk(pos); + if (!chunk.isEmpty()) { + dimension.chunkTopographyUpdated(chunk); + } + } + } + } + + @Override public String toString() { + return "Chunk: " + position.toString(); } } diff --git a/chunky/src/java/se/llbit/chunky/world/CubicDimension.java b/chunky/src/java/se/llbit/chunky/world/CubicDimension.java index 8f074c38c2..979b301e6d 100644 --- a/chunky/src/java/se/llbit/chunky/world/CubicDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/CubicDimension.java @@ -17,13 +17,13 @@ import static se.llbit.chunky.world.region.ImposterCubicRegion.blockToCube; import static se.llbit.chunky.world.region.ImposterCubicRegion.cubeToCubicRegion; -public class CubicDimension extends Dimension { +public class CubicDimension extends JavaDimension { /** * @param dimensionDirectory Minecraft world directory. * @param timestamp */ - protected CubicDimension(World world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + protected CubicDimension(JavaWorld world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { super(world, dimensionId, dimensionDirectory, playerEntities, timestamp); } @@ -54,7 +54,7 @@ public synchronized Region getRegionWithinRange(RegionPosition pos, int minY, in return regionMap.computeIfAbsent(pos.getLong(), p -> { // check if the region is present in the world directory Region region = EmptyRegion.instance; - if (regionExistsWithinRange(pos, minY, maxY)) { + if (this.regionExistsWithinRange(pos, minY, maxY)) { region = createRegion(pos); } return region; diff --git a/chunky/src/java/se/llbit/chunky/world/Dimension.java b/chunky/src/java/se/llbit/chunky/world/Dimension.java index 187f86caa2..509439e12c 100644 --- a/chunky/src/java/se/llbit/chunky/world/Dimension.java +++ b/chunky/src/java/se/llbit/chunky/world/Dimension.java @@ -1,7 +1,6 @@ package se.llbit.chunky.world; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntIntPair; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.chunk.ChunkData; import se.llbit.chunky.chunk.GenericChunkData; @@ -12,7 +11,7 @@ import se.llbit.chunky.world.listeners.ChunkDeletionListener; import se.llbit.chunky.world.listeners.ChunkTopographyListener; import se.llbit.chunky.world.listeners.ChunkUpdateListener; -import se.llbit.chunky.world.region.*; +import se.llbit.chunky.world.region.RegionChangeWatcher; import se.llbit.math.Vector3; import se.llbit.math.Vector3i; import se.llbit.util.annotation.Nullable; @@ -23,10 +22,8 @@ /** * */ -public class Dimension { - private final World world; - - protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); +public abstract class Dimension { + protected final World world; protected final File dimensionDirectory; private Set playerEntities; @@ -64,37 +61,12 @@ public int getDimensionId() { * Reload player data. * @return {@code true} if player data was reloaded. */ - public synchronized boolean reloadPlayerData() { - return this.world.reloadPlayerData(); - } - - /** Add a chunk deletion listener. */ - public void addChunkDeletionListener(ChunkDeletionListener listener) { - synchronized (chunkDeletionListeners) { - chunkDeletionListeners.add(listener); - } - } - - /** Add a region discovery listener. */ - public void addChunkUpdateListener(ChunkUpdateListener listener) { - synchronized (chunkUpdateListeners) { - chunkUpdateListeners.add(listener); - } - } - - private void fireChunkDeleted(ChunkPosition chunk) { - synchronized (chunkDeletionListeners) { - for (ChunkDeletionListener listener : chunkDeletionListeners) - listener.chunkDeleted(chunk); - } - } + public abstract boolean reloadPlayerData(); /** * @return The chunk at the given position */ - public synchronized Chunk getChunk(ChunkPosition pos) { - return getRegion(pos.getRegionPosition()).getChunk(pos); - } + public abstract Chunk getChunk(ChunkPosition pos); /** * Returns a ChunkData instance that is compatible with the given chunk version. @@ -114,56 +86,16 @@ public ChunkData createChunkData(@Nullable ChunkData chunkData, int minY, int ma return new GenericChunkData(); } - public Region createRegion(RegionPosition pos) { - return new MCRegion(pos, this); - } - - public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { - return new MCRegionChangeWatcher(worldMapLoader, mapView); - } - /** - * @param pos Region position - * @return The region at the given position + * WARNING: In some dimensions this could be from {@link Integer#MIN_VALUE} to {@link Integer#MAX_VALUE} + *

+ * Lower bound is inclusive, upper is exclusive + * + * @return The height range of the dimension. */ - public synchronized Region getRegion(RegionPosition pos) { - return regionMap.computeIfAbsent(pos.getLong(), p -> { - // check if the region is present in the world directory - Region region = EmptyRegion.instance; - if (regionExists(pos)) { - region = createRegion(pos); - } - return region; - }); - } - - public Region getRegionWithinRange(RegionPosition pos, int yMin, int yMax) { - return getRegion(pos); - } - - /** Set the region for the given position. */ - public synchronized void setRegion(RegionPosition pos, Region region) { - regionMap.put(pos.getLong(), region); - } + public abstract IntIntPair heightRange(); - /** - * @param pos region position - * @return {@code true} if a region file exists for the given position - */ - public boolean regionExists(RegionPosition pos) { - File regionFile = new File(getRegionDirectory(), pos.getMcaName()); - return regionFile.exists(); - } - - /** - * @param pos Position of the region to load - * @param minY Minimum block Y (inclusive) - * @param maxY Maximum block Y (exclusive) - * @return Whether the region exists - */ - public boolean regionExistsWithinRange(RegionPosition pos, int minY, int maxY) { - return this.regionExists(pos); - } + public abstract RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView); /** * Get the data directory for the given dimension. @@ -174,12 +106,6 @@ protected synchronized File getDimensionDirectory() { return dimensionDirectory; } - /** - * @return File object pointing to the region file directory - */ - public synchronized File getRegionDirectory() { - return new File(getDimensionDirectory(), "region"); - } /** * Get the current player position as an optional vector. @@ -202,43 +128,53 @@ public Heightmap getHeightmap() { return heightmap; } - /** Called when a new region has been discovered by the region parser. */ - public void regionDiscovered(RegionPosition pos) { - synchronized (this) { - regionMap.computeIfAbsent(pos.getLong(), p -> createRegion(pos)); - } + @Override public String toString() { + return dimensionDirectory.getName() ; } - /** Notify region update listeners. */ - private void fireChunkUpdated(ChunkPosition chunk) { - synchronized (chunkUpdateListeners) { - for (ChunkUpdateListener listener : chunkUpdateListeners) { - listener.chunkUpdated(chunk); - } + /** Add a chunk deletion listener. */ + public void addChunkDeletionListener(ChunkDeletionListener listener) { + synchronized (chunkDeletionListeners) { + chunkDeletionListeners.add(listener); } } - /** Notify region update listeners. */ - private void fireRegionUpdated(RegionPosition region) { - synchronized (chunkUpdateListeners) { - for (ChunkUpdateListener listener : chunkUpdateListeners) { - listener.regionUpdated(region); - } + /** + * Called when chunks have been deleted from this world. + * Triggers the chunk deletion listeners. + * + * @param pos Position of deleted chunk + */ + public void chunkDeleted(ChunkPosition pos) { + synchronized (chunkDeletionListeners) { + for (ChunkDeletionListener listener : chunkDeletionListeners) + listener.chunkDeleted(pos); } } - @Override public String toString() { - return dimensionDirectory.getName() ; + /** Add a region discovery listener. */ + public void addChunkUpdateListener(ChunkUpdateListener listener) { + synchronized (chunkUpdateListeners) { + chunkUpdateListeners.add(listener); + } } /** Called when a chunk has been updated. */ public void chunkUpdated(ChunkPosition chunk) { - fireChunkUpdated(chunk); + synchronized (chunkUpdateListeners) { + for (ChunkUpdateListener listener : chunkUpdateListeners) { + listener.chunkUpdated(chunk); + } + } } /** Called when a chunk has been updated. */ public void regionUpdated(RegionPosition region) { - fireRegionUpdated(region); + synchronized (chunkUpdateListeners) { + for (ChunkUpdateListener listener : chunkUpdateListeners) { + listener.regionUpdated(region); + } + } } /** Add a chunk discovery listener */ @@ -266,6 +202,8 @@ public void chunkTopographyUpdated(Chunk chunk) { } } + public abstract boolean regionExistsWithinRange(RegionPosition regionPos, int yMin, int yMax); + public Optional getSpawnPosition() { return Optional.ofNullable(this.spawnPos); } @@ -274,19 +212,9 @@ public void setSpawnPos(@Nullable Vector3i spawnPos) { this.spawnPos = spawnPos; } - /** - * Called when chunks have been deleted from this world. - * Triggers the chunk deletion listeners. - * - * @param pos Position of deleted chunk - */ - public void chunkDeleted(ChunkPosition pos) { - fireChunkDeleted(pos); - } + public abstract boolean chunkChangedSince(ChunkPosition chunkPosition, int timestamp); - public Date getLastModified() { - return new Date(this.dimensionDirectory.lastModified()); - } + public abstract Date getLastModified(); /** * Load entities from world the file. diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java index 3cda4f7ec6..77ce9e5589 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java @@ -1,6 +1,15 @@ package se.llbit.chunky.world; +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import se.llbit.chunky.map.MapView; +import se.llbit.chunky.map.WorldMapLoader; +import se.llbit.chunky.world.region.MCRegionChangeWatcher; +import se.llbit.chunky.world.region.RegionChangeWatcher; + +import java.time.Instant; import java.util.Collections; +import java.util.Date; public class EmptyDimension extends Dimension { public static final EmptyDimension INSTANCE = new EmptyDimension(); @@ -9,8 +18,42 @@ private EmptyDimension() { super(EmptyWorld.INSTANCE, 0, null, Collections.emptySet(), -1); } + @Override + public boolean reloadPlayerData() { + return false; + } + + @Override + public Chunk getChunk(ChunkPosition pos) { + return EmptyRegionChunk.INSTANCE; + } + + @Override + public IntIntPair heightRange() { + return new IntIntImmutablePair(0, 0); + } + + @Override + public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { + return new MCRegionChangeWatcher(worldMapLoader, mapView); + } + @Override public String toString() { return "[empty dimension]"; } + @Override + public boolean regionExistsWithinRange(RegionPosition regionPos, int yMin, int yMax) { + return false; + } + + @Override + public boolean chunkChangedSince(ChunkPosition chunkPosition, int timestamp) { + return false; + } + + @Override + public Date getLastModified() { + return Date.from(Instant.EPOCH); + } } \ No newline at end of file diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java index b584e4f4b3..d8a63fa2e4 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java @@ -16,6 +16,11 @@ */ package se.llbit.chunky.world; +import se.llbit.chunky.ui.ProgressTracker; + +import java.io.File; +import java.util.Collection; + /** * Represents an empty or non-existent world. * @@ -31,8 +36,16 @@ private EmptyWorld() { this.currentDimension = EmptyDimension.INSTANCE; } + @Override + public void loadDimension(int dimensionId) { } + + @Override + public void exportChunksToZip(File target, Collection chunks, ProgressTracker progress) { } + + @Override + public void exportWorldToZip(File target, ProgressTracker progress) { } + @Override public String toString() { return "[empty world]"; } - } diff --git a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java index 1ea4ed8460..622f77e350 100644 --- a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java @@ -21,6 +21,9 @@ import java.util.Map; import java.util.Set; +import static se.llbit.chunky.world.JavaChunk.SECTION_BYTES; +import static se.llbit.chunky.world.JavaChunk.SECTION_HALF_NIBBLES; + /** * An implementation of a cube wrapper for pre flattening cubic chunks (1.10, 1.11, 1.12) * @@ -57,9 +60,9 @@ public synchronized boolean loadChunk(@NotNull Mutable mutableChunkDa } Set request = new HashSet<>(); - request.add(Chunk.DATAVERSION); - request.add(Chunk.LEVEL_SECTIONS); - request.add(Chunk.LEVEL_BIOMES); + request.add(JavaChunk.DATAVERSION); + request.add(JavaChunk.LEVEL_SECTIONS); + request.add(JavaChunk.LEVEL_BIOMES); Map> data = getCubeTags(request); // TODO: improve error handling here. if (data == null) { @@ -98,7 +101,7 @@ private void loadSurfaceCubic(Map> data, ChunkData chu Integer yPos = entry.getKey(); Map cubeData = entry.getValue(); - Tag sections = cubeData.get(LEVEL_SECTIONS); + Tag sections = cubeData.get(JavaChunk.LEVEL_SECTIONS); if (sections.isList()) { // extractBiomeData(cubeData.get(LEVEL_BIOMES), chunkData); if (version == ChunkVersion.PRE_FLATTENING || version == ChunkVersion.POST_FLATTENING) { @@ -124,7 +127,7 @@ private int[] extractHeightmapDataCubic(Map cubeData, ChunkData chu private static final int CUBE_DIAMETER_IN_BLOCKS = 16; private void loadBlockDataCubic(int cubeY, Map cubeData, ChunkData chunkData, BlockPalette blockPalette, int yMin, int yMax) { - Tag sections = cubeData.get(LEVEL_SECTIONS); + Tag sections = cubeData.get(JavaChunk.LEVEL_SECTIONS); if(sections.isList()) { ListTag sectionTags = sections.asList(); if(sectionTags.size() == 1) { @@ -163,11 +166,11 @@ private void loadBlockDataCubic(int cubeY, Map cubeData, ChunkData @Override public synchronized void getChunkData(@NotNull Mutable reuseChunkData, BlockPalette palette, BiomePalette biomePalette, int minY, int maxY) { Set request = new HashSet<>(); - request.add(DATAVERSION); - request.add(LEVEL_SECTIONS); - request.add(LEVEL_BIOMES); - request.add(LEVEL_ENTITIES); - request.add(LEVEL_TILEENTITIES); + request.add(JavaChunk.DATAVERSION); + request.add(JavaChunk.LEVEL_SECTIONS); + request.add(JavaChunk.LEVEL_BIOMES); + request.add(JavaChunk.LEVEL_ENTITIES); + request.add(JavaChunk.LEVEL_TILEENTITIES); Map> data = getCubeTags(request); if(reuseChunkData.get() == null || reuseChunkData.get() instanceof EmptyChunkData) { reuseChunkData.set(dimension.createChunkData(reuseChunkData.get(), Integer.MIN_VALUE, Integer.MAX_VALUE)); @@ -184,10 +187,10 @@ public synchronized void getChunkData(@NotNull Mutable reuseChunkData Integer yPos = entry.getKey(); Map cubeData = entry.getValue(); - Tag sections = cubeData.get(LEVEL_SECTIONS); - Tag biomesTag = cubeData.get(LEVEL_BIOMES); - Tag entitiesTag = cubeData.get(LEVEL_ENTITIES); - Tag tileEntitiesTag = cubeData.get(LEVEL_TILEENTITIES); + Tag sections = cubeData.get(JavaChunk.LEVEL_SECTIONS); + Tag biomesTag = cubeData.get(JavaChunk.LEVEL_BIOMES); + Tag entitiesTag = cubeData.get(JavaChunk.LEVEL_ENTITIES); + Tag tileEntitiesTag = cubeData.get(JavaChunk.LEVEL_TILEENTITIES); // TODO: add biomes support once we have 3d biomes support // if (biomesTag.isByteArray(X_MAX * Z_MAX) || biomesTag.isIntArray(X_MAX * Z_MAX)) { // extractBiomeData(biomesTag, reuseChunkData); diff --git a/chunky/src/java/se/llbit/chunky/world/JavaChunk.java b/chunky/src/java/se/llbit/chunky/world/JavaChunk.java new file mode 100644 index 0000000000..5c7e00b346 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/JavaChunk.java @@ -0,0 +1,377 @@ +package se.llbit.chunky.world; + +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import se.llbit.chunky.block.legacy.LegacyBlocks; +import se.llbit.chunky.chunk.BlockPalette; +import se.llbit.chunky.chunk.ChunkData; +import se.llbit.chunky.chunk.ChunkLoadingException; +import se.llbit.chunky.chunk.EmptyChunkData; +import se.llbit.chunky.chunk.biome.BiomeDataFactory; +import se.llbit.chunky.map.BiomeLayer; +import se.llbit.chunky.map.IconLayer; +import se.llbit.chunky.map.SurfaceLayer; +import se.llbit.chunky.world.biome.ArrayBiomePalette; +import se.llbit.chunky.world.biome.BiomePalette; +import se.llbit.chunky.world.region.MCRegion; +import se.llbit.log.Log; +import se.llbit.math.QuickMath; +import se.llbit.nbt.CompoundTag; +import se.llbit.nbt.ListTag; +import se.llbit.nbt.SpecificTag; +import se.llbit.nbt.Tag; +import se.llbit.util.BitBuffer; +import se.llbit.util.Mutable; +import se.llbit.util.annotation.NotNull; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static se.llbit.util.NbtUtil.getTagFromNames; +import static se.llbit.util.NbtUtil.tagFromMap; + +public class JavaChunk extends Chunk { + public static final String DATAVERSION = ".DataVersion"; + public static final String LEVEL_HEIGHTMAP = ".Level.HeightMap"; + public static final String LEVEL_SECTIONS = ".Level.Sections"; + public static final String LEVEL_BIOMES = ".Level.Biomes"; + public static final String LEVEL_ENTITIES = ".Level.Entities"; + public static final String ENTITIES_POST_20W45A = ".Entities"; + public static final String LEVEL_TILEENTITIES = ".Level.TileEntities"; + public static final String BLOCK_ENTITIES_POST_21W43A = ".block_entities"; + + public static final String SECTIONS_POST_21W39A = ".sections"; + public static final String BIOMES_POST_21W39A = ".biomes"; + + public static final int SECTION_BYTES = X_MAX * SECTION_Y_MAX * Z_MAX; + public static final int SECTION_HALF_NIBBLES = SECTION_BYTES / 2; + private static final int CHUNK_BYTES = X_MAX * Y_MAX * Z_MAX; + + public static final int DATAVERSION_20W17A = 2529; + public static final int DATAVERSION_20W45A = 2681; // entities moved into separate region files + + public JavaChunk(ChunkPosition pos, JavaDimension dimension) { + super(pos, dimension); + } + + /** + * @param request fresh request set + * @return loaded data, or null if something went wrong + */ + private Map getChunkTags(Set request) throws ChunkLoadingException { + MCRegion region = (MCRegion) ((JavaDimension) dimension).getRegion(position.getRegionPosition()); + Mutable timestamp = new Mutable<>(dataTimestamp); + Map chunkTags = region.getChunkTags(this.position, request, timestamp); + this.dataTimestamp = timestamp.get(); + return chunkTags; + } + + /** + * @param request fresh request set + * @return loaded data, or null if something went wrong + */ + private Map getEntityTags(Set request) throws ChunkLoadingException { + MCRegion region = (MCRegion) ((JavaDimension) dimension).getRegion(position.getRegionPosition()); + return region.getEntityTags(this.position, request); + } + + public synchronized boolean loadChunk(@NotNull Mutable chunkData, int yMin, int yMax) { + if (!shouldReloadChunk()) { + return false; + } + + Set request = new HashSet<>(); + request.add(JavaChunk.DATAVERSION); + request.add(JavaChunk.LEVEL_SECTIONS); + request.add(JavaChunk.SECTIONS_POST_21W39A); + request.add(JavaChunk.LEVEL_BIOMES); + request.add(JavaChunk.BIOMES_POST_21W39A); + request.add(JavaChunk.LEVEL_HEIGHTMAP); + + Map dataMap; + try { + dataMap = getChunkTags(request); + } catch (ChunkLoadingException e) { // we don't want to crash the map view if a chunk fails to load, so we warn the user + Log.warn(String.format("Failed to load chunk %s", position), e); + return false; + } + // TODO: improve error handling here. + if (dataMap == null) { + return false; + } + Tag data = tagFromMap(dataMap); + + surfaceTimestamp = dataTimestamp; + version = chunkVersion(data); + IntIntImmutablePair chunkBounds = inclusiveChunkBounds(data); + chunkData.set(this.dimension.createChunkData(chunkData.get(), chunkBounds.leftInt(), chunkBounds.rightInt())); + loadSurface(data, chunkData.get(), yMin, yMax); + biomesTimestamp = dataTimestamp; + + dimension.chunkUpdated(position); + return true; + } + + private void loadSurface(@NotNull Tag data, ChunkData chunkData, int yMin, int yMax) { + if (data == null) { + surface = IconLayer.CORRUPT; + return; + } + + Heightmap heightmap = dimension.getHeightmap(); + Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); + if (sections.isList()) { + if (version == ChunkVersion.PRE_FLATTENING || version == ChunkVersion.POST_FLATTENING) { + BiomePalette biomePalette = new ArrayBiomePalette(); + BiomeDataFactory.loadBiomeData(chunkData, data, biomePalette, yMin, yMax); + biomes = new BiomeLayer(chunkData, biomePalette); + + BlockPalette palette = new BlockPalette(); + palette.unsynchronize(); //only this RegionParser will use this palette + loadBlockData(data, chunkData, palette, yMin, yMax); + + int[] heightmapData = extractHeightmapData(data, chunkData); + updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); + + surface = new SurfaceLayer(dimension.getDimensionId(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + queueTopography(); + } + } else { + surface = IconLayer.CORRUPT; + } + } + + private int[] extractHeightmapData(@NotNull Tag data, ChunkData chunkData) { + Tag heightmapTag = data.get(LEVEL_HEIGHTMAP); + if (heightmapTag.isIntArray(X_MAX * Z_MAX)) { + return heightmapTag.intArray(); + } else { + int[] fallback = new int[X_MAX * Z_MAX]; + for (int i = 0; i < fallback.length; ++i) { + fallback[i] = chunkData.maxY(); + } + return fallback; + } + } + + /** Detect Minecraft version that generated the chunk. */ + private static ChunkVersion chunkVersion(@NotNull Tag data) { + Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); + if (sections.isList()) { + for (SpecificTag section : sections.asList()) { + if (!section.get("Palette").isList()) { + if (section.get("Blocks").isByteArray(SECTION_BYTES)) { + return ChunkVersion.PRE_FLATTENING; + } + } + } + return ChunkVersion.POST_FLATTENING; + } + return ChunkVersion.UNKNOWN; + } + + private static void loadBlockData(@NotNull Tag data, @NotNull ChunkData chunkData, + BlockPalette blockPalette, int minY, int maxY) { + + Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); + if (sections.isList()) { + for (SpecificTag section : sections.asList()) { + Tag yTag = section.get("Y"); + int sectionY = yTag.byteValue(); + int sectionMinBlockY = sectionY << 4; + + if(sectionY < minY >> 4 || sectionY-1 > (maxY >> 4)+1) + continue; //skip parsing sections that are outside requested bounds + + Tag blockPaletteTag = getTagFromNames(section, "Palette", "block_states\\palette"); + if (blockPaletteTag.isList()) { + ListTag localBlockPalette = blockPaletteTag.asList(); + // Bits per block: + int bpb = 4; + if (localBlockPalette.size() > 16) { + bpb = QuickMath.log2(QuickMath.nextPow2(localBlockPalette.size())); + } + + int dataSize = (4096 * bpb) / 64; + Tag blockStates = getTagFromNames(section, "BlockStates", "block_states\\data"); + + if (blockStates.isLongArray(dataSize)) { + // since 20w17a, block states are aligned to 64-bit boundaries, so there are 64 % bpb + // unused bits per block state; if so, the array is longer than the expected data size + boolean isAligned = data.get(DATAVERSION).intValue() >= DATAVERSION_20W17A; + if (isAligned) { + // entries are 64-bit-padded, re-calculate the bits per block + // this is the dataSize calculation from above reverted, we know the actual data size + bpb = blockStates.longArray().length / 64; + } + + int[] subpalette = new int[localBlockPalette.size()]; + int paletteIndex = 0; + for (Tag item : localBlockPalette.asList()) { + subpalette[paletteIndex] = blockPalette.put(item); + paletteIndex += 1; + } + BitBuffer buffer = new BitBuffer(blockStates.longArray(), bpb, isAligned); + for (int y = 0; y < SECTION_Y_MAX; y++) { + int blockY = sectionMinBlockY + y; + for (int z = 0; z < Z_MAX; z++) { + for(int x = 0; x < X_MAX; x++) { + int b0 = buffer.read(); + if (b0 < subpalette.length) { + chunkData.setBlockAt(x, blockY, z, subpalette[b0]); + } + } + } + } + } else { + // Single block palette + if (localBlockPalette.size() == 1) { + // Check it is not air block + int block = blockPalette.put(localBlockPalette.get(0)); + if (block != blockPalette.airId) { + // Set the entire section + for (int y = 0; y < SECTION_Y_MAX; y++) { + int blockY = sectionMinBlockY + y; + for (int z = 0; z < Z_MAX; z++) { + for(int x = 0; x < X_MAX; x++) { + chunkData.setBlockAt(x, blockY, z, block); + } + } + } + } + } + } + } else { + int yOffset = sectionY & 0xFF; + + Tag dataTag = section.get("Data"); + byte[] blockDataBytes = new byte[(Chunk.X_MAX * Chunk.Y_MAX * Chunk.Z_MAX) / 2]; + if (dataTag.isByteArray(SECTION_HALF_NIBBLES)) { + System.arraycopy(dataTag.byteArray(), 0, blockDataBytes, SECTION_HALF_NIBBLES * yOffset, + SECTION_HALF_NIBBLES); + } + + Tag blocksTag = section.get("Blocks"); + if (blocksTag.isByteArray(SECTION_BYTES)) { + byte[] blocksBytes = new byte[Chunk.X_MAX * Chunk.Y_MAX * Chunk.Z_MAX]; + System.arraycopy(blocksTag.byteArray(), 0, blocksBytes, SECTION_BYTES * yOffset, + SECTION_BYTES); + + int offset = SECTION_BYTES * yOffset; + for (int y = 0; y < SECTION_Y_MAX; y++) { + int blockY = sectionMinBlockY + y; + for (int z = 0; z < Z_MAX; z++) { + for (int x = 0; x < X_MAX; x++) { + chunkData.setBlockAt(x, blockY, z, blockPalette.put( + LegacyBlocks.getTag(offset, blocksBytes, blockDataBytes))); + offset += 1; + } + } + } + } + } + } + } + } + + public synchronized void getChunkData(@NotNull Mutable reuseChunkData, BlockPalette palette, BiomePalette biomePalette, int minY, int maxY) throws ChunkLoadingException { + Set request = new HashSet<>(); + request.add(DATAVERSION); + request.add(LEVEL_SECTIONS); + request.add(SECTIONS_POST_21W39A); + request.add(LEVEL_BIOMES); + request.add(BIOMES_POST_21W39A); + request.add(LEVEL_ENTITIES); + request.add(LEVEL_TILEENTITIES); + request.add(BLOCK_ENTITIES_POST_21W43A); + Map dataMap = getChunkTags(request); + // TODO: improve error handling here. + if (dataMap == null) { + throw new ChunkLoadingException(String.format("Got null data for chunk %s", this.position)); + } + Tag data = tagFromMap(dataMap); + + int dataVersion = data.get(DATAVERSION).intValue(); + + IntIntImmutablePair chunkBounds = inclusiveChunkBounds(data); + + if(reuseChunkData.get() == null || reuseChunkData.get() instanceof EmptyChunkData) { + reuseChunkData.set(dimension.createChunkData(reuseChunkData.get(), chunkBounds.leftInt(), chunkBounds.rightInt())); + } else { + reuseChunkData.get().clear(); + } + ChunkData chunkData = reuseChunkData.get(); //unwrap mutable, for ease of use + + version = chunkVersion(data); + Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); + Tag entitiesTag = data.get(LEVEL_ENTITIES); + Tag tileEntitiesTag = getTagFromNames(data, LEVEL_TILEENTITIES, BLOCK_ENTITIES_POST_21W43A); + + BiomeDataFactory.loadBiomeData(chunkData, data, biomePalette, minY, maxY); + if (sections.isList()) { + loadBlockData(data, chunkData, palette, minY, maxY); + + if (entitiesTag.isList()) { + for (SpecificTag tag : (ListTag) entitiesTag) { + if (tag.isCompoundTag()) + chunkData.addEntity((CompoundTag) tag); + } + } + + if (tileEntitiesTag.isList()) { + for (SpecificTag tag : (ListTag) tileEntitiesTag) { + if (tag.isCompoundTag()) + chunkData.addTileEntity((CompoundTag) tag); + } + } + } + + // post 20w45A entities + if (dataVersion >= DATAVERSION_20W45A) { + Set entitiesRequest = new HashSet<>(); + entitiesRequest.add(ENTITIES_POST_20W45A); + + Map entitiesMap = getEntityTags(entitiesRequest); + if (entitiesMap != null) { + entitiesTag = entitiesMap.get(".Entities"); + if (entitiesTag.isList()) { + for (SpecificTag tag : (ListTag) entitiesTag) { + if (tag.isCompoundTag()) + chunkData.addEntity((CompoundTag) tag); + } + } + } + } + } + + + /** + * @return The min and max blockY for a given section array + */ + private IntIntImmutablePair inclusiveChunkBounds(Tag chunkData) { + Tag sections = getTagFromNames(chunkData, LEVEL_SECTIONS, SECTIONS_POST_21W39A); + int minSectionY = Integer.MAX_VALUE; + int maxSectionY = Integer.MIN_VALUE; + if (sections.isList()) { + for (SpecificTag section : sections.asList()) { + byte sectionY = (byte) section.get("Y").byteValue(); + if (sectionY < minSectionY) { + minSectionY = sectionY; + } + if (sectionY > maxSectionY) { + maxSectionY = sectionY; + } + } + } + + return new IntIntImmutablePair(minSectionY << 4, (maxSectionY << 4) + 15); + } + + + /** + * @return The version of this chunk. + */ + public ChunkVersion getVersion() { + return version; + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java new file mode 100644 index 0000000000..281962e942 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java @@ -0,0 +1,122 @@ +package se.llbit.chunky.world; + +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import se.llbit.chunky.map.MapView; +import se.llbit.chunky.map.WorldMapLoader; +import se.llbit.chunky.world.region.*; + +import java.io.File; +import java.util.*; + +public class JavaDimension extends Dimension { + protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); + + /** + * @param dimensionDirectory Minecraft world directory. + * @param timestamp + */ + protected JavaDimension(JavaWorld world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + super(world, dimensionId, dimensionDirectory, playerEntities, timestamp); + } + + /** + * Reload player data. + * @return {@code true} if player data was reloaded. + */ + public synchronized boolean reloadPlayerData() { + return ((JavaWorld) this.world).reloadPlayerData(); + } + + /** + * @return The chunk at the given position + */ + public synchronized Chunk getChunk(ChunkPosition pos) { + return getRegion(pos.getRegionPosition()).getChunk(pos); + } + + @Override + public IntIntPair heightRange() { + return ((JavaWorld) this.world).versionId >= JavaWorld.VERSION_21W06A ? + new IntIntImmutablePair(-64, 320) : + new IntIntImmutablePair(0, 256); + } + + + public Region createRegion(RegionPosition pos) { + return new MCRegion(pos, this); + } + + public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { + return new MCRegionChangeWatcher(worldMapLoader, mapView); + } + + @Override + public boolean chunkChangedSince(ChunkPosition chunkPosition, int timestamp) { + Region region = regionMap.get(chunkPosition.getRegionPosition().getLong()); + return region.chunkChangedSince(chunkPosition, timestamp); + } + + /** Called when a new region has been discovered by the region parser. */ + public void regionDiscovered(RegionPosition pos) { + synchronized (this) { + regionMap.computeIfAbsent(pos.getLong(), p -> createRegion(pos)); + } + } + + /** + * @param pos Region position + * @return The region at the given position + */ + public synchronized Region getRegion(RegionPosition pos) { + return regionMap.computeIfAbsent(pos.getLong(), p -> { + // check if the region is present in the world directory + Region region = EmptyRegion.instance; + if (regionExists(pos)) { + region = createRegion(pos); + } + return region; + }); + } + + public Region getRegionWithinRange(RegionPosition pos, int yMin, int yMax) { + return getRegion(pos); + } + + /** Set the region for the given position. */ + public synchronized void setRegion(RegionPosition pos, Region region) { + regionMap.put(pos.getLong(), region); + } + + /** + * @param pos region position + * @return {@code true} if a region file exists for the given position + */ + public boolean regionExists(RegionPosition pos) { + File regionFile = new File(getRegionDirectory(), pos.getMcaName()); + return regionFile.exists(); + } + + /** + * @param pos Position of the region to load + * @param minY Minimum block Y (inclusive) + * @param maxY Maximum block Y (exclusive) + * @return Whether the region exists + */ + public boolean regionExistsWithinRange(RegionPosition pos, int minY, int maxY) { + return this.regionExists(pos); + } + + /** + * @return File object pointing to the region file directory + */ + public synchronized File getRegionDirectory() { + return new File(getDimensionDirectory(), "region"); + } + + public Date getLastModified() { + return new Date(this.dimensionDirectory.lastModified()); + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java new file mode 100644 index 0000000000..8c9f793af9 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java @@ -0,0 +1,374 @@ +/* Copyright (c) 2010-2015 Jesper Öqvist + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.world; + +import se.llbit.chunky.ui.ProgressTracker; +import se.llbit.chunky.world.region.MCRegion; +import se.llbit.log.Log; +import se.llbit.math.Vector3i; +import se.llbit.nbt.NamedTag; +import se.llbit.nbt.Tag; +import se.llbit.util.MinecraftText; +import se.llbit.util.Pair; +import se.llbit.util.annotation.NotNull; + +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * The World class contains information about the currently viewed world. + * It has a map of all chunks in the world and is responsible for parsing + * chunks when needed. All rendering is done through the WorldRenderer class. + * + * @author Jesper Öqvist + */ +public class JavaWorld extends World{ + + /** The currently supported NBT version of level.dat files. */ + public static final int NBT_VERSION = 19133; + + /** Minimum level.dat data version of tall worlds (21w06a). */ + public static final int VERSION_21W06A = 2694; + public static final int VERSION_1_12_2 = 1343; + + int versionId; + + /** + * @param levelName name of the world (not the world directory). + * @param worldDirectory Minecraft world directory. + * @param seed + * @param timestamp + */ + protected JavaWorld(String levelName, File worldDirectory, long seed, long timestamp) { + super(levelName, worldDirectory, seed, timestamp); + } + + public void loadDimension(int dimensionId) { + currentDimension = loadDimension(this, this.worldDirectory, dimensionId, -1, Collections.emptySet()); + currentDimension.reloadPlayerData(); + } + + /** + * Parse player location and level name. + * + * @return {@code true} if the world data was loaded + */ + public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnings warnings) { + String levelName = worldDirectory.getName(); // Default level name. + File worldFile = new File(worldDirectory, "level.dat"); + long modtime = worldFile.lastModified(); + try (FileInputStream fin = new FileInputStream(worldFile); + InputStream gzin = new GZIPInputStream(fin); + DataInputStream in = new DataInputStream(gzin)) { + Set request = new HashSet<>(); + request.add(".Data.version"); + request.add(".Data.Version.Id"); + request.add(".Data.RandomSeed"); + request.add(".Data.Player"); + request.add(".Data.LevelName"); + request.add(".Data.GameType"); + request.add(".Data.isCubicWorld"); + Map result = NamedTag.quickParse(in, request); + + Tag version = result.get(".Data.version"); + if (warnings == LoggedWarnings.NORMAL && version.intValue() != NBT_VERSION) { + Log.warnf("The world format for the world %s is not supported by Chunky.\n" + "Will attempt to load the world anyway.", + levelName); + } + Tag versionId = result.get(".Data.Version.Id"); + Tag player = result.get(".Data.Player"); + Tag spawnX = player.get("SpawnX"); + Tag spawnY = player.get("SpawnY"); + Tag spawnZ = player.get("SpawnZ"); + Tag gameType = result.get(".Data.GameType"); + Tag randomSeed = result.get(".Data.RandomSeed"); + levelName = MinecraftText.removeFormatChars(result.get(".Data.LevelName").stringValue(levelName)); + + long seed = randomSeed.longValue(0); + + Set playerEntities = getPlayerEntityData(worldDirectory, dimensionId, player); + + JavaWorld world = new JavaWorld(levelName, worldDirectory, seed, modtime); + world.gameMode = gameType.intValue(0); + world.versionId = versionId.intValue(); + + Dimension dimension = loadDimension(world, worldDirectory, dimensionId, modtime, playerEntities); + + boolean haveSpawnPos = !(spawnX.isError() || spawnY.isError() || spawnZ.isError()); + if (haveSpawnPos) { + dimension.setSpawnPos(new Vector3i(spawnX.intValue(0), spawnY.intValue(0), spawnZ.intValue(0))); + } + + world.currentDimension = dimension; + + return world; + } catch (FileNotFoundException e) { + if (warnings == LoggedWarnings.NORMAL) { + Log.infof("Could not find level.dat file for world %s!", levelName); + } + } catch (IOException e) { + if (warnings == LoggedWarnings.NORMAL) { + Log.infof("Could not read the level.dat file for world %s!", levelName); + } + } + return EmptyWorld.INSTANCE; + } + + @NotNull + protected static Dimension loadDimension(JavaWorld world, File worldDirectory, int dimensionId, long modtime, Set playerEntities) { + Dimension dimension; + File dimensionDirectory = dimensionId == 0 ? worldDirectory : new File(worldDirectory, "DIM" + dimensionId); + if (new File(dimensionDirectory, "region3d").exists()) { + dimension = new CubicDimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); + } else { + dimension = new JavaDimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); + } + return dimension; + } + + @NotNull + static Set getPlayerEntityData(File worldDirectory, int dimensionId, Tag player) { + Set playerEntities = new HashSet<>(); + if (!player.isError()) { + playerEntities.add(new PlayerEntityData(player)); + } + loadAdditionalPlayers(worldDirectory, playerEntities); + // Filter for the players only within the requested dimension + playerEntities = playerEntities.stream().filter(playerData -> playerData.dimension == dimensionId).collect(Collectors.toSet()); + return playerEntities; + } + + /** + * Reload player data for the current dimension. This method is not in Dimension because players are per-world, not per-dimension + * @return {@code true} if player data was reloaded. + */ + synchronized boolean reloadPlayerData() { + if (worldDirectory == null) { + return false; + } + File worldFile = new File(worldDirectory, "level.dat"); + long lastModified = worldFile.lastModified(); + if (lastModified == timestamp) { + return false; + } + Log.infof("world %s: timestamp updated: reading player data", levelName); + timestamp = lastModified; + + try (FileInputStream fin = new FileInputStream(worldFile); + InputStream gzin = new GZIPInputStream(fin); + DataInputStream in = new DataInputStream(gzin)) { + Set request = new HashSet<>(); + request.add(".Data.Player"); + Map result = NamedTag.quickParse(in, request); + Tag player = result.get(".Data.Player"); + + currentDimension.setPlayerEntities(getPlayerEntityData(worldDirectory, currentDimensionId, player)); + } catch (IOException e) { + Log.infof("Could not read the level.dat file for world %s while trying to reload player data!", levelName); + return false; + } + return true; + } + + private static void loadAdditionalPlayers(File worldDirectory, Set playerEntities) { + loadPlayerData(new File(worldDirectory, "players"), playerEntities); + loadPlayerData(new File(worldDirectory, "playerdata"), playerEntities); + } + + private static void loadPlayerData(File playerdata, Set playerEntities) { + if (playerdata.isDirectory()) { + File[] players = playerdata.listFiles(); + if (players != null) { + for (File player : players) { + try (DataInputStream in = new DataInputStream( + new GZIPInputStream(new FileInputStream(player)))) { + playerEntities.add(new PlayerEntityData(NamedTag.read(in).unpack())); + } catch (IOException e) { + Log.infof("Could not read player data file '%s'", player.getAbsolutePath()); + } + } + } + } + } + + @Override + public synchronized JavaDimension currentDimension() { + return (JavaDimension) this.currentDimension; + } + + /** + * @deprecated Use {@link JavaWorld#currentDimension()} -> {@link Dimension#getDimensionDirectory()} ()}. Removed once there are no more usages + */ + @Deprecated + protected synchronized File getDataDirectory(int dimension) { + return dimension == 0 ? + worldDirectory : + new File(worldDirectory, "DIM" + dimension); + } + + /** + @deprecated Use {@link JavaWorld#currentDimension()} -> {@link JavaDimension#getRegionDirectory()}. Removed once there are no more usages + */ + @Deprecated + protected synchronized File getRegionDirectory(int dimension) { + return new File(getDataDirectory(dimension), "region"); + } + + + /** + * Export the given chunks to a Zip archive. + * The Zip arhive is written without compression since the chunks are + * already compressed with GZip. + * + * @throws IOException + */ + public synchronized void exportChunksToZip(File target, Collection chunks, + ProgressTracker progress) throws IOException { + + Map> regionMap = new HashMap<>(); + + for (ChunkPosition chunk : chunks) { + RegionPosition regionPosition = chunk.getRegionPosition(); + Set chunkSet = regionMap.computeIfAbsent(regionPosition, k -> new HashSet<>()); + chunkSet.add(new ChunkPosition(chunk.x & 31, chunk.z & 31)); + } + + int work = 0; + progress.setJobSize(regionMap.size() + 1); + + String regionDirectory = + currentDimensionId == 0 ? currentDimension().getDimensionDirectory().getName() : + currentDimension().getDimensionDirectory().getName() + "/DIM" + currentDimensionId; + regionDirectory += "/region"; + + try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(target))) { + writeLevelDatToZip(zout); + progress.setProgress(++work); + + for (Map.Entry> entry : regionMap.entrySet()) { + + if (progress.isInterrupted()) + break; + + RegionPosition region = entry.getKey(); + + appendRegionToZip(zout, currentDimension().getRegionDirectory(), region, + regionDirectory + "/" + region.getMcaName(), entry.getValue()); + + progress.setProgress(++work); + } + } + } + + /** + * Export the world to a zip file. The chunks which are included + * depends on the selected chunks. If any chunks are selected, then + * only those chunks are exported. If no chunks are selected then all + * chunks are exported. + * + * @throws IOException + */ + public synchronized void exportWorldToZip(File target, ProgressTracker progress) + throws IOException { + System.out.println("exporting all dimensions to " + target.getName()); + + final Collection> regions = new LinkedList<>(); + + WorldScanner.Operator operator = (regionDirectory, x, z) -> + regions.add(new Pair<>(regionDirectory, new RegionPosition(x, z))); + // TODO make this more dynamic + File overworld = getRegionDirectory(OVERWORLD_DIMENSION); + WorldScanner.findExistingChunks(overworld, operator); + WorldScanner.findExistingChunks(getRegionDirectory(NETHER_DIMENSION), operator); + WorldScanner.findExistingChunks(getRegionDirectory(END_DIMENSION), operator); + + int work = 0; + progress.setJobSize(regions.size() + 1); + + try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(target))) { + writeLevelDatToZip(zout); + progress.setProgress(++work); + + for (Pair region : regions) { + + if (progress.isInterrupted()) { + break; + } + + String regionDirectory = (region.thing1 == overworld) ? + worldDirectory.getName() : + worldDirectory.getName() + "/" + region.thing1.getParentFile().getName(); + regionDirectory += "/region"; + appendRegionToZip(zout, region.thing1, region.thing2, + regionDirectory + "/" + region.thing2.getMcaName(), null); + + progress.setProgress(++work); + } + } + } + + /** + * Write this worlds level.dat file to a ZipOutputStream. + * + * @throws IOException + */ + private void writeLevelDatToZip(ZipOutputStream zout) throws IOException { + File levelDat = new File(worldDirectory, "level.dat"); + try (FileInputStream in = new FileInputStream(levelDat)) { + zout.putNextEntry(new ZipEntry(worldDirectory.getName() + "/" + "level.dat")); + byte[] buf = new byte[4096]; + int len; + while ((len = in.read(buf)) > 0) { + zout.write(buf, 0, len); + } + zout.closeEntry(); + } + } + + private void appendRegionToZip(ZipOutputStream zout, File regionDirectory, + RegionPosition regionPos, String regionZipFileName, Set chunks) + throws IOException { + + zout.putNextEntry(new ZipEntry(regionZipFileName)); + MCRegion.writeRegion(regionDirectory, regionPos, new DataOutputStream(zout), chunks); + zout.closeEntry(); + } + + public int getVersionId() { + return versionId; + } + + /** + * @return true if the given directory exists and + * contains a level.dat file + */ + public static boolean isWorldDir(File worldDir) { + if (worldDir != null && worldDir.isDirectory()) { + File levelDat = new File(worldDir, "level.dat"); + return levelDat.exists() && levelDat.isFile(); + } + return false; + } + + public Date getLastModified() { + return new Date(this.worldDirectory.lastModified()); + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/World.java b/chunky/src/java/se/llbit/chunky/world/World.java index ac3a2967ca..f7609e0ff2 100644 --- a/chunky/src/java/se/llbit/chunky/world/World.java +++ b/chunky/src/java/se/llbit/chunky/world/World.java @@ -17,21 +17,9 @@ package se.llbit.chunky.world; import se.llbit.chunky.ui.ProgressTracker; -import se.llbit.chunky.world.region.MCRegion; -import se.llbit.log.Log; -import se.llbit.math.Vector3i; -import se.llbit.nbt.NamedTag; -import se.llbit.nbt.Tag; -import se.llbit.util.MinecraftText; -import se.llbit.util.Pair; -import se.llbit.util.annotation.NotNull; import java.io.*; import java.util.*; -import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; /** * The World class contains information about the currently viewed world. @@ -40,11 +28,7 @@ * * @author Jesper Öqvist */ -public class World implements Comparable { - - /** The currently supported NBT version of level.dat files. */ - public static final int NBT_VERSION = 19133; - +public abstract class World implements Comparable { /** Overworld dimension index. */ public static final int OVERWORLD_DIMENSION = 0; @@ -57,23 +41,18 @@ public class World implements Comparable { /** Default sea water level. */ public static final int SEA_LEVEL = 63; - /** Minimum level.dat data version of tall worlds (21w06a). */ - public static final int VERSION_21W06A = 2694; - public static final int VERSION_1_12_2 = 1343; - private final File worldDirectory; + protected final File worldDirectory; protected Dimension currentDimension; protected int currentDimensionId; - private final String levelName; - private int gameMode = 0; - private final long seed; - - private int versionId; + protected final String levelName; + protected int gameMode = 0; + protected final long seed; /** Timestamp for level.dat when player data was last loaded. */ - private long timestamp; + protected long timestamp; /** * @param levelName name of the world (not the world directory). @@ -93,10 +72,7 @@ public enum LoggedWarnings { SILENT } - public void loadDimension(int dimensionId) { - currentDimension = loadDimension(this, this.worldDirectory, dimensionId, -1, Collections.emptySet()); - currentDimension.reloadPlayerData(); - } + public abstract void loadDimension(int dimensionId); /** * Parse player location and level name. @@ -107,141 +83,7 @@ public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnin if (worldDirectory == null) { return EmptyWorld.INSTANCE; } - String levelName = worldDirectory.getName(); // Default level name. - File worldFile = new File(worldDirectory, "level.dat"); - long modtime = worldFile.lastModified(); - try (FileInputStream fin = new FileInputStream(worldFile); - InputStream gzin = new GZIPInputStream(fin); - DataInputStream in = new DataInputStream(gzin)) { - Set request = new HashSet<>(); - request.add(".Data.version"); - request.add(".Data.Version.Id"); - request.add(".Data.RandomSeed"); - request.add(".Data.Player"); - request.add(".Data.LevelName"); - request.add(".Data.GameType"); - request.add(".Data.isCubicWorld"); - Map result = NamedTag.quickParse(in, request); - - Tag version = result.get(".Data.version"); - if (warnings == LoggedWarnings.NORMAL && version.intValue() != NBT_VERSION) { - Log.warnf("The world format for the world %s is not supported by Chunky.\n" + "Will attempt to load the world anyway.", - levelName); - } - Tag versionId = result.get(".Data.Version.Id"); - Tag player = result.get(".Data.Player"); - Tag spawnX = player.get("SpawnX"); - Tag spawnY = player.get("SpawnY"); - Tag spawnZ = player.get("SpawnZ"); - Tag gameType = result.get(".Data.GameType"); - Tag randomSeed = result.get(".Data.RandomSeed"); - levelName = MinecraftText.removeFormatChars(result.get(".Data.LevelName").stringValue(levelName)); - - long seed = randomSeed.longValue(0); - - Set playerEntities = getPlayerEntityData(worldDirectory, dimensionId, player); - - World world = new World(levelName, worldDirectory, seed, modtime); - world.gameMode = gameType.intValue(0); - world.versionId = versionId.intValue(); - - Dimension dimension = loadDimension(world, worldDirectory, dimensionId, modtime, playerEntities); - - boolean haveSpawnPos = !(spawnX.isError() || spawnY.isError() || spawnZ.isError()); - if (haveSpawnPos) { - dimension.setSpawnPos(new Vector3i(spawnX.intValue(0), spawnY.intValue(0), spawnZ.intValue(0))); - } - - world.currentDimension = dimension; - - return world; - } catch (FileNotFoundException e) { - if (warnings == LoggedWarnings.NORMAL) { - Log.infof("Could not find level.dat file for world %s!", levelName); - } - } catch (IOException e) { - if (warnings == LoggedWarnings.NORMAL) { - Log.infof("Could not read the level.dat file for world %s!", levelName); - } - } - return EmptyWorld.INSTANCE; - } - - @NotNull - private static Dimension loadDimension(World world, File worldDirectory, int dimensionId, long modtime, Set playerEntities) { - Dimension dimension; - File dimensionDirectory = dimensionId == 0 ? worldDirectory : new File(worldDirectory, "DIM" + dimensionId); - if (new File(dimensionDirectory, "region3d").exists()) { - dimension = new CubicDimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); - } else { - dimension = new Dimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); - } - return dimension; - } - - @NotNull - private static Set getPlayerEntityData(File worldDirectory, int dimensionId, Tag player) { - Set playerEntities = new HashSet<>(); - if (!player.isError()) { - playerEntities.add(new PlayerEntityData(player)); - } - loadAdditionalPlayers(worldDirectory, playerEntities); - // Filter for the players only within the requested dimension - playerEntities = playerEntities.stream().filter(playerData -> playerData.dimension == dimensionId).collect(Collectors.toSet()); - return playerEntities; - } - - /** - * Reload player data for the current dimension. This method is not in Dimension because players are per-world, not per-dimension - * @return {@code true} if player data was reloaded. - */ - synchronized boolean reloadPlayerData() { - if (worldDirectory == null) { - return false; - } - File worldFile = new File(worldDirectory, "level.dat"); - long lastModified = worldFile.lastModified(); - if (lastModified == timestamp) { - return false; - } - Log.infof("world %s: timestamp updated: reading player data", levelName); - timestamp = lastModified; - - try (FileInputStream fin = new FileInputStream(worldFile); - InputStream gzin = new GZIPInputStream(fin); - DataInputStream in = new DataInputStream(gzin)) { - Set request = new HashSet<>(); - request.add(".Data.Player"); - Map result = NamedTag.quickParse(in, request); - Tag player = result.get(".Data.Player"); - - currentDimension.setPlayerEntities(getPlayerEntityData(worldDirectory, currentDimensionId, player)); - } catch (IOException e) { - Log.infof("Could not read the level.dat file for world %s while trying to reload player data!", levelName); - return false; - } - return true; - } - - private static void loadAdditionalPlayers(File worldDirectory, Set playerEntities) { - loadPlayerData(new File(worldDirectory, "players"), playerEntities); - loadPlayerData(new File(worldDirectory, "playerdata"), playerEntities); - } - - private static void loadPlayerData(File playerdata, Set playerEntities) { - if (playerdata.isDirectory()) { - File[] players = playerdata.listFiles(); - if (players != null) { - for (File player : players) { - try (DataInputStream in = new DataInputStream( - new GZIPInputStream(new FileInputStream(player)))) { - playerEntities.add(new PlayerEntityData(NamedTag.read(in).unpack())); - } catch (IOException e) { - Log.infof("Could not read player data file '%s'", player.getAbsolutePath()); - } - } - } - } + return JavaWorld.loadWorld(worldDirectory, dimensionId, warnings); } /** @@ -266,24 +108,6 @@ public File getWorldDirectory() { return worldDirectory; } - /** - * @deprecated Use {@link World#currentDimension()} -> {@link Dimension#getDimensionDirectory()} ()}. Removed once there are no more usages - */ - @Deprecated - protected synchronized File getDataDirectory(int dimension) { - return dimension == 0 ? - worldDirectory : - new File(worldDirectory, "DIM" + dimension); - } - - /** - @deprecated Use {@link World#currentDimension()} -> {@link Dimension#getRegionDirectory()}. Removed once there are no more usages - */ - @Deprecated - protected synchronized File getRegionDirectory(int dimension) { - return new File(getDataDirectory(dimension), "region"); - } - /** * Export the given chunks to a Zip archive. @@ -292,43 +116,8 @@ protected synchronized File getRegionDirectory(int dimension) { * * @throws IOException */ - public synchronized void exportChunksToZip(File target, Collection chunks, - ProgressTracker progress) throws IOException { - - Map> regionMap = new HashMap<>(); - - for (ChunkPosition chunk : chunks) { - RegionPosition regionPosition = chunk.getRegionPosition(); - Set chunkSet = regionMap.computeIfAbsent(regionPosition, k -> new HashSet<>()); - chunkSet.add(new ChunkPosition(chunk.x & 31, chunk.z & 31)); - } - - int work = 0; - progress.setJobSize(regionMap.size() + 1); - - String regionDirectory = - currentDimensionId == 0 ? currentDimension().getDimensionDirectory().getName() : - currentDimension().getDimensionDirectory().getName() + "/DIM" + currentDimensionId; - regionDirectory += "/region"; - - try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(target))) { - writeLevelDatToZip(zout); - progress.setProgress(++work); - - for (Map.Entry> entry : regionMap.entrySet()) { - - if (progress.isInterrupted()) - break; - - RegionPosition region = entry.getKey(); - - appendRegionToZip(zout, currentDimension.getRegionDirectory(), region, - regionDirectory + "/" + region.getMcaName(), entry.getValue()); - - progress.setProgress(++work); - } - } - } + public abstract void exportChunksToZip(File target, Collection chunks, ProgressTracker progress) + throws IOException; /** * Export the world to a zip file. The chunks which are included @@ -338,71 +127,8 @@ public synchronized void exportChunksToZip(File target, Collection> regions = new LinkedList<>(); - - WorldScanner.Operator operator = (regionDirectory, x, z) -> - regions.add(new Pair<>(regionDirectory, new RegionPosition(x, z))); - // TODO make this more dynamic - File overworld = getRegionDirectory(OVERWORLD_DIMENSION); - WorldScanner.findExistingChunks(overworld, operator); - WorldScanner.findExistingChunks(getRegionDirectory(NETHER_DIMENSION), operator); - WorldScanner.findExistingChunks(getRegionDirectory(END_DIMENSION), operator); - - int work = 0; - progress.setJobSize(regions.size() + 1); - - try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(target))) { - writeLevelDatToZip(zout); - progress.setProgress(++work); - - for (Pair region : regions) { - - if (progress.isInterrupted()) { - break; - } - - String regionDirectory = (region.thing1 == overworld) ? - worldDirectory.getName() : - worldDirectory.getName() + "/" + region.thing1.getParentFile().getName(); - regionDirectory += "/region"; - appendRegionToZip(zout, region.thing1, region.thing2, - regionDirectory + "/" + region.thing2.getMcaName(), null); - - progress.setProgress(++work); - } - } - } - - /** - * Write this worlds level.dat file to a ZipOutputStream. - * - * @throws IOException - */ - private void writeLevelDatToZip(ZipOutputStream zout) throws IOException { - File levelDat = new File(worldDirectory, "level.dat"); - try (FileInputStream in = new FileInputStream(levelDat)) { - zout.putNextEntry(new ZipEntry(worldDirectory.getName() + "/" + "level.dat")); - byte[] buf = new byte[4096]; - int len; - while ((len = in.read(buf)) > 0) { - zout.write(buf, 0, len); - } - zout.closeEntry(); - } - } - - private void appendRegionToZip(ZipOutputStream zout, File regionDirectory, - RegionPosition regionPos, String regionZipFileName, Set chunks) - throws IOException { - - zout.putNextEntry(new ZipEntry(regionZipFileName)); - MCRegion.writeRegion(regionDirectory, regionPos, new DataOutputStream(zout), chunks); - zout.closeEntry(); - } + public abstract void exportWorldToZip(File target, ProgressTracker progress) + throws IOException; @Override public String toString() { return levelName + " (" + worldDirectory.getName() + ")"; @@ -413,10 +139,6 @@ public String levelName() { return levelName; } - public int getVersionId() { - return versionId; - } - /** * @return true if the given directory exists and * contains a level.dat file diff --git a/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java b/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java index e6cee2dda4..62bec7037e 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java +++ b/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java @@ -63,7 +63,7 @@ public class MCRegion implements Region { private final Chunk[] chunks = new Chunk[NUM_CHUNKS]; private final RegionPosition position; - private final Dimension dimension; + private final JavaDimension dimension; private final String fileName; private long regionFileTime = 0; private final int[] chunkTimestamps = new int[NUM_CHUNKS]; @@ -80,7 +80,7 @@ private static int getMCAChunkIndex(ChunkPosition chunkPos) { * * @param pos the region position */ - public MCRegion(RegionPosition pos, Dimension dimension) { + public MCRegion(RegionPosition pos, JavaDimension dimension) { this.dimension = dimension; fileName = pos.getMcaName(); position = pos; @@ -156,7 +156,7 @@ public synchronized void parse(int minY, int maxY) { int loc = file.readInt(); if (loc != 0) { if (chunk.isEmpty()) { - chunk = new Chunk(pos, dimension); + chunk = new JavaChunk(pos, dimension); setChunk(pos, chunk); } } else { diff --git a/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java b/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java index b3e9e3bcc0..7b1e502479 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java +++ b/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java @@ -20,9 +20,8 @@ import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.map.MapView; import se.llbit.chunky.map.WorldMapLoader; -import se.llbit.chunky.world.ChunkPosition; import se.llbit.chunky.world.ChunkView; -import se.llbit.chunky.world.Dimension; +import se.llbit.chunky.world.JavaDimension; import se.llbit.chunky.world.RegionPosition; /** @@ -39,7 +38,7 @@ public MCRegionChangeWatcher(WorldMapLoader loader, MapView mapView) { try { while (!isInterrupted()) { sleep(3000); - Dimension dimension = mapLoader.getWorld().currentDimension(); + JavaDimension dimension = (JavaDimension) mapLoader.getWorld().currentDimension(); if (dimension.reloadPlayerData()) { if (PersistentSettings.getFollowPlayer()) { Platform.runLater(() -> dimension.getPlayerPos().ifPresent(mapView::panTo)); diff --git a/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java b/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java index b25f0771c0..bcdc9f4764 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java +++ b/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java @@ -17,8 +17,6 @@ package se.llbit.chunky.world.region; import se.llbit.chunky.chunk.ChunkData; -import se.llbit.chunky.chunk.GenericChunkData; -import se.llbit.chunky.chunk.SimpleChunkData; import se.llbit.chunky.map.MapView; import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.world.*; @@ -60,7 +58,7 @@ public RegionParser(WorldMapLoader loader, RegionQueue queue, MapView mapView) { } ChunkView map = mapView.getMapView(); if (map.isRegionVisible(position)) { - Dimension dimension = mapLoader.getWorld().currentDimension(); + JavaDimension dimension = (JavaDimension) mapLoader.getWorld().currentDimension(); // FIXME: don't cast Region region = dimension.getRegionWithinRange(position, mapView.getYMin(), mapView.getYMax()); region.parse(mapView.getYMin(), mapView.getYMax()); Mutable chunkData = new Mutable<>(null); From 560f904307b1242572ef87b3c708365aae81025d Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Thu, 5 Dec 2024 14:30:08 +0000 Subject: [PATCH 2/7] Basics of WorldFormat interface --- .../se/llbit/chunky/map/WorldMapLoader.java | 2 +- .../se/llbit/chunky/renderer/scene/Scene.java | 2 +- .../ui/controller/WorldChooserController.java | 20 ++++++++++++---- .../java/se/llbit/chunky/world/Dimension.java | 23 ++++--------------- .../se/llbit/chunky/world/EmptyDimension.java | 2 +- .../llbit/chunky/world/EmptyRegionChunk.java | 2 +- .../se/llbit/chunky/world/EmptyWorld.java | 11 ++++++++- .../se/llbit/chunky/world/JavaDimension.java | 18 ++++++++++++++- .../java/se/llbit/chunky/world/JavaWorld.java | 11 +++++++-- .../src/java/se/llbit/chunky/world/World.java | 23 ++++++++----------- .../world/worldformat/JavaWorldFormat.java | 19 +++++++++++++++ .../chunky/world/worldformat/Loadable.java | 10 ++++++++ .../chunky/world/worldformat/Renderable.java | 10 ++++++++ .../chunky/world/worldformat/WorldFormat.java | 15 ++++++++++++ 14 files changed, 124 insertions(+), 44 deletions(-) create mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java create mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java create mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java create mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java diff --git a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index daafe632e0..cc53c56a88 100644 --- a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java +++ b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java @@ -68,7 +68,7 @@ public WorldMapLoader(ChunkyFxController controller, MapView mapView) { * This is called when a new world is loaded */ public void loadWorld(File worldDir) { - if (World.isWorldDir(worldDir)) { + if (JavaWorld.isWorldDir(worldDir)) { if (world != null) { world.currentDimension().removeChunkTopographyListener(this); } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index e6cd3ce0c6..c4246b2d9a 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -543,7 +543,7 @@ public synchronized void loadScene(RenderContext context, String sceneName, Task loadedWorld = EmptyWorld.INSTANCE; if (!worldPath.isEmpty()) { File worldDirectory = new File(worldPath); - if (World.isWorldDir(worldDirectory)) { + if (JavaWorld.isWorldDir(worldDirectory)) { loadedWorld = World.loadWorld(worldDirectory, worldDimension, World.LoggedWarnings.NORMAL); } else { Log.info("Could not load world: " + worldPath); diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java index a2575d6808..88e8ddcf37 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java @@ -31,14 +31,16 @@ import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.resources.MinecraftFinder; import se.llbit.chunky.resources.ResourcePackLoader; -import se.llbit.chunky.resources.TexturePackLoader; import se.llbit.chunky.ui.TableSortConfigSerializer; +import se.llbit.chunky.world.EmptyWorld; import se.llbit.chunky.world.World; +import se.llbit.chunky.world.worldformat.WorldFormat; import se.llbit.fxutil.Dialogs; import se.llbit.json.JsonArray; import se.llbit.log.Log; import java.io.File; +import java.io.IOException; import java.net.URL; import java.text.DateFormat; import java.util.*; @@ -186,7 +188,7 @@ private void fillWorldList(final File worldSavesDir) { statusLabel.setText("Loading worlds list..."); disableControls(true); - Task> loadWorldsTask = new Task>() { + Task> loadWorldsTask = new Task<>() { @Override protected List call() { List worlds = new ArrayList<>(); @@ -194,9 +196,17 @@ protected List call() { File[] worldDirs = worldSavesDir.listFiles(); if (worldDirs != null) { for (File dir : worldDirs) { - if (World.isWorldDir(dir)) { - worlds.add(World.loadWorld(dir, World.OVERWORLD_DIMENSION, - World.LoggedWarnings.SILENT)); + for (WorldFormat worldFormat : WorldFormat.worldFormats) { + if (worldFormat.isValid(dir.toPath())) { + try { + World world = worldFormat.loadWorld(dir.toPath(), String.valueOf(World.OVERWORLD_DIMENSION)); + if (world != EmptyWorld.INSTANCE) { + worlds.add(world); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } } diff --git a/chunky/src/java/se/llbit/chunky/world/Dimension.java b/chunky/src/java/se/llbit/chunky/world/Dimension.java index 509439e12c..65badaa8d2 100644 --- a/chunky/src/java/se/llbit/chunky/world/Dimension.java +++ b/chunky/src/java/se/llbit/chunky/world/Dimension.java @@ -16,7 +16,6 @@ import se.llbit.math.Vector3i; import se.llbit.util.annotation.Nullable; -import java.io.File; import java.util.*; /** @@ -24,8 +23,6 @@ */ public abstract class Dimension { protected final World world; - - protected final File dimensionDirectory; private Set playerEntities; private final Heightmap heightmap = new Heightmap(); @@ -42,13 +39,11 @@ public abstract class Dimension { private long timestamp; /** - * @param dimensionDirectory Minecraft world directory. * @param timestamp */ - protected Dimension(World world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + protected Dimension(World world, int dimensionId, Set playerEntities, long timestamp) { this.world = world; this.dimensionId = dimensionId; - this.dimensionDirectory = dimensionDirectory; this.playerEntities = playerEntities; this.timestamp = timestamp; } @@ -97,15 +92,6 @@ public ChunkData createChunkData(@Nullable ChunkData chunkData, int minY, int ma public abstract RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView); - /** - * Get the data directory for the given dimension. - * - * @return File object pointing to the data directory - */ - protected synchronized File getDimensionDirectory() { - return dimensionDirectory; - } - /** * Get the current player position as an optional vector. @@ -128,9 +114,10 @@ public Heightmap getHeightmap() { return heightmap; } - @Override public String toString() { - return dimensionDirectory.getName() ; - } + /** + * @return A user presentable name of the dimension + */ + public abstract String toString(); /** Add a chunk deletion listener. */ public void addChunkDeletionListener(ChunkDeletionListener listener) { diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java index 77ce9e5589..b7a642913c 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java @@ -15,7 +15,7 @@ public class EmptyDimension extends Dimension { public static final EmptyDimension INSTANCE = new EmptyDimension(); private EmptyDimension() { - super(EmptyWorld.INSTANCE, 0, null, Collections.emptySet(), -1); + super(EmptyWorld.INSTANCE, 0, Collections.emptySet(), -1); } @Override diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java b/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java index 8ec109eda3..776c5925f3 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java @@ -26,7 +26,7 @@ /** * Empty or non-existent chunk in a region that does not exist. - * In the {@link ChunkMap map view} an {@link EmptyChunk} is represented as gray. + * In the {@link ChunkMap map view} an {@link EmptyRegionChunk} is represented as gray. * * @author Jesper Öqvist */ diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java index d8a63fa2e4..1c701b6d84 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java @@ -20,6 +20,8 @@ import java.io.File; import java.util.Collection; +import java.util.Collections; +import java.util.Set; /** * Represents an empty or non-existent world. @@ -37,7 +39,14 @@ private EmptyWorld() { } @Override - public void loadDimension(int dimensionId) { } + public Set listDimensions() { + return Collections.emptySet(); + } + + @Override + public Dimension loadDimension(int dimensionId) { + return EmptyDimension.INSTANCE; + } @Override public void exportChunksToZip(File target, Collection chunks, ProgressTracker progress) { } diff --git a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java index 281962e942..c1e3368e2a 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java @@ -13,13 +13,15 @@ public class JavaDimension extends Dimension { protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); + protected final File dimensionDirectory; /** * @param dimensionDirectory Minecraft world directory. * @param timestamp */ protected JavaDimension(JavaWorld world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { - super(world, dimensionId, dimensionDirectory, playerEntities, timestamp); + super(world, dimensionId, playerEntities, timestamp); + this.dimensionDirectory = dimensionDirectory; } /** @@ -119,4 +121,18 @@ public synchronized File getRegionDirectory() { public Date getLastModified() { return new Date(this.dimensionDirectory.lastModified()); } + + /** + * Get the data directory for the given dimension. + * + * @return File object pointing to the data directory + */ + protected synchronized File getDimensionDirectory() { + return dimensionDirectory; + } + + @Override + public String toString() { + return dimensionDirectory.getName() ; + } } diff --git a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java index 8c9f793af9..5fb034f7b3 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java @@ -16,6 +16,7 @@ */ package se.llbit.chunky.world; +import it.unimi.dsi.fastutil.ints.IntArraySet; import se.llbit.chunky.ui.ProgressTracker; import se.llbit.chunky.world.region.MCRegion; import se.llbit.log.Log; @@ -40,7 +41,7 @@ * * @author Jesper Öqvist */ -public class JavaWorld extends World{ +public class JavaWorld extends World { /** The currently supported NBT version of level.dat files. */ public static final int NBT_VERSION = 19133; @@ -61,9 +62,15 @@ protected JavaWorld(String levelName, File worldDirectory, long seed, long times super(levelName, worldDirectory, seed, timestamp); } - public void loadDimension(int dimensionId) { + @Override + public Set listDimensions() { + return new IntArraySet(new int[] { -1, 0, 1 }); + } + + public Dimension loadDimension(int dimensionId) { currentDimension = loadDimension(this, this.worldDirectory, dimensionId, -1, Collections.emptySet()); currentDimension.reloadPlayerData(); + return currentDimension; } /** diff --git a/chunky/src/java/se/llbit/chunky/world/World.java b/chunky/src/java/se/llbit/chunky/world/World.java index f7609e0ff2..0c51de2803 100644 --- a/chunky/src/java/se/llbit/chunky/world/World.java +++ b/chunky/src/java/se/llbit/chunky/world/World.java @@ -72,13 +72,22 @@ public enum LoggedWarnings { SILENT } - public abstract void loadDimension(int dimensionId); + /** + * The dimensions returned here are later provided to {@link #loadDimension(int)} when requesting a dimension be + * loaded. + * + * @return List the viewable dimensions within the world. + */ + public abstract Set listDimensions(); + + public abstract Dimension loadDimension(int dimensionId); /** * Parse player location and level name. * * @return {@code true} if the world data was loaded */ + @Deprecated public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnings warnings) { if (worldDirectory == null) { return EmptyWorld.INSTANCE; @@ -139,18 +148,6 @@ public String levelName() { return levelName; } - /** - * @return true if the given directory exists and - * contains a level.dat file - */ - public static boolean isWorldDir(File worldDir) { - if (worldDir != null && worldDir.isDirectory()) { - File levelDat = new File(worldDir, "level.dat"); - return levelDat.exists() && levelDat.isFile(); - } - return false; - } - /** * @return String describing the game-mode of this world */ diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java new file mode 100644 index 0000000000..10128b887a --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java @@ -0,0 +1,19 @@ +package se.llbit.chunky.world.worldformat; + +import se.llbit.chunky.world.JavaWorld; +import se.llbit.chunky.world.World; + +import java.io.IOException; +import java.nio.file.Path; + +public class JavaWorldFormat implements WorldFormat { + @Override + public World loadWorld(Path path, String dimension) throws IOException { + return JavaWorld.loadWorld(path.toFile(), Integer.parseInt(dimension), World.LoggedWarnings.SILENT); + } + + @Override + public boolean isValid(Path path) { + return JavaWorld.isWorldDir(path.toFile()); + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java b/chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java new file mode 100644 index 0000000000..ef0d6c458b --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java @@ -0,0 +1,10 @@ +package se.llbit.chunky.world.worldformat; + +import java.nio.file.Path; + +public interface Loadable { + boolean isValid(Path path); +} + + + diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java b/chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java new file mode 100644 index 0000000000..9f68a08e65 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java @@ -0,0 +1,10 @@ +package se.llbit.chunky.world.worldformat; + +import se.llbit.math.Octree; + +/** For simple world formats that only support being rendered directly */ +public interface Renderable extends Loadable { + void populateData(RenderableData data); + + record RenderableData(Octree octree) {} +} diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java new file mode 100644 index 0000000000..7ba7a8e04c --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java @@ -0,0 +1,15 @@ +package se.llbit.chunky.world.worldformat; + +import se.llbit.chunky.world.World; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +/** For worlds that have multiple dimensions, and fully support the map view */ +public interface WorldFormat extends Loadable { + Collection worldFormats = List.of(new JavaWorldFormat()); + + World loadWorld(Path path, String dimension) throws IOException; +} From 9cd732fb2048134d77a5875d98df31a10386ebdd Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Sat, 23 Aug 2025 12:05:21 +0100 Subject: [PATCH 3/7] Remove redundant interfaces --- .../ui/controller/ChunkyFxController.java | 24 ++++++++----------- .../chunky/world/worldformat/Loadable.java | 10 -------- .../chunky/world/worldformat/Renderable.java | 10 -------- .../chunky/world/worldformat/WorldFormat.java | 3 ++- 4 files changed, 12 insertions(+), 35 deletions(-) delete mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java delete mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java index c24973f918..439fce669e 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java @@ -75,11 +75,7 @@ import se.llbit.chunky.ui.RenderCanvasFx; import se.llbit.chunky.ui.UILogReceiver; import se.llbit.chunky.renderer.scene.*; -import se.llbit.chunky.world.ChunkSelectionTracker; -import se.llbit.chunky.world.ChunkView; -import se.llbit.chunky.world.EmptyWorld; -import se.llbit.chunky.world.Icon; -import se.llbit.chunky.world.World; +import se.llbit.chunky.world.*; import se.llbit.fx.ToolPane; import se.llbit.fxutil.Dialogs; import se.llbit.fxutil.GroupedChangeListener; @@ -364,7 +360,7 @@ public void exportMapView() { "This scene shows a different world than the one that is currently loaded. Do you want to load the world of this scene?"); Dialogs.stayOnTop(loadWorldConfirm); if (loadWorldConfirm.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.YES) { - mapLoader.loadWorld(newWorld.getWorldDirectory()); + mapLoader.loadWorld(newWorld); getChunkSelection().setSelection(chunky.getSceneManager().getScene().getChunks()); } } @@ -629,14 +625,14 @@ public File getSceneFile(String fileName) { trackCameraBtn.selectedProperty().bindBidirectional(trackCamera); trackCameraBtn.setTooltip(new Tooltip("Center the map view over the camera.")); - int currentDimension = mapLoader.getDimension(); - overworldBtn.setSelected(currentDimension == World.OVERWORLD_DIMENSION); + String currentDimension = mapLoader.getDimension(); + overworldBtn.setSelected(currentDimension.equals(JavaWorld.OVERWORLD_DIMENSION_ID)); overworldBtn.setTooltip(new Tooltip("Full of grass and Creepers!")); - netherBtn.setSelected(currentDimension == World.NETHER_DIMENSION); + netherBtn.setSelected(currentDimension.equals(JavaWorld.NETHER_DIMENSION_ID)); netherBtn.setTooltip(new Tooltip("The land of Zombie Pig-men.")); - endBtn.setSelected(currentDimension == World.END_DIMENSION); + endBtn.setSelected(currentDimension.equals(JavaWorld.END_DIMENSION_ID)); endBtn.setTooltip(new Tooltip("Watch out for the dragon.")); changeWorldBtn.setOnAction(e -> { @@ -652,13 +648,13 @@ public File getSceneFile(String fileName) { reloadWorldBtn.setOnAction(e -> mapLoader.reloadWorld()); overworldBtn.setGraphic(new ImageView(Icon.grass.fxImage())); - overworldBtn.setOnAction(e -> mapLoader.setDimension(World.OVERWORLD_DIMENSION)); + overworldBtn.setOnAction(e -> mapLoader.setDimension(JavaWorld.OVERWORLD_DIMENSION_ID)); netherBtn.setGraphic(new ImageView(Icon.netherrack.fxImage())); - netherBtn.setOnAction(e -> mapLoader.setDimension(World.NETHER_DIMENSION)); + netherBtn.setOnAction(e -> mapLoader.setDimension(JavaWorld.NETHER_DIMENSION_ID)); endBtn.setGraphic(new ImageView(Icon.endStone.fxImage())); - endBtn.setOnAction(e -> mapLoader.setDimension(World.END_DIMENSION)); + endBtn.setOnAction(e -> mapLoader.setDimension(JavaWorld.END_DIMENSION_ID)); loadScene.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN)); loadSceneFile.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)); @@ -676,7 +672,7 @@ public File getSceneFile(String fileName) { mapOverlay.setOnKeyPressed(map::onKeyPressed); mapOverlay.setOnKeyReleased(map::onKeyReleased); - mapLoader.loadWorld(PersistentSettings.getLastWorld()); + mapLoader.loadWorldFromDirectory(PersistentSettings.getLastWorld()); IntIntPair heightRange = mapLoader.getWorld().currentDimension().heightRange(); mapView.setYMin(heightRange.firstInt()); mapView.setYMax(heightRange.secondInt()); diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java b/chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java deleted file mode 100644 index ef0d6c458b..0000000000 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/Loadable.java +++ /dev/null @@ -1,10 +0,0 @@ -package se.llbit.chunky.world.worldformat; - -import java.nio.file.Path; - -public interface Loadable { - boolean isValid(Path path); -} - - - diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java b/chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java deleted file mode 100644 index 9f68a08e65..0000000000 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/Renderable.java +++ /dev/null @@ -1,10 +0,0 @@ -package se.llbit.chunky.world.worldformat; - -import se.llbit.math.Octree; - -/** For simple world formats that only support being rendered directly */ -public interface Renderable extends Loadable { - void populateData(RenderableData data); - - record RenderableData(Octree octree) {} -} diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java index 7ba7a8e04c..b9c391b354 100644 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java @@ -8,7 +8,8 @@ import java.util.List; /** For worlds that have multiple dimensions, and fully support the map view */ -public interface WorldFormat extends Loadable { +public interface WorldFormat { + // TODO: Registerable Collection worldFormats = List.of(new JavaWorldFormat()); World loadWorld(Path path, String dimension) throws IOException; From 485d772116ae6a9373623484abf59764eef1ebc4 Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Sat, 23 Aug 2025 12:31:11 +0100 Subject: [PATCH 4/7] Change dimension ids into strings (eg: minecraft:overworld) --- .../se/llbit/chunky/map/WorldMapLoader.java | 11 ++-- .../se/llbit/chunky/renderer/scene/Scene.java | 15 +++-- .../src/java/se/llbit/chunky/ui/ChunkMap.java | 2 +- .../se/llbit/chunky/world/CubicDimension.java | 2 +- .../java/se/llbit/chunky/world/Dimension.java | 6 +- .../se/llbit/chunky/world/EmptyDimension.java | 2 +- .../se/llbit/chunky/world/EmptyWorld.java | 6 +- .../chunky/world/ImposterCubicChunk.java | 2 +- .../java/se/llbit/chunky/world/JavaChunk.java | 2 +- .../se/llbit/chunky/world/JavaDimension.java | 2 +- .../java/se/llbit/chunky/world/JavaWorld.java | 62 +++++++++++++------ .../llbit/chunky/world/PlayerEntityData.java | 12 +++- .../src/java/se/llbit/chunky/world/World.java | 35 ++++------- .../world/worldformat/JavaWorldFormat.java | 4 +- .../se/llbit/chunky/PersistentSettings.java | 10 +-- 15 files changed, 102 insertions(+), 71 deletions(-) diff --git a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index cc53c56a88..a49079b06c 100644 --- a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java +++ b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java @@ -46,7 +46,7 @@ public class WorldMapLoader implements ChunkTopographyListener, ChunkViewListene private final ChunkTopographyUpdater topographyUpdater = new ChunkTopographyUpdater(); /** The dimension to load in the current world. */ - private int currentDimensionId = PersistentSettings.getDimension(); + private String currentDimensionId = PersistentSettings.getDimension(); private List> worldLoadListeners = new ArrayList<>(); @@ -174,10 +174,11 @@ private void updateRegionChangeWatcher(Dimension dimension) { /** * Set the current dimension. * - * @param value Must be a valid dimension index (0, -1, 1) + * @param value Must be a valid dimension see {@link World#listDimensions()} */ - public void setDimension(int value) { - if (value != currentDimensionId) { + // TODO: change this to show the available dimensions in the UI. + public void setDimension(String value) { + if (!value.equals(currentDimensionId)) { currentDimensionId = value; PersistentSettings.setDimension(currentDimensionId); @@ -187,7 +188,7 @@ public void setDimension(int value) { } /** Get the currently loaded dimension. */ - public int getDimension() { + public String getDimension() { return currentDimensionId; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index c4246b2d9a..8275f42dd1 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -190,7 +190,7 @@ public class Scene implements JsonSerializable, Refreshable { */ protected int rayDepth = PersistentSettings.getRayDepthDefault(); protected String worldPath = ""; - protected int worldDimension = 0; + protected String worldDimension = PersistentSettings.DEFAULT_DIMENSION; protected RenderMode mode = RenderMode.PREVIEW; protected int dumpFrequency = DEFAULT_DUMP_FREQUENCY; protected boolean saveSnapshots = false; @@ -761,7 +761,7 @@ public synchronized void reloadChunks(TaskTracker taskTracker) { Log.warn("Can not reload chunks for scene - world directory not found!"); return; } - loadedWorld = World.loadWorld(loadedWorld.getWorldDirectory(), worldDimension, World.LoggedWarnings.NORMAL); + loadedWorld.loadDimension(worldDimension); loadChunks(taskTracker, loadedWorld, chunks); refresh(); } @@ -794,7 +794,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec loadedWorld = world; worldPath = loadedWorld.getWorldDirectory().getAbsolutePath(); - worldDimension = world.currentDimensionId(); + worldDimension = world.currentDimension().id(); if (chunksToLoad.isEmpty()) { return; @@ -2908,7 +2908,14 @@ else if(waterShader.equals("SIMPLEX")) if (json.get("world").isObject()) { JsonObject world = json.get("world").object(); worldPath = world.get("path").stringValue(worldPath); - worldDimension = world.get("dimension").intValue(worldDimension); + + String dimensionString = world.get("dimension").stringValue(""); + if (dimensionString.isEmpty()) { + // legacy int-based dimension indices + worldDimension = JavaWorld.VANILLA_DIMENSION_IDX_TO_ID.get(world.get("dimension").intValue(0)); + } else { + worldDimension = dimensionString; + } } if (json.get("camera").isObject()) { diff --git a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java index 4a5f7b8730..33e971ecb4 100644 --- a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java +++ b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java @@ -585,7 +585,7 @@ private void drawPlayers(GraphicsContext gc) { World world = mapLoader.getWorld(); double blockScale = mapView.scale / 16.; for (PlayerEntityData player : world.currentDimension().getPlayerPositions()) { - if (player.dimension == world.currentDimensionId()) { + if (player.dimension.equals(world.currentDimension().id())) { int px = (int) QuickMath.floor(player.x * blockScale); int py = (int) QuickMath.floor(player.y); int pz = (int) QuickMath.floor(player.z * blockScale); diff --git a/chunky/src/java/se/llbit/chunky/world/CubicDimension.java b/chunky/src/java/se/llbit/chunky/world/CubicDimension.java index 979b301e6d..046e5818be 100644 --- a/chunky/src/java/se/llbit/chunky/world/CubicDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/CubicDimension.java @@ -23,7 +23,7 @@ public class CubicDimension extends JavaDimension { * @param dimensionDirectory Minecraft world directory. * @param timestamp */ - protected CubicDimension(JavaWorld world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + protected CubicDimension(JavaWorld world, String dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { super(world, dimensionId, dimensionDirectory, playerEntities, timestamp); } diff --git a/chunky/src/java/se/llbit/chunky/world/Dimension.java b/chunky/src/java/se/llbit/chunky/world/Dimension.java index 65badaa8d2..48601c4bb3 100644 --- a/chunky/src/java/se/llbit/chunky/world/Dimension.java +++ b/chunky/src/java/se/llbit/chunky/world/Dimension.java @@ -27,7 +27,7 @@ public abstract class Dimension { private final Heightmap heightmap = new Heightmap(); - private final int dimensionId; + private final String dimensionId; private final Collection chunkDeletionListeners = new LinkedList<>(); private final Collection chunkTopographyListeners = new LinkedList<>(); @@ -41,14 +41,14 @@ public abstract class Dimension { /** * @param timestamp */ - protected Dimension(World world, int dimensionId, Set playerEntities, long timestamp) { + protected Dimension(World world, String dimensionId, Set playerEntities, long timestamp) { this.world = world; this.dimensionId = dimensionId; this.playerEntities = playerEntities; this.timestamp = timestamp; } - public int getDimensionId() { + public String id() { return dimensionId; } diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java index b7a642913c..9d24cd1e63 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java @@ -15,7 +15,7 @@ public class EmptyDimension extends Dimension { public static final EmptyDimension INSTANCE = new EmptyDimension(); private EmptyDimension() { - super(EmptyWorld.INSTANCE, 0, Collections.emptySet(), -1); + super(EmptyWorld.INSTANCE, World.OVERWORLD_DIMENSION_ID, Collections.emptySet(), -1); } @Override diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java index 1c701b6d84..78a1342213 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java @@ -21,6 +21,7 @@ import java.io.File; import java.util.Collection; import java.util.Collections; +import java.util.Optional; import java.util.Set; /** @@ -39,12 +40,13 @@ private EmptyWorld() { } @Override - public Set listDimensions() { + public Set listDimensions() { return Collections.emptySet(); } @Override - public Dimension loadDimension(int dimensionId) { + @Override + public EmptyDimension loadDimension(String dimension) { return EmptyDimension.INSTANCE; } diff --git a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java index 622f77e350..c8a2b129af 100644 --- a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java @@ -113,7 +113,7 @@ private void loadSurfaceCubic(Map> data, ChunkData chu int[] heightmapData = extractHeightmapDataCubic(null, chunkData); updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - surface = new SurfaceLayer(dimension.getDimensionId(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + surface = new SurfaceLayer(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.id()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); } private int[] extractHeightmapDataCubic(Map cubeData, ChunkData chunkData) { diff --git a/chunky/src/java/se/llbit/chunky/world/JavaChunk.java b/chunky/src/java/se/llbit/chunky/world/JavaChunk.java index 5c7e00b346..c0dd06d472 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaChunk.java @@ -133,7 +133,7 @@ private void loadSurface(@NotNull Tag data, ChunkData chunkData, int yMin, int y int[] heightmapData = extractHeightmapData(data, chunkData); updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - surface = new SurfaceLayer(dimension.getDimensionId(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + surface = new SurfaceLayer(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.id()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); queueTopography(); } } else { diff --git a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java index c1e3368e2a..07d3312d82 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java @@ -19,7 +19,7 @@ public class JavaDimension extends Dimension { * @param dimensionDirectory Minecraft world directory. * @param timestamp */ - protected JavaDimension(JavaWorld world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + protected JavaDimension(JavaWorld world, String dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { super(world, dimensionId, playerEntities, timestamp); this.dimensionDirectory = dimensionDirectory; } diff --git a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java index 5fb034f7b3..c2bb2f7f32 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java @@ -16,11 +16,12 @@ */ package se.llbit.chunky.world; -import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArraySet; import se.llbit.chunky.ui.ProgressTracker; import se.llbit.chunky.world.region.MCRegion; import se.llbit.log.Log; -import se.llbit.math.Vector3i; import se.llbit.nbt.NamedTag; import se.llbit.nbt.Tag; import se.llbit.util.MinecraftText; @@ -50,6 +51,25 @@ public class JavaWorld extends World { public static final int VERSION_21W06A = 2694; public static final int VERSION_1_12_2 = 1343; + /** Nether dimension index. */ + public static final int NETHER_DIMENSION_IDX = -1; + + /** Overworld dimension index. */ + public static final int OVERWORLD_DIMENSION_IDX = 0; + + /** End dimension index. */ + public static final int END_DIMENSION_IDX = 1; + + public static final Map VANILLA_DIMENSION_ID_TO_IDX = Collections.unmodifiableMap(new Object2IntOpenHashMap<>( + new String[] { NETHER_DIMENSION_ID, OVERWORLD_DIMENSION_ID, END_DIMENSION_ID }, + new int[] { NETHER_DIMENSION_IDX, OVERWORLD_DIMENSION_IDX, END_DIMENSION_IDX } + )); + + public static final Map VANILLA_DIMENSION_IDX_TO_ID = Collections.unmodifiableMap(new Int2ObjectOpenHashMap<>( + new int[] { NETHER_DIMENSION_IDX, OVERWORLD_DIMENSION_IDX, END_DIMENSION_IDX }, + new String[] { NETHER_DIMENSION_ID, OVERWORLD_DIMENSION_ID, END_DIMENSION_ID } + )); + int versionId; /** @@ -63,12 +83,12 @@ protected JavaWorld(String levelName, File worldDirectory, long seed, long times } @Override - public Set listDimensions() { - return new IntArraySet(new int[] { -1, 0, 1 }); + public Set listDimensions() { + return new ObjectArraySet<>(VANILLA_DIMENSION_ID_TO_IDX.keySet()); } - public Dimension loadDimension(int dimensionId) { - currentDimension = loadDimension(this, this.worldDirectory, dimensionId, -1, Collections.emptySet()); + public Dimension loadDimension(String dimension) { + currentDimension = loadDimension(this, this.worldDirectory, dimension, -1, Collections.emptySet()); currentDimension.reloadPlayerData(); return currentDimension; } @@ -78,7 +98,7 @@ public Dimension loadDimension(int dimensionId) { * * @return {@code true} if the world data was loaded */ - public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnings warnings) { + public static World loadWorld(File worldDirectory, LoggedWarnings warnings) { String levelName = worldDirectory.getName(); // Default level name. File worldFile = new File(worldDirectory, "level.dat"); long modtime = worldFile.lastModified(); @@ -140,9 +160,9 @@ public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnin } @NotNull - protected static Dimension loadDimension(JavaWorld world, File worldDirectory, int dimensionId, long modtime, Set playerEntities) { - Dimension dimension; - File dimensionDirectory = dimensionId == 0 ? worldDirectory : new File(worldDirectory, "DIM" + dimensionId); + protected static JavaDimension loadDimension(JavaWorld world, File worldDirectory, String dimensionId, long modtime, Set playerEntities) { + JavaDimension dimension; + File dimensionDirectory = dimensionId.equals(JavaWorld.OVERWORLD_DIMENSION_ID) ? worldDirectory : new File(worldDirectory, "DIM" + JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimensionId)); if (new File(dimensionDirectory, "region3d").exists()) { dimension = new CubicDimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); } else { @@ -152,14 +172,14 @@ protected static Dimension loadDimension(JavaWorld world, File worldDirectory, i } @NotNull - static Set getPlayerEntityData(File worldDirectory, int dimensionId, Tag player) { + static Set getPlayerEntityData(File worldDirectory, String dimensionId, Tag player) { Set playerEntities = new HashSet<>(); if (!player.isError()) { playerEntities.add(new PlayerEntityData(player)); } loadAdditionalPlayers(worldDirectory, playerEntities); // Filter for the players only within the requested dimension - playerEntities = playerEntities.stream().filter(playerData -> playerData.dimension == dimensionId).collect(Collectors.toSet()); + playerEntities = playerEntities.stream().filter(playerData -> playerData.dimension.equals(dimensionId)).collect(Collectors.toSet()); return playerEntities; } @@ -187,7 +207,7 @@ synchronized boolean reloadPlayerData() { Map result = NamedTag.quickParse(in, request); Tag player = result.get(".Data.Player"); - currentDimension.setPlayerEntities(getPlayerEntityData(worldDirectory, currentDimensionId, player)); + currentDimension.setPlayerEntities(getPlayerEntityData(worldDirectory, currentDimension.id(), player)); } catch (IOException e) { Log.infof("Could not read the level.dat file for world %s while trying to reload player data!", levelName); return false; @@ -249,6 +269,10 @@ protected synchronized File getRegionDirectory(int dimension) { */ public synchronized void exportChunksToZip(File target, Collection chunks, ProgressTracker progress) throws IOException { + if (this.currentDimension == EmptyDimension.INSTANCE) { + return; + } + JavaDimension currentDim = (JavaDimension) this.currentDimension; Map> regionMap = new HashMap<>(); @@ -262,8 +286,8 @@ public synchronized void exportChunksToZip(File target, Collection regions.add(new Pair<>(regionDirectory, new RegionPosition(x, z))); // TODO make this more dynamic - File overworld = getRegionDirectory(OVERWORLD_DIMENSION); + File overworld = getRegionDirectory(OVERWORLD_DIMENSION_IDX); WorldScanner.findExistingChunks(overworld, operator); - WorldScanner.findExistingChunks(getRegionDirectory(NETHER_DIMENSION), operator); - WorldScanner.findExistingChunks(getRegionDirectory(END_DIMENSION), operator); + WorldScanner.findExistingChunks(getRegionDirectory(NETHER_DIMENSION_IDX), operator); + WorldScanner.findExistingChunks(getRegionDirectory(END_DIMENSION_IDX), operator); int work = 0; progress.setJobSize(regions.size() + 1); diff --git a/chunky/src/java/se/llbit/chunky/world/PlayerEntityData.java b/chunky/src/java/se/llbit/chunky/world/PlayerEntityData.java index 19182f04c4..14fa4ef655 100644 --- a/chunky/src/java/se/llbit/chunky/world/PlayerEntityData.java +++ b/chunky/src/java/se/llbit/chunky/world/PlayerEntityData.java @@ -18,6 +18,7 @@ package se.llbit.chunky.world; import se.llbit.nbt.CompoundTag; +import se.llbit.nbt.StringTag; import se.llbit.nbt.Tag; public class PlayerEntityData { @@ -27,7 +28,7 @@ public class PlayerEntityData { public final double z; public final double rotation; public final double pitch; - public final int dimension; + public final String dimension; public Tag feet = new CompoundTag(); public Tag legs = new CompoundTag(); public Tag head = new CompoundTag(); @@ -58,7 +59,12 @@ public PlayerEntityData(Tag player) { z = pos.get(2).doubleValue(); this.rotation = rotation.get(0).floatValue(); pitch = rotation.get(1).floatValue(); - dimension = player.get("Dimension").intValue(); + Tag dimensionTag = player.get("Dimension"); + if (dimensionTag instanceof StringTag) { + dimension = dimensionTag.stringValue(); + } else { + dimension = JavaWorld.VANILLA_DIMENSION_IDX_TO_ID.get(dimensionTag.intValue()); + } int selectedItem = player.get("SelectedItemSlot").intValue(0); @@ -89,7 +95,7 @@ public PlayerEntityData(Tag player) { @Override public String toString() { - return String.format("%d: %d, %d, %d", dimension, (int) x, (int) y, (int) z); + return String.format("%s: %d, %d, %d", dimension, (int) x, (int) y, (int) z); } @Override diff --git a/chunky/src/java/se/llbit/chunky/world/World.java b/chunky/src/java/se/llbit/chunky/world/World.java index 0c51de2803..dd00e4bf6a 100644 --- a/chunky/src/java/se/llbit/chunky/world/World.java +++ b/chunky/src/java/se/llbit/chunky/world/World.java @@ -29,23 +29,16 @@ * @author Jesper Öqvist */ public abstract class World implements Comparable { - /** Overworld dimension index. */ - public static final int OVERWORLD_DIMENSION = 0; - - /** Nether dimension index. */ - public static final int NETHER_DIMENSION = -1; - - /** End dimension index. */ - public static final int END_DIMENSION = 1; + public static final String OVERWORLD_DIMENSION_ID = "minecraft:overworld"; + public static final String NETHER_DIMENSION_ID = "minecraft:the_nether"; + public static final String END_DIMENSION_ID = "minecraft:the_end"; /** Default sea water level. */ public static final int SEA_LEVEL = 63; - protected final File worldDirectory; - protected Dimension currentDimension; - protected int currentDimensionId; + protected Dimension currentDimension = EmptyDimension.INSTANCE; protected final String levelName; protected int gameMode = 0; @@ -73,22 +66,28 @@ public enum LoggedWarnings { } /** - * The dimensions returned here are later provided to {@link #loadDimension(int)} when requesting a dimension be + * The dimensions returned here are later provided to {@link #loadDimension(String)} when requesting a dimension be * loaded. * * @return List the viewable dimensions within the world. */ - public abstract Set listDimensions(); + public abstract Set listDimensions(); public abstract Dimension loadDimension(int dimensionId); + /** + * @param dimension The dimension to load, guaranteed to be one of the dimensions previously returned by {@link #availableDimensions()} + * @return The loaded dimension + */ + public abstract Dimension loadDimension(String dimension); + /** * Parse player location and level name. * * @return {@code true} if the world data was loaded */ @Deprecated - public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnings warnings) { + public static World loadWorld(File worldDirectory, String dimensionId, LoggedWarnings warnings) { if (worldDirectory == null) { return EmptyWorld.INSTANCE; } @@ -102,14 +101,6 @@ public synchronized Dimension currentDimension() { return this.currentDimension; } - /** - * @return The current dimension - */ - public synchronized int currentDimensionId() { - return this.currentDimensionId; - } - - /** * @return The world directory */ diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java index 10128b887a..1a169387e3 100644 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java @@ -8,8 +8,8 @@ public class JavaWorldFormat implements WorldFormat { @Override - public World loadWorld(Path path, String dimension) throws IOException { - return JavaWorld.loadWorld(path.toFile(), Integer.parseInt(dimension), World.LoggedWarnings.SILENT); + public World loadWorld(Path path) throws IOException { + return JavaWorld.loadWorld(path.toFile(), World.LoggedWarnings.SILENT); } @Override diff --git a/lib/src/se/llbit/chunky/PersistentSettings.java b/lib/src/se/llbit/chunky/PersistentSettings.java index c43e884c31..f9b260d920 100644 --- a/lib/src/se/llbit/chunky/PersistentSettings.java +++ b/lib/src/se/llbit/chunky/PersistentSettings.java @@ -67,7 +67,7 @@ public final class PersistentSettings { public static final int DEFAULT_BRANCH_COUNT = 10; public static final int DEFAULT_SPP_TARGET = 1000; - public static final int DEFAULT_DIMENSION = 0; + public static final String DEFAULT_DIMENSION = "minecraft:overworld"; /** * Default canvas width. @@ -452,13 +452,13 @@ public static boolean getSingleColorTextures() { return settings.getBool("singleColorTextures", false); } - public static void setDimension(int value) { - settings.setInt("dimension", value); + public static void setDimension(String value) { + settings.setString("dimension", value); save(); } - public static int getDimension() { - return settings.getInt("dimension", DEFAULT_DIMENSION); + public static String getDimension() { + return settings.getString("dimension", DEFAULT_DIMENSION); } public static boolean getLoadPlayers() { From b6e812c23b43d6f3179696f6f143d3fdb3167c5c Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Sat, 23 Aug 2025 17:36:41 +0100 Subject: [PATCH 5/7] Replace all world loading with worldformat --- .../se/llbit/chunky/map/WorldMapLoader.java | 65 ++++++++++------ .../se/llbit/chunky/renderer/scene/Scene.java | 8 +- .../ui/controller/WorldChooserController.java | 18 +---- .../se/llbit/chunky/world/EmptyWorld.java | 6 +- .../se/llbit/chunky/world/JavaDimension.java | 9 ++- .../java/se/llbit/chunky/world/JavaWorld.java | 76 ++++++++++--------- .../src/java/se/llbit/chunky/world/World.java | 21 ++--- .../world/worldformat/JavaWorldFormat.java | 5 ++ .../chunky/world/worldformat/WorldFormat.java | 54 ++++++++++++- 9 files changed, 161 insertions(+), 101 deletions(-) diff --git a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index a49079b06c..973900d6f0 100644 --- a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java +++ b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java @@ -16,6 +16,7 @@ */ package se.llbit.chunky.map; +import java.util.Optional; import java.util.function.BiConsumer; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.renderer.ChunkViewListener; @@ -25,6 +26,8 @@ import se.llbit.chunky.world.region.RegionParser; import se.llbit.chunky.world.region.RegionQueue; import se.llbit.chunky.world.listeners.ChunkTopographyListener; +import se.llbit.chunky.world.worldformat.WorldFormat; +import se.llbit.log.Log; import java.io.File; import java.util.ArrayList; @@ -64,28 +67,44 @@ public WorldMapLoader(ChunkyFxController controller, MapView mapView) { topographyUpdater.start(); } + public void loadWorldFromDirectory(File worldLocation) { + if (worldLocation == null) { + return; + } + this.loadWorld(WorldFormat.loadWorld(worldLocation).orElse(EmptyWorld.INSTANCE)); + } /** * This is called when a new world is loaded */ - public void loadWorld(File worldDir) { - if (JavaWorld.isWorldDir(worldDir)) { - if (world != null) { - world.currentDimension().removeChunkTopographyListener(this); - } - boolean isSameWorld = !(world instanceof EmptyWorld) && worldDir.equals(world.getWorldDirectory()); - World newWorld = World.loadWorld(worldDir, currentDimensionId, World.LoggedWarnings.NORMAL); - newWorld.currentDimension().addChunkTopographyListener(this); - synchronized (this) { - world = newWorld; - updateRegionChangeWatcher(newWorld.currentDimension()); - - File newWorldDir = world.getWorldDirectory(); - if (newWorldDir != null && !newWorldDir.equals(PersistentSettings.getLastWorld())) { - PersistentSettings.setLastWorld(newWorldDir); - } + public void loadWorld(World newWorld) { + if (this.world != null) { + this.world.currentDimension().removeChunkTopographyListener(this); + } + boolean isSameWorld = !(this.world instanceof EmptyWorld) && newWorld.getWorldDirectory().equals(this.world.getWorldDirectory()); + + Optional dimensionToLoad = Optional.of(world.currentDimension()) + .map(Dimension::id) + .filter(dimension -> newWorld.availableDimensions().contains(dimension)) + .or(newWorld::defaultDimension) + .or(() -> newWorld.availableDimensions().stream().findFirst()); + + if (dimensionToLoad.isEmpty()) { + Log.infof("No dimension loaded for world %s", newWorld.toString()); + return; + } + + Dimension loadedDim = newWorld.loadDimension(dimensionToLoad.get()); + loadedDim.addChunkTopographyListener(this); + synchronized (this) { + this.world = newWorld; + updateRegionChangeWatcher(loadedDim); + + File newWorldDir = this.world.getWorldDirectory(); + if (!newWorldDir.equals(PersistentSettings.getLastWorld())) { + PersistentSettings.setLastWorld(newWorldDir); } - worldLoadListeners.forEach(listener -> listener.accept(newWorld, isSameWorld)); } + worldLoadListeners.forEach(listener -> listener.accept(newWorld, isSameWorld)); } /** @@ -151,14 +170,12 @@ public void regionUpdated(RegionPosition region) { public void reloadWorld() { topographyUpdater.clearQueue(); world.currentDimension().removeChunkTopographyListener(this); - World newWorld = World.loadWorld(world.getWorldDirectory(), currentDimensionId, - World.LoggedWarnings.NORMAL); - newWorld.currentDimension().addChunkTopographyListener(this); + world.loadDimension(currentDimensionId); + world.currentDimension().addChunkTopographyListener(this); synchronized (this) { - world = newWorld; - updateRegionChangeWatcher(newWorld.currentDimension()); + updateRegionChangeWatcher(world.currentDimension()); } - worldLoadListeners.forEach(listener -> listener.accept(newWorld, true)); + worldLoadListeners.forEach(listener -> listener.accept(world, true)); viewUpdated(mapView.getMapView()); // update visible chunks immediately } @@ -174,7 +191,7 @@ private void updateRegionChangeWatcher(Dimension dimension) { /** * Set the current dimension. * - * @param value Must be a valid dimension see {@link World#listDimensions()} + * @param value Must be a valid dimension see {@link World#availableDimensions()} */ // TODO: change this to show the available dimensions in the UI. public void setDimension(String value) { diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 8275f42dd1..8469b6c540 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -53,6 +53,7 @@ import se.llbit.chunky.world.biome.Biome; import se.llbit.chunky.world.biome.BiomePalette; import se.llbit.chunky.world.biome.Biomes; +import se.llbit.chunky.world.worldformat.WorldFormat; import se.llbit.json.*; import se.llbit.log.Log; import se.llbit.math.*; @@ -543,11 +544,8 @@ public synchronized void loadScene(RenderContext context, String sceneName, Task loadedWorld = EmptyWorld.INSTANCE; if (!worldPath.isEmpty()) { File worldDirectory = new File(worldPath); - if (JavaWorld.isWorldDir(worldDirectory)) { - loadedWorld = World.loadWorld(worldDirectory, worldDimension, World.LoggedWarnings.NORMAL); - } else { - Log.info("Could not load world: " + worldPath); - } + loadedWorld = WorldFormat.loadWorld(worldDirectory).orElse(EmptyWorld.INSTANCE); + loadedWorld.loadDimension(this.worldDimension); } loadDump(context, taskTracker); diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java index 88e8ddcf37..04548c422d 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java @@ -40,7 +40,6 @@ import se.llbit.log.Log; import java.io.File; -import java.io.IOException; import java.net.URL; import java.text.DateFormat; import java.util.*; @@ -135,7 +134,7 @@ public void populate(WorldMapLoader mapLoader) { File directory = chooser.showDialog(stage); if (directory != null) { if (directory.isDirectory()) { - this.loadWorld(World.loadWorld(directory, mapLoader.getDimension(), World.LoggedWarnings.NORMAL), mapLoader); + this.loadWorld(WorldFormat.loadWorld(directory).orElse(EmptyWorld.INSTANCE), mapLoader); stage.close(); } else { Log.warn("Non-directory selected."); @@ -171,7 +170,7 @@ private void loadWorld(World world, WorldMapLoader mapLoader) { } } }); - mapLoader.loadWorld(world.getWorldDirectory()); + mapLoader.loadWorld(world); } /** @@ -196,18 +195,7 @@ protected List call() { File[] worldDirs = worldSavesDir.listFiles(); if (worldDirs != null) { for (File dir : worldDirs) { - for (WorldFormat worldFormat : WorldFormat.worldFormats) { - if (worldFormat.isValid(dir.toPath())) { - try { - World world = worldFormat.loadWorld(dir.toPath(), String.valueOf(World.OVERWORLD_DIMENSION)); - if (world != EmptyWorld.INSTANCE) { - worlds.add(world); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } + WorldFormat.loadWorld(dir).ifPresent(worlds::add); } } } diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java index 78a1342213..bed0ed4b0e 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java @@ -40,11 +40,15 @@ private EmptyWorld() { } @Override - public Set listDimensions() { + public Set availableDimensions() { return Collections.emptySet(); } @Override + public Optional defaultDimension() { + return Optional.empty(); + } + @Override public EmptyDimension loadDimension(String dimension) { return EmptyDimension.INSTANCE; diff --git a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java index 07d3312d82..28f4fba13d 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java @@ -10,6 +10,7 @@ import java.io.File; import java.util.*; +import java.util.stream.Collectors; public class JavaDimension extends Dimension { protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); @@ -29,7 +30,13 @@ protected JavaDimension(JavaWorld world, String dimensionId, File dimensionDirec * @return {@code true} if player data was reloaded. */ public synchronized boolean reloadPlayerData() { - return ((JavaWorld) this.world).reloadPlayerData(); + boolean changed = ((JavaWorld) this.world).reloadPlayerData(); + if (changed) { + this.setPlayerEntities(((JavaWorld) this.world).playerEntities.stream() + .filter(player -> player.dimension.equals(this.id())) + .collect(Collectors.toSet())); + } + return changed; } /** diff --git a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java index c2bb2f7f32..8289982c5e 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java @@ -22,6 +22,7 @@ import se.llbit.chunky.ui.ProgressTracker; import se.llbit.chunky.world.region.MCRegion; import se.llbit.log.Log; +import se.llbit.math.Vector3i; import se.llbit.nbt.NamedTag; import se.llbit.nbt.Tag; import se.llbit.util.MinecraftText; @@ -70,7 +71,17 @@ public class JavaWorld extends World { new String[] { NETHER_DIMENSION_ID, OVERWORLD_DIMENSION_ID, END_DIMENSION_ID } )); - int versionId; + protected int versionId; + + /** + * In a java world player data is per-world and not per-dimension, so we store it here. + */ + protected final Set playerEntities; + + /** + * In a java world spawn position is per-world and not per-dimension, so we store it here. + */ + protected final Vector3i spawnPos; /** * @param levelName name of the world (not the world directory). @@ -78,18 +89,30 @@ public class JavaWorld extends World { * @param seed * @param timestamp */ - protected JavaWorld(String levelName, File worldDirectory, long seed, long timestamp) { + protected JavaWorld(String levelName, File worldDirectory, long seed, long timestamp, Set playerEntities, Vector3i spawnPos) { super(levelName, worldDirectory, seed, timestamp); + this.playerEntities = playerEntities; + this.spawnPos = spawnPos; } @Override - public Set listDimensions() { + public Set availableDimensions() { return new ObjectArraySet<>(VANILLA_DIMENSION_ID_TO_IDX.keySet()); } + @Override + public Optional defaultDimension() { + return Optional.of(OVERWORLD_DIMENSION_ID); + } + public Dimension loadDimension(String dimension) { - currentDimension = loadDimension(this, this.worldDirectory, dimension, -1, Collections.emptySet()); - currentDimension.reloadPlayerData(); + currentDimension = loadDimension( + this, + this.worldDirectory, + dimension, + -1, + this.playerEntities.stream().filter(player -> player.dimension.equals(dimension)).collect(Collectors.toSet()) + ); return currentDimension; } @@ -122,7 +145,7 @@ public static World loadWorld(File worldDirectory, LoggedWarnings warnings) { } Tag versionId = result.get(".Data.Version.Id"); Tag player = result.get(".Data.Player"); - Tag spawnX = player.get("SpawnX"); + Tag spawnX = player.get("SpawnX"); // TODO: not sure what to do with spawn location now. I guess for java worlds: the world should store it and the dimension should set it in the map view when loaded...? Tag spawnY = player.get("SpawnY"); Tag spawnZ = player.get("SpawnZ"); Tag gameType = result.get(".Data.GameType"); @@ -131,20 +154,19 @@ public static World loadWorld(File worldDirectory, LoggedWarnings warnings) { long seed = randomSeed.longValue(0); - Set playerEntities = getPlayerEntityData(worldDirectory, dimensionId, player); - - JavaWorld world = new JavaWorld(levelName, worldDirectory, seed, modtime); - world.gameMode = gameType.intValue(0); - world.versionId = versionId.intValue(); - - Dimension dimension = loadDimension(world, worldDirectory, dimensionId, modtime, playerEntities); + Set playerEntities = getPlayerEntityData(worldDirectory, player); boolean haveSpawnPos = !(spawnX.isError() || spawnY.isError() || spawnZ.isError()); + Vector3i spawnPos; if (haveSpawnPos) { - dimension.setSpawnPos(new Vector3i(spawnX.intValue(0), spawnY.intValue(0), spawnZ.intValue(0))); + spawnPos = new Vector3i(spawnX.intValue(0), spawnY.intValue(0), spawnZ.intValue(0)); + } else { + spawnPos = new Vector3i(0, 0, 0); } - world.currentDimension = dimension; + JavaWorld world = new JavaWorld(levelName, worldDirectory, seed, modtime, playerEntities, spawnPos); + world.gameMode = gameType.intValue(0); + world.versionId = versionId.intValue(); return world; } catch (FileNotFoundException e) { @@ -172,14 +194,12 @@ protected static JavaDimension loadDimension(JavaWorld world, File worldDirector } @NotNull - static Set getPlayerEntityData(File worldDirectory, String dimensionId, Tag player) { + static Set getPlayerEntityData(File worldDirectory, Tag player) { Set playerEntities = new HashSet<>(); if (!player.isError()) { playerEntities.add(new PlayerEntityData(player)); } loadAdditionalPlayers(worldDirectory, playerEntities); - // Filter for the players only within the requested dimension - playerEntities = playerEntities.stream().filter(playerData -> playerData.dimension.equals(dimensionId)).collect(Collectors.toSet()); return playerEntities; } @@ -206,8 +226,8 @@ synchronized boolean reloadPlayerData() { request.add(".Data.Player"); Map result = NamedTag.quickParse(in, request); Tag player = result.get(".Data.Player"); - - currentDimension.setPlayerEntities(getPlayerEntityData(worldDirectory, currentDimension.id(), player)); + this.playerEntities.clear(); + this.playerEntities.addAll(getPlayerEntityData(worldDirectory, player)); } catch (IOException e) { Log.infof("Could not read the level.dat file for world %s while trying to reload player data!", levelName); return false; @@ -236,25 +256,12 @@ private static void loadPlayerData(File playerdata, Set player } } - @Override - public synchronized JavaDimension currentDimension() { - return (JavaDimension) this.currentDimension; - } - - /** - * @deprecated Use {@link JavaWorld#currentDimension()} -> {@link Dimension#getDimensionDirectory()} ()}. Removed once there are no more usages - */ - @Deprecated protected synchronized File getDataDirectory(int dimension) { return dimension == 0 ? worldDirectory : new File(worldDirectory, "DIM" + dimension); } - /** - @deprecated Use {@link JavaWorld#currentDimension()} -> {@link JavaDimension#getRegionDirectory()}. Removed once there are no more usages - */ - @Deprecated protected synchronized File getRegionDirectory(int dimension) { return new File(getDataDirectory(dimension), "region"); } @@ -399,7 +406,4 @@ public static boolean isWorldDir(File worldDir) { return false; } - public Date getLastModified() { - return new Date(this.worldDirectory.lastModified()); - } } diff --git a/chunky/src/java/se/llbit/chunky/world/World.java b/chunky/src/java/se/llbit/chunky/world/World.java index dd00e4bf6a..7948529f3c 100644 --- a/chunky/src/java/se/llbit/chunky/world/World.java +++ b/chunky/src/java/se/llbit/chunky/world/World.java @@ -71,9 +71,13 @@ public enum LoggedWarnings { * * @return List the viewable dimensions within the world. */ - public abstract Set listDimensions(); + public abstract Set availableDimensions(); - public abstract Dimension loadDimension(int dimensionId); + /** + * MUST be one of {@link #availableDimensions()} + * @return The preferred default dimension of this world (typically the overworld) + */ + public abstract Optional defaultDimension(); /** * @param dimension The dimension to load, guaranteed to be one of the dimensions previously returned by {@link #availableDimensions()} @@ -81,19 +85,6 @@ public enum LoggedWarnings { */ public abstract Dimension loadDimension(String dimension); - /** - * Parse player location and level name. - * - * @return {@code true} if the world data was loaded - */ - @Deprecated - public static World loadWorld(File worldDirectory, String dimensionId, LoggedWarnings warnings) { - if (worldDirectory == null) { - return EmptyWorld.INSTANCE; - } - return JavaWorld.loadWorld(worldDirectory, dimensionId, warnings); - } - /** * @return The current dimension */ diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java index 1a169387e3..442bc6fc0f 100644 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java @@ -12,6 +12,11 @@ public World loadWorld(Path path) throws IOException { return JavaWorld.loadWorld(path.toFile(), World.LoggedWarnings.SILENT); } + @Override + public String name() { + return "Java (Anvil)"; + } + @Override public boolean isValid(Path path) { return JavaWorld.isWorldDir(path.toFile()); diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java index b9c391b354..4837a476e5 100644 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java @@ -1,16 +1,62 @@ package se.llbit.chunky.world.worldformat; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import se.llbit.chunky.world.EmptyWorld; import se.llbit.chunky.world.World; +import se.llbit.log.Log; +import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.Collection; -import java.util.List; +import java.util.*; /** For worlds that have multiple dimensions, and fully support the map view */ public interface WorldFormat { // TODO: Registerable Collection worldFormats = List.of(new JavaWorldFormat()); - World loadWorld(Path path, String dimension) throws IOException; -} + // Should this go somewhere else? + static Optional loadWorld(File dir) { + Map worldsByFormat = new Object2ObjectOpenHashMap<>(); + + for (WorldFormat worldFormat : WorldFormat.worldFormats) { + if (worldFormat.isValid(dir.toPath())) { + try { + World world = worldFormat.loadWorld(dir.toPath()); + if (world != EmptyWorld.INSTANCE) { + worldsByFormat.put(worldFormat.name(), world); + } + } catch (IOException e) { + Log.error(String.format("An error occurred when trying to load a world using format `%s` from %s", worldFormat.name(), dir.getAbsolutePath()), e); + } + } + } + if (worldsByFormat.size() > 1) { + // Maybe allow the user to select which? + // This method is called from a variety of different popup/menu situations, is this ^ possible? + Log.warn(String.format("The directory %s has multiple valid world formats: %s", dir.getAbsolutePath(), String.join(", ", worldsByFormat.keySet()))); + } + return worldsByFormat.values().stream().findFirst(); + } + + /** + * @return The user-recognisable name of the world format. Shown to the user if this format has issues or throws. + */ + String name(); + + /** + * This method will be called on every possible world directory (typically this is every directory in `.minecraft/saves`). + * + * @param path The path to the world. + * @return Whether this is a valid world under this world format. + */ + boolean isValid(Path path); + + /** + * Load the world at the given path + * @param path The path to the world. + * @return The loaded world + * @throws IOException When something goes wrong when loading the world. + */ + World loadWorld(Path path) throws IOException; +} \ No newline at end of file From cf31ed1a2435b7698f43fcd54472898f890f3f3d Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Wed, 20 Aug 2025 02:02:28 +0100 Subject: [PATCH 6/7] Rename methods, address review comments --- .../java/se/llbit/chunky/map/WorldMapLoader.java | 2 +- .../se/llbit/chunky/renderer/scene/Scene.java | 2 +- chunky/src/java/se/llbit/chunky/ui/ChunkMap.java | 2 +- .../java/se/llbit/chunky/world/Dimension.java | 15 +++++++++------ .../se/llbit/chunky/world/EmptyDimension.java | 2 +- .../llbit/chunky/world/ImposterCubicChunk.java | 2 +- .../java/se/llbit/chunky/world/JavaChunk.java | 2 +- .../se/llbit/chunky/world/JavaDimension.java | 8 ++++---- .../java/se/llbit/chunky/world/JavaWorld.java | 16 +++++++--------- .../chunky/world/worldformat/WorldFormat.java | 3 --- 10 files changed, 26 insertions(+), 28 deletions(-) diff --git a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index 973900d6f0..8765a46db1 100644 --- a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java +++ b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java @@ -83,7 +83,7 @@ public void loadWorld(World newWorld) { boolean isSameWorld = !(this.world instanceof EmptyWorld) && newWorld.getWorldDirectory().equals(this.world.getWorldDirectory()); Optional dimensionToLoad = Optional.of(world.currentDimension()) - .map(Dimension::id) + .map(Dimension::getId) .filter(dimension -> newWorld.availableDimensions().contains(dimension)) .or(newWorld::defaultDimension) .or(() -> newWorld.availableDimensions().stream().findFirst()); diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 8469b6c540..1407b6e218 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -792,7 +792,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec loadedWorld = world; worldPath = loadedWorld.getWorldDirectory().getAbsolutePath(); - worldDimension = world.currentDimension().id(); + worldDimension = world.currentDimension().getId(); if (chunksToLoad.isEmpty()) { return; diff --git a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java index 33e971ecb4..5491bd1ab7 100644 --- a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java +++ b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java @@ -585,7 +585,7 @@ private void drawPlayers(GraphicsContext gc) { World world = mapLoader.getWorld(); double blockScale = mapView.scale / 16.; for (PlayerEntityData player : world.currentDimension().getPlayerPositions()) { - if (player.dimension.equals(world.currentDimension().id())) { + if (player.dimension.equals(world.currentDimension().getId())) { int px = (int) QuickMath.floor(player.x * blockScale); int py = (int) QuickMath.floor(player.y); int pz = (int) QuickMath.floor(player.z * blockScale); diff --git a/chunky/src/java/se/llbit/chunky/world/Dimension.java b/chunky/src/java/se/llbit/chunky/world/Dimension.java index 48601c4bb3..97b001ae5d 100644 --- a/chunky/src/java/se/llbit/chunky/world/Dimension.java +++ b/chunky/src/java/se/llbit/chunky/world/Dimension.java @@ -48,7 +48,15 @@ protected Dimension(World world, String dimensionId, Set playe this.timestamp = timestamp; } - public String id() { + /** + * @return A user presentable name of the dimension + */ + public abstract String getName(); + + /** + * @return The dimension id, such as: {@code minecraft:overworld} (See {@link World#OVERWORLD_DIMENSION_ID}) + */ + public String getId() { return dimensionId; } @@ -114,11 +122,6 @@ public Heightmap getHeightmap() { return heightmap; } - /** - * @return A user presentable name of the dimension - */ - public abstract String toString(); - /** Add a chunk deletion listener. */ public void addChunkDeletionListener(ChunkDeletionListener listener) { synchronized (chunkDeletionListeners) { diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java index 9d24cd1e63..95260bf140 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java @@ -38,7 +38,7 @@ public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoad return new MCRegionChangeWatcher(worldMapLoader, mapView); } - @Override public String toString() { + @Override public String getName() { return "[empty dimension]"; } diff --git a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java index c8a2b129af..78e42fda08 100644 --- a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java @@ -113,7 +113,7 @@ private void loadSurfaceCubic(Map> data, ChunkData chu int[] heightmapData = extractHeightmapDataCubic(null, chunkData); updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - surface = new SurfaceLayer(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.id()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + surface = new SurfaceLayer(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.getId()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); } private int[] extractHeightmapDataCubic(Map cubeData, ChunkData chunkData) { diff --git a/chunky/src/java/se/llbit/chunky/world/JavaChunk.java b/chunky/src/java/se/llbit/chunky/world/JavaChunk.java index c0dd06d472..02f40777df 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaChunk.java @@ -133,7 +133,7 @@ private void loadSurface(@NotNull Tag data, ChunkData chunkData, int yMin, int y int[] heightmapData = extractHeightmapData(data, chunkData); updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - surface = new SurfaceLayer(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.id()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + surface = new SurfaceLayer(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.getId()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); queueTopography(); } } else { diff --git a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java index 28f4fba13d..8f1852ac9e 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java @@ -33,7 +33,7 @@ public synchronized boolean reloadPlayerData() { boolean changed = ((JavaWorld) this.world).reloadPlayerData(); if (changed) { this.setPlayerEntities(((JavaWorld) this.world).playerEntities.stream() - .filter(player -> player.dimension.equals(this.id())) + .filter(player -> player.dimension.equals(this.getId())) .collect(Collectors.toSet())); } return changed; @@ -121,7 +121,7 @@ public boolean regionExistsWithinRange(RegionPosition pos, int minY, int maxY) { /** * @return File object pointing to the region file directory */ - public synchronized File getRegionDirectory() { + public File getRegionDirectory() { return new File(getDimensionDirectory(), "region"); } @@ -134,12 +134,12 @@ public Date getLastModified() { * * @return File object pointing to the data directory */ - protected synchronized File getDimensionDirectory() { + protected File getDimensionDirectory() { return dimensionDirectory; } @Override - public String toString() { + public String getName() { return dimensionDirectory.getName() ; } } diff --git a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java index 8289982c5e..759df21ac1 100644 --- a/chunky/src/java/se/llbit/chunky/world/JavaWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java @@ -71,7 +71,7 @@ public class JavaWorld extends World { new String[] { NETHER_DIMENSION_ID, OVERWORLD_DIMENSION_ID, END_DIMENSION_ID } )); - protected int versionId; + protected final int versionId; /** * In a java world player data is per-world and not per-dimension, so we store it here. @@ -89,10 +89,12 @@ public class JavaWorld extends World { * @param seed * @param timestamp */ - protected JavaWorld(String levelName, File worldDirectory, long seed, long timestamp, Set playerEntities, Vector3i spawnPos) { + protected JavaWorld(String levelName, File worldDirectory, long seed, long timestamp, Set playerEntities, Vector3i spawnPos, int gameMode, int versionId) { super(levelName, worldDirectory, seed, timestamp); this.playerEntities = playerEntities; this.spawnPos = spawnPos; + this.gameMode = gameMode; + this.versionId = versionId; } @Override @@ -164,11 +166,7 @@ public static World loadWorld(File worldDirectory, LoggedWarnings warnings) { spawnPos = new Vector3i(0, 0, 0); } - JavaWorld world = new JavaWorld(levelName, worldDirectory, seed, modtime, playerEntities, spawnPos); - world.gameMode = gameType.intValue(0); - world.versionId = versionId.intValue(); - - return world; + return new JavaWorld(levelName, worldDirectory, seed, modtime, playerEntities, spawnPos, gameType.intValue(0), versionId.intValue()); } catch (FileNotFoundException e) { if (warnings == LoggedWarnings.NORMAL) { Log.infof("Could not find level.dat file for world %s!", levelName); @@ -293,8 +291,8 @@ public synchronized void exportChunksToZip(File target, Collection loadWorld(File dir) { return worldsByFormat.values().stream().findFirst(); } - /** - * @return The user-recognisable name of the world format. Shown to the user if this format has issues or throws. - */ String name(); /** From bcf8d121a2cc5a1a58b1563b92db61635f47c997 Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Sat, 23 Aug 2025 18:16:53 +0100 Subject: [PATCH 7/7] Make WorldFormats registerable --- .../se/llbit/chunky/map/WorldMapLoader.java | 4 +- .../se/llbit/chunky/renderer/scene/Scene.java | 4 +- .../ui/controller/WorldChooserController.java | 6 +-- .../world/worldformat/JavaWorldFormat.java | 19 +++++-- .../chunky/world/worldformat/WorldFormat.java | 37 +------------ .../world/worldformat/WorldFormats.java | 54 +++++++++++++++++++ 6 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormats.java diff --git a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index 8765a46db1..a4e72d7ab6 100644 --- a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java +++ b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java @@ -26,7 +26,7 @@ import se.llbit.chunky.world.region.RegionParser; import se.llbit.chunky.world.region.RegionQueue; import se.llbit.chunky.world.listeners.ChunkTopographyListener; -import se.llbit.chunky.world.worldformat.WorldFormat; +import se.llbit.chunky.world.worldformat.WorldFormats; import se.llbit.log.Log; import java.io.File; @@ -71,7 +71,7 @@ public void loadWorldFromDirectory(File worldLocation) { if (worldLocation == null) { return; } - this.loadWorld(WorldFormat.loadWorld(worldLocation).orElse(EmptyWorld.INSTANCE)); + this.loadWorld(WorldFormats.createWorld(worldLocation).orElse(EmptyWorld.INSTANCE)); } /** * This is called when a new world is loaded diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 1407b6e218..240fc8bb5f 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -53,7 +53,7 @@ import se.llbit.chunky.world.biome.Biome; import se.llbit.chunky.world.biome.BiomePalette; import se.llbit.chunky.world.biome.Biomes; -import se.llbit.chunky.world.worldformat.WorldFormat; +import se.llbit.chunky.world.worldformat.WorldFormats; import se.llbit.json.*; import se.llbit.log.Log; import se.llbit.math.*; @@ -544,7 +544,7 @@ public synchronized void loadScene(RenderContext context, String sceneName, Task loadedWorld = EmptyWorld.INSTANCE; if (!worldPath.isEmpty()) { File worldDirectory = new File(worldPath); - loadedWorld = WorldFormat.loadWorld(worldDirectory).orElse(EmptyWorld.INSTANCE); + loadedWorld = WorldFormats.createWorld(worldDirectory).orElse(EmptyWorld.INSTANCE); loadedWorld.loadDimension(this.worldDimension); } diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java index 04548c422d..28fcaa2681 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java @@ -34,7 +34,7 @@ import se.llbit.chunky.ui.TableSortConfigSerializer; import se.llbit.chunky.world.EmptyWorld; import se.llbit.chunky.world.World; -import se.llbit.chunky.world.worldformat.WorldFormat; +import se.llbit.chunky.world.worldformat.WorldFormats; import se.llbit.fxutil.Dialogs; import se.llbit.json.JsonArray; import se.llbit.log.Log; @@ -134,7 +134,7 @@ public void populate(WorldMapLoader mapLoader) { File directory = chooser.showDialog(stage); if (directory != null) { if (directory.isDirectory()) { - this.loadWorld(WorldFormat.loadWorld(directory).orElse(EmptyWorld.INSTANCE), mapLoader); + this.loadWorld(WorldFormats.createWorld(directory).orElse(EmptyWorld.INSTANCE), mapLoader); stage.close(); } else { Log.warn("Non-directory selected."); @@ -195,7 +195,7 @@ protected List call() { File[] worldDirs = worldSavesDir.listFiles(); if (worldDirs != null) { for (File dir : worldDirs) { - WorldFormat.loadWorld(dir).ifPresent(worlds::add); + WorldFormats.createWorld(dir).ifPresent(worlds::add); } } } diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java index 442bc6fc0f..7ac39abb1c 100644 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java @@ -3,22 +3,31 @@ import se.llbit.chunky.world.JavaWorld; import se.llbit.chunky.world.World; -import java.io.IOException; import java.nio.file.Path; public class JavaWorldFormat implements WorldFormat { @Override - public World loadWorld(Path path) throws IOException { - return JavaWorld.loadWorld(path.toFile(), World.LoggedWarnings.SILENT); + public String getName() { + return "Java (Anvil)"; } @Override - public String name() { - return "Java (Anvil)"; + public String getDescription() { + return "The Minecraft world format for Java worlds since 1.2.1 (12w07a)"; + } + + @Override + public String getId() { + return "JAVA_ANVIL"; } @Override public boolean isValid(Path path) { return JavaWorld.isWorldDir(path.toFile()); } + + @Override + public World loadWorld(Path path) { + return JavaWorld.loadWorld(path.toFile(), World.LoggedWarnings.SILENT); + } } diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java index 91cb490621..7eebf73e13 100644 --- a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java @@ -1,46 +1,13 @@ package se.llbit.chunky.world.worldformat; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import se.llbit.chunky.world.EmptyWorld; import se.llbit.chunky.world.World; -import se.llbit.log.Log; +import se.llbit.util.Registerable; -import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.*; /** For worlds that have multiple dimensions, and fully support the map view */ -public interface WorldFormat { - // TODO: Registerable - Collection worldFormats = List.of(new JavaWorldFormat()); - - // Should this go somewhere else? - static Optional loadWorld(File dir) { - Map worldsByFormat = new Object2ObjectOpenHashMap<>(); - - for (WorldFormat worldFormat : WorldFormat.worldFormats) { - if (worldFormat.isValid(dir.toPath())) { - try { - World world = worldFormat.loadWorld(dir.toPath()); - if (world != EmptyWorld.INSTANCE) { - worldsByFormat.put(worldFormat.name(), world); - } - } catch (IOException e) { - Log.error(String.format("An error occurred when trying to load a world using format `%s` from %s", worldFormat.name(), dir.getAbsolutePath()), e); - } - } - } - if (worldsByFormat.size() > 1) { - // Maybe allow the user to select which? - // This method is called from a variety of different popup/menu situations, is this ^ possible? - Log.warn(String.format("The directory %s has multiple valid world formats: %s", dir.getAbsolutePath(), String.join(", ", worldsByFormat.keySet()))); - } - return worldsByFormat.values().stream().findFirst(); - } - - String name(); - +public interface WorldFormat extends Registerable { /** * This method will be called on every possible world directory (typically this is every directory in `.minecraft/saves`). * diff --git a/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormats.java b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormats.java new file mode 100644 index 0000000000..38cbc32a32 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormats.java @@ -0,0 +1,54 @@ +package se.llbit.chunky.world.worldformat; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import se.llbit.chunky.world.EmptyWorld; +import se.llbit.chunky.world.World; +import se.llbit.log.Log; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class WorldFormats { + private static final Map worldFormatsById = new Object2ObjectOpenHashMap<>(); + + public static void addWorldFormat(WorldFormat worldFormat) { + worldFormatsById.put(worldFormat.getId(), worldFormat); + } + + public static Map getWorldFormats() { + return Collections.unmodifiableMap(worldFormatsById); + } + + public static WorldFormat getWorldFormat(String id) { + return worldFormatsById.get(id); + } + + static { + addWorldFormat(new JavaWorldFormat()); + } + + public static Optional createWorld(File dir) { + Map providedWorlds = new Object2ObjectOpenHashMap<>(); + + getWorldFormats().forEach((id, format) -> { + if (format.isValid(dir.toPath())) { + try { + World world = format.loadWorld(dir.toPath()); + if (world != EmptyWorld.INSTANCE) { + providedWorlds.put(format.getId(), world); + } + } catch (IOException e) { + Log.error(String.format("An error occurred when trying to load a world using format `%s` from %s", format.getName(), dir.getAbsolutePath()), e); + } + } + }); + + if (providedWorlds.size() > 1) { + // Maybe allow the user to select which? + // This method is called from a variety of different popup/menu situations, is this ^ possible? + Log.warn(String.format("The directory %s has multiple valid world formats: %s", dir.getAbsolutePath(), String.join(", ", providedWorlds.keySet()))); + } + return providedWorlds.values().stream().findFirst(); + } +}