/* Copyright (c) 2010-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.map; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.paint.Color; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.main.ZipExportJob; import se.llbit.chunky.renderer.ChunkViewListener; import se.llbit.chunky.ui.ChunkyFxController; import se.llbit.chunky.ui.MapViewMode; import se.llbit.chunky.ui.ProgressTracker; import se.llbit.chunky.world.Block; import se.llbit.chunky.world.Chunk; import se.llbit.chunky.world.ChunkPosition; import se.llbit.chunky.world.ChunkSelectionTracker; import se.llbit.chunky.world.ChunkTopographyUpdater; import se.llbit.chunky.world.ChunkView; import se.llbit.chunky.world.DeleteChunksJob; import se.llbit.chunky.world.EmptyWorld; import se.llbit.chunky.world.RegionChangeMonitor; import se.llbit.chunky.world.RegionParser; import se.llbit.chunky.world.RegionQueue; import se.llbit.chunky.world.World; import se.llbit.chunky.world.listeners.ChunkTopographyListener; import se.llbit.math.Vector3; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class WorldMapLoader implements ChunkTopographyListener { private final ChunkyFxController controller; private BooleanProperty trackPlayer = new SimpleBooleanProperty(PersistentSettings.getFollowPlayer()); private BooleanProperty trackCamera = new SimpleBooleanProperty(PersistentSettings.getFollowCamera()); private World world = EmptyWorld.instance; private final RegionQueue regionQueue = new RegionQueue(); private final ChunkTopographyUpdater topographyUpdater = new ChunkTopographyUpdater(); private final RegionChangeMonitor refresher = new RegionChangeMonitor(this); private int currentDimension = PersistentSettings.getDimension(); protected ChunkSelectionTracker chunkSelection = new ChunkSelectionTracker(); private BooleanProperty highlightEnabled = new SimpleBooleanProperty(false); private Block highlightBlock = Block.get(Block.DIAMONDORE_ID); private Color highlightColor = Color.CRIMSON; private volatile ObjectProperty<ChunkView> map = new SimpleObjectProperty<>(ChunkView.EMPTY); private volatile ChunkView minimap = ChunkView.EMPTY; int minimapWidth = 100; int minimapHeight = 100; private List<ChunkViewListener> viewListeners = new ArrayList<>(); private List<Runnable> worldLoadListeners = new ArrayList<>(); public WorldMapLoader(ChunkyFxController controller) { this.controller = controller; map.addListener((observable, oldValue, newValue) -> { if (oldValue.layer != newValue.layer) { world.setCurrentLayer(newValue.layer); for (ChunkViewListener listener : viewListeners) { listener.layerChanged(newValue.layer); } } if (newValue.shouldRepaint(oldValue)) { viewUpdated(newValue); } }); trackPlayer.addListener(e -> { boolean track = trackPlayer.get(); PersistentSettings.setFollowPlayer(track); if (track) { panToPlayer(); } }); trackCamera.addListener(e -> { boolean track = trackCamera.get(); PersistentSettings.setFollowCamera(track); if (track) { controller.panToCamera(); } }); highlightEnabled.addListener(e -> { setRenderer(MapViewMode.LAYER); notifyViewUpdated(); }); // Start worker threads. RegionParser[] regionParsers = new RegionParser[3]; for (int i = 0; i < regionParsers.length; ++i) { regionParsers[i] = new RegionParser(this, regionQueue); regionParsers[i].start(); } topographyUpdater.start(); refresher.start(); } public void loadWorld(File worldDir) { if (worldDir != null && World.isWorldDir(worldDir)) { loadWorld(new World(worldDir, false)); } } /** * This is called when a new world is loaded, and when switching to a * different dimension. * * <p>NB: When switching dimension this is called with newWorld = world. */ public synchronized void loadWorld(World newWorld) { // Dispose old world. world.dispose(); newWorld.reload(); newWorld.addChunkUpdateListener(controller); newWorld.addChunkUpdateListener(controller.getMap()); newWorld.addChunkUpdateListener(controller.getMinimap()); chunkSelection.clearSelection(); world = newWorld; world.addChunkDeletionListener(chunkSelection); world.addChunkTopographyListener(this); // Dimension must be set before chunks are loaded. world.setDimension(currentDimension); Vector3 playerPos = world.playerPos(); if (playerPos != null) { panToPlayer(); } else { panTo(0, 0); } File newWorldDir = world.getWorldDirectory(); if (newWorldDir != null) { PersistentSettings.setLastWorld(newWorldDir); } worldLoadListeners.forEach(Runnable::run); } public void addWorldLoadListener(Runnable callback) { worldLoadListeners.add(callback); } /** * Called when the map view has changed. */ public synchronized void viewUpdated(ChunkView mapView) { refresher.setView(mapView); minimap = new ChunkView(mapView.x, mapView.z, minimapWidth, minimapHeight, 1, MapViewMode.BIOMES, mapView.layer); int rx0 = Math.min(minimap.prx0, mapView.prx0); int rx1 = Math.max(minimap.prx1, mapView.prx1); int rz0 = Math.min(minimap.prz0, mapView.prz0); int rz1 = Math.max(minimap.prz1, mapView.prz1); // Enqueue visible regions and chunks. for (int rx = rx0; rx <= rx1; ++rx) { for (int rz = rz0; rz <= rz1; ++rz) { regionQueue.add(ChunkPosition.get(rx, rz)); } } notifyViewUpdated(); } /** * Called when the map has been resized. */ public void setMapSize(int width, int height) { ChunkView mapView = map.get(); if (width != mapView.width || height != mapView.height) { map.set(new ChunkView(mapView.x, mapView.z, width, height, mapView.scale, mapView.renderer, mapView.layer)); } } /** * Called when the minimap has been resized. */ public void setMinimapSize(int width, int height) { if (width != minimapWidth || height != minimapHeight) { minimapWidth = width; minimapHeight = height; viewUpdated(map.get()); } } /** * Move the map view. */ public synchronized void moveView(double dx, double dz) { ChunkView mapView = map.get(); panTo(mapView.x + dx, mapView.z + dz); viewUpdated(map.get()); } /** Set the map view by block coordinates. */ public void panTo(Vector3 pos) { // Convert from block coordinates to chunk coordinates. panTo(pos.x / 16, pos.z / 16); } /** * Move the map view. */ public synchronized void panTo(double x, double z) { ChunkView mapView = map.get(); map.set(new ChunkView(x, z, mapView.width, mapView.height, mapView.scale, mapView.renderer, mapView.layer)); } /** * Set the currently viewed layer. */ public synchronized void setLayer(int layer) { ChunkView mapView = map.get(); map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, mapView.scale, mapView.renderer, layer)); } /** * Select specific chunk. */ public synchronized void selectChunk(int cx, int cz) { chunkSelection.selectChunk(world, cx, cz); } /** * @return The current world */ public World getWorld() { return world; } /** * @return The name of the current world */ public String getWorldName() { return world.levelName(); } /** * @return The current map view */ public ChunkView getMapView() { return map.get(); } /** * @return The map view property. */ public ObjectProperty<ChunkView> getMapViewProperty() { return map; } /** * @return The current minimap view */ public ChunkView getMinimapView() { return minimap; } /** * @return The current map renderer */ public MapViewMode getChunkRenderer() { return map.get().renderer; } /** * @return The chunk selection tracker */ public ChunkSelectionTracker getChunkSelection() { return chunkSelection; } public void panToPlayer() { Vector3 pos = world.playerPos(); if (pos != null) { panTo(pos); } } public void addViewListener(ChunkViewListener listener) { viewListeners.add(listener); } /** * The region was changed. */ public void regionUpdated(ChunkPosition region) { regionQueue.add(region); } public synchronized Vector3 getPosition() { ChunkView mapView = map.get(); return new Vector3(mapView.x, world.currentLayer(), mapView.z); } /** * @return The currently viewed layer */ public synchronized int getLayer() { return world.currentLayer(); } @Override public void chunksTopographyUpdated(Chunk chunk) { topographyUpdater.addChunk(chunk); } /** * Flush all cached chunks and regions, forcing them to be reloaded * for the current world. */ public synchronized void reloadWorld() { world.reload(); notifyViewUpdated(); } /** * Set the current map renderer. */ public synchronized void setRenderer(MapViewMode renderer) { ChunkView mapView = map.get(); if (renderer != mapView.renderer) { map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, mapView.scale, renderer, mapView.layer)); } } /** * Toggle chunk selection. */ public synchronized void toggleChunkSelection(int cx, int cz) { chunkSelection.toggleChunk(world, cx, cz); } /** * Set the current dimension. * * @param value Must be a valid dimension index */ 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); } } /** * @return <code>true</code> if chunks or regions are currently being parsed */ public boolean isLoading() { return !regionQueue.isEmpty(); } /** * Clears the chunk selection. */ public synchronized void clearChunkSelection() { chunkSelection.clearSelection(); } /** * @return The current block scale of the map view */ public int getScale() { return map.get().scale; } /** * Called when the map view has been dragged by the user. */ public void viewDragged(int dx, int dy) { double scale = getScale(); moveView(dx / scale, dy / scale); viewListeners.forEach(ChunkViewListener::viewMoved); } /** * Select chunks within a rectangle. */ public void selectChunks(int cx0, int cx1, int cz0, int cz1) { chunkSelection.selectChunks(world, cx0, cz0, cx1, cz1); } /** * Deselect chunks within a rectangle. */ public void deselectChunks(int cx0, int cx1, int cz0, int cz1) { chunkSelection.deselectChunks(cx0, cz0, cx1, cz1); } /** * Delete the currently selected chunks from the current world. */ public void deleteSelectedChunks(ProgressTracker progress) { Collection<ChunkPosition> selected = chunkSelection.getSelection(); if (!selected.isEmpty() && !progress.isBusy()) { DeleteChunksJob job = new DeleteChunksJob(world, selected, progress); job.start(); } } /** * Export the selected chunks to a zip file. */ public synchronized void exportZip(File targetFile, ProgressTracker progress) { new ZipExportJob(world, chunkSelection.getSelection(), targetFile, progress).start(); } /** * Select the region containing the given chunk. */ public void selectRegion(int cx, int cz) { chunkSelection.selectRegion(world, cx, cz); } /** * Modify the block scale of the map view. */ public synchronized void setScale(int blockScale) { ChunkView mapView = map.get(); blockScale = ChunkView.clampScale(blockScale); if (blockScale != mapView.scale) { map.set(new ChunkView(mapView.x, mapView.z, mapView.width, mapView.height, blockScale, mapView.renderer, mapView.layer)); } } /** * @return The current highlight color */ public Color highlightColor() { return highlightColor; } /** * @return The currently highlighted block type */ public Block highlightBlock() { return highlightBlock; } public BooleanProperty highlightEnabledProperty() { return highlightEnabled; } public boolean highlightEnabled() { return highlightEnabled.get(); } /** * Set a new block type to highlight. */ public void highlightBlock(Block hlBlock) { this.highlightBlock = hlBlock; if (highlightEnabled.get()) { // TODO: make separate highlight update event. notifyViewUpdated(); } } /** * Set a new highlight color. */ public void highlightColor(Color newColor) { highlightColor = newColor; if (highlightEnabled.get()) { // TODO: make separate highlight update event. notifyViewUpdated(); } } /** * Notify view listeners that the view has changed. */ private void notifyViewUpdated() { viewListeners.forEach(ChunkViewListener::viewUpdated); } public int getDimension() { return currentDimension; } public BooleanProperty trackPlayerProperty() { return trackPlayer; } public BooleanProperty trackCameraProperty() { return trackCamera; } public void drawCameraVisualization() { viewListeners.forEach(ChunkViewListener::cameraPositionUpdated); } }