From 4f84bfa179ce7d20a42be015fab3add486121448 Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Thu, 10 Aug 2023 08:58:18 +0100 Subject: [PATCH] Split World and Dimension into their own classes (#1611) * 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 --- .../src/java/se/llbit/chunky/map/MapTile.java | 6 +- .../se/llbit/chunky/map/WorldMapLoader.java | 62 +-- .../se/llbit/chunky/renderer/scene/Scene.java | 14 +- .../chunky/renderer/scene/SceneEntities.java | 6 +- .../src/java/se/llbit/chunky/ui/ChunkMap.java | 52 ++- .../ui/controller/ChunkyFxController.java | 8 +- .../src/java/se/llbit/chunky/world/Chunk.java | 30 +- .../chunky/world/ChunkSelectionTracker.java | 42 +- .../{CubicWorld.java => CubicDimension.java} | 63 +-- .../java/se/llbit/chunky/world/Dimension.java | 312 +++++++++++++ .../se/llbit/chunky/world/EmptyChunk.java | 2 +- .../se/llbit/chunky/world/EmptyDimension.java | 16 + .../llbit/chunky/world/EmptyRegionChunk.java | 2 +- .../se/llbit/chunky/world/EmptyWorld.java | 5 +- .../chunky/world/ImposterCubicChunk.java | 25 +- .../src/java/se/llbit/chunky/world/World.java | 422 ++++-------------- .../region/CubicRegionChangeWatcher.java | 51 --- .../world/region/ImposterCubicRegion.java | 26 +- .../llbit/chunky/world/region/MCRegion.java | 24 +- .../world/region/MCRegionChangeWatcher.java | 18 +- .../chunky/world/region/RegionParser.java | 9 +- 21 files changed, 586 insertions(+), 609 deletions(-) rename chunky/src/java/se/llbit/chunky/world/{CubicWorld.java => CubicDimension.java} (61%) create mode 100644 chunky/src/java/se/llbit/chunky/world/Dimension.java create mode 100644 chunky/src/java/se/llbit/chunky/world/EmptyDimension.java delete mode 100644 chunky/src/java/se/llbit/chunky/world/region/CubicRegionChangeWatcher.java diff --git a/chunky/src/java/se/llbit/chunky/map/MapTile.java b/chunky/src/java/se/llbit/chunky/map/MapTile.java index eec2c0fc91..19951a4671 100644 --- a/chunky/src/java/se/llbit/chunky/map/MapTile.java +++ b/chunky/src/java/se/llbit/chunky/map/MapTile.java @@ -62,7 +62,7 @@ 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) { @@ -70,8 +70,8 @@ public void draw(MapBuffer buffer, WorldMapLoader mapLoader, ChunkView view, } } } 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) { diff --git a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java index b64841e4ed..30b8c6359a 100644 --- a/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java +++ b/chunky/src/java/se/llbit/chunky/map/WorldMapLoader.java @@ -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; @@ -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> worldLoadListeners = new ArrayList<>(); @@ -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) { @@ -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 callback) { worldLoadListeners.add(callback); @@ -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(); } @@ -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; } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index 57d2927d46..0fbf095fa5 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -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; @@ -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(); @@ -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++) { @@ -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 }); } @@ -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); } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java b/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java index 73b69e44a1..9921302b83 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/SceneEntities.java @@ -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; @@ -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 players = world.playerEntities(); + Collection players = dimension.getPlayerEntities(); int done = 1; int target = players.size(); for (PlayerEntity entity : players) { diff --git a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java index 8f93151174..fe3f537e04 100644 --- a/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java +++ b/chunky/src/java/se/llbit/chunky/ui/ChunkMap.java @@ -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; @@ -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); } } @@ -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())); @@ -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 { @@ -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); @@ -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() { @@ -682,7 +680,7 @@ 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: @@ -690,7 +688,7 @@ public void selectVisibleChunks(ChunkView cv, se.llbit.chunky.renderer.scene.Sce 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); } } } @@ -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(), diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java index 8f6e6effde..a7b337c340 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/ChunkyFxController.java @@ -455,9 +455,9 @@ public File getSceneFile(String fileName) { if (!reloaded) { chunkSelection.clearSelection(); } - world.addChunkDeletionListener(chunkSelection); - Optional playerPos = world.playerPos(); - world.addChunkUpdateListener(map); + world.currentDimension().addChunkDeletionListener(chunkSelection); + Optional playerPos = world.currentDimension().getPlayerPos(); + world.currentDimension().addChunkUpdateListener(map); Platform.runLater( () -> { @@ -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)); } }); diff --git a/chunky/src/java/se/llbit/chunky/world/Chunk.java b/chunky/src/java/se/llbit/chunky/world/Chunk.java index 5e166d93e6..34a81eec68 100644 --- a/chunky/src/java/se/llbit/chunky/world/Chunk.java +++ b/chunky/src/java/se/llbit/chunky/world/Chunk.java @@ -86,7 +86,7 @@ public class Chunk { protected volatile AbstractLayer surface = IconLayer.UNKNOWN; protected volatile AbstractLayer biomes = IconLayer.UNKNOWN; - private final World world; + private final Dimension dimension; protected int dataTimestamp = 0; protected int surfaceTimestamp = 0; @@ -94,8 +94,8 @@ public class Chunk { protected ChunkVersion version = ChunkVersion.UNKNOWN; - public Chunk(ChunkPosition pos, World world) { - this.world = world; + public Chunk(ChunkPosition pos, Dimension dimension) { + this.dimension = dimension; this.position = pos; } @@ -116,7 +116,7 @@ public int biomeColor() { * @return loaded data, or null if something went wrong */ private Map getChunkTags(Set request) throws ChunkLoadingException { - MCRegion region = (MCRegion) world.getRegion(position.getRegionPosition()); + MCRegion region = (MCRegion) dimension.getRegion(position.getRegionPosition()); Mutable timestamp = new Mutable<>(dataTimestamp); Map chunkTags = region.getChunkTags(this.position, request, timestamp); this.dataTimestamp = timestamp.get(); @@ -128,7 +128,7 @@ private Map getChunkTags(Set request) throws ChunkLoadingEx * @return loaded data, or null if something went wrong */ private Map getEntityTags(Set request) throws ChunkLoadingException { - MCRegion region = (MCRegion) world.getRegion(position.getRegionPosition()); + MCRegion region = (MCRegion) dimension.getRegion(position.getRegionPosition()); return region.getEntityTags(this.position, request); } @@ -179,11 +179,11 @@ public synchronized boolean loadChunk(@NotNull Mutable chunkData, int surfaceTimestamp = dataTimestamp; version = chunkVersion(data); - chunkData.set(this.world.createChunkData(chunkData.get(), data.get(DATAVERSION).intValue())); + chunkData.set(this.dimension.createChunkData(chunkData.get(), data.get(DATAVERSION).intValue())); loadSurface(data, chunkData.get(), yMin, yMax); biomesTimestamp = dataTimestamp; - world.chunkUpdated(position); + dimension.chunkUpdated(position); return true; } @@ -193,7 +193,7 @@ private void loadSurface(@NotNull Tag data, ChunkData chunkData, int yMin, int y return; } - Heightmap heightmap = world.heightmap(); + Heightmap heightmap = dimension.getHeightmap(); Tag sections = getTagFromNames(data, LEVEL_SECTIONS, SECTIONS_POST_21W39A); if (sections.isList()) { if (version == ChunkVersion.PRE_FLATTENING || version == ChunkVersion.POST_FLATTENING) { @@ -208,7 +208,7 @@ private void loadSurface(@NotNull Tag data, ChunkData chunkData, int yMin, int y int[] heightmapData = extractHeightmapData(data, chunkData); updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - surface = new SurfaceLayer(world.currentDimension(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + surface = new SurfaceLayer(dimension.getDimensionId(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); queueTopography(); } } else { @@ -375,7 +375,7 @@ protected boolean shouldReloadChunk() { if (timestamp == 0) { return true; } - Region region = world.getRegion(position.getRegionPosition()); + Region region = dimension.getRegion(position.getRegionPosition()); return region.chunkChangedSince(position, timestamp); } @@ -383,9 +383,9 @@ protected void queueTopography() { for (int x = -1; x <= 1; ++x) { for (int z = -1; z <= 1; ++z) { ChunkPosition pos = new ChunkPosition(position.x + x, position.z + z); - Chunk chunk = world.getChunk(pos); + Chunk chunk = dimension.getChunk(pos); if (!chunk.isEmpty()) { - world.chunkTopographyUpdated(chunk); + dimension.chunkTopographyUpdated(chunk); } } } @@ -430,8 +430,8 @@ public boolean isEmpty() { * Render the topography of this chunk. */ public synchronized void renderTopography() { - surface.renderTopography(position, world.heightmap()); - world.chunkUpdated(position); + surface.renderTopography(position, dimension.getHeightmap()); + dimension.chunkUpdated(position); } /** @@ -462,7 +462,7 @@ public synchronized void getChunkData(@NotNull Mutable reuseChunkData int dataVersion = data.get(DATAVERSION).intValue(); if(reuseChunkData.get() == null || reuseChunkData.get() instanceof EmptyChunkData) { - reuseChunkData.set(world.createChunkData(reuseChunkData.get(), dataVersion)); + reuseChunkData.set(dimension.createChunkData(reuseChunkData.get(), dataVersion)); } else { reuseChunkData.get().clear(); } diff --git a/chunky/src/java/se/llbit/chunky/world/ChunkSelectionTracker.java b/chunky/src/java/se/llbit/chunky/world/ChunkSelectionTracker.java index d0ea93ae1a..8830dd9992 100644 --- a/chunky/src/java/se/llbit/chunky/world/ChunkSelectionTracker.java +++ b/chunky/src/java/se/llbit/chunky/world/ChunkSelectionTracker.java @@ -61,9 +61,9 @@ private boolean setChunk(ChunkPosition pos, boolean selected) { /** * @return Whether the selection changed */ - private boolean setChunk(World world, ChunkPosition pos, boolean selected) { + private boolean setChunk(Dimension dimension, ChunkPosition pos, boolean selected) { //Only need to check if the chunk isn't empty on selecting a chunk, as it must exist if it's already selected - Chunk chunk = world.getChunk(pos); + Chunk chunk = dimension.getChunk(pos); if(selected && (chunk == EmptyRegionChunk.INSTANCE || chunk == EmptyChunk.INSTANCE)) { return false; } @@ -74,7 +74,7 @@ private boolean setChunk(World world, ChunkPosition pos, boolean selected) { * Minimum and maximum are both INCLUSIVE * @return Whether the selection changed */ - private boolean setChunksWithinRegion(World world, ChunkPosition regionPos, int minX, int maxX, int minZ, int maxZ, boolean selected) { + private boolean setChunksWithinRegion(Dimension dimension, ChunkPosition regionPos, int minX, int maxX, int minZ, int maxZ, boolean selected) { BitSet selectedChunksForRegion = selectedChunksByRegion.computeIfAbsent(regionPos.getLong(), p -> new BitSet(MCRegion.CHUNKS_X * MCRegion.CHUNKS_Z)); Collection changedChunks = new ArrayList<>(); @@ -86,7 +86,7 @@ private boolean setChunksWithinRegion(World world, ChunkPosition regionPos, int boolean previousValue = selectedChunksForRegion.get(bitIndex); if(previousValue != selected) { ChunkPosition chunkPos = new ChunkPosition(chunkX, chunkZ); - Chunk chunk = world.getChunk(chunkPos); + Chunk chunk = dimension.getChunk(chunkPos); if(chunk != EmptyRegionChunk.INSTANCE && chunk != EmptyChunk.INSTANCE) { selectionChanged = true; selectedChunksForRegion.set(bitIndex, selected); @@ -111,8 +111,8 @@ private boolean setChunksWithinRegion(World world, ChunkPosition regionPos, int /** * @return Whether the selection changed */ - private boolean setRegion(World world, ChunkPosition regionPos, boolean selected) { - return setChunks(world, regionPos.x << 5, regionPos.z << 5, (regionPos.x << 5) + 31, (regionPos.z << 5) + 31, selected); + private boolean setRegion(Dimension dimension, ChunkPosition regionPos, boolean selected) { + return setChunks(dimension, regionPos.x << 5, regionPos.z << 5, (regionPos.x << 5) + 31, (regionPos.z << 5) + 31, selected); } private boolean isChunkSelected(ChunkPosition pos) { @@ -203,12 +203,12 @@ private void notifyChunkSelectionChange() { * @param cx chunk x-position * @param cz chunk z-position */ - public synchronized void toggleChunk(World world, int cx, int cz) { + public synchronized void toggleChunk(Dimension dimension, int cx, int cz) { ChunkPosition chunk = new ChunkPosition(cx, cz); if (isChunkSelected(chunk)) { - setChunk(world, chunk, false); - } else if (!world.getChunk(chunk).isEmpty()) { - setChunk(world, chunk, true); + setChunk(dimension, chunk, false); + } else if (!dimension.getChunk(chunk).isEmpty()) { + setChunk(dimension, chunk, true); } notifyChunkSelectionChange(); } @@ -219,10 +219,10 @@ public synchronized void toggleChunk(World world, int cx, int cz) { * @param cx chunk x-position * @param cz chunk z-position */ - public synchronized void selectChunk(World world, int cx, int cz) { + public synchronized void selectChunk(Dimension dimension, int cx, int cz) { ChunkPosition chunk = new ChunkPosition(cx, cz); - if (!world.getChunk(chunk).isEmpty()) { - setChunk(world, chunk, true); + if (!dimension.getChunk(chunk).isEmpty()) { + setChunk(dimension, chunk, true); notifyChunkSelectionChange(); } } @@ -233,9 +233,9 @@ public synchronized void selectChunk(World world, int cx, int cz) { * @param cx chunk x-position * @param cz chunk z-position */ - public synchronized void toggleRegion(World world, int cx, int cz) { + public synchronized void toggleRegion(Dimension dimension, int cx, int cz) { ChunkPosition chunk = new ChunkPosition(cx, cz); - setRegion(world, new ChunkPosition(cx >> 5, cz >> 5), !isChunkSelected(chunk)); + setRegion(dimension, new ChunkPosition(cx >> 5, cz >> 5), !isChunkSelected(chunk)); notifyChunkSelectionChange(); } @@ -245,7 +245,7 @@ public synchronized void toggleRegion(World world, int cx, int cz) { * * @return true if anything was changed, false if no chunks were selected. */ - public synchronized boolean setChunks(World world, int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ, boolean selected) { + public synchronized boolean setChunks(Dimension dimension, int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ, boolean selected) { boolean selectionChanged = false; // If selection area must contain complete regions @@ -277,10 +277,10 @@ public synchronized boolean setChunks(World world, int minChunkX, int minChunkZ, for (int regionZ = minRegionZ; regionZ < maxRegionZ + 1; regionZ++) { if(regionX >= minInnerRegionX && regionX < maxInnerRegionX && regionZ >= minInnerRegionZ && regionZ < maxInnerRegionZ) { // this region is an inner region, we set all chunks within it - selectionChanged |= setRegion(world, new ChunkPosition(regionX, regionZ), selected); + selectionChanged |= setRegion(dimension, new ChunkPosition(regionX, regionZ), selected); } else { // this region is an outer region, we set only the chunks within the bounds of the selection area - selectionChanged |= setChunksWithinRegion(world, new ChunkPosition(regionX, regionZ), + selectionChanged |= setChunksWithinRegion(dimension, new ChunkPosition(regionX, regionZ), Math.max(regionX << 5, minChunkX), Math.min(((regionX + 1) << 5) - 1, maxChunkX), Math.max(regionZ << 5, minChunkZ), Math.min(((regionZ + 1) << 5) - 1, maxChunkZ), selected); @@ -297,7 +297,7 @@ public synchronized boolean setChunks(World world, int minChunkX, int minChunkZ, for (int regionX = minRegionX; regionX < maxRegionX + 1; regionX++) { for (int regionZ = minRegionZ; regionZ < maxRegionZ + 1; regionZ++) { - selectionChanged |= setChunksWithinRegion(world, new ChunkPosition(regionX, regionZ), + selectionChanged |= setChunksWithinRegion(dimension, new ChunkPosition(regionX, regionZ), Math.max(regionX << 5, minChunkX), Math.min(((regionX + 1) << 5) - 1, maxChunkX), Math.max(regionZ << 5, minChunkZ), Math.min(((regionZ + 1) << 5) - 1, maxChunkZ), selected); @@ -316,7 +316,7 @@ public synchronized boolean setChunks(World world, int minChunkX, int minChunkZ, * * @return true if anything was changed, false if no chunks were selected. */ - public synchronized boolean setChunkRadius(World world, int centerChunkX, int centerChunkZ, float chunksRadius, boolean selected) { + public synchronized boolean setChunkRadius(Dimension dimension, int centerChunkX, int centerChunkZ, float chunksRadius, boolean selected) { // could be optimised to call setRegion() for regions entirely inside the circle, if necessary boolean selectionChanged = false; @@ -333,7 +333,7 @@ public synchronized boolean setChunkRadius(World world, int centerChunkX, int ce int chunkXOffset = chunkX - centerChunkX; int chunkZOffset = chunkZ - centerChunkZ; if (chunkXOffset * chunkXOffset + chunkZOffset * chunkZOffset < radiusSquared) { - selectionChanged |= setChunk(world, ChunkPosition.get(chunkX, chunkZ), selected); + selectionChanged |= setChunk(dimension, ChunkPosition.get(chunkX, chunkZ), selected); } } } diff --git a/chunky/src/java/se/llbit/chunky/world/CubicWorld.java b/chunky/src/java/se/llbit/chunky/world/CubicDimension.java similarity index 61% rename from chunky/src/java/se/llbit/chunky/world/CubicWorld.java rename to chunky/src/java/se/llbit/chunky/world/CubicDimension.java index cc5e806ce6..117ddf631b 100644 --- a/chunky/src/java/se/llbit/chunky/world/CubicWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/CubicDimension.java @@ -3,52 +3,36 @@ import se.llbit.chunky.chunk.ChunkData; import se.llbit.chunky.chunk.GenericChunkData; import se.llbit.chunky.chunk.biome.BiomeData2d; -import se.llbit.chunky.map.MapView; -import se.llbit.chunky.map.WorldMapLoader; -import se.llbit.chunky.ui.ProgressTracker; -import se.llbit.chunky.world.region.*; -import se.llbit.log.Log; +import se.llbit.chunky.world.region.EmptyRegion; +import se.llbit.chunky.world.region.ImposterCubicRegion; +import se.llbit.chunky.world.region.Region; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; import java.util.Set; import java.util.stream.Stream; import static se.llbit.chunky.world.region.ImposterCubicRegion.blockToCube; import static se.llbit.chunky.world.region.ImposterCubicRegion.cubeToCubicRegion; -public class CubicWorld extends World { +public class CubicDimension extends Dimension { /** - * @param levelName name of the world (not the world directory). - * @param worldDirectory Minecraft world directory. - * @param dimension the dimension to load. - * @param playerEntities - * @param haveSpawnPos - * @param seed + * @param dimensionDirectory Minecraft world directory. * @param timestamp */ - protected CubicWorld(String levelName, File worldDirectory, int dimension, Set playerEntities, - boolean haveSpawnPos, long seed, long timestamp) { - super(levelName, worldDirectory, dimension, playerEntities, haveSpawnPos, seed, timestamp); + protected CubicDimension(World world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + super(world, dimensionId, dimensionDirectory, playerEntities, timestamp); } /** * @return File object pointing to the region file directory */ + @Override public synchronized File getRegionDirectory() { - return new File(getDataDirectory(), "region3d"); - } - - /** - * @return File object pointing to the region file directory for - * the given dimension - */ - protected synchronized File getRegionDirectory(int dimension) { - return new File(getDataDirectory(dimension), "region3d"); + return new File(dimensionDirectory, "region3d"); } @Override @@ -66,26 +50,6 @@ public Region createRegion(ChunkPosition pos) { return new ImposterCubicRegion(pos, this); } - @Override - public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { - return new CubicRegionChangeWatcher(worldMapLoader, mapView); - } - - /** - * @param pos Region position - * @return The region at the given position - */ - public synchronized Region getRegion(ChunkPosition pos) { - return regionMap.computeIfAbsent(pos.getLong(), (p) -> { - // check if the region is present in the world directory - Region region = EmptyRegion.instance; - if (regionExists(pos)) { - region = createRegion(pos); - } - return region; - }); - } - public synchronized Region getRegionWithinRange(ChunkPosition pos, int minY, int maxY) { return regionMap.computeIfAbsent(pos.getLong(), p -> { // check if the region is present in the world directory @@ -145,13 +109,4 @@ public void regionDiscovered(ChunkPosition pos) { regionMap.computeIfAbsent(pos.getLong(), (p) -> createRegion(pos)); } } - - public synchronized void exportChunksToZip(File target, Collection chunks, ProgressTracker progress) - throws IOException { - Log.warn("Not implemented by cubicchunks worlds"); - } - - public synchronized void exportWorldToZip(File target, ProgressTracker progress) throws IOException { - Log.warn("Not implemented by cubicchunks worlds"); - } } diff --git a/chunky/src/java/se/llbit/chunky/world/Dimension.java b/chunky/src/java/se/llbit/chunky/world/Dimension.java new file mode 100644 index 0000000000..6bb5bb7fba --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/Dimension.java @@ -0,0 +1,312 @@ +package se.llbit.chunky.world; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import se.llbit.chunky.PersistentSettings; +import se.llbit.chunky.chunk.ChunkData; +import se.llbit.chunky.chunk.GenericChunkData; +import se.llbit.chunky.chunk.SimpleChunkData; +import se.llbit.chunky.map.MapView; +import se.llbit.chunky.map.WorldMapLoader; +import se.llbit.chunky.entity.PlayerEntity; +import se.llbit.chunky.world.listeners.ChunkDeletionListener; +import se.llbit.chunky.world.listeners.ChunkTopographyListener; +import se.llbit.chunky.world.listeners.ChunkUpdateListener; +import se.llbit.chunky.world.region.*; +import se.llbit.math.Vector3; +import se.llbit.math.Vector3i; +import se.llbit.util.annotation.Nullable; + +import java.io.File; +import java.util.*; + +/** + * + */ +public class Dimension { + private final World world; + + protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); + + protected final File dimensionDirectory; + private Set playerEntities; + + private final Heightmap heightmap = new Heightmap(); + + private final int dimensionId; + + private final Collection chunkDeletionListeners = new LinkedList<>(); + private final Collection chunkTopographyListeners = new LinkedList<>(); + private final Collection chunkUpdateListeners = new LinkedList<>(); + + private Vector3i spawnPos = null; + + /** Timestamp for level.dat when player data was last loaded. */ + private long timestamp; + + /** + * @param dimensionDirectory Minecraft world directory. + * @param timestamp + */ + protected Dimension(World world, int dimensionId, File dimensionDirectory, Set playerEntities, long timestamp) { + this.world = world; + this.dimensionId = dimensionId; + this.dimensionDirectory = dimensionDirectory; + this.playerEntities = playerEntities; + this.timestamp = timestamp; + } + + public int getDimensionId() { + return dimensionId; + } + + /** + * Reload player data. + * @return {@code true} if player data was reloaded. + */ + public synchronized boolean reloadPlayerData() { + return this.world.reloadPlayerData(); + } + + /** Add a chunk deletion listener. */ + public void addChunkDeletionListener(ChunkDeletionListener listener) { + synchronized (chunkDeletionListeners) { + chunkDeletionListeners.add(listener); + } + } + + /** Add a region discovery listener. */ + public void addChunkUpdateListener(ChunkUpdateListener listener) { + synchronized (chunkUpdateListeners) { + chunkUpdateListeners.add(listener); + } + } + + private void fireChunkDeleted(ChunkPosition chunk) { + synchronized (chunkDeletionListeners) { + for (ChunkDeletionListener listener : chunkDeletionListeners) + listener.chunkDeleted(chunk); + } + } + + /** + * @return The chunk at the given position + */ + public synchronized Chunk getChunk(ChunkPosition pos) { + return getRegion(pos.getRegionPosition()).getChunk(pos); + } + + /** + * Returns a ChunkData instance that is compatible with the given chunk version. + * The provided ChunkData instance may or may not be re-used. + */ + public ChunkData createChunkData(@Nullable ChunkData chunkData, int chunkVersion) { + if(chunkVersion >= World.VERSION_21W06A) { + if(chunkData instanceof GenericChunkData) { + return chunkData; + } + return new GenericChunkData(); + } else { + if(chunkData instanceof SimpleChunkData) { + return chunkData; + } + return new SimpleChunkData(); + } + } + + public Region createRegion(ChunkPosition pos) { + return new MCRegion(pos, this); + } + + public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { + return new MCRegionChangeWatcher(worldMapLoader, mapView); + } + + /** + * @param pos Region position + * @return The region at the given position + */ + public synchronized Region getRegion(ChunkPosition pos) { + return regionMap.computeIfAbsent(pos.getLong(), p -> { + // check if the region is present in the world directory + Region region = EmptyRegion.instance; + if (regionExists(pos)) { + region = createRegion(pos); + } + return region; + }); + } + + public Region getRegionWithinRange(ChunkPosition pos, int yMin, int yMax) { + return getRegion(pos); + } + + /** Set the region for the given position. */ + public synchronized void setRegion(ChunkPosition pos, Region region) { + regionMap.put(pos.getLong(), region); + } + + /** + * @param pos region position + * @return {@code true} if a region file exists for the given position + */ + public boolean regionExists(ChunkPosition pos) { + File regionFile = new File(getRegionDirectory(), MCRegion.getFileName(pos)); + return regionFile.exists(); + } + + /** + * @param pos Position of the region to load + * @param minY Minimum block Y (inclusive) + * @param maxY Maximum block Y (exclusive) + * @return Whether the region exists + */ + public boolean regionExistsWithinRange(ChunkPosition pos, int minY, int maxY) { + return this.regionExists(pos); + } + + /** + * Get the data directory for the given dimension. + * + * @return File object pointing to the data directory + */ + protected synchronized File getDimensionDirectory() { + return dimensionDirectory; + } + + /** + * @return File object pointing to the region file directory + */ + public synchronized File getRegionDirectory() { + return new File(getDimensionDirectory(), "region"); + } + + /** + * Get the current player position as an optional vector. + * + *

The result is empty if this is not a single player world. + */ + public synchronized Optional getPlayerPos() { + if (!playerEntities.isEmpty()) { + PlayerEntityData pos = playerEntities.iterator().next(); + return Optional.of(new Vector3(pos.x, pos.y, pos.z)); + } else { + return Optional.empty(); + } + } + + /** + * @return The chunk heightmap + */ + public Heightmap getHeightmap() { + return heightmap; + } + + /** Called when a new region has been discovered by the region parser. */ + public void regionDiscovered(ChunkPosition pos) { + synchronized (this) { + regionMap.computeIfAbsent(pos.getLong(), p -> createRegion(pos)); + } + } + + /** Notify region update listeners. */ + private void fireChunkUpdated(ChunkPosition chunk) { + synchronized (chunkUpdateListeners) { + for (ChunkUpdateListener listener : chunkUpdateListeners) { + listener.chunkUpdated(chunk); + } + } + } + + /** Notify region update listeners. */ + private void fireRegionUpdated(ChunkPosition region) { + synchronized (chunkUpdateListeners) { + for (ChunkUpdateListener listener : chunkUpdateListeners) { + listener.regionUpdated(region); + } + } + } + + @Override public String toString() { + return dimensionDirectory.getName() ; + } + + /** Called when a chunk has been updated. */ + public void chunkUpdated(ChunkPosition chunk) { + fireChunkUpdated(chunk); + } + + /** Called when a chunk has been updated. */ + public void regionUpdated(ChunkPosition region) { + fireRegionUpdated(region); + } + + /** Add a chunk discovery listener */ + public void addChunkTopographyListener(ChunkTopographyListener listener) { + synchronized (chunkTopographyListeners) { + chunkTopographyListeners.add(listener); + } + } + + /** Remove a chunk discovery listener */ + public void removeChunkTopographyListener(ChunkTopographyListener listener) { + synchronized (chunkTopographyListeners) { + chunkTopographyListeners.remove(listener); + } + } + + /** + * Notifies listeners that the height gradient of a chunk may have changed. + * + * @param chunk The chunk + */ + public void chunkTopographyUpdated(Chunk chunk) { + for (ChunkTopographyListener listener : chunkTopographyListeners) { + listener.chunksTopographyUpdated(chunk); + } + } + + public Optional getSpawnPosition() { + return Optional.ofNullable(this.spawnPos); + } + + public void setSpawnPos(@Nullable Vector3i spawnPos) { + this.spawnPos = spawnPos; + } + + /** + * Called when chunks have been deleted from this world. + * Triggers the chunk deletion listeners. + * + * @param pos Position of deleted chunk + */ + public void chunkDeleted(ChunkPosition pos) { + fireChunkDeleted(pos); + } + + public Date getLastModified() { + return new Date(this.dimensionDirectory.lastModified()); + } + + /** + * Load entities from world the file. + * This is usually the single player entity in a local save. + */ + public synchronized Collection getPlayerEntities() { + Collection list = new LinkedList<>(); + if (PersistentSettings.getLoadPlayers()) { + for (PlayerEntityData data : playerEntities) { + list.add(new PlayerEntity(data)); + } + } + return list; + } + + public synchronized Collection getPlayerPositions() { + return Collections.unmodifiableSet(playerEntities); + } + + public synchronized void setPlayerEntities(Set playerEntities) { + this.playerEntities = playerEntities; + } +} diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyChunk.java b/chunky/src/java/se/llbit/chunky/world/EmptyChunk.java index 35829cba77..84ccfaf8d8 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyChunk.java @@ -42,7 +42,7 @@ public class EmptyChunk extends Chunk { } private EmptyChunk() { - super(new ChunkPosition(0, 0), EmptyWorld.INSTANCE); + super(new ChunkPosition(0, 0), EmptyDimension.INSTANCE); surface = IconLayer.CORRUPT; } diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java new file mode 100644 index 0000000000..3cda4f7ec6 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/world/EmptyDimension.java @@ -0,0 +1,16 @@ +package se.llbit.chunky.world; + +import java.util.Collections; + +public class EmptyDimension extends Dimension { + public static final EmptyDimension INSTANCE = new EmptyDimension(); + + private EmptyDimension() { + super(EmptyWorld.INSTANCE, 0, null, Collections.emptySet(), -1); + } + + @Override public String toString() { + return "[empty dimension]"; + } + +} \ No newline at end of file diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java b/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java index a46e95c585..70d5e0b775 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyRegionChunk.java @@ -42,7 +42,7 @@ public class EmptyRegionChunk extends Chunk { } private EmptyRegionChunk() { - super(new ChunkPosition(0, 0), EmptyWorld.INSTANCE); + super(new ChunkPosition(0, 0), EmptyDimension.INSTANCE); surface = IconLayer.CORRUPT; } diff --git a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java index b6abbb6810..b584e4f4b3 100644 --- a/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java +++ b/chunky/src/java/se/llbit/chunky/world/EmptyWorld.java @@ -16,8 +16,6 @@ */ package se.llbit.chunky.world; -import java.util.Collections; - /** * Represents an empty or non-existent world. * @@ -29,7 +27,8 @@ public class EmptyWorld extends World { public static final EmptyWorld INSTANCE = new EmptyWorld(); private EmptyWorld() { - super("[empty world]", null, OVERWORLD_DIMENSION, Collections.emptySet(), false, 0, -1); + super("[empty world]", null, 0, -1); + this.currentDimension = EmptyDimension.INSTANCE; } @Override public String toString() { diff --git a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java index cf1109541b..7ebac64107 100644 --- a/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java +++ b/chunky/src/java/se/llbit/chunky/world/ImposterCubicChunk.java @@ -21,28 +21,25 @@ import java.util.Map; import java.util.Set; -import static se.llbit.chunky.world.World.VERSION_1_12_2; - /** * An implementation of a cube wrapper for pre flattening cubic chunks (1.10, 1.11, 1.12) * * Represents an infinitely tall column of 16x16x16 Cubes */ public class ImposterCubicChunk extends Chunk { - private final CubicWorld world; + private final CubicDimension dimension; - public ImposterCubicChunk(ChunkPosition pos, World world) { - super(pos, world); - assert world instanceof CubicWorld; - this.world = (CubicWorld) world; + public ImposterCubicChunk(ChunkPosition pos, Dimension dimension) { + super(pos, dimension); + assert dimension instanceof CubicDimension; + this.dimension = (CubicDimension) dimension; - assert world.getVersionId() <= VERSION_1_12_2; version = ChunkVersion.PRE_FLATTENING; } private Map> getCubeTags(Set request) { Mutable timestamp = new Mutable<>(dataTimestamp); - ImposterCubicRegion region = (ImposterCubicRegion) world.getRegion(position.getRegionPosition()); + ImposterCubicRegion region = (ImposterCubicRegion) dimension.getRegion(position.getRegionPosition()); Map> cubeTagsInColumn = region.getCubeTagsInColumn(position, request, timestamp); dataTimestamp = timestamp.get(); return cubeTagsInColumn; @@ -70,7 +67,7 @@ public synchronized boolean loadChunk(@NotNull Mutable mutableChunkDa } surfaceTimestamp = dataTimestamp; - mutableChunkData.set(this.world.createChunkData(mutableChunkData.get(), 0)); //chunk version ignored for cubic worlds + mutableChunkData.set(this.dimension.createChunkData(mutableChunkData.get(), 0)); //chunk version ignored for cubic worlds ChunkData chunkData = mutableChunkData.get(); loadSurfaceCubic(data, chunkData, yMin, yMax); biomes = IconLayer.UNKNOWN; @@ -82,7 +79,7 @@ public synchronized boolean loadChunk(@NotNull Mutable mutableChunkDa // } else { // loadBiomes(data, chunkData); // } - world.chunkUpdated(position); + dimension.chunkUpdated(position); return true; } @@ -92,7 +89,7 @@ private void loadSurfaceCubic(Map> data, ChunkData chu return; } - Heightmap heightmap = world.heightmap(); + Heightmap heightmap = dimension.getHeightmap(); BlockPalette palette = new BlockPalette(); BiomePalette biomePalette = new ArrayBiomePalette(); biomePalette.put(Biomes.biomesPrePalette[0]); //We don't currently support cubic chunks biomes, and so default to ocean @@ -113,7 +110,7 @@ private void loadSurfaceCubic(Map> data, ChunkData chu int[] heightmapData = extractHeightmapDataCubic(null, chunkData); updateHeightmap(heightmap, position, chunkData, heightmapData, palette, yMax); - surface = new SurfaceLayer(world.currentDimension(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); + surface = new SurfaceLayer(dimension.getDimensionId(), chunkData, palette, biomePalette, yMin, yMax, heightmapData); } private int[] extractHeightmapDataCubic(Map cubeData, ChunkData chunkData) { @@ -173,7 +170,7 @@ public synchronized void getChunkData(@NotNull Mutable reuseChunkData request.add(LEVEL_TILEENTITIES); Map> data = getCubeTags(request); if(reuseChunkData.get() == null || reuseChunkData.get() instanceof EmptyChunkData) { - reuseChunkData.set(world.createChunkData(reuseChunkData.get(), 0)); + reuseChunkData.set(dimension.createChunkData(reuseChunkData.get(), 0)); } else { reuseChunkData.get().clear(); } diff --git a/chunky/src/java/se/llbit/chunky/world/World.java b/chunky/src/java/se/llbit/chunky/world/World.java index 756afbafff..51c8a4d089 100644 --- a/chunky/src/java/se/llbit/chunky/world/World.java +++ b/chunky/src/java/se/llbit/chunky/world/World.java @@ -16,37 +16,19 @@ */ package se.llbit.chunky.world; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import se.llbit.chunky.PersistentSettings; -import se.llbit.chunky.chunk.ChunkData; -import se.llbit.chunky.chunk.GenericChunkData; -import se.llbit.chunky.chunk.SimpleChunkData; -import se.llbit.chunky.map.MapView; -import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.ui.ProgressTracker; -import se.llbit.chunky.entity.PlayerEntity; -import se.llbit.chunky.world.listeners.ChunkDeletionListener; -import se.llbit.chunky.world.listeners.ChunkTopographyListener; -import se.llbit.chunky.world.listeners.ChunkUpdateListener; -import se.llbit.chunky.world.region.*; +import se.llbit.chunky.world.region.MCRegion; import se.llbit.log.Log; -import se.llbit.math.Vector3; +import se.llbit.math.Vector3i; import se.llbit.nbt.NamedTag; import se.llbit.nbt.Tag; import se.llbit.util.MinecraftText; import se.llbit.util.Pair; -import se.llbit.util.annotation.Nullable; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; +import se.llbit.util.annotation.NotNull; + +import java.io.*; import java.util.*; +import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -79,49 +61,29 @@ public class World implements Comparable { public static final int VERSION_21W06A = 2694; public static final int VERSION_1_12_2 = 1343; - protected final Long2ObjectMap regionMap = new Long2ObjectOpenHashMap<>(); - private final File worldDirectory; - private Set playerEntities; - private final boolean haveSpawnPos; - private int playerDimension = 0; - private final int dimension; - private final Heightmap heightmap = new Heightmap(); + protected Dimension currentDimension; + protected int currentDimensionId; private final String levelName; - - private final Collection chunkDeletionListeners = new LinkedList<>(); - private final Collection chunkTopographyListeners = new LinkedList<>(); - private final Collection chunkUpdateListeners = new LinkedList<>(); - private int spawnX; - private int spawnY; - private int spawnZ; - private int gameMode = 0; + private final long seed; private int versionId; - private final long seed; - /** Timestamp for level.dat when player data was last loaded. */ private long timestamp; /** * @param levelName name of the world (not the world directory). * @param worldDirectory Minecraft world directory. - * @param dimension the dimension to load. - * @param haveSpawnPos * @param seed * @param timestamp */ - protected World(String levelName, File worldDirectory, int dimension, - Set playerEntities, boolean haveSpawnPos, long seed, long timestamp) { + protected World(String levelName, File worldDirectory, long seed, long timestamp) { this.levelName = levelName; this.worldDirectory = worldDirectory; - this.dimension = dimension; - this.playerEntities = playerEntities; - this.haveSpawnPos = haveSpawnPos; this.seed = seed; this.timestamp = timestamp; } @@ -131,12 +93,17 @@ public enum LoggedWarnings { SILENT } + public void loadDimension(int dimensionId) { + currentDimension = loadDimension(this, this.worldDirectory, dimensionId, -1, Collections.emptySet()); + currentDimension.reloadPlayerData(); + } + /** * Parse player location and level name. * * @return {@code true} if the world data was loaded */ - public static World loadWorld(File worldDirectory, int dimension, LoggedWarnings warnings) { + public static World loadWorld(File worldDirectory, int dimensionId, LoggedWarnings warnings) { if (worldDirectory == null) { return EmptyWorld.INSTANCE; } @@ -172,30 +139,21 @@ public static World loadWorld(File worldDirectory, int dimension, LoggedWarnings long seed = randomSeed.longValue(0); - Set playerEntities = new HashSet<>(); - if (!player.isError()) { - playerEntities.add(new PlayerEntityData(player)); - } - loadAdditionalPlayers(worldDirectory, playerEntities); + Set playerEntities = getPlayerEntityData(worldDirectory, dimensionId, player); - boolean haveSpawnPos = !(spawnX.isError() || spawnY.isError() || spawnZ.isError()); + World world = new World(levelName, worldDirectory, seed, modtime); + world.gameMode = gameType.intValue(0); + world.versionId = versionId.intValue(); - World world; - Tag isCubic = result.get(".Data.isCubicWorld"); - if (isCubic != null && isCubic.byteValue(0) == 1) { - world = new CubicWorld(levelName, worldDirectory, dimension, - playerEntities, haveSpawnPos, seed, modtime); - } else { - world = new World(levelName, worldDirectory, dimension, - playerEntities, haveSpawnPos, seed, modtime); + Dimension dimension = loadDimension(world, worldDirectory, dimensionId, modtime, playerEntities); + + boolean haveSpawnPos = !(spawnX.isError() || spawnY.isError() || spawnZ.isError()); + if (haveSpawnPos) { + dimension.setSpawnPos(new Vector3i(spawnX.intValue(0), spawnY.intValue(0), spawnZ.intValue(0))); } - world.spawnX = spawnX.intValue(); - world.spawnY = spawnY.intValue(); - world.spawnZ = spawnZ.intValue(); - world.gameMode = gameType.intValue(0); - world.playerDimension = player.get("Dimension").intValue(); - world.versionId = versionId.intValue(); + world.currentDimension = dimension; + return world; } catch (FileNotFoundException e) { if (warnings == LoggedWarnings.NORMAL) { @@ -209,11 +167,35 @@ public static World loadWorld(File worldDirectory, int dimension, LoggedWarnings return EmptyWorld.INSTANCE; } + @NotNull + private static Dimension loadDimension(World world, File worldDirectory, int dimensionId, long modtime, Set playerEntities) { + Dimension dimension; + File dimensionDirectory = dimensionId == 0 ? worldDirectory : new File(worldDirectory, "DIM" + dimensionId); + if (new File(dimensionDirectory, "region3d").exists()) { + dimension = new CubicDimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); + } else { + dimension = new Dimension(world, dimensionId, dimensionDirectory, playerEntities, modtime); + } + return dimension; + } + + @NotNull + private static Set getPlayerEntityData(File worldDirectory, int dimensionId, Tag player) { + Set playerEntities = new HashSet<>(); + if (!player.isError()) { + playerEntities.add(new PlayerEntityData(player)); + } + loadAdditionalPlayers(worldDirectory, playerEntities); + // Filter for the players only within the requested dimension + playerEntities = playerEntities.stream().filter(playerData -> playerData.dimension == dimensionId).collect(Collectors.toSet()); + return playerEntities; + } + /** - * Reload player data. + * Reload player data for the current dimension. This method is not in Dimension because players are per-world, not per-dimension * @return {@code true} if player data was reloaded. */ - public synchronized boolean reloadPlayerData() { + synchronized boolean reloadPlayerData() { if (worldDirectory == null) { return false; } @@ -224,33 +206,21 @@ public synchronized boolean reloadPlayerData() { } Log.infof("world %s: timestamp updated: reading player data", levelName); timestamp = lastModified; - World temp = loadWorld(worldDirectory, dimension, LoggedWarnings.SILENT); - if (temp != EmptyWorld.INSTANCE) { - // Copy over new player data. - playerEntities = temp.playerEntities; - } - return true; - } -/** Add a chunk deletion listener. */ - public void addChunkDeletionListener(ChunkDeletionListener listener) { - synchronized (chunkDeletionListeners) { - chunkDeletionListeners.add(listener); - } - } - - /** Add a region discovery listener. */ - public void addChunkUpdateListener(ChunkUpdateListener listener) { - synchronized (chunkUpdateListeners) { - chunkUpdateListeners.add(listener); - } - } + try (FileInputStream fin = new FileInputStream(worldFile); + InputStream gzin = new GZIPInputStream(fin); + DataInputStream in = new DataInputStream(gzin)) { + Set request = new HashSet<>(); + request.add(".Data.Player"); + Map result = NamedTag.quickParse(in, request); + Tag player = result.get(".Data.Player"); - private void fireChunkDeleted(ChunkPosition chunk) { - synchronized (chunkDeletionListeners) { - for (ChunkDeletionListener listener : chunkDeletionListeners) - listener.chunkDeleted(chunk); + currentDimension.setPlayerEntities(getPlayerEntityData(worldDirectory, currentDimensionId, player)); + } catch (IOException e) { + Log.infof("Could not read the level.dat file for world %s while trying to reload player data!", levelName); + return false; } + return true; } private static void loadAdditionalPlayers(File worldDirectory, Set playerEntities) { @@ -275,183 +245,45 @@ private static void loadPlayerData(File playerdata, Set player } /** - * @return The chunk at the given position + * @return The current dimension */ - public synchronized Chunk getChunk(ChunkPosition pos) { - return getRegion(pos.getRegionPosition()).getChunk(pos); + public synchronized Dimension currentDimension() { + return this.currentDimension; } /** - * Returns a ChunkData instance that is compatible with the given chunk version. - * The provided ChunkData instance may or may not be re-used. - */ - public ChunkData createChunkData(@Nullable ChunkData chunkData, int chunkVersion) { - if(chunkVersion >= World.VERSION_21W06A) { - if(chunkData instanceof GenericChunkData) { - return chunkData; - } - return new GenericChunkData(); - } else { - if(chunkData instanceof SimpleChunkData) { - return chunkData; - } - return new SimpleChunkData(); - } - } - - public Region createRegion(ChunkPosition pos) { - return new MCRegion(pos, this); - } - - public RegionChangeWatcher createRegionChangeWatcher(WorldMapLoader worldMapLoader, MapView mapView) { - return new MCRegionChangeWatcher(worldMapLoader, mapView); - } - - /** - * @param pos Region position - * @return The region at the given position + * @return The current dimension */ - public synchronized Region getRegion(ChunkPosition pos) { - return regionMap.computeIfAbsent(pos.getLong(), p -> { - // check if the region is present in the world directory - Region region = EmptyRegion.instance; - if (regionExists(pos)) { - region = createRegion(pos); - } - return region; - }); - } - - public Region getRegionWithinRange(ChunkPosition pos, int yMin, int yMax) { - return getRegion(pos); - } - - /** Set the region for the given position. */ - public synchronized void setRegion(ChunkPosition pos, Region region) { - regionMap.put(pos.getLong(), region); + public synchronized int currentDimensionId() { + return this.currentDimensionId; } - /** - * @param pos region position - * @return {@code true} if a region file exists for the given position - */ - public boolean regionExists(ChunkPosition pos) { - File regionFile = new File(getRegionDirectory(), MCRegion.getFileName(pos)); - return regionFile.exists(); - } /** - * @param pos Position of the region to load - * @param minY Minimum block Y (inclusive) - * @param maxY Maximum block Y (exclusive) - * @return Whether the region exists + * @return The world directory */ - public boolean regionExistsWithinRange(ChunkPosition pos, int minY, int maxY) { - return this.regionExists(pos); + public File getWorldDirectory() { + return worldDirectory; } /** - * Get the data directory for the given dimension. - * - * @param dimension the dimension - * @return File object pointing to the data directory + * @deprecated Use {@link World#currentDimension()} -> {@link Dimension#getDimensionDirectory()} ()}. Removed once there are no more usages */ + @Deprecated protected synchronized File getDataDirectory(int dimension) { return dimension == 0 ? - worldDirectory : - new File(worldDirectory, "DIM" + dimension); + worldDirectory : + new File(worldDirectory, "DIM" + dimension); } /** - * Get the data directory for the current dimension - * - * @return File object pointing to the data directory - */ - public synchronized File getDataDirectory() { - return getDataDirectory(dimension); - } - - /** - * @return File object pointing to the region file directory - */ - public synchronized File getRegionDirectory() { - return new File(getDataDirectory(), "region"); - } - - /** - * @return File object pointing to the region file directory for - * the given dimension + @deprecated Use {@link World#currentDimension()} -> {@link Dimension#getRegionDirectory()}. Removed once there are no more usages */ + @Deprecated protected synchronized File getRegionDirectory(int dimension) { return new File(getDataDirectory(dimension), "region"); } - /** - * Get the current player position as an optional vector. - * - *

The result is empty if this is not a single player world. - */ - public synchronized Optional playerPos() { - if (!playerEntities.isEmpty() && playerDimension == dimension) { - PlayerEntityData pos = playerEntities.iterator().next(); - return Optional.of(new Vector3(pos.x, pos.y, pos.z)); - } else { - return Optional.empty(); - } - } - - /** - * @return true if there is spawn position information - */ - public synchronized boolean haveSpawnPos() { - return haveSpawnPos && playerDimension == 0; - } - - /** - * @return The current dimension - */ - public synchronized int currentDimension() { - return dimension; - } - - /** - * @return The chunk heightmap - */ - public Heightmap heightmap() { - return heightmap; - } - - /** - * @return The world director - */ - public File getWorldDirectory() { - return worldDirectory; - } - - /** Called when a new region has been discovered by the region parser. */ - public void regionDiscovered(ChunkPosition pos) { - synchronized (this) { - regionMap.computeIfAbsent(pos.getLong(), p -> createRegion(pos)); - } - } - - /** Notify region update listeners. */ - private void fireChunkUpdated(ChunkPosition chunk) { - synchronized (chunkUpdateListeners) { - for (ChunkUpdateListener listener : chunkUpdateListeners) { - listener.chunkUpdated(chunk); - } - } - } - - /** Notify region update listeners. */ - private void fireRegionUpdated(ChunkPosition region) { - synchronized (chunkUpdateListeners) { - for (ChunkUpdateListener listener : chunkUpdateListeners) { - listener.regionUpdated(region); - } - } - } /** * Export the given chunks to a Zip archive. @@ -461,7 +293,7 @@ private void fireRegionUpdated(ChunkPosition region) { * @throws IOException */ public synchronized void exportChunksToZip(File target, Collection chunks, - ProgressTracker progress) throws IOException { + ProgressTracker progress) throws IOException { Map> regionMap = new HashMap<>(); @@ -475,7 +307,8 @@ public synchronized void exportChunksToZip(File target, Collection> regions = new LinkedList<>(); - regions.clear(); WorldScanner.Operator operator = (regionDirectory, x, z) -> regions.add(new Pair<>(regionDirectory, new ChunkPosition(x, z))); @@ -581,62 +413,6 @@ public String levelName() { return levelName; } - /** Called when a chunk has been updated. */ - public void chunkUpdated(ChunkPosition chunk) { - fireChunkUpdated(chunk); - } - - /** Called when a chunk has been updated. */ - public void regionUpdated(ChunkPosition region) { - fireRegionUpdated(region); - } - - /** Add a chunk discovery listener */ - public void addChunkTopographyListener(ChunkTopographyListener listener) { - synchronized (chunkTopographyListeners) { - chunkTopographyListeners.add(listener); - } - } - - /** Remove a chunk discovery listener */ - public void removeChunkTopographyListener(ChunkTopographyListener listener) { - synchronized (chunkTopographyListeners) { - chunkTopographyListeners.remove(listener); - } - } - - /** - * Notifies listeners that the height gradient of a chunk may have changed. - * - * @param chunk The chunk - */ - public void chunkTopographyUpdated(Chunk chunk) { - for (ChunkTopographyListener listener : chunkTopographyListeners) { - listener.chunksTopographyUpdated(chunk); - } - } - - /** - * @return The spawn Z position - */ - public double spawnPosZ() { - return spawnZ; - } - - /** - * @return The spawn Y position - */ - public double spawnPosY() { - return spawnY; - } - - /** - * @return The spawn X position - */ - public double spawnPosX() { - return spawnX; - } - public int getVersionId() { return versionId; } @@ -653,16 +429,6 @@ public static boolean isWorldDir(File worldDir) { return false; } - /** - * Called when chunks have been deleted from this world. - * Triggers the chunk deletion listeners. - * - * @param pos Position of deleted chunk - */ - public void chunkDeleted(ChunkPosition pos) { - fireChunkDeleted(pos); - } - /** * @return String describing the game-mode of this world */ @@ -692,24 +458,6 @@ public Date getLastModified() { return new Date(this.worldDirectory.lastModified()); } - /** - * Load entities from world the file. - * This is usually the single player entity in a local save. - */ - public synchronized Collection playerEntities() { - Collection list = new LinkedList<>(); - if (PersistentSettings.getLoadPlayers()) { - for (PlayerEntityData data : playerEntities) { - list.add(new PlayerEntity(data)); - } - } - return list; - } - - public synchronized Collection getPlayerPositions() { - return Collections.unmodifiableSet(playerEntities); - } - /** * Get the resource pack that is bundled with this world, i.e. the contained resourced directory or resources.zip. * diff --git a/chunky/src/java/se/llbit/chunky/world/region/CubicRegionChangeWatcher.java b/chunky/src/java/se/llbit/chunky/world/region/CubicRegionChangeWatcher.java deleted file mode 100644 index 47902b9bd1..0000000000 --- a/chunky/src/java/se/llbit/chunky/world/region/CubicRegionChangeWatcher.java +++ /dev/null @@ -1,51 +0,0 @@ -package se.llbit.chunky.world.region; - -import javafx.application.Platform; -import se.llbit.chunky.PersistentSettings; -import se.llbit.chunky.map.MapView; -import se.llbit.chunky.map.WorldMapLoader; -import se.llbit.chunky.world.ChunkPosition; -import se.llbit.chunky.world.ChunkView; -import se.llbit.chunky.world.CubicWorld; - -public class CubicRegionChangeWatcher extends RegionChangeWatcher { - public CubicRegionChangeWatcher(WorldMapLoader loader, MapView mapView) { - super(loader, mapView, "Cubic Region Refresher"); - } - - @Override public void run() { - try { - while (!isInterrupted()) { - sleep(3000); - CubicWorld world = (CubicWorld) mapLoader.getWorld(); - if (world.reloadPlayerData()) { - if (PersistentSettings.getFollowPlayer()) { - Platform.runLater(() -> world.playerPos().ifPresent(mapView::panTo)); - } - } - ChunkView theView = view; - for (int rx = theView.prx0; rx <= theView.prx1; ++rx) { - for (int rz = theView.prz0; rz <= theView.prz1; ++rz) { - Region region = world.getRegionWithinRange(new ChunkPosition(rx, rz), theView.yMin, theView.yMax); - if (region.isEmpty()) { - ChunkPosition pos = new ChunkPosition(rx, rz); - if (world.regionExistsWithinRange(pos, theView.yMin, theView.yMax)) { - region = world.createRegion(pos); - } - world.setRegion(pos, region); - region.parse(theView.yMin, theView.yMax); - world.regionDiscovered(pos); - mapLoader.regionUpdated(pos); - } else if (region.hasChanged()) { - region.parse(theView.yMin, theView.yMax); - ChunkPosition pos = region.getPosition(); - mapLoader.regionUpdated(pos); - } - } - } - } - } catch (InterruptedException e) { - // Interrupted. - } - } -} diff --git a/chunky/src/java/se/llbit/chunky/world/region/ImposterCubicRegion.java b/chunky/src/java/se/llbit/chunky/world/region/ImposterCubicRegion.java index ed02db8c04..72f1b901d6 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/ImposterCubicRegion.java +++ b/chunky/src/java/se/llbit/chunky/world/region/ImposterCubicRegion.java @@ -38,7 +38,7 @@ public class ImposterCubicRegion implements Region { private int minRegionY = Integer.MAX_VALUE; private int maxRegionY = Integer.MIN_VALUE; - private final World world; + private final CubicDimension dimension; /** * Cubes don't have a timestamp, the hacky solution is to average the region timestamps. @@ -50,8 +50,8 @@ public class ImposterCubicRegion implements Region { * One flag per region column */ private final boolean[] anyUpdated = new boolean[DIAMETER_IN_CUBIC_REGIONS*DIAMETER_IN_CUBIC_REGIONS]; - public ImposterCubicRegion(ChunkPosition pos, World world) { - this.world = world; + public ImposterCubicRegion(ChunkPosition pos, CubicDimension dimension) { + this.dimension = dimension; mcRegionPos = pos; min3drPosition = new ChunkPosition(mcRegionToMinCubicRegion(pos.x), mcRegionToMinCubicRegion(pos.z)); for (int z = 0; z < DIAMETER_IN_VANILLA_CHUNKS; ++z) { @@ -176,19 +176,19 @@ public synchronized void parse(int minY, int maxY) { Chunk chunk = getChunk(localX, localZ); if (columnsWithCubes.get(localX + localZ*DIAMETER_IN_VANILLA_CHUNKS)) { if(chunk.isEmpty()) { - chunk = new ImposterCubicChunk(pos, world); + chunk = new ImposterCubicChunk(pos, dimension); setChunk(localX, localZ, chunk); } } else { if (!chunk.isEmpty()) { - world.chunkDeleted(pos); + dimension.chunkDeleted(pos); setChunk(localX, localZ, EmptyChunk.INSTANCE); } } } } - world.regionUpdated(mcRegionPos); + dimension.regionUpdated(mcRegionPos); } /** @@ -203,7 +203,7 @@ private void discoverRegionsInRange(int minRegionY, int maxRegionY) { for (int localZ = 0; localZ < DIAMETER_IN_CUBIC_REGIONS; localZ++) { for (int localX = 0; localX < DIAMETER_IN_CUBIC_REGIONS; localX++) { String fileName = get3drNameForPos(min3drPosition.x + localX, regionY, min3drPosition.z + localZ); - File regionFile = new File(world.getRegionDirectory(), fileName); + File regionFile = new File(dimension.getRegionDirectory(), fileName); int regionIndex = localX + localZ * DIAMETER_IN_CUBIC_REGIONS; @@ -292,7 +292,7 @@ public synchronized boolean hasChanged() { for (CubicRegion112[] regions : internalRegions.values()) { for (CubicRegion112 region : regions) { if(region != null) { - File file = new File(world.getRegionDirectory(), region.fileName); + File file = new File(dimension.getRegionDirectory(), region.fileName); long lastModified = file.lastModified(); if (lastModified != region.regionTimestamp) { return true; @@ -317,13 +317,13 @@ public boolean hasRegionInRangeChanged(int minRegionY, int maxRegionY) { for (int localRegionZ = 0; localRegionZ < DIAMETER_IN_CUBIC_REGIONS; localRegionZ++) { CubicRegion112 region = regions[getRegionIndex(localRegionX, localRegionZ)]; if (region != null) { //check known region - File file = new File(world.getRegionDirectory(), region.fileName); + File file = new File(dimension.getRegionDirectory(), region.fileName); long lastModified = file.lastModified(); if (lastModified != region.regionTimestamp) { return true; } } else { //check unknown region - File file = new File(world.getRegionDirectory(), get3drNameForPos(min3drPosition.x + localRegionX, yPos, min3drPosition.z + localRegionZ)); + File file = new File(dimension.getRegionDirectory(), get3drNameForPos(min3drPosition.x + localRegionX, yPos, min3drPosition.z + localRegionZ)); if(file.exists()) return true; } @@ -332,7 +332,7 @@ public boolean hasRegionInRangeChanged(int minRegionY, int maxRegionY) { } else { //check if any unknown layers exist for (int localRegionX = 0; localRegionX < DIAMETER_IN_CUBIC_REGIONS; localRegionX++) { for (int localRegionZ = 0; localRegionZ < DIAMETER_IN_CUBIC_REGIONS; localRegionZ++) { - File file = new File(world.getRegionDirectory(), get3drNameForPos(min3drPosition.x + localRegionX, yPos, min3drPosition.z + localRegionZ)); + File file = new File(dimension.getRegionDirectory(), get3drNameForPos(min3drPosition.x + localRegionX, yPos, min3drPosition.z + localRegionZ)); if(file.exists()) return true; } @@ -392,7 +392,7 @@ private CubicRegion112(int x, int y, int z, String fileName) { } public void parse() { - File regionFile = new File(world.getRegionDirectory(), fileName); + File regionFile = new File(dimension.getRegionDirectory(), fileName); long modtime = regionFile.lastModified(); if (regionTimestamp == modtime) { return; @@ -424,7 +424,7 @@ public void parse() { public Map> getCubeTagsInColumn(int localX, int localZ, Set request) { Map> tagMapsByLocalY = new HashMap<>(); - try (RandomAccessFile file = new RandomAccessFile(new File(world.getRegionDirectory(), this.fileName), "r")) { + try (RandomAccessFile file = new RandomAccessFile(new File(dimension.getRegionDirectory(), this.fileName), "r")) { long length = file.length(); if (length < SECTOR_SIZE_BYTES) { Log.warn("Missing header in region file!"); diff --git a/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java b/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java index 3fb988d54f..af4bb640d2 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java +++ b/chunky/src/java/se/llbit/chunky/world/region/MCRegion.java @@ -63,7 +63,7 @@ public class MCRegion implements Region { private final Chunk[] chunks = new Chunk[NUM_CHUNKS]; private final ChunkPosition position; - private final World world; + private final Dimension dimension; private final String fileName; private long regionFileTime = 0; private final int[] chunkTimestamps = new int[NUM_CHUNKS]; @@ -80,8 +80,8 @@ private static int getMCAChunkIndex(ChunkPosition chunkPos) { * * @param pos the region position */ - public MCRegion(ChunkPosition pos, World world) { - this.world = world; + public MCRegion(ChunkPosition pos, Dimension dimension) { + this.dimension = dimension; fileName = pos.getMcaName(); position = pos; for (int z = 0; z < CHUNKS_Z; ++z) { @@ -118,7 +118,7 @@ public synchronized void deleteChunk(ChunkPosition chunkPos) { if (!chunk.isEmpty()) { chunk.reset(); setChunk(chunkPos, EmptyChunk.INSTANCE); - world.chunkDeleted(chunkPos); + dimension.chunkDeleted(chunkPos); } } @@ -129,7 +129,7 @@ public synchronized void deleteChunk(ChunkPosition chunkPos) { */ @Override public synchronized void parse(int minY, int maxY) { - File regionFile = new File(world.getRegionDirectory(), fileName); + File regionFile = new File(dimension.getRegionDirectory(), fileName); if (!regionFile.isFile()) { return; } @@ -156,12 +156,12 @@ public synchronized void parse(int minY, int maxY) { int loc = file.readInt(); if (loc != 0) { if (chunk.isEmpty()) { - chunk = new Chunk(pos, world); + chunk = new Chunk(pos, dimension); setChunk(pos, chunk); } } else { if (!chunk.isEmpty()) { - world.chunkDeleted(pos); + dimension.chunkDeleted(pos); } } } @@ -171,7 +171,7 @@ public synchronized void parse(int minY, int maxY) { chunkTimestamps[i] = file.readInt(); } - world.regionUpdated(position); + dimension.regionUpdated(position); } catch (IOException e) { Log.warn("Failed to read region: " + e.getMessage()); } @@ -238,14 +238,14 @@ private static Map parseNbtFromChunkDataSource(ChunkPosition positi */ @NotNull private ChunkDataSource getChunkData(ChunkPosition chunkPos) { - File regionDirectory = world.getRegionDirectory(); + File regionDirectory = dimension.getRegionDirectory(); ChunkDataSource data = getChunkDataSource(chunkPos, regionDirectory); chunkTimestamps[getMCAChunkIndex(chunkPos)] = data.timestamp; return data; } @NotNull private ChunkDataSource getEntityData(ChunkPosition chunkPos) { - File regionDirectory = world.getRegionDirectory(); + File regionDirectory = dimension.getRegionDirectory(); regionDirectory = new File(regionDirectory.getParentFile(), "entities"); return getChunkDataSource(chunkPos, regionDirectory); } @@ -364,7 +364,7 @@ private static ChunkDataSource.CompressionScheme readCompressionScheme(RandomAcc */ public void deleteChunkFromRegion(ChunkPosition chunkPos) { // Just write zero in the entry for the chunk in the location table. - File regionDirectory = world.getRegionDirectory(); + File regionDirectory = dimension.getRegionDirectory(); int x = chunkPos.x & 31; int z = chunkPos.z & 31; File regionFile = new File(regionDirectory, fileName); @@ -436,7 +436,7 @@ public static synchronized void writeRegion(File regionDirectory, ChunkPosition @Override public boolean hasChanged() { - File regionFile = new File(world.getRegionDirectory(), fileName); + File regionFile = new File(dimension.getRegionDirectory(), fileName); return regionFileTime != regionFile.lastModified(); } diff --git a/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java b/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java index b5cdbd25b4..2f2c7a8369 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java +++ b/chunky/src/java/se/llbit/chunky/world/region/MCRegionChangeWatcher.java @@ -22,7 +22,7 @@ import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.world.ChunkPosition; import se.llbit.chunky.world.ChunkView; -import se.llbit.chunky.world.World; +import se.llbit.chunky.world.Dimension; /** * Monitors filesystem for changes to region files. @@ -38,24 +38,24 @@ public MCRegionChangeWatcher(WorldMapLoader loader, MapView mapView) { try { while (!isInterrupted()) { sleep(3000); - World world = mapLoader.getWorld(); - if (world.reloadPlayerData()) { + Dimension dimension = mapLoader.getWorld().currentDimension(); + if (dimension.reloadPlayerData()) { if (PersistentSettings.getFollowPlayer()) { - Platform.runLater(() -> world.playerPos().ifPresent(mapView::panTo)); + Platform.runLater(() -> dimension.getPlayerPos().ifPresent(mapView::panTo)); } } ChunkView theView = view; for (int rx = theView.prx0; rx <= theView.prx1; ++rx) { for (int rz = theView.prz0; rz <= theView.prz1; ++rz) { ChunkPosition pos = new ChunkPosition(rx, rz); - Region region = world.getRegion(pos); + Region region = dimension.getRegionWithinRange(pos, theView.yMin, theView.yMax); if (region.isEmpty()) { - if (world.regionExists(pos)) { - region = world.createRegion(pos); + if (dimension.regionExistsWithinRange(pos, theView.yMin, theView.yMax)) { + region = dimension.createRegion(pos); } - world.setRegion(pos, region); + dimension.setRegion(pos, region); region.parse(theView.yMin, theView.yMax); - world.regionDiscovered(pos); + dimension.regionDiscovered(pos); mapLoader.regionUpdated(pos); } else if (region.hasChanged()) { region.parse(theView.yMin, theView.yMax); diff --git a/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java b/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java index d80f0559ab..299d896acf 100644 --- a/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java +++ b/chunky/src/java/se/llbit/chunky/world/region/RegionParser.java @@ -21,10 +21,7 @@ import se.llbit.chunky.chunk.SimpleChunkData; import se.llbit.chunky.map.MapView; import se.llbit.chunky.map.WorldMapLoader; -import se.llbit.chunky.world.Chunk; -import se.llbit.chunky.world.ChunkPosition; -import se.llbit.chunky.world.ChunkView; -import se.llbit.chunky.world.World; +import se.llbit.chunky.world.*; import se.llbit.log.Log; import se.llbit.util.Mutable; @@ -63,8 +60,8 @@ public RegionParser(WorldMapLoader loader, RegionQueue queue, MapView mapView) { } ChunkView map = mapView.getMapView(); if (map.isRegionVisible(position)) { - World world = mapLoader.getWorld(); - Region region = world.getRegionWithinRange(position, mapView.getYMin(), mapView.getYMax()); + Dimension dimension = mapLoader.getWorld().currentDimension(); + Region region = dimension.getRegionWithinRange(position, mapView.getYMin(), mapView.getYMax()); region.parse(mapView.getYMin(), mapView.getYMax()); Mutable chunkData = new Mutable<>(null); for (Chunk chunk : region) {