Skip to content

Commit

Permalink
Merge pull request #590 from mircokroon/map-improvements
Browse files Browse the repository at this point in the history
Map improvements
  • Loading branch information
mircokroon authored Aug 27, 2023
2 parents df72afd + a4ad4d1 commit 69ed430
Show file tree
Hide file tree
Showing 29 changed files with 1,585 additions and 779 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img src="https://i.imgur.com/yH8SH5C.png">

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/game/data/LevelData.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package game.data;

import static util.ExceptionHandling.attempt;

import config.Config;
import config.Option;
import config.Version;
Expand Down Expand Up @@ -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<Tag> fn) {
Expand Down
30 changes: 28 additions & 2 deletions src/main/java/game/data/WorldManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public class WorldManager {
private boolean writeChunks;
private boolean isStarted;
private boolean isPaused;
private Set<Dimension> savingDimension;
private final Set<Dimension> savingDimension;

private ContainerManager containerManager;
private CommandBlockManager commandBlockManager;
Expand All @@ -85,7 +85,8 @@ public class WorldManager {
private final RenderDistanceExtender renderDistanceExtender;

private BiConsumer<CoordinateDouble3D, Double> playerPosListener;
private CoordinateDouble3D playerPosition;
private final CoordinateDouble3D playerPosition;
private boolean isBelowGround = false;
private double playerRotation = 0;
private Dimension dimension;
private final EntityRegistry entityRegistry;
Expand Down Expand Up @@ -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<CoordinateDim2D, Region> regions) {
regions.values().forEach(Region::unloadAll);
}
Expand Down Expand Up @@ -398,6 +418,8 @@ public void start() {
}

private void save(Dimension dimension, Map<CoordinateDim2D, Region> regions) {
checkAboveSurface();

if (!writeChunks) {
return;
}
Expand Down Expand Up @@ -721,5 +743,9 @@ public boolean canForget(CoordinateDim2D co) {
public int countExtendedChunks() {
return renderDistanceExtender.countLoaded();
}

public boolean isBelowGround() {
return isBelowGround;
}
}

71 changes: 71 additions & 0 deletions src/main/java/game/data/chunk/Cave.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 15 additions & 2 deletions src/main/java/game/data/chunk/Chunk.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -40,6 +41,12 @@ public static void registerNbtModifier(BiConsumer<Chunk, Tag> 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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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) {
Expand Down
155 changes: 155 additions & 0 deletions src/main/java/game/data/chunk/ChunkHeightHandler.java
Original file line number Diff line number Diff line change
@@ -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<Coordinate3D> 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<Cave> findCaves(int x, int z) {
int surface = heightAt(x, z);
surface = Math.min(60, surface);

List<Cave> 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<Cave> getCaves(int x, int z) {
return caves[z << 4 | x];
}
}
Loading

0 comments on commit 69ed430

Please sign in to comment.