Skip to content

Commit

Permalink
Split World and Dimension into their own classes (chunky-dev#1611)
Browse files Browse the repository at this point in the history
* Split the concepts of Dimension and World into their own class.

World handles per-world data, and loading Dimensions

Dimension handles regions, chunks, loading chunks, and any per-dimension data.

This commit fails to run without commenting out all of the CubicChunks classes

* Properly handle loading cubic dimensions

* Don't reload world when switching dimensions, only load the new dimension

* Correct following project naming conventions
  • Loading branch information
NotStirred authored and leMaik committed Sep 9, 2023
1 parent 087869d commit 4f84bfa
Show file tree
Hide file tree
Showing 21 changed files with 586 additions and 609 deletions.
6 changes: 3 additions & 3 deletions chunky/src/java/se/llbit/chunky/map/MapTile.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ public void drawCached(MapBuffer buffer, WorldMapLoader mapLoader, ChunkView vie
public void draw(MapBuffer buffer, WorldMapLoader mapLoader, ChunkView view,
ChunkSelectionTracker selection) {
if (scale >= 16) {
Chunk chunk = mapLoader.getWorld().getChunk(pos);
Chunk chunk = mapLoader.getWorld().currentDimension().getChunk(pos);
renderChunk(chunk);
if (!(chunk instanceof EmptyChunk) && selection.isSelected(pos)) {
for (int i = 0; i < tileWidth * tileWidth; ++i) {
pixels[i] = selectionTint(pixels[i]);
}
}
} else {
boolean isValid = mapLoader.getWorld().regionExistsWithinRange(pos, view.yMin, view.yMax);
Region region = mapLoader.getWorld().getRegionWithinRange(pos, view.yMin, view.yMax);
boolean isValid = mapLoader.getWorld().currentDimension().regionExistsWithinRange(pos, view.yMin, view.yMax);
Region region = mapLoader.getWorld().currentDimension().getRegionWithinRange(pos, view.yMin, view.yMax);
int pixelOffset = 0;
for (int z = 0; z < 32; ++z) {
for (int x = 0; x < 32; ++x) {
Expand Down
62 changes: 33 additions & 29 deletions chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,10 @@
import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.renderer.ChunkViewListener;
import se.llbit.chunky.ui.controller.ChunkyFxController;
import se.llbit.chunky.world.Chunk;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.ChunkTopographyUpdater;
import se.llbit.chunky.world.ChunkView;
import se.llbit.chunky.world.EmptyWorld;
import se.llbit.chunky.world.*;
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.World;
import se.llbit.chunky.world.listeners.ChunkTopographyListener;

import java.io.File;
Expand All @@ -51,7 +46,7 @@ public class WorldMapLoader implements ChunkTopographyListener, ChunkViewListene
private final ChunkTopographyUpdater topographyUpdater = new ChunkTopographyUpdater();

/** The dimension to load in the current world. */
private int currentDimension = PersistentSettings.getDimension();
private int currentDimensionId = PersistentSettings.getDimension();

private List<BiConsumer<World, Boolean>> worldLoadListeners = new ArrayList<>();

Expand All @@ -70,20 +65,19 @@ public WorldMapLoader(ChunkyFxController controller, MapView mapView) {
}

/**
* This is called when a new world is loaded, and when switching to a
* different dimension.
* This is called when a new world is loaded
*/
public void loadWorld(File worldDir) {
if (World.isWorldDir(worldDir)) {
if (world != null) {
world.removeChunkTopographyListener(this);
world.currentDimension().removeChunkTopographyListener(this);
}
boolean isSameWorld = !(world instanceof EmptyWorld) && worldDir.equals(world.getWorldDirectory());
World newWorld = World.loadWorld(worldDir, currentDimension, World.LoggedWarnings.NORMAL);
newWorld.addChunkTopographyListener(this);
World newWorld = World.loadWorld(worldDir, currentDimensionId, World.LoggedWarnings.NORMAL);
newWorld.currentDimension().addChunkTopographyListener(this);
synchronized (this) {
world = newWorld;
updateRegionChangeWatcher(newWorld);
updateRegionChangeWatcher(newWorld.currentDimension());

File newWorldDir = world.getWorldDirectory();
if (newWorldDir != null) {
Expand All @@ -94,6 +88,20 @@ public void loadWorld(File worldDir) {
}
}

/**
* This is called when switching to a different dimension within the same world
*/
public void loadDimension() {
world.currentDimension().removeChunkTopographyListener(this);

world.loadDimension(currentDimensionId);
world.currentDimension().addChunkTopographyListener(this);
synchronized (this) {
updateRegionChangeWatcher(world.currentDimension());
}
worldLoadListeners.forEach(listener -> listener.accept(world, false));
}

/** Adds a listener to be notified when a new world has been loaded. */
public void addWorldLoadListener(BiConsumer<World, Boolean> callback) {
worldLoadListeners.add(callback);
Expand Down Expand Up @@ -142,24 +150,24 @@ public void regionUpdated(ChunkPosition region) {
*/
public void reloadWorld() {
topographyUpdater.clearQueue();
world.removeChunkTopographyListener(this);
World newWorld = World.loadWorld(world.getWorldDirectory(), currentDimension,
world.currentDimension().removeChunkTopographyListener(this);
World newWorld = World.loadWorld(world.getWorldDirectory(), currentDimensionId,
World.LoggedWarnings.NORMAL);
newWorld.addChunkTopographyListener(this);
newWorld.currentDimension().addChunkTopographyListener(this);
synchronized (this) {
world = newWorld;
updateRegionChangeWatcher(newWorld);
updateRegionChangeWatcher(newWorld.currentDimension());
}
worldLoadListeners.forEach(listener -> listener.accept(newWorld, true));
viewUpdated(mapView.getMapView()); // update visible chunks immediately
}

/** Stops the current RegionChangeWatcher, and creates a new one for the specified world */
private void updateRegionChangeWatcher(World newWorld) {
private void updateRegionChangeWatcher(Dimension dimension) {
if(regionChangeWatcher != null) {
regionChangeWatcher.interrupt();
}
regionChangeWatcher = newWorld.createRegionChangeWatcher(this, mapView);
regionChangeWatcher = dimension.createRegionChangeWatcher(this, mapView);
regionChangeWatcher.start();
}

Expand All @@ -169,21 +177,17 @@ private void updateRegionChangeWatcher(World newWorld) {
* @param value Must be a valid dimension index (0, -1, 1)
*/
public void setDimension(int value) {
if (value != currentDimension) {
currentDimension = value;
PersistentSettings.setDimension(currentDimension);

// Note: here we are loading the same world again just to load the
// next dimension. However, this is a very ugly way to handle dimension
// switching and it would probably be much better to just create a fresh
// world instance to load.
loadWorld(world.getWorldDirectory());
if (value != currentDimensionId) {
currentDimensionId = value;
PersistentSettings.setDimension(currentDimensionId);

loadDimension();
viewUpdated(mapView.getMapView()); // update visible chunks immediately
}
}

/** Get the currently loaded dimension. */
public int getDimension() {
return currentDimension;
return currentDimensionId;
}
}
14 changes: 8 additions & 6 deletions chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -815,12 +815,14 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec

BiomeStructure.Factory biomeStructureFactory = BiomeStructure.get(this.biomeStructureImplementation);

Dimension dimension = world.currentDimension();

try (TaskTracker.Task task = taskTracker.task("(1/6) Loading regions")) {
task.update(2, 1);

loadedWorld = world;
worldPath = loadedWorld.getWorldDirectory().getAbsolutePath();
worldDimension = world.currentDimension();
worldDimension = world.currentDimensionId();

if (chunksToLoad.isEmpty()) {
return;
Expand All @@ -847,12 +849,12 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
}

for (ChunkPosition region : regions) {
world.getRegion(region).parse(yMin, yMax);
dimension.getRegion(region).parse(yMin, yMax);
}
}

try (TaskTracker.Task task = taskTracker.task("(2/6) Loading entities")) {
entities.loadPlayers(task, world);
entities.loadPlayers(task, dimension);
}

BiomePalette biomePalette = new ArrayBiomePalette();
Expand All @@ -877,7 +879,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> nextChunkDataTask = executor.submit(() -> { //Initialise first chunk data for the for loop
world.getChunk(chunkPositions[0]).getChunkData(loadingChunkData, palette, biomePalette, yMin, yMax);
dimension.getChunk(chunkPositions[0]).getChunkData(loadingChunkData, palette, biomePalette, yMin, yMax);
return null; // runnable can't throw non-RuntimeExceptions, so we use a callable instead and have to return something
});
for (int i = 0; i < chunkPositions.length; i++) {
Expand Down Expand Up @@ -909,7 +911,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec
if (i + 1 < chunkPositions.length) { // schedule next task if possible
final int finalI = i;
nextChunkDataTask = executor.submit(() -> { //request chunk data for the next iteration of the loop
world.getChunk(chunkPositions[finalI + 1]).getChunkData(loadingChunkData, palette, biomePalette, yMin, yMax);
dimension.getChunk(chunkPositions[finalI + 1]).getChunkData(loadingChunkData, palette, biomePalette, yMin, yMax);
return null; // runnable can't throw non-RuntimeExceptions, so we use a callable instead and have to return something
});
}
Expand Down Expand Up @@ -1202,7 +1204,7 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec

if (!chunkData.isEmpty()){
nonEmptyChunks.add(cp);
if (world.getChunk(cp).getVersion() == ChunkVersion.PRE_FLATTENING) {
if (dimension.getChunk(cp).getVersion() == ChunkVersion.PRE_FLATTENING) {
legacyChunks.add(cp);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import se.llbit.chunky.entity.Entity;
import se.llbit.chunky.entity.PaintingEntity;
import se.llbit.chunky.entity.PlayerEntity;
import se.llbit.chunky.world.World;
import se.llbit.chunky.world.Dimension;
import se.llbit.json.JsonArray;
import se.llbit.json.JsonObject;
import se.llbit.json.JsonValue;
Expand Down Expand Up @@ -99,14 +99,14 @@ public boolean intersect(Ray ray) {
return hit;
}

public void loadPlayers(TaskTracker.Task task, World world) {
public void loadPlayers(TaskTracker.Task task, Dimension dimension) {
entities.clear();
if (actors.isEmpty() && PersistentSettings.getLoadPlayers()) {
// We don't load actor entities if some already exists. Loading actor entities
// risks resetting posed actors when reloading chunks for an existing scene.
actors.clear();
profiles = new HashMap<>();
Collection<PlayerEntity> players = world.playerEntities();
Collection<PlayerEntity> players = dimension.getPlayerEntities();
int done = 1;
int target = players.size();
for (PlayerEntity entity : players) {
Expand Down
52 changes: 25 additions & 27 deletions chunky/src/java/se/llbit/chunky/ui/ChunkMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,20 @@
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.ChunkSelectionTracker;
import se.llbit.chunky.world.ChunkView;
import se.llbit.chunky.world.Dimension;
import se.llbit.chunky.world.Icon;
import se.llbit.chunky.world.PlayerEntityData;
import se.llbit.chunky.world.World;
import se.llbit.chunky.world.listeners.ChunkUpdateListener;
import se.llbit.chunky.world.region.MCRegion;
import se.llbit.log.Log;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
import se.llbit.math.Vector2;
import se.llbit.math.Vector3;
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 @@ -330,8 +329,8 @@ protected synchronized void selectWithinRect() {
// If ctrlModifier to deselect, then do deselect
// If no ctrlModifier, select chunks
// but if they are all already selected, then deselect.
if (ctrlModifier || !chunkSelection.setChunks(mapLoader.getWorld(), x0, z0, x1, z1, true)) //TODO: what the..?
chunkSelection.setChunks(mapLoader.getWorld(), x0, z0, x1, z1, false);
if (ctrlModifier || !chunkSelection.setChunks(mapLoader.getWorld().currentDimension(), x0, z0, x1, z1, true)) //TODO: what the..?
chunkSelection.setChunks(mapLoader.getWorld().currentDimension(), x0, z0, x1, z1, false);
}
}

Expand Down Expand Up @@ -486,7 +485,7 @@ private ChunkPosition getChunk(MouseEvent event) {
int worldBlockZ = cz * Chunk.Z_MAX + bz;
ChunkPosition cp = new ChunkPosition(cx, cz);
if (!mouseDown) {
Chunk hoveredChunk = mapLoader.getWorld().getChunk(cp);
Chunk hoveredChunk = mapLoader.getWorld().currentDimension().getChunk(cp);
if (!hoveredChunk.isEmpty()) {
tooltip.setText(
String.format("%s, %s\nBlock: [%s, %s]\n%d chunks selected", hoveredChunk.toString(), hoveredChunk.biomeAt(bx, bz), worldBlockX, worldBlockZ, chunkSelection.size()));
Expand Down Expand Up @@ -546,9 +545,9 @@ public void onMouseReleased(MouseEvent event) {
int cx = (int) QuickMath.floor(theView.x + (x - getWidth() / 2) / scale);
int cz = (int) QuickMath.floor(theView.z + (y - getHeight() / 2) / scale);
if (theView.scale >= 16) {
chunkSelection.toggleChunk(mapLoader.getWorld(), cx, cz);
chunkSelection.toggleChunk(mapLoader.getWorld().currentDimension(), cx, cz);
} else {
chunkSelection.toggleRegion(mapLoader.getWorld(), cx, cz);
chunkSelection.toggleRegion(mapLoader.getWorld().currentDimension(), cx, cz);
}
}
} else {
Expand Down Expand Up @@ -598,8 +597,8 @@ private void drawPlayers(GraphicsContext gc) {
ChunkView mapView = new ChunkView(view); // Make thread-local copy.
World world = mapLoader.getWorld();
double blockScale = mapView.scale / 16.;
for (PlayerEntityData player : world.getPlayerPositions()) {
if (player.dimension == world.currentDimension()) {
for (PlayerEntityData player : world.currentDimension().getPlayerPositions()) {
if (player.dimension == world.currentDimensionId()) {
int px = (int) QuickMath.floor(player.x * blockScale);
int py = (int) QuickMath.floor(player.y);
int pz = (int) QuickMath.floor(player.z * blockScale);
Expand All @@ -618,19 +617,18 @@ private void drawSpawn(GraphicsContext gc) {
ChunkView mapView = new ChunkView(view); // Make thread-local copy.
World world = mapLoader.getWorld();
double blockScale = mapView.scale / 16.;
if (!world.haveSpawnPos()) {
return;
}
int px = (int) QuickMath.floor(world.spawnPosX() * blockScale);
int py = (int) QuickMath.floor(world.spawnPosY());
int pz = (int) QuickMath.floor(world.spawnPosZ() * 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(8, QuickMath.min(16, blockScale * 2));
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.home.fxImage(), ppx, ppy, pw, pw);
world.currentDimension().getSpawnPosition().ifPresent(spawnPos -> {
int px = (int) QuickMath.floor(spawnPos.x * blockScale);
int py = (int) QuickMath.floor(spawnPos.y);
int pz = (int) QuickMath.floor(spawnPos.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(8, QuickMath.min(16, blockScale * 2));
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.home.fxImage(), ppx, ppy, pw, pw);
});
}

public ChunkView getView() {
Expand Down Expand Up @@ -682,15 +680,15 @@ public void selectVisibleChunks(ChunkView cv, se.llbit.chunky.renderer.scene.Sce
norm[3].cross(corners[0], corners[3]);
norm[3].normalize();

World world = mapLoader.getWorld();
Dimension currentDimension = mapLoader.getWorld().currentDimension();
for (int x = cv.px0; x <= cv.px1; ++x) {
for (int z = cv.pz0; z <= cv.pz1; ++z) {
// Chunk top center position:
Vector3 pos = new Vector3((x + 0.5) * 16, 63, (z + 0.5) * 16);
pos.sub(o);
if (norm[0].dot(pos) > CHUNK_SELECT_RADIUS && norm[1].dot(pos) > CHUNK_SELECT_RADIUS
&& norm[2].dot(pos) > CHUNK_SELECT_RADIUS && norm[3].dot(pos) > CHUNK_SELECT_RADIUS) {
chunkSelection.selectChunk(world, x, z);
chunkSelection.selectChunk(currentDimension, x, z);
}
}
}
Expand Down Expand Up @@ -871,7 +869,7 @@ private MenuItem createSelectChunksInRadiusMenuItem(MapView mapView, ChunkSelect

if (selectionRadiusDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
chunkSelection.setChunkRadius(
mapLoader.getWorld(),
mapLoader.getWorld().currentDimension(),
this.selectionRadiusDialog.getSelectionX(),
this.selectionRadiusDialog.getSelectionZ(),
this.selectionRadiusDialog.getRadius(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,9 @@ public File getSceneFile(String fileName) {
if (!reloaded) {
chunkSelection.clearSelection();
}
world.addChunkDeletionListener(chunkSelection);
Optional<Vector3> playerPos = world.playerPos();
world.addChunkUpdateListener(map);
world.currentDimension().addChunkDeletionListener(chunkSelection);
Optional<Vector3> playerPos = world.currentDimension().getPlayerPos();
world.currentDimension().addChunkUpdateListener(map);

Platform.runLater(
() -> {
Expand Down Expand Up @@ -498,7 +498,7 @@ public File getSceneFile(String fileName) {
boolean track = trackPlayer.get();
PersistentSettings.setFollowPlayer(track);
if (track) {
mapLoader.withWorld(world -> world.playerPos().ifPresent(mapView::panTo));
mapLoader.withWorld(world -> world.currentDimension().getPlayerPos().ifPresent(mapView::panTo));
}
});

Expand Down
Loading

0 comments on commit 4f84bfa

Please sign in to comment.