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/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index daafe632e0..a4e72d7ab6 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.WorldFormats; +import se.llbit.log.Log; import java.io.File; import java.util.ArrayList; @@ -46,7 +49,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<>(); @@ -64,28 +67,44 @@ public WorldMapLoader(ChunkyFxController controller, MapView mapView) { topographyUpdater.start(); } + public void loadWorldFromDirectory(File worldLocation) { + if (worldLocation == null) { + return; + } + this.loadWorld(WorldFormats.createWorld(worldLocation).orElse(EmptyWorld.INSTANCE)); + } /** * This is called when a new world is loaded */ - public void loadWorld(File worldDir) { - if (World.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::getId) + .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,10 +191,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#availableDimensions()} */ - 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 +205,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 e2770c1cac..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,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.WorldFormats; import se.llbit.json.*; import se.llbit.log.Log; import se.llbit.math.*; @@ -190,7 +191,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; @@ -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 (World.isWorldDir(worldDirectory)) { - loadedWorld = World.loadWorld(worldDirectory, worldDimension, World.LoggedWarnings.NORMAL); - } else { - Log.info("Could not load world: " + worldPath); - } + loadedWorld = WorldFormats.createWorld(worldDirectory).orElse(EmptyWorld.INSTANCE); + loadedWorld.loadDimension(this.worldDimension); } loadDump(context, taskTracker); @@ -761,7 +759,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 +792,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec loadedWorld = world; worldPath = loadedWorld.getWorldDirectory().getAbsolutePath(); - worldDimension = world.currentDimensionId(); + worldDimension = world.currentDimension().getId(); if (chunksToLoad.isEmpty()) { return; @@ -821,7 +819,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 +1190,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); } } @@ -2907,7 +2906,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 a7603177fc..5491bd1ab7 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; @@ -593,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().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/ui/controller/ChunkyFxController.java b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java index 7fd253d564..439fce669e 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; @@ -74,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; @@ -363,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()); } } @@ -482,19 +479,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); @@ -632,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 -> { @@ -655,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)); @@ -679,14 +672,10 @@ public File getSceneFile(String fileName) { mapOverlay.setOnKeyPressed(map::onKeyPressed); 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); - } + mapLoader.loadWorldFromDirectory(PersistentSettings.getLastWorld()); + 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/controller/WorldChooserController.java b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java index a2575d6808..28fcaa2681 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/WorldChooserController.java @@ -31,9 +31,10 @@ 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.WorldFormats; import se.llbit.fxutil.Dialogs; import se.llbit.json.JsonArray; import se.llbit.log.Log; @@ -133,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(WorldFormats.createWorld(directory).orElse(EmptyWorld.INSTANCE), mapLoader); stage.close(); } else { Log.warn("Non-directory selected."); @@ -169,7 +170,7 @@ private void loadWorld(World world, WorldMapLoader mapLoader) { } } }); - mapLoader.loadWorld(world.getWorldDirectory()); + mapLoader.loadWorld(world); } /** @@ -186,7 +187,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,10 +195,7 @@ 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)); - } + WorldFormats.createWorld(dir).ifPresent(worlds::add); } } } 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..046e5818be 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, String 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..97b001ae5d 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,28 +11,23 @@ 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; -import java.io.File; import java.util.*; /** * */ -public class Dimension { - private final World world; - - protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); - - protected final File dimensionDirectory; +public abstract class Dimension { + protected final World world; private Set playerEntities; 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<>(); @@ -45,18 +39,24 @@ public 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, String dimensionId, Set playerEntities, long timestamp) { this.world = world; this.dimensionId = dimensionId; - this.dimensionDirectory = dimensionDirectory; this.playerEntities = playerEntities; this.timestamp = timestamp; } - public int getDimensionId() { + /** + * @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; } @@ -64,37 +64,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,72 +89,17 @@ 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 - */ - 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); - } - - /** - * Get the data directory for the given dimension. + * 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 File object pointing to the data directory + * @return The height range of the dimension. */ - protected synchronized File getDimensionDirectory() { - return dimensionDirectory; - } + public abstract IntIntPair heightRange(); + + public abstract RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView); - /** - * @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 +122,49 @@ 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)); + /** Add a chunk deletion listener. */ + public void addChunkDeletionListener(ChunkDeletionListener listener) { + synchronized (chunkDeletionListeners) { + chunkDeletionListeners.add(listener); } } - /** Notify region update listeners. */ - private void fireChunkUpdated(ChunkPosition chunk) { - synchronized (chunkUpdateListeners) { - for (ChunkUpdateListener listener : chunkUpdateListeners) { - listener.chunkUpdated(chunk); - } + /** + * 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); } } - /** Notify region update listeners. */ - private void fireRegionUpdated(RegionPosition region) { + /** Add a region discovery listener. */ + public void addChunkUpdateListener(ChunkUpdateListener listener) { synchronized (chunkUpdateListeners) { - for (ChunkUpdateListener listener : chunkUpdateListeners) { - listener.regionUpdated(region); - } + chunkUpdateListeners.add(listener); } } - @Override public String toString() { - return dimensionDirectory.getName() ; - } - /** 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 +192,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 +202,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..95260bf140 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java @@ -1,16 +1,59 @@ 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(); private EmptyDimension() { - super(EmptyWorld.INSTANCE, 0, null, Collections.emptySet(), -1); + super(EmptyWorld.INSTANCE, World.OVERWORLD_DIMENSION_ID, 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 String toString() { + @Override + public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { + return new MCRegionChangeWatcher(worldMapLoader, mapView); + } + + @Override public String getName() { 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/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 b584e4f4b3..bed0ed4b0e 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java @@ -16,6 +16,14 @@ */ package se.llbit.chunky.world; +import se.llbit.chunky.ui.ProgressTracker; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + /** * Represents an empty or non-existent world. * @@ -31,8 +39,28 @@ private EmptyWorld() { this.currentDimension = EmptyDimension.INSTANCE; } + @Override + public Set availableDimensions() { + return Collections.emptySet(); + } + + @Override + public Optional defaultDimension() { + return Optional.empty(); + } + + @Override + public EmptyDimension loadDimension(String dimension) { + return EmptyDimension.INSTANCE; + } + + @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..78e42fda08 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) { @@ -110,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.getId()), chunkData, palette, biomePalette, yMin, yMax, heightmapData); } private int[] extractHeightmapDataCubic(Map cubeData, ChunkData chunkData) { @@ -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..02f40777df --- /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(JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(dimension.getId()), 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..8f1852ac9e --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/JavaDimension.java @@ -0,0 +1,145 @@ +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.*; +import java.util.stream.Collectors; + +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, String dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + super(world, dimensionId, playerEntities, timestamp); + this.dimensionDirectory = dimensionDirectory; + } + + /** + * Reload player data. + * @return {@code true} if player data was reloaded. + */ + 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.getId())) + .collect(Collectors.toSet())); + } + return changed; + } + + /** + * @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 File getRegionDirectory() { + return new File(getDimensionDirectory(), "region"); + } + + 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 File getDimensionDirectory() { + return dimensionDirectory; + } + + @Override + 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 new file mode 100644 index 0000000000..759df21ac1 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/JavaWorld.java @@ -0,0 +1,407 @@ +/* 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 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; +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; + + /** 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 } + )); + + protected final 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). + * @param worldDirectory Minecraft world directory. + * @param seed + * @param timestamp + */ + 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 + 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, + this.playerEntities.stream().filter(player -> player.dimension.equals(dimension)).collect(Collectors.toSet()) + ); + return currentDimension; + } + + /** + * Parse player location and level name. + * + * @return {@code true} if the world data was loaded + */ + 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(); + 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"); // 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"); + Tag randomSeed = result.get(".Data.RandomSeed"); + levelName = MinecraftText.removeFormatChars(result.get(".Data.LevelName").stringValue(levelName)); + + long seed = randomSeed.longValue(0); + + Set playerEntities = getPlayerEntityData(worldDirectory, player); + + boolean haveSpawnPos = !(spawnX.isError() || spawnY.isError() || spawnZ.isError()); + Vector3i spawnPos; + if (haveSpawnPos) { + spawnPos = new Vector3i(spawnX.intValue(0), spawnY.intValue(0), spawnZ.intValue(0)); + } else { + spawnPos = new Vector3i(0, 0, 0); + } + + 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); + } + } 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 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 { + dimension = new JavaDimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); + } + return dimension; + } + + @NotNull + static Set getPlayerEntityData(File worldDirectory, Tag player) { + Set playerEntities = new HashSet<>(); + if (!player.isError()) { + playerEntities.add(new PlayerEntityData(player)); + } + loadAdditionalPlayers(worldDirectory, playerEntities); + 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"); + 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; + } + 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()); + } + } + } + } + } + + protected synchronized File getDataDirectory(int dimension) { + return dimension == 0 ? + worldDirectory : + new File(worldDirectory, "DIM" + dimension); + } + + 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 { + if (this.currentDimension == EmptyDimension.INSTANCE) { + return; + } + JavaDimension currentDim = (JavaDimension) this.currentDimension; + + 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 = + currentDim.getId().equals(JavaWorld.OVERWORLD_DIMENSION_ID) ? currentDim.getDimensionDirectory().getName() : + currentDim.getDimensionDirectory().getName() + "/DIM" + JavaWorld.VANILLA_DIMENSION_ID_TO_IDX.get(currentDim.getId()); + 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, currentDim.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_IDX); + WorldScanner.findExistingChunks(overworld, operator); + WorldScanner.findExistingChunks(getRegionDirectory(NETHER_DIMENSION_IDX), operator); + WorldScanner.findExistingChunks(getRegionDirectory(END_DIMENSION_IDX), 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; + } + +} 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 ac3a2967ca..7948529f3c 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,40 +28,24 @@ * * @author Jesper Öqvist */ -public class World implements Comparable { - - /** The currently supported NBT version of level.dat files. */ - public static final int NBT_VERSION = 19133; - - /** 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 abstract class World implements Comparable { + 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; - /** 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 Dimension currentDimension; - protected int currentDimensionId; + protected final File worldDirectory; - private final String levelName; - private int gameMode = 0; - private final long seed; + protected Dimension currentDimension = EmptyDimension.INSTANCE; - 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,172 +65,33 @@ public enum LoggedWarnings { SILENT } - public void loadDimension(int dimensionId) { - currentDimension = loadDimension(this, this.worldDirectory, dimensionId, -1, Collections.emptySet()); - currentDimension.reloadPlayerData(); - } - /** - * Parse player location and level name. + * The dimensions returned here are later provided to {@link #loadDimension(String)} when requesting a dimension be + * loaded. * - * @return {@code true} if the world data was loaded + * @return List the viewable dimensions within the world. */ - public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnings warnings) { - 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; - } + public abstract Set availableDimensions(); /** - * 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. + * MUST be one of {@link #availableDimensions()} + * @return The preferred default dimension of this world (typically the overworld) */ - 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()); - } - } - } - } - } + public abstract Optional defaultDimension(); /** - * @return The current dimension + * @param dimension The dimension to load, guaranteed to be one of the dimensions previously returned by {@link #availableDimensions()} + * @return The loaded dimension */ - public synchronized Dimension currentDimension() { - return this.currentDimension; - } + public abstract Dimension loadDimension(String dimension); /** * @return The current dimension */ - public synchronized int currentDimensionId() { - return this.currentDimensionId; + public synchronized Dimension currentDimension() { + return this.currentDimension; } - /** * @return The world directory */ @@ -266,24 +99,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 +107,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 +118,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,22 +130,6 @@ public String levelName() { return levelName; } - 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; - } - /** * @return String describing the game-mode of this world */ 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); 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..7ac39abb1c --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/JavaWorldFormat.java @@ -0,0 +1,33 @@ +package se.llbit.chunky.world.worldformat; + +import se.llbit.chunky.world.JavaWorld; +import se.llbit.chunky.world.World; + +import java.nio.file.Path; + +public class JavaWorldFormat implements WorldFormat { + @Override + public String getName() { + return "Java (Anvil)"; + } + + @Override + 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 new file mode 100644 index 0000000000..7eebf73e13 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/worldformat/WorldFormat.java @@ -0,0 +1,26 @@ +package se.llbit.chunky.world.worldformat; + +import se.llbit.chunky.world.World; +import se.llbit.util.Registerable; + +import java.io.IOException; +import java.nio.file.Path; + +/** For worlds that have multiple dimensions, and fully support the map view */ +public interface WorldFormat extends Registerable { + /** + * 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 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(); + } +} 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() {