diff --git a/README.md b/README.md
index 3edb78c6..94b2ffdc 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Windows launcher: [world-downloader-launcher.exe](https://github.com/mircokroon/
Latest cross-platform jar (command-line support): [world-downloader.jar](https://github.com/mircokroon/minecraft-world-downloader/releases/latest/download/world-downloader.jar)
### Basic usage
-[Download](https://github.com/mircokroon/minecraft-world-downloader/releases/latest/download/world-downloader.exe) the latest release and run it. Enter the server address in the address field and press start.
+[Download](https://github.com/mircokroon/minecraft-world-downloader-launcher/releases/latest/download/world-downloader-launcher.exe) the latest release and run it. Enter the server address in the address field and press start.
diff --git a/src/main/java/config/Config.java b/src/main/java/config/Config.java
index 3286bdf0..ab939e3f 100644
--- a/src/main/java/config/Config.java
+++ b/src/main/java/config/Config.java
@@ -434,6 +434,10 @@ public static PacketInjector getPacketInjector() {
usage = "Draw extended chunks to map")
public boolean drawExtendedChunks = false;
+ @Option(name = "--enable-cave-mode",
+ usage = "Enable automatically switching to cave render mode when underground.")
+ public boolean enableCaveRenderMode = false;
+
// not really important enough to have an option for, can change it in config file
public boolean smoothZooming = true;
@@ -500,6 +504,9 @@ public static boolean smoothZooming() {
public static boolean markOldChunks() {
return instance.markOldChunks;
}
+ public static boolean enableCaveRenderMode() {
+ return instance.enableCaveRenderMode;
+ }
public static MicrosoftAuthHandler getMicrosoftAuth() {
return instance.microsoftAuth;
diff --git a/src/main/java/game/data/LevelData.java b/src/main/java/game/data/LevelData.java
index d220c5c4..8d3c27f1 100644
--- a/src/main/java/game/data/LevelData.java
+++ b/src/main/java/game/data/LevelData.java
@@ -1,5 +1,7 @@
package game.data;
+import static util.ExceptionHandling.attempt;
+
import config.Config;
import config.Option;
import config.Version;
@@ -38,6 +40,7 @@ public LevelData(WorldManager worldManager) {
this.outputDir = PathUtils.toPath(Config.getWorldOutputDir());
this.file = Paths.get(outputDir.toString(), "level.dat").toFile();
this.levelDataModifiers = new ArrayList<>();
+ attempt(this::load);
}
public void registerModifier(Consumer fn) {
diff --git a/src/main/java/game/data/WorldManager.java b/src/main/java/game/data/WorldManager.java
index 2e606f8a..6fb0d9cf 100644
--- a/src/main/java/game/data/WorldManager.java
+++ b/src/main/java/game/data/WorldManager.java
@@ -76,7 +76,7 @@ public class WorldManager {
private boolean writeChunks;
private boolean isStarted;
private boolean isPaused;
- private Set savingDimension;
+ private final Set savingDimension;
private ContainerManager containerManager;
private CommandBlockManager commandBlockManager;
@@ -85,7 +85,8 @@ public class WorldManager {
private final RenderDistanceExtender renderDistanceExtender;
private BiConsumer playerPosListener;
- private CoordinateDouble3D playerPosition;
+ private final CoordinateDouble3D playerPosition;
+ private boolean isBelowGround = false;
private double playerRotation = 0;
private Dimension dimension;
private final EntityRegistry entityRegistry;
@@ -175,6 +176,25 @@ private void saveAndUnloadChunks() {
this.regions = new ConcurrentHashMap<>();
}
+ private void checkAboveSurface() {
+ Coordinate3D discrete = this.playerPosition.discretize();
+
+ Coordinate3D local = discrete.globalToChunkLocal();
+
+ if (dimension == Dimension.NETHER) {
+ isBelowGround = local.getY() < 128;
+ return;
+ }
+
+ Chunk c = getChunk(discrete.globalToChunk());
+ if (c == null) {
+ return;
+ }
+
+ int height = c.getChunkHeightHandler().heightAt(local.getX(), local.getZ()) - 5;
+ isBelowGround = local.getY() < height;
+ }
+
private void unloadChunks(Map regions) {
regions.values().forEach(Region::unloadAll);
}
@@ -398,6 +418,8 @@ public void start() {
}
private void save(Dimension dimension, Map regions) {
+ checkAboveSurface();
+
if (!writeChunks) {
return;
}
@@ -721,5 +743,9 @@ public boolean canForget(CoordinateDim2D co) {
public int countExtendedChunks() {
return renderDistanceExtender.countLoaded();
}
+
+ public boolean isBelowGround() {
+ return isBelowGround;
+ }
}
diff --git a/src/main/java/game/data/chunk/Cave.java b/src/main/java/game/data/chunk/Cave.java
new file mode 100644
index 00000000..74fb952e
--- /dev/null
+++ b/src/main/java/game/data/chunk/Cave.java
@@ -0,0 +1,71 @@
+package game.data.chunk;
+
+import game.data.chunk.palette.BlockState;
+import game.data.chunk.palette.SimpleColor;
+import java.util.Objects;
+
+/**
+ * Cave class used in image rendering
+ */
+public class Cave {
+ int y;
+ int depth;
+ BlockState block;
+
+ Cave(int y, BlockState block) {
+ this.y = y;
+ this.block = block;
+ this.depth = 1;
+ }
+
+ public void addDepth() {
+ depth += 1;
+ }
+
+ public int y() { return y; }
+
+ public int depth() { return depth; }
+
+ public BlockState block() { return block; }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) { return true; }
+ if (obj == null || obj.getClass() != this.getClass()) { return false; }
+ var that = (Cave) obj;
+ return this.y == that.y &&
+ this.depth == that.depth &&
+ Objects.equals(this.block, that.block);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(y, depth, block);
+ }
+
+ @Override
+ public String toString() {
+ return "Cave[" +
+ "y=" + y + ", " +
+ "depth=" + depth + ", " +
+ "block=" + block + ']';
+ }
+
+ /**
+ * Adjust colour based on height and depth of cave
+ */
+ public SimpleColor getColor() {
+ double brightness = 230 * (0.05 + (Math.log(depth) / Math.log(80)) * 0.9);
+ SimpleColor caveDepth = new SimpleColor(10, brightness / 2, brightness)
+ .blendWith(new SimpleColor(brightness, 10, 10), map(-80, 100, y));
+
+ SimpleColor blockCol = block.getColor();
+ return caveDepth.blendWith(blockCol, .85);
+ }
+
+ private double map(double min, double max, double val) {
+ if (val < min) { return 0; }
+ if (val > max) { return 1; }
+ return (val - min) / (max - min);
+ }
+}
diff --git a/src/main/java/game/data/chunk/Chunk.java b/src/main/java/game/data/chunk/Chunk.java
index 9c3d7702..12bd0683 100644
--- a/src/main/java/game/data/chunk/Chunk.java
+++ b/src/main/java/game/data/chunk/Chunk.java
@@ -13,6 +13,7 @@
import game.protocol.Protocol;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import org.apache.commons.lang3.mutable.MutableBoolean;
import packets.DataTypeProvider;
import packets.builder.PacketBuilder;
import se.llbit.nbt.*;
@@ -40,6 +41,12 @@ public static void registerNbtModifier(BiConsumer fn) {
private boolean saved;
private ChunkImageFactory imageFactory;
+ public ChunkHeightHandler getChunkHeightHandler() {
+ return chunkHeightHandler;
+ }
+
+ private ChunkHeightHandler chunkHeightHandler;
+
private final int dataVersion;
public Chunk(CoordinateDim2D location, int dataVersion) {
@@ -480,6 +487,8 @@ public void unload() {
public ChunkImageFactory getChunkImageFactory() {
if (imageFactory == null) {
+ chunkHeightHandler = new ChunkHeightHandler(this);
+
// assignment should happen before running initialisation code
imageFactory = new ChunkImageFactory(this);
imageFactory.initialise();
@@ -518,7 +527,8 @@ public void updateBlock(Coordinate3D coords, int blockStateId, boolean suppressU
}
if (this.imageFactory != null) {
- this.imageFactory.updateHeight(coords);
+ this.chunkHeightHandler.updateHeight(coords);
+ this.imageFactory.generateImages();
}
}
@@ -544,7 +554,10 @@ public void updateBlocks(Coordinate3D pos, DataTypeProvider provider) {
updateBlock(blockPos, blockId, true);
}
- this.getChunkImageFactory().recomputeHeights(toUpdate);
+ boolean wasChanged = this.chunkHeightHandler.recomputeHeights(toUpdate);
+ if (wasChanged) {
+ imageFactory.generateImages();
+ }
}
public void updateLight(DataTypeProvider provider) {
diff --git a/src/main/java/game/data/chunk/ChunkHeightHandler.java b/src/main/java/game/data/chunk/ChunkHeightHandler.java
new file mode 100644
index 00000000..4eeb9f58
--- /dev/null
+++ b/src/main/java/game/data/chunk/ChunkHeightHandler.java
@@ -0,0 +1,155 @@
+package game.data.chunk;
+
+import game.data.chunk.palette.BlockState;
+import game.data.chunk.palette.SimpleColor;
+import game.data.coordinates.Coordinate3D;
+import game.data.dimension.Dimension;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.apache.commons.lang3.mutable.MutableBoolean;
+
+public class ChunkHeightHandler {
+ private int[] heightMap;
+ private int[] heightMapBelowBedrock;
+ private List[] caves;
+ Chunk c;
+
+ public ChunkHeightHandler(Chunk c) {
+ this.c = c;
+
+ this.computeHeightMap();
+ }
+
+ private void computeHeightMap() {
+ if (this.heightMap != null) {
+ return;
+ }
+
+ this.heightMapBelowBedrock = new int[Chunk.SECTION_WIDTH * Chunk.SECTION_WIDTH];
+ this.heightMap = new int[Chunk.SECTION_WIDTH * Chunk.SECTION_WIDTH];
+ this.caves = new List[Chunk.SECTION_WIDTH * Chunk.SECTION_WIDTH];
+ for (int x = 0; x < Chunk.SECTION_WIDTH; x++) {
+ for (int z = 0; z < Chunk.SECTION_WIDTH; z++) {
+ heightMapBelowBedrock[z << 4 | x] = computeHeight(x, z, true);
+ heightMap[z << 4 | x] = computeHeight(x, z, false);
+ caves[z << 4 | x] = findCaves(x, z);
+ }
+ }
+ }
+
+ /**
+ * Computes the height at a given location. When we are in the nether, we want to try and make it clear where there
+ * is an opening, and where there is not. For this we skip the first two chunks sections (these will be mostly solid
+ * anyway, but may contain misleading caves). We then only count blocks after we've found some air space.
+ */
+ private int computeHeight(int x, int z, boolean ignoredBedrockAbove) {
+ // if we're in the Nether, we want to find an air block before we start counting blocks.
+ boolean isNether = ignoredBedrockAbove && c.location.getDimension().equals(Dimension.NETHER);
+ int topSection = isNether ? 5 : c.getMaxBlockSection();
+
+ MutableBoolean foundAir = new MutableBoolean(!isNether);
+
+ for (int sectionY = topSection; sectionY >= c.getMinBlockSection(); sectionY--) {
+ ChunkSection cs = c.getChunkSection(sectionY);
+ if (cs == null) {
+ foundAir.setTrue();
+ continue;
+ }
+
+ int height = cs.computeHeight(x, z, foundAir);
+
+ if (height < 0) { continue; }
+
+ // if we're in the nether we can't find
+ if (isNether && sectionY == topSection && height == 15) {
+ return 127;
+ }
+ return (sectionY * Chunk.SECTION_HEIGHT) + height;
+ }
+ return isNether ? 127 : 0;
+ }
+
+ /**
+ * We need to update the image only if the updated block was either the top layer, or above the top layer.
+ * Technically this does not take transparent blocks into account, but that's fine.
+ */
+ public boolean updateHeight(Coordinate3D coords) {
+ if (coords.getY() >= heightAt(coords.getX(), coords.getZ())) {
+ recomputeHeight(coords.getX(), coords.getZ());
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Recompute the heights in the given coordinate collection. We keep track of which heights actually changed, and
+ * only redraw if we have to.
+ */
+ public boolean recomputeHeights(Collection toUpdate) {
+ boolean hasChanged = false;
+ for (Coordinate3D pos : toUpdate) {
+ if (pos.getY() >= heightAt(pos.getX(), pos.getZ())) {
+ hasChanged |= recomputeHeight(pos.getX(), pos.getZ());
+ }
+ }
+ return hasChanged;
+ }
+
+ private boolean recomputeHeight(int x, int z) {
+ int beforeAboveBedrock = heightMapBelowBedrock[z << 4 | x];
+ int before = heightMap[z << 4 | x];
+
+ int afterAboveBedrock = computeHeight(x, z, true);
+ heightMapBelowBedrock[z << 4 | x] = afterAboveBedrock;
+
+ int after = computeHeight(x, z, false);
+ heightMap[z << 4 | x] = after;
+ caves[z << 4 | x] = getCaves(x, z);
+
+ return before != after || beforeAboveBedrock != afterAboveBedrock;
+ }
+
+ public int heightAt(int x, int z) {
+ return heightAt(x, z, false);
+ }
+
+ public int heightAt(int x, int z, boolean belowBedrock) {
+ return belowBedrock ? heightMapBelowBedrock[z << 4 | x] : heightMap[z << 4 | x];
+ }
+
+ private List findCaves(int x, int z) {
+ int surface = heightAt(x, z);
+ surface = Math.min(60, surface);
+
+ List caves = new ArrayList<>();
+
+ int base = c.getMinBlockSection() * Chunk.SECTION_HEIGHT;
+ BlockState state = null;
+
+ Cave cave = null;
+ boolean inCave = false;
+ for (int y = base; y < surface; y++) {
+ BlockState curState = c.getBlockStateAt(x, y, z);
+
+ boolean isEmpty = curState == null || curState.getColor() == SimpleColor.BLACK;
+ if (inCave && isEmpty) {
+ cave.addDepth();
+ } else if (inCave) {
+ inCave = false;
+ } else if (isEmpty && state != null) {
+ cave = new Cave(y, state);
+ caves.add(cave);
+ inCave = true;
+ }
+ state = curState;
+ }
+
+ return caves;
+ }
+
+ public List getCaves(int x, int z) {
+ return caves[z << 4 | x];
+ }
+}
diff --git a/src/main/java/game/data/chunk/ChunkImageFactory.java b/src/main/java/game/data/chunk/ChunkImageFactory.java
index c968c466..8ec3891d 100644
--- a/src/main/java/game/data/chunk/ChunkImageFactory.java
+++ b/src/main/java/game/data/chunk/ChunkImageFactory.java
@@ -1,323 +1,278 @@
-package game.data.chunk;
-
-import game.data.WorldManager;
-import game.data.chunk.palette.BlockState;
-import game.data.chunk.palette.SimpleColor;
-import game.data.chunk.palette.blending.IBlendEquation;
-import game.data.coordinates.Coordinate3D;
-import game.data.coordinates.CoordinateDim2D;
-import game.data.dimension.Dimension;
-import java.nio.IntBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.function.BiConsumer;
-import javafx.scene.image.Image;
-import javafx.scene.image.WritableImage;
-import javafx.scene.image.WritablePixelFormat;
-import org.apache.commons.lang3.mutable.MutableBoolean;
-
-/**
- * Handles creating images from a Chunk.
- */
-public class ChunkImageFactory {
- private final List registeredCallbacks = new ArrayList<>(2);
- private final Runnable requestImage = this::requestImage;;
- private BiConsumer onImageDone;
- private Runnable onSaved;
-
- private final Chunk c;
- private Chunk south;
- private Chunk north;
-
- private int[] heightMap;
-
- private boolean drawnBefore = false;
-
- public ChunkImageFactory(Chunk c) {
- this.c = c;
-
- c.setOnUnload(this::unload);
- }
-
- /**
- * Since image factories will call upon image factories of neighbouring chunks, we need to make
- * sure it can be assigned first so that we don't end up making duplicates and causing memory
- * leaks. Initialise should be called immediately after assignment.
- */
- public void initialise() {
- computeHeightMap();
- WorldManager.getInstance().chunkLoadedCallback(c);
- }
-
- /**
- * Set handler for when the image has been created.
- */
- public void onComplete(BiConsumer onComplete) {
- this.onImageDone = onComplete;
- }
-
- public void onSaved(Runnable onSaved) {
- this.onSaved = onSaved;
- }
-
- public void markSaved() {
- if (this.onSaved != null) {
- this.onSaved.run();
- }
- }
-
- private void registerChunkLoadCallback(CoordinateDim2D coordinates) {
- registeredCallbacks.add(coordinates);
- WorldManager.getInstance().registerChunkLoadCallback(coordinates, requestImage);
- }
-
- public void unload() {
- for (CoordinateDim2D coords : registeredCallbacks) {
- WorldManager.getInstance().deregisterChunkLoadCallback(coords, requestImage);
- }
- }
-
- public int heightAt(int x, int z) {
- return heightMap[z << 4 | x];
- }
-
- /**
- * Compares the blocks south and north, use the gradient to get a multiplier for the colour.
- * @return a colour multiplier to adjust the color value by. If they elevations are the same it will be 1.0, if the
- * northern block is above the current its 0.8, otherwise its 1.2.
- */
- private double getColorShader(int x, int y, int z) {
- int yNorth = getOtherHeight(x, z, 0, -1, north);
- if (yNorth < 0) { yNorth = y; }
-
- int ySouth = getOtherHeight(x, z, 15, 1, south);
- if (ySouth < 0) { ySouth = y; }
-
- if (ySouth < yNorth) {
- return 0.6 + (0.4 / (1 + yNorth - ySouth));
- } else if (ySouth > yNorth) {
- return 1.6 - (0.6 / Math.sqrt(ySouth - yNorth));
- }
- return 1;
- }
-
- /**
- * Get the height of a neighbouring block. If the block is not on this chunk, either load it or register a callback
- * for when it becomes available.
- */
- private int getOtherHeight(int x, int z, int zLimit, int offsetZ, Chunk other) {
- if (z != zLimit) {
- return heightAt(x, z + offsetZ);
- }
-
- if (other == null) {
- return -1;
- } else {
- return other.getChunkImageFactory().heightAt(x, 15 - zLimit);
- }
- }
-
-
- public void requestImage() {
- // this method is only called either the first time by the UI, or subsequently by callbacks
- // when adjacent chunks load in. This means that if we only had a single callback registered
- // we don't need to worry about de-registering it anymore.
- if (drawnBefore && registeredCallbacks.size() == 1) {
- registeredCallbacks.clear();
- }
-
- createImage();
- }
- /**
- * Generate and return the overview image for this chunk.
- */
- private void createImage() {
- WritableImage i = new WritableImage(Chunk.SECTION_WIDTH, Chunk.SECTION_WIDTH);
- int[] output = new int[Chunk.SECTION_WIDTH * Chunk.SECTION_WIDTH];
- WritablePixelFormat format = WritablePixelFormat.getIntArgbInstance();
-
- // setup north/south chunks
- setupAdjacentChunks();
- drawnBefore = true;
-
- try {
- for (int x = 0; x < Chunk.SECTION_WIDTH; x++) {
- for (int z = 0; z < Chunk.SECTION_WIDTH; z++) {
- int y = heightAt(x, z);
- BlockState blockState = c.getBlockStateAt(x, y, z);
-
- SimpleColor color;
- if (blockState == null) {
- output[x + Chunk.SECTION_WIDTH * z] = new SimpleColor(0).toARGB();
- continue;
- } else {
- color = shadeTransparent(blockState, x, y, z);
- }
-
- color = color.shaderMultiply(getColorShader(x, y, z));
-
- // mark new chunks in a red-ish outline
- if (c.isNewChunk() && ((x == 0 || x == 15) || (z == 0 || z == 15))) {
- color = color.highlight();
- }
-
- output[x + Chunk.SECTION_WIDTH * z] = color.toARGB();
- }
- }
- i.getPixelWriter().setPixels(
- 0, 0,
- Chunk.SECTION_WIDTH, Chunk.SECTION_WIDTH,
- format, output, 0, Chunk.SECTION_WIDTH
- );
- } catch (Exception ex) {
- System.out.println("Unable to draw picture for chunk at " + c.location);
- ex.printStackTrace();
- clearAdjacentChunks();
- }
-
- if (this.onImageDone != null) {
- this.onImageDone.accept(i, c.isSaved());
- }
- clearAdjacentChunks();
- }
-
- /**
- * Clear references to north/south chunks after drawing. If we don't do this we may keep the
- * chunks from getting GCd causing memory leaks (especially when moving long distances in north/
- * south direction)
- */
- private void clearAdjacentChunks() {
- this.north = null;
- this.south = null;
- }
-
- private void setupAdjacentChunks() {
- CoordinateDim2D coordinateSouth = c.location.addWithDimension(0, 1);
- this.south = WorldManager.getInstance().getChunk(coordinateSouth);
-
- CoordinateDim2D coordinateNorth = c.location.addWithDimension(0, -1);
- this.north = WorldManager.getInstance().getChunk(coordinateNorth);
-
- if (!drawnBefore) {
- if (this.south == null) {
- registerChunkLoadCallback(coordinateSouth);
- }
- if (this.north == null) {
- registerChunkLoadCallback(coordinateNorth);
- }
- }
- }
-
- private SimpleColor shadeTransparent(BlockState blockState, int x, int y, int z) {
- SimpleColor color = blockState.getColor();
- BlockState next;
- for (int level = y - 1; blockState.isTransparent() && level >= 0; level--) {
- next = c.getBlockStateAt(x, level, z);
-
- if (next == blockState) {
- continue;
- } else if (next == null) {
- break;
- }
-
- IBlendEquation equation = blockState.getTransparencyEquation();
- double ratio = equation.getRatio(y - level);
- color = color.blendWith(next.getColor(), ratio);
-
- // stop once the contribution to the colour is less than 10%
- if (ratio > 0.90) {
- break;
- }
- blockState = next;
- }
- return color;
- }
-
- private void computeHeightMap() {
- if (this.heightMap != null) {
- return;
- }
-
- this.heightMap = new int[Chunk.SECTION_WIDTH * Chunk.SECTION_WIDTH];
- for (int x = 0; x < Chunk.SECTION_WIDTH; x++) {
- for (int z = 0; z < Chunk.SECTION_WIDTH; z++) {
- heightMap[z << 4 | x] = computeHeight(x, z);
- }
- }
- }
-
- /**
- * Computes the height at a given location. When we are in the nether, we want to try and make it clear where there
- * is an opening, and where there is not. For this we skip the first two chunks sections (these will be mostly solid
- * anyway, but may contain misleading caves). We then only count blocks after we've found some air space.
- */
- private int computeHeight(int x, int z) {
- // if we're in the Nether, we want to find an air block before we start counting blocks.
- boolean isNether = c.location.getDimension().equals(Dimension.NETHER);
- int topSection = isNether ? 5 : c.getMaxBlockSection();
-
- MutableBoolean foundAir = new MutableBoolean(!isNether);
-
- for (int sectionY = topSection; sectionY >= c.getMinBlockSection(); sectionY--) {
- ChunkSection cs = c.getChunkSection(sectionY);
- if (cs == null) {
- foundAir.setTrue();
- continue;
- }
-
- int height = cs.computeHeight(x, z, foundAir);
-
- if (height < 0) { continue; }
-
- // if we're in the nether we can't find
- if (isNether && sectionY == topSection && height == 15) {
- return 127;
- }
- return (sectionY * Chunk.SECTION_HEIGHT) + height;
- }
- return isNether ? 127 : 0;
- }
-
- /**
- * We need to update the image only if the updated block was either the top layer, or above the top layer.
- * Technically this does not take transparent blocks into account, but that's fine.
- */
- public void updateHeight(Coordinate3D coords) {
- if (coords.getY() >= heightAt(coords.getX(), coords.getZ())) {
- recomputeHeight(coords.getX(), coords.getZ());
- createImage();
- }
- }
-
- /**
- * Recompute the heights in the given coordinate collection. We keep track of which heights actually changed, and
- * only redraw if we have to.
- */
- public void recomputeHeights(Collection toUpdate) {
- boolean hasChanged = false;
- for (Coordinate3D pos : toUpdate) {
- if (pos.getY() >= heightAt(pos.getX(), pos.getZ())) {
- hasChanged |= recomputeHeight(pos.getX(), pos.getZ());
- }
- }
- if (hasChanged) {
- createImage();
- }
- }
-
- private boolean recomputeHeight(int x, int z) {
- int before = heightMap[z << 4 | x];
- int after = computeHeight(x, z);
- heightMap[z << 4 | x] = after;
-
- return before != after;
- }
-
- @Override
- public String toString() {
- return "ChunkImageFactory{" +
- "c=" + c.location +
- '}';
- }
-}
+package game.data.chunk;
+
+import game.data.WorldManager;
+import game.data.chunk.palette.BlockState;
+import game.data.chunk.palette.SimpleColor;
+import game.data.chunk.palette.blending.IBlendEquation;
+import game.data.coordinates.CoordinateDim2D;
+import gui.images.ImageMode;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import javafx.scene.image.Image;
+import javafx.scene.image.WritableImage;
+import javafx.scene.image.WritablePixelFormat;
+
+/**
+ * Handles creating images from a Chunk.
+ */
+public class ChunkImageFactory {
+ private final List registeredCallbacks = new ArrayList<>(2);
+ private final Runnable requestImage = this::requestImage;
+ ;
+ private BiConsumer