/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.regions.file.basic; import com.jcwhatever.nucleus.regions.IRegion; import com.jcwhatever.nucleus.regions.data.RegionChunkSection; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.coords.IChunkCoords; import com.jcwhatever.nucleus.utils.file.BasicByteWriter; import com.jcwhatever.nucleus.utils.file.SerializableBlockEntity; import com.jcwhatever.nucleus.utils.file.SerializableFurnitureEntity; import com.jcwhatever.nucleus.utils.observer.future.FutureSubscriber; import com.jcwhatever.nucleus.utils.observer.future.IFuture; import com.jcwhatever.nucleus.utils.observer.future.IFuture.FutureStatus; import com.jcwhatever.nucleus.utils.performance.queued.Iteration3DTask; import com.jcwhatever.nucleus.utils.performance.queued.QueueProject; import com.jcwhatever.nucleus.utils.performance.queued.TaskConcurrency; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.BlockState; import org.bukkit.entity.Entity; import org.bukkit.plugin.Plugin; import javax.annotation.Nullable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayDeque; import java.util.Queue; /* * Writes a regions chunk section to file. */ public class RegionChunkFileWriter { public static final int SAVE_FILE_VERSION = 4; private final Plugin _plugin; private final IRegion _region; private final World _world; private final ChunkSnapshot _snapshot; private final RegionChunkSection _section; private final Queue<SerializableBlockEntity> _tileEntities; private final Queue<SerializableFurnitureEntity> _entities; private boolean _isSaving; /** * Constructor. * * @param region The region the snapshot is for. * @param coords The coordinates of the chunk to snapshot. */ public RegionChunkFileWriter (IRegion region, IChunkCoords coords) { this(region, coords.getX(), coords.getZ()); } /** * Constructor. * * @param region The region the snapshot is for. * @param chunkX The X coordinates of the chunk. * @param chunkZ The Y coordinates of the chunk. */ public RegionChunkFileWriter (IRegion region, int chunkX, int chunkZ) { if (!region.isDefined()) throw new RuntimeException("Cannot get a snapshot from an undefined region."); //noinspection ConstantConditions Chunk chunk = region.getWorld().getChunkAt(chunkX, chunkZ); // region.getWorld() is not null because region is defined _plugin = region.getPlugin(); _region = region; _world = region.getWorld(); _snapshot = chunk.getChunkSnapshot(); _section = new RegionChunkSection(region, _snapshot); // get tile entities from chunk BlockState[] tileEntities = chunk.getTileEntities(); _tileEntities = new ArrayDeque<>(tileEntities.length); for (BlockState tile : tileEntities) { // make sure the tile entity is contained within the section if (_section.containsBlockCoords(tile.getX(), tile.getY(), tile.getZ())) _tileEntities.add(new SerializableBlockEntity(tile)); } // get entities from chunk Entity[] entities = chunk.getEntities(); _entities = new ArrayDeque<>(25); Location entityLocation = new Location(null, 0, 0, 0); for (Entity entity : entities) { if (!entity.isValid()) continue; if (!SerializableFurnitureEntity.isFurnitureEntity(entity)) continue; entity.getLocation(entityLocation); // make sure the entity is contained within the section if (!_section.containsBlockCoords( entityLocation.getBlockX(), entityLocation.getBlockY(), entityLocation.getBlockZ())) continue; _entities.add(new SerializableFurnitureEntity(entity)); } } /** * Get the region the snapshot is for. */ public IRegion getRegion() { return _region; } /** * Determine if the snapshot is in the process * of saving to a file. */ public boolean isSaving() { return _isSaving; } /** * Save the chunk section snapshot to a file. * * <p>Runs task immediately.</p> * * @param file The file to save to. */ public IFuture saveData(File file) { return saveData(file, null); } /** * Save the chunk section snapshot to a file. * * <p>Runs task immediately if no {@link QueueProject} is provided.</p> * * <p>If a {@link QueueProject} is provided, the task is not run.</p> * * @param file The file to save to. * @param project The optional project to add tasks to. */ public IFuture saveData(File file, @Nullable QueueProject project) { PreCon.notNull(file); boolean runNow = project == null; if (project == null) project = new QueueProject(_plugin); if (isSaving()) return project.cancel("Cannot save region because it is already saving."); _isSaving = true; SaveChunkIterator iterator = new SaveChunkIterator(file, 8192, _section.getStartChunkX(), _section.getStartY(), _section.getStartChunkZ(), _section.getEndChunkX(), _section.getEndY(), _section.getEndChunkZ()); project.addTask(iterator); if (runNow) { project.run(); } return project.getResult().onStatus(new FutureSubscriber() { @Override public void on(FutureStatus status, @Nullable CharSequence message) { _isSaving = false; } }); } /** * Iteration worker for saving a region area within a specified * chunk to a file. */ private final class SaveChunkIterator extends Iteration3DTask { private BasicByteWriter writer; private final File file; public SaveChunkIterator (File file, long segmentSize, int chunkXStart, int yStart, int chunkZStart, int xEnd, int yEnd, int zEnd) { super(_plugin, TaskConcurrency.ASYNC, segmentSize, chunkXStart, yStart, chunkZStart, xEnd, yEnd, zEnd); this.file = file; } /** * Write file header. */ @Override protected void onIterateBegin() { try { writer = new BasicByteWriter(new FileOutputStream(file)); writer.write(SAVE_FILE_VERSION); // write region name writer.write(_region.getName()); // write world name writer.write(_world.getName()); // write section info writer.write(_section); // write chunk volume writer.write(getVolume()); } catch (IOException io) { io.printStackTrace(); fail("IOException while writing region header."); _isSaving = false; } } /** * Save section blocks to file */ @Override public void onIterateItem(int x, int y, int z) { Material type = Material.getMaterial(_snapshot.getBlockTypeId(x, y, z)); int data = _snapshot.getBlockData(x, y, z); int light = _snapshot.getBlockEmittedLight(x, y, z); int skylight = _snapshot.getBlockSkyLight(x, y, z); int ls = (light << 4) | skylight; try { writer.writeSmallString(type.name()); writer.write((short)data); writer.write((byte)ls); } catch (IOException ioe) { ioe.printStackTrace(); fail("IOException while writing region data."); _isSaving = false; } } /** * Write Block Entities and Entities to end of file after block data is * successfully written. */ @Override protected void onPreComplete() { try { // write Block Entities writer.write(_tileEntities.size()); while (!_tileEntities.isEmpty()) { SerializableBlockEntity tileEntity = _tileEntities.remove(); writer.write(tileEntity); } // write Entities writer.write(_entities.size()); while (!_entities.isEmpty()) { SerializableFurnitureEntity entity = _entities.remove(); writer.write(entity); } } catch (IOException e) { e.printStackTrace(); fail("Failed to write block entities and/or entities to file."); } } @Override protected void onEnd() { cleanup(); } private void cleanup() { _isSaving = false; if (writer != null) { try { // close chunk file writer.close(); } catch (IOException io) { io.printStackTrace(); fail("IOException while closing file input stream."); } } } } }