Skip to content

Commit a1d74b7

Browse files
authored
Add CubicChunks support (#1031)
* Make Region an interface, add MCRegion for vanilla regions * World now handles creation of region objects, Region now handles parsing datastream into NBT data * Add yMin variable to the map view * Create region package ready for additional region formats * Add min and max Y to getChunkData requests. Added bonus of speeding up chunk parsing, in addition to being required for CubicChunks * Add min and max Y parameters to Region#parse * Add cubic chunks support * Fix incorrectly reparsing an imposter even if the regions haven't changed (led to continually refreshing map view) * Correct using incorrect header size for checks * Lots of javadoc. Renaming and refactoring for readability. No impactful code changes * Use Log.warn instead of err.print in region parsing, remove dead code * Use Log.warnf instead of Log.warn(String.format) * Add CubicRegionChangeWatcher to track CubicRegion updates * prevent weird race condition if map view is still loading regions when loading chunks into scene * optimise getRegion for map view and region parser to avoid extremely slow iteration over all files in region dir * implement suggestions
1 parent b34ca16 commit a1d74b7

25 files changed

Lines changed: 1291 additions & 187 deletions

chunky/src/java/se/llbit/chunky/map/MapTile.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import se.llbit.chunky.world.ChunkPosition;
2222
import se.llbit.chunky.world.ChunkSelectionTracker;
2323
import se.llbit.chunky.world.ChunkView;
24-
import se.llbit.chunky.world.Region;
24+
import se.llbit.chunky.world.region.Region;
2525

2626
/**
2727
* A tile in the 2D world map or minimap. The tile contains either a chunk or a region.
@@ -73,7 +73,7 @@ public void draw(MapBuffer buffer, WorldMapLoader mapLoader, ChunkView view,
7373
}
7474
}
7575
} else {
76-
Region region = mapLoader.getWorld().getRegion(pos);
76+
Region region = mapLoader.getWorld().getRegionWithinRange(pos, view.yMin, view.yMax);
7777
int pixelOffset = 0;
7878
for (int z = 0; z < 32; ++z) {
7979
for (int x = 0; x < 32; ++x) {

chunky/src/java/se/llbit/chunky/map/MapView.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public synchronized Vector2 getPosition() {
5151
public void setMapSize(int width, int height) {
5252
ChunkView mapView = map.get();
5353
if (width != mapView.width || height != mapView.height) {
54-
map.set(new ChunkView(mapView.x, mapView.z, width, height, mapView.scale, mapView.yMax));
54+
map.set(new ChunkView(mapView.x, mapView.z, width, height, mapView.scale, mapView.yMin, mapView.yMax));
5555
notifyViewUpdated();
5656
}
5757
}
@@ -76,7 +76,7 @@ public void panTo(Vector3 pos) {
7676
public synchronized void panTo(double x, double z) {
7777
ChunkView mapView = map.get();
7878
map.set(new ChunkView(x, z,
79-
mapView.width, mapView.height, mapView.scale, mapView.yMax));
79+
mapView.width, mapView.height, mapView.scale, mapView.yMin, mapView.yMax));
8080
notifyViewUpdated();
8181
}
8282

@@ -95,7 +95,7 @@ public synchronized void setScale(int blockScale) {
9595
ChunkView mapView = map.get();
9696
blockScale = ChunkView.clampScale(blockScale);
9797
if (blockScale != mapView.scale) {
98-
map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, blockScale, mapView.yMax));
98+
map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, blockScale, mapView.yMin, mapView.yMax));
9999
notifyViewUpdated();
100100
}
101101
}
@@ -139,8 +139,24 @@ public int getYMax() {
139139
public void setYMax(int yMax) {
140140
ChunkView mapView = map.get();
141141
if (yMax != mapView.yMax) {
142-
map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, mapView.scale, yMax));
142+
map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, mapView.scale, mapView.yMin, yMax));
143143
notifyViewUpdated();
144144
}
145145
}
146+
147+
public int getYMin() {
148+
return map.get().yMin;
149+
}
150+
151+
/**
152+
* Change the maximum y layer that is shown.
153+
*/
154+
public void setYMin(int yMin) {
155+
ChunkView mapView = map.get();
156+
if (yMin != mapView.yMin) {
157+
map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, mapView.scale, yMin, mapView.yMax));
158+
notifyViewUpdated();
159+
}
160+
}
161+
146162
}

