uk.co.thinkofdeath.thinkcraft.bukkit.world.ChunkManager.java Source code

Java tutorial

Introduction

Here is the source code for uk.co.thinkofdeath.thinkcraft.bukkit.world.ChunkManager.java

Source

/*
 * Copyright 2014 Matthew Collins
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package uk.co.thinkofdeath.thinkcraft.bukkit.world;

import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.util.ReferenceCountUtil;
import org.bukkit.Chunk;
import org.bukkit.ChunkSnapshot;
import org.bukkit.World;
import uk.co.thinkofdeath.thinkcraft.bukkit.ThinkMapPlugin;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.zip.GZIPOutputStream;

public class ChunkManager {

    private final ThinkMapPlugin plugin;
    private final World world;
    private final TLongSet activeChunks = new TLongHashSet();
    private final ReadWriteLock worldLock = new ReentrantReadWriteLock();
    private final ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;

    public ChunkManager(ThinkMapPlugin plugin, World world) {
        this.plugin = plugin;
        this.world = world;
    }

    /**
     * Marks the chunk as active so that the map viewer will
     * grab a live copy of the chunk when the client requests it
     *
     * @param chunk
     *         The chunk to mark as active
     */
    public void activateChunk(Chunk chunk) {
        synchronized (activeChunks) {
            activeChunks.add(chunkKey(chunk.getX(), chunk.getZ()));
        }
    }

    /**
     * Marks the as inactive so the map viewer will use a
     * cached version of the chunk
     *
     * @param chunk
     *         The chunk to mark as inactive
     */
    public void deactivateChunk(Chunk chunk) {
        // Stop the map viewer from requesting live versions
        synchronized (activeChunks) {
            activeChunks.remove(chunkKey(chunk.getX(), chunk.getZ()));
        }
        // Grab a final copy to save to the region file
        final ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, true, false);
        plugin.getServer().getScheduler().runTaskAsynchronously(plugin, new Runnable() {

            @Override
            public void run() {
                try {
                    // Lock the world for writing
                    Lock lock = worldLock.writeLock();
                    lock.lock();

                    File worldFolder = new File(plugin.getWorldDir(), world.getName());
                    if (!worldFolder.exists() && !worldFolder.mkdirs()) {
                        throw new RuntimeException("Failed to create world folder");
                    }

                    ByteBuf data = allocator.buffer();

                    try (RandomAccessFile region = new RandomAccessFile(
                            new File(worldFolder,
                                    String.format("region_%d-%d.dat", snapshot.getX() >> 5, snapshot.getZ() >> 5)),
                            "rw")) {
                        // Save and compress the chunk
                        gzipChunk(snapshot, data);

                        if (region.length() < 4096 * 3) {
                            // Init header with enough space for size + location
                            // with a little bit extra for expansion
                            region.seek(4096 * 3);
                            region.writeByte(0);
                        }
                        int id = ((snapshot.getX() & 0x1F) | ((snapshot.getZ() & 0x1F) << 5));
                        region.seek(8 * id);
                        int offset = region.readInt();
                        int size = region.readInt();
                        if (offset != 0) {
                            // Try and reuse the old space
                            if (data.readableBytes() < ((size / 4096) + 1) * 4096) {
                                size = data.readableBytes();
                                region.seek(8 * id);
                                region.writeInt(offset);
                                region.writeInt(size);
                                region.seek(offset * 4096);
                                byte[] bytes = new byte[data.readableBytes()];
                                data.readBytes(bytes);
                                region.write(bytes);
                                return;
                            }
                        }

                        // Search for a new location

                        // Fill in the used spaces first
                        boolean[] usedSpace = new boolean[(int) ((region.length() / 4096) + 1)];
                        usedSpace[0] = usedSpace[1] = usedSpace[2] = true;
                        for (int i = 0; i < 32 * 32; i++) {
                            if (i == id)
                                continue;
                            region.seek(8 * i);
                            int oo = region.readInt();
                            int os = region.readInt();
                            for (int j = oo; j < oo + ((os / 4096) + 1); j++) {
                                usedSpace[j] = true;
                            }
                        }
                        offset = usedSpace.length;
                        size = data.readableBytes();
                        // Search though every location until a location with a large enough
                        // space is found
                        search: for (int i = 2; i < usedSpace.length; i++) {
                            if (!usedSpace[i]) {
                                for (int j = i + 1; j < i + ((size / 4096) + 1); j++) {
                                    if (j >= usedSpace.length || usedSpace[j]) {
                                        i += ((size / 4096) + 1);
                                        continue search;
                                    }
                                }
                                offset = i;
                                break;
                            }
                        }
                        region.seek(offset * 4096);
                        byte[] bytes = new byte[data.readableBytes()];
                        data.readBytes(bytes);
                        region.write(bytes);
                        region.seek(8 * id);
                        region.writeInt(offset);
                        region.writeInt(size);
                    } finally {
                        lock.unlock();
                        ReferenceCountUtil.release(data);
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    // Reads the chunk data for the location
    private byte[] getChunkData(final int x, final int z) {
        File worldFolder = new File(plugin.getWorldDir(), world.getName());
        Lock lock = worldLock.readLock();
        lock.lock();
        try (RandomAccessFile region = new RandomAccessFile(
                new File(worldFolder, String.format("region_%d-%d.dat", x >> 5, z >> 5)), "r")) {
            if (region.length() < 4096 * 3)
                return null;
            int id = ((x & 0x1F) | ((z & 0x1F) << 5));
            // Read the header
            region.seek(8 * id);
            int offset = region.readInt();
            int size = region.readInt();
            if (offset == 0) { // No entry
                return null;
            }
            region.seek(offset * 4096);
            byte[] data = new byte[size];
            region.readFully(data);
            return data;
        } catch (FileNotFoundException e) {
            return null;
        } catch (IOException e) {
            return null;
        } finally {
            lock.unlock();
        }
    }

    // Gets the gzip'd chunk data for the location and stores it
    // in out, returns false if the chunk wasn't loaded for any reason
    public boolean getChunkBytes(final int x, final int z, ByteBuf out) {
        ChunkSnapshot chunk = null;
        boolean shouldGrabChunk;
        // Check if the chunk is already loaded
        synchronized (activeChunks) {
            shouldGrabChunk = activeChunks.contains(chunkKey(x, z));
        }
        if (shouldGrabChunk) {
            try {
                chunk = plugin.getServer().getScheduler().callSyncMethod(plugin, new Callable<ChunkSnapshot>() {
                    @Override
                    public ChunkSnapshot call() throws Exception {
                        synchronized (activeChunks) {
                            // Double check to prevent a race where a chunk could unload
                            // between the first check and grabbing the chunk
                            if (activeChunks.contains(chunkKey(x, z))) {
                                return world.getChunkAt(x, z).getChunkSnapshot(false, true, false);
                            } else {
                                return null;
                            }
                        }
                    }
                }).get(2, TimeUnit.SECONDS); // Time-out is encase the plugin is disabled
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            } catch (TimeoutException e) {
                plugin.getLogger().warning("Failed to load chunk on time. Time out");
            }
        }
        if (chunk == null) { // Inactive chunk
            byte[] data = getChunkData(x, z);
            if (data == null) {
                return false;
            }
            out.writeBytes(data);
            return true;
        }
        // Active chunk
        gzipChunk(chunk, out);
        return true;
    }

    // Gzips a ChunkSnapshot and stores it in out
    private void gzipChunk(ChunkSnapshot chunk, ByteBuf out) {
        int mask = 0;
        int count = 0;
        for (int i = 0; i < 16; i++) {
            if (!chunk.isSectionEmpty(i)) {
                mask |= 1 << i;
                count++;
            }
        }
        ByteBuf data = allocator.buffer(16 * 16 * 16 * 4 * count + 3 + 256);
        data.writeByte(1); // The chunk exists
        data.writeShort(mask);
        int offset = 0;
        int blockDataOffset = 16 * 16 * 16 * 2 * count;
        int skyDataOffset = blockDataOffset + 16 * 16 * 16 * count;
        for (int i = 0; i < 16; i++) {
            if (!chunk.isSectionEmpty(i)) {
                for (int oy = 0; oy < 16; oy++) {
                    for (int oz = 0; oz < 16; oz++) {
                        for (int ox = 0; ox < 16; ox++) {
                            int y = oy + (i << 4);
                            int id = chunk.getBlockTypeId(ox, y, oz);
                            int dValue = chunk.getBlockData(ox, y, oz);
                            data.setShort((offset << 1) + 3, (id << 4) | dValue);

                            data.setByte(blockDataOffset + offset + 3, chunk.getBlockEmittedLight(ox, y, oz));
                            data.setByte(skyDataOffset + offset + 3, chunk.getBlockSkyLight(ox, y, oz));

                            offset++;
                        }
                    }
                }
            }
        }
        for (int x = 0; x < 16; x++) {
            for (int z = 0; z < 16; z++) {
                data.setByte(skyDataOffset + offset + 3 + x + z * 16, ThinkBiome.bukkitToId(chunk.getBiome(x, z)));
            }
        }
        data.writerIndex(data.capacity());
        try {
            GZIPOutputStream gzip = new GZIPOutputStream(new ByteBufOutputStream(out));
            byte[] bytes = new byte[data.readableBytes()];
            data.readBytes(bytes);
            gzip.write(bytes);
            gzip.close();
        } catch (IOException e) {
            throw new RuntimeException();
        } finally {
            data.release();
        }
    }

    // Used for the activeChunks set
    private static long chunkKey(int x, int z) {
        return ((long) x << 32) | z & 0xFFFFFFFL;
    }
}