Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions chunky/src/java/se/llbit/chunky/map/MapTile.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ 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);
HeightRange heightRange = new HeightRange(view.yMin, view.yMax);
boolean isValid = mapLoader.getWorld().currentDimension().hasRegionWithinRange(regionPos, heightRange);
Region region = mapLoader.getWorld().currentDimension().getRegionWithinRange(regionPos, heightRange);
int pixelOffset = 0;
for (int z = 0; z < 32; ++z) {
for (int x = 0; x < 32; ++x) {
Expand Down
100 changes: 76 additions & 24 deletions chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@
*/
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;
import se.llbit.chunky.ui.controller.ChunkyFxController;
import se.llbit.chunky.world.*;
import se.llbit.chunky.world.java.JavaWorldFormat;
import se.llbit.chunky.world.region.RegionChangeWatcher;
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 se.llbit.util.annotation.Nullable;

import java.io.File;
import java.util.ArrayList;
Expand Down Expand Up @@ -64,28 +69,75 @@ public WorldMapLoader(ChunkyFxController controller, MapView mapView) {
topographyUpdater.start();
}

public void loadWorldFromDirectory(@Nullable File worldLocation, @Nullable String worldFormatId) {
if (worldLocation != null) {
if (worldFormatId == null || worldFormatId.isEmpty()) {
worldFormatId = JavaWorldFormat.ID;
}
Optional<World.Info> info = WorldFormats.getWorldFormat(worldFormatId)
.flatMap(format -> format.getWorldInfo(worldLocation.toPath())) // attempt to get the given format
.or(() -> WorldFormats.getInfos(worldLocation.toPath()).stream().findFirst()); // get any format
info.ifPresent(this::loadWorld);
return;
}
setWorld(EmptyWorld.INSTANCE);
}

/**
* This is called when a new world is loaded
* Load the world referred to by the {@link World.Info}
*
* @return The loaded world. May be {@link EmptyWorld} if loading failed.
*/
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 World loadWorld(World.Info info) {
World newWorld = WorldFormats.createWorld(info);

Optional<Dimension.Identifier> dimensionToLoad = Optional.of(world.currentDimension())
.map(Dimension::getDimensionId)
.filter(dimension -> newWorld.getAvailableDimensions().contains(dimension))
.or(newWorld::getDefaultDimension)
.or(() -> newWorld.getAvailableDimensions().stream().findFirst());

if (dimensionToLoad.isPresent()) {
newWorld.loadDimension(dimensionToLoad.get());
} else {
Log.infof("No dimension loaded for world %s", info.toString());
}

setWorld(newWorld);
return this.world;
}

/**
* Sets the map view world.
* <p>This is intended to be called with worlds with a dimension already loaded, as it will not trigger dimension
* loading.</p>
*
* @param newWorld The world to set
*/
public void setWorld(World newWorld) {
if (this.world != null) {
this.world.currentDimension().removeChunkTopographyListener(this);
}

boolean isSameWorld = !(this.world instanceof EmptyWorld) && newWorld.getInfo().path().equals(this.world.getInfo().path());

Dimension loadedDim = newWorld.currentDimension();
if (loadedDim == EmptyWorld.INSTANCE.currentDimension()) {
Log.warn("Map view world was set but it has no dimension!");
}

loadedDim.addChunkTopographyListener(this);
synchronized (this) {
this.world = newWorld;
updateRegionChangeWatcher(loadedDim);

File newWorldDir = this.world.getInfo().path().toFile();
if (!newWorldDir.equals(PersistentSettings.getLastWorld())) {
PersistentSettings.setLastWorld(newWorldDir);
}
worldLoadListeners.forEach(listener -> listener.accept(newWorld, isSameWorld));
PersistentSettings.setLastWorldFormat(newWorld.getInfo().worldFormat().getId());
}
worldLoadListeners.forEach(listener -> listener.accept(newWorld, isSameWorld));
}

/**
Expand Down Expand Up @@ -151,14 +203,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);
Comment on lines 205 to +207

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the listeners, loading the same dimension again, and adding the listeners could be handled internally in e.g. world.currentDimension().reload() or World.reloadCurrentDimension()

WorldMapLoader and World were tightly coupled before, so this might be out-of-scope for this PR.

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
}

Expand All @@ -173,6 +223,8 @@ private void updateRegionChangeWatcher(Dimension dimension) {

/**
* Set the current dimension.
*
* @param value Must be a valid dimension see {@link World#getAvailableDimensions()}
*/
public void setDimension(Dimension.Identifier value) {
if (value != currentDimensionId) {
Expand Down
35 changes: 24 additions & 11 deletions chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
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.region.MCRegion;
import se.llbit.chunky.world.java.JavaWorldFormat;
import se.llbit.chunky.world.region.Region;
import se.llbit.chunky.world.worldformat.WorldFormats;
import se.llbit.json.*;
import se.llbit.log.Log;
import se.llbit.math.*;
Expand All @@ -67,6 +69,7 @@
import se.llbit.util.mojangapi.MinecraftProfile;

import java.io.*;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -192,6 +195,7 @@ public class Scene implements JsonSerializable {
*/
protected int rayDepth = PersistentSettings.getRayDepthDefault();
protected String worldPath = "";
protected String worldFormat = JavaWorldFormat.ID;
protected Dimension.Identifier worldDimension = Dimension.Identifier.OVERWORLD;
protected RenderMode mode = RenderMode.PREVIEW;
protected int dumpFrequency = DEFAULT_DUMP_FREQUENCY;
Expand Down Expand Up @@ -404,6 +408,7 @@ public synchronized void copyState(Scene other, boolean copyChunks) {
if (copyChunks) {
loadedWorld = other.loadedWorld;
worldPath = other.worldPath;
worldFormat = other.worldFormat;
worldDimension = other.worldDimension;

// The octree reference is overwritten to save time.
Expand Down Expand Up @@ -546,11 +551,17 @@ 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);
Path worldDirectory = Path.of(worldPath);
Optional<World.Info> info = WorldFormats.getWorldFormat(worldFormat) // only try to load the world as its known world format.
.flatMap(format -> format.getWorldInfo(worldDirectory));

if (info.isPresent()) {
World newWorld = WorldFormats.createWorld(info.get());
loadedWorld = newWorld;
loadedWorld.loadDimension(this.worldDimension);
if (newWorld == EmptyWorld.INSTANCE) {
Log.info("Could not load world: " + worldPath);
}
}
}

Expand Down Expand Up @@ -767,7 +778,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, ChunkSelectionTracker.selectionByRegion(chunks));
refresh();
}
Expand Down Expand Up @@ -803,7 +814,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Map<Re
task.update(2, 1);

loadedWorld = world;
worldPath = loadedWorld.getWorldDirectory().getAbsolutePath();
worldPath = loadedWorld.getInfo().path().toAbsolutePath().toString();
worldFormat = world.getInfo().worldFormat().getId();
worldDimension = world.currentDimension().getDimensionId();

if (chunksToLoadByRegion.isEmpty()) {
Expand Down Expand Up @@ -860,8 +872,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Map<Re

ExecutorService executor = Executors.newSingleThreadExecutor();

ChunkData[] regionParsingDataArray = new ChunkData[MCRegion.CHUNKS_X * MCRegion.CHUNKS_Z];
ChunkData[] chunkLoadingDataArray = new ChunkData[MCRegion.CHUNKS_X * MCRegion.CHUNKS_Z];
ChunkData[] regionParsingDataArray = new ChunkData[Region.CHUNKS_X * Region.CHUNKS_Z];
ChunkData[] chunkLoadingDataArray = new ChunkData[Region.CHUNKS_X * Region.CHUNKS_Z];

BiFunction<RegionPosition, ChunkData[], Future<List<ObjectObjectImmutablePair<ChunkPosition, ChunkData>>>> createRegionDataFuture = (regionPosition, chunkDataArray) -> executor.submit(() -> {
List<ChunkPosition> chunkPositionsToLoad = chunksToLoadByRegion.get(regionPosition);
Expand Down Expand Up @@ -2670,6 +2682,7 @@ public void setUseCustomWaterColor(boolean value) {
// Save world info.
JsonObject world = new JsonObject();
world.add("path", worldPath);
world.add("worldType", loadedWorld.getInfo().worldFormat().getId());
world.add("dimension", worldDimension.getNamespacedName());
json.add("world", world);
}
Expand Down Expand Up @@ -2992,7 +3005,7 @@ else if(waterShader.equals("SIMPLEX"))
if (json.get("world").isObject()) {
JsonObject world = json.get("world").object();
worldPath = world.get("path").stringValue(worldPath);

worldFormat = world.get("worldFormat").stringValue(JavaWorldFormat.ID);
if (world.get("dimension") instanceof JsonString) {
// dimension already is a string (or undefined)
worldDimension = Dimension.Identifier.fromNamespacedName(world.get("dimension").stringValue("minecraft:overworld"));
Expand Down
36 changes: 13 additions & 23 deletions chunky/src/java/se/llbit/chunky/ui/ChunkMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,19 +39,17 @@
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;
import se.llbit.chunky.world.region.MCRegion;
import se.llbit.chunky.world.region.Region;
import se.llbit.log.Log;
import se.llbit.math.*;

import java.awt.*;
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;
Expand Down Expand Up @@ -227,8 +219,8 @@ public ChunkMap(WorldMapLoader loader, ChunkyFxController controller,
if (view.chunkScale >= 16) {
int minChunkX = region.x << 5;
int minChunkZ = region.z << 5;
for (int chunkX = minChunkX; chunkX < minChunkX + MCRegion.CHUNKS_X; chunkX++) {
for (int chunkZ = minChunkZ; chunkZ < minChunkZ + MCRegion.CHUNKS_Z; chunkZ++) {
for (int chunkX = minChunkX; chunkX < minChunkX + Region.CHUNKS_X; chunkX++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still assumes that all worlds are made out of regions with 32x32 chunks, doesn't it?

@NotStirred NotStirred Jun 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, currently I've got "virtual" regions in bedrock that just pass calls to the dimension.

Any worldformat could use the virtual regions, and they aren't format specific (schematics/etc.). Maybe it's the simplest approach.

It does mean every dimension needs to hold onto a bunch of virtual regions that don't do much, which is unfortunate

for (int chunkZ = minChunkZ; chunkZ < minChunkZ + Region.CHUNKS_Z; chunkZ++) {
mapBuffer.drawTile(mapLoader, new ChunkPosition(chunkX, chunkZ), chunkSelection);
}
}
Expand Down Expand Up @@ -596,18 +588,16 @@ private void drawPlayers(GraphicsContext gc) {
World world = mapLoader.getWorld();
double blockScale = mapView.scale / 16.;
for (PlayerEntityData player : world.currentDimension().getPlayerPositions()) {
if (player.dimension.equals(world.currentDimension().getDimensionId())) {
int px = (int) QuickMath.floor(player.x * blockScale);
int py = (int) QuickMath.floor(player.y);
int pz = (int) QuickMath.floor(player.z * blockScale);
int ppx = px - (int) QuickMath.floor(mapView.x0 * mapView.scale);
int ppy = pz - (int) QuickMath.floor(mapView.z0 * mapView.scale);
int pw = (int) QuickMath.max(16, QuickMath.min(32, blockScale * 4));
ppx = Math.min(mapView.width - pw, Math.max(0, ppx - pw / 2));
ppy = Math.min(mapView.height - pw, Math.max(0, ppy - pw / 2));

gc.drawImage(Icon.player.fxImage(), ppx, ppy, pw, pw);
}
int px = (int) QuickMath.floor(player.x * blockScale);
int py = (int) QuickMath.floor(player.y);
int pz = (int) QuickMath.floor(player.z * blockScale);
int ppx = px - (int) QuickMath.floor(mapView.x0 * mapView.scale);
int ppy = pz - (int) QuickMath.floor(mapView.z0 * mapView.scale);
int pw = (int) QuickMath.max(16, QuickMath.min(32, blockScale * 4));
ppx = Math.min(mapView.width - pw, Math.max(0, ppx - pw / 2));
ppy = Math.min(mapView.height - pw, Math.max(0, ppy - pw / 2));

gc.drawImage(Icon.player.fxImage(), ppx, ppy, pw, pw);
}
}

Expand Down
Loading
Loading