chunky/src/java/se/llbit/chunky/map/SurfaceLayer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ public class SurfaceLayer extends BitmapLayer {
5050
* @param dim current dimension
5151
* @param chunkData data for the chunk
5252
*/
53-
public SurfaceLayer(int dim, ChunkData chunkData, BlockPalette palette, int yMax) {
53+
public SurfaceLayer(int dim, ChunkData chunkData, BlockPalette palette, int yMin, int yMax) {
5454
bitmap = new int[Chunk.X_MAX * Chunk.Z_MAX];
5555
topo = new int[Chunk.X_MAX * Chunk.Z_MAX];
5656
for (int x = 0; x < Chunk.X_MAX; ++x) {
5757
for (int z = 0; z < Chunk.Z_MAX; ++z) {
5858

5959
// Find the topmost non-empty block.
6060
int y = Math.min(chunkData.maxY() - 1, yMax);
61-
int minY = chunkData.minY();
61+
int minY = Math.max(chunkData.minY(), yMin);
6262
for (; y > minY; --y) {
6363
if (palette.get(chunkData.getBlockAt(x, y, z)) != Air.INSTANCE) {
6464
break;

chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import se.llbit.chunky.world.ChunkTopographyUpdater;
2626
import se.llbit.chunky.world.ChunkView;
2727
import se.llbit.chunky.world.EmptyWorld;
28-
import se.llbit.chunky.world.RegionChangeWatcher;
29-
import se.llbit.chunky.world.RegionParser;
30-
import se.llbit.chunky.world.RegionQueue;
28+
import se.llbit.chunky.world.region.RegionChangeWatcher;
29+
import se.llbit.chunky.world.region.RegionParser;
30+
import se.llbit.chunky.world.region.RegionQueue;
3131
import se.llbit.chunky.world.World;
3232
import se.llbit.chunky.world.listeners.ChunkTopographyListener;
3333

@@ -41,10 +41,12 @@
4141
*/
4242
public class WorldMapLoader implements ChunkTopographyListener, ChunkViewListener {
4343
private final ChunkyFxController controller;
44+
private final MapView mapView;
4445

4546
private World world = EmptyWorld.INSTANCE;
4647

4748
private final RegionQueue regionQueue = new RegionQueue();
49+
private RegionChangeWatcher regionChangeWatcher = null;
4850

4951
private final ChunkTopographyUpdater topographyUpdater = new ChunkTopographyUpdater();
5052

@@ -55,8 +57,8 @@ public class WorldMapLoader implements ChunkTopographyListener, ChunkViewListene
5557

5658
public WorldMapLoader(ChunkyFxController controller, MapView mapView) {
5759
this.controller = controller;
60+
this.mapView = mapView;
5861
mapView.addViewListener(this);
59-
RegionChangeWatcher regionWatcher = new RegionChangeWatcher(this, mapView);
6062

6163
// Start worker threads.
6264
RegionParser[] regionParsers = new RegionParser[Integer.parseInt(System.getProperty("chunky.mapLoaderThreads", String.valueOf(PersistentSettings.getNumThreads())))];
@@ -65,7 +67,6 @@ public WorldMapLoader(ChunkyFxController controller, MapView mapView) {
6567
regionParsers[i].start();
6668
}
6769
topographyUpdater.start();
68-
regionWatcher.start();
6970
}
7071

7172
/**
@@ -81,6 +82,8 @@ public void loadWorld(File worldDir) {
8182
newWorld.addChunkTopographyListener(this);
8283
synchronized (this) {
8384
world = newWorld;
85+
updateRegionChangeWatcher(newWorld);
86+
8487
File newWorldDir = world.getWorldDirectory();
8588
if (newWorldDir != null) {
8689
PersistentSettings.setLastWorld(newWorldDir);
@@ -144,10 +147,20 @@ public void reloadWorld() {
144147
newWorld.addChunkTopographyListener(this);
145148
synchronized (this) {
146149
world = newWorld;
150+
updateRegionChangeWatcher(newWorld);
147151
}
148152
worldLoadListeners.forEach(listener -> listener.accept(newWorld, true));
149153
}
150154

155+
/** Stops the current RegionChangeWatcher, and creates a new one for the specified world */
156+
private void updateRegionChangeWatcher(World newWorld) {
157+
if(regionChangeWatcher != null) {
158+
regionChangeWatcher.interrupt();
159+
}
160+
regionChangeWatcher = newWorld.createRegionChangeWatcher(this, mapView);
161+
regionChangeWatcher.start();
162+
}
163+
151164
/**
152165
* Set the current dimension.
153166
*

chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -853,16 +853,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
853853

854854
isLoading = true;
855855

856-
boolean isTallWorld = world.getVersionId() >= World.VERSION_21W06A;
857-
if (isTallWorld) {
858-
// snapshot 21w06a or later, don't limit yMin/yMax to allow custom height worlds
859-
yMin = yClipMin;
860-
yMax = yClipMax;
861-
} else {
862-
// treat as 0 - 256 world
863-
yMin = Math.max(0, yClipMin);
864-
yMax = Math.min(256, yClipMax);
865-
}
856+
yMin = yClipMin;
857+
yMax = yClipMax;
866858

867859
Set<ChunkPosition> loadedChunks = new HashSet<>();
868860
int numChunks = 0;
@@ -894,7 +886,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
894886
}
895887

896888
for (ChunkPosition region : regions) {
897-
world.getRegion(region).parse();
889+
world.getRegion(region).parse(yMin, yMax);
898890
}
899891
}
900892

@@ -940,15 +932,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
940932
Set<ChunkPosition> legacyChunks = new HashSet<>();
941933
Heightmap biomeIdMap = new Heightmap();
942934

943-
ChunkData chunkData1;
944-
ChunkData chunkData2;
945-
if (isTallWorld) { //snapshot 21w06a, treat as -64 - 320
946-
chunkData1 = new GenericChunkData(); // chunk loading will switch between these two, using one asynchronously to load the data
947-
chunkData2 = new GenericChunkData(); // while the other is used to add to the octree
948-
} else { //Treat as 0 - 256 world
949-
chunkData1 = new SimpleChunkData();
950-
chunkData2 = new SimpleChunkData();
951-
}
935+
ChunkData chunkData1 = world.createChunkData(); // chunk loading will switch between these two, using one asynchronously to load the data
936+
ChunkData chunkData2 = world.createChunkData(); // while the other is used to add to the octree
952937

953938
try (TaskTracker.Task task = taskTracker.task("(3/6) Loading chunks")) {
954939
int done = 1;
@@ -963,7 +948,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
963948

964949
ExecutorService executor = Executors.newSingleThreadExecutor();
965950
Future<?> nextChunkDataTask = executor.submit(() -> { //Initialise first chunk data for the for loop
966-
world.getChunk(chunkPositions[0]).getChunkData(chunkData1, palette);
951+
world.getChunk(chunkPositions[0]).getChunkData(chunkData1, palette, yMin, yMax);
967952
});
968953
for (int i = 0; i < chunkPositions.length; i++) {
969954
ChunkPosition cp = chunkPositions[i];
@@ -987,10 +972,10 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
987972
}
988973

989974
if(usingFirstChunkData) {
990-
world.getChunk(chunkPositions[i]).getChunkData(chunkData1, palette);
975+
world.getChunk(chunkPositions[i]).getChunkData(chunkData1, palette, yMin, yMax);
991976
}
992977
else {
993-
world.getChunk(chunkPositions[i]).getChunkData(chunkData2, palette);
978+
world.getChunk(chunkPositions[i]).getChunkData(chunkData2, palette, yMin, yMax);
994979
}
995980
} catch(ExecutionException e) {
996981
throw new RuntimeException(e.getCause());
@@ -1010,8 +995,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
1010995

1011996
if (i + 1 < chunkPositions.length) { //if has next request next
1012997
final int finalI = i;
1013-
nextChunkDataTask = executor.submit(() -> { //Initialise first chunk data for the for loop
1014-
world.getChunk(chunkPositions[finalI + 1]).getChunkData(nextChunkData, palette);
998+
nextChunkDataTask = executor.submit(() -> { //request chunk data for the next iteration of the loop
999+
world.getChunk(chunkPositions[finalI + 1]).getChunkData(nextChunkData, palette, yMin, yMax);
10151000
});
10161001
}
10171002
}

chunky/src/java/se/llbit/chunky/ui/ChunkyFxController.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ public class ChunkyFxController
122122
@FXML private ToggleButton netherBtn;
123123
@FXML private ToggleButton endBtn;
124124
@FXML private IntegerAdjuster scale;
125+
@FXML private IntegerAdjuster yMin;
125126
@FXML private IntegerAdjuster yMax;
126127
@FXML private ToggleButton trackPlayerBtn;
127128
@FXML private ToggleButton trackCameraBtn;
@@ -393,7 +394,7 @@ public synchronized void exportZip(File targetFile, ProgressTracker progress) {
393394
map = new ChunkMap(mapLoader, this, mapView, chunkSelection,
394395
mapCanvas, mapOverlay);
395396

396-
AtomicBoolean ignoreYMaxUpdate = new AtomicBoolean(false); // used to not trigger a world reload after changing the world, see #926
397+
AtomicBoolean ignoreYUpdate = new AtomicBoolean(false); // used to not trigger a world reload after changing the world, see #926
397398
mapLoader.addWorldLoadListener(
398399
(world, reloaded) -> {
399400
if (!reloaded) {
@@ -409,15 +410,19 @@ public synchronized void exportZip(File targetFile, ProgressTracker progress) {
409410
mapView.panTo(playerPos.orElse(new Vector3(0, 0, 0)));
410411
}
411412
if (!reloaded) {
412-
ignoreYMaxUpdate.set(true);
413+
ignoreYUpdate.set(true);
413414
if (mapLoader.getWorld().getVersionId() >= World.VERSION_21W06A) {
415+
yMin.setRange(-64, 320);
416+
yMin.set(-64);
414417
yMax.setRange(-64, 320);
415418
yMax.set(320);
416419
} else {
420+
yMin.setRange(0, 256);
421+
yMin.set(0);
417422
yMax.setRange(0, 256);
418423
yMax.set(256);
419424
}
420-
ignoreYMaxUpdate.set(false);
425+
ignoreYUpdate.set(false);
421426
}
422427
map.redrawMap();
423428
mapName.setText(world.levelName());
@@ -485,8 +490,14 @@ public synchronized void exportZip(File targetFile, ProgressTracker progress) {
485490
}));
486491
scale.valueProperty().addListener(new GroupedChangeListener<>(group,
487492
(observable, oldValue, newValue) -> mapView.setScale(newValue.intValue())));
493+
yMin.valueProperty().addListener(new GroupedChangeListener<>(group, (observable, oldValue, newValue) -> {
494+
if (!ignoreYUpdate.get()) {
495+
mapView.setYMin(newValue.intValue());
496+
mapLoader.reloadWorld();
497+
}
498+
}));
488499
yMax.valueProperty().addListener(new GroupedChangeListener<>(group, (observable, oldValue, newValue) -> {
489-
if (!ignoreYMaxUpdate.get()) {
500+
if (!ignoreYUpdate.get()) {
490501
mapView.setYMax(newValue.intValue());
491502
mapLoader.reloadWorld();
492503
}
@@ -655,8 +666,10 @@ public synchronized void exportZip(File targetFile, ProgressTracker progress) {
655666

656667
mapLoader.loadWorld(PersistentSettings.getLastWorld());
657668
if (mapLoader.getWorld().getVersionId() >= World.VERSION_21W06A) {
669+
mapView.setYMin(-64);
658670
mapView.setYMax(320);
659671
} else {
672+
mapView.setYMin(0);
660673
mapView.setYMax(256);
661674
}
662675

0 commit comments

Comments
 (0)