/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * 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. */ /* * Copyright (c) 2011-2014 Glowstone - Tad Hardesty * Copyright (c) 2010-2011 Lightstone - Graham Edgecombe * * 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 org.lanternpowered.server.data.io.anvil; import static org.lanternpowered.server.data.io.anvil.RegionFileCache.REGION_AREA; import static org.lanternpowered.server.data.io.anvil.RegionFileCache.REGION_MASK; import static org.lanternpowered.server.data.io.anvil.RegionFileCache.REGION_SIZE; import com.flowpowered.math.vector.Vector3i; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import org.lanternpowered.server.block.tile.LanternTileEntity; import org.lanternpowered.server.data.io.ChunkIOService; import org.lanternpowered.server.data.io.store.ObjectSerializer; import org.lanternpowered.server.data.io.store.ObjectSerializerRegistry; import org.lanternpowered.server.data.persistence.nbt.NbtDataContainerInputStream; import org.lanternpowered.server.data.persistence.nbt.NbtDataContainerOutputStream; import org.lanternpowered.server.data.util.DataQueries; import org.lanternpowered.server.entity.LanternEntity; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.util.NibbleArray; import org.lanternpowered.server.world.LanternWorld; import org.lanternpowered.server.world.chunk.LanternChunk; import org.lanternpowered.server.world.chunk.LanternChunk.ChunkSection; import org.lanternpowered.server.world.chunk.LanternChunk.ChunkSectionSnapshot; import org.spongepowered.api.data.DataContainer; import org.spongepowered.api.data.DataQuery; import org.spongepowered.api.data.DataView; import org.spongepowered.api.data.MemoryDataContainer; import org.spongepowered.api.data.persistence.InvalidDataException; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.world.Location; import org.spongepowered.api.world.storage.ChunkDataStream; import org.spongepowered.api.world.storage.WorldProperties; import java.io.DataInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import javax.annotation.Nullable; public class AnvilChunkIOService implements ChunkIOService { private static final DataQuery VERSION = DataQuery.of("V"); // byte private static final DataQuery LEVEL = DataQuery.of("Level"); // compound private static final DataQuery SECTIONS = DataQuery.of("Sections"); // array private static final DataQuery X = DataQuery.of("xPos"); // int private static final DataQuery Z = DataQuery.of("zPos"); // int private static final DataQuery Y = DataQuery.of("Y"); // byte private static final DataQuery BLOCKS = DataQuery.of("Blocks"); // byte array private static final DataQuery BLOCKS_EXTRA = DataQuery.of("Add"); // (nibble) byte array private static final DataQuery DATA = DataQuery.of("Data"); // (nibble) byte array private static final DataQuery BLOCK_LIGHT = DataQuery.of("BlockLight"); // (nibble) byte array private static final DataQuery SKY_LIGHT = DataQuery.of("SkyLight"); // (nibble) byte array private static final DataQuery TERRAIN_POPULATED = DataQuery.of("TerrainPopulated"); // (boolean) byte private static final DataQuery LIGHT_POPULATED = DataQuery.of("LightPopulated"); // (boolean) byte private static final DataQuery BIOMES = DataQuery.of("Biomes"); // byte array // A extra tag for the biomes to support the custom biomes private static final DataQuery BIOMES_EXTRA = DataQuery.of("BiomesE"); // byte array private static final DataQuery HEIGHT_MAP = DataQuery.of("HeightMap"); // int array private static final DataQuery LAST_UPDATE = DataQuery.of("LastUpdate"); // long private static final DataQuery TRACKER_DATA_TABLE = DataQuery.of("BlockPosTable"); private static final DataQuery TRACKER_BLOCK_POS = DataQuery.of("pos"); private static final DataQuery TRACKER_ENTRY_CREATOR = DataQuery.of("owner"); private static final DataQuery TRACKER_ENTRY_NOTIFIER = DataQuery.of("notifier"); private static final DataQuery TILE_ENTITY_X = DataQuery.of("x"); private static final DataQuery TILE_ENTITY_Y = DataQuery.of("y"); private static final DataQuery TILE_ENTITY_Z = DataQuery.of("z"); private static final DataQuery TILE_ENTITIES = DataQuery.of("TileEntities"); private static final DataQuery INHABITED_TIME = DataQuery.of("InhabitedTime"); private static final DataQuery ENTITIES = DataQuery.of("Entities"); private final LanternWorld world; private final RegionFileCache cache; private final Path baseDir; // TODO: Consider the session.lock file public AnvilChunkIOService(Path baseDir, LanternWorld world) { this.cache = new RegionFileCache(baseDir); this.baseDir = baseDir; this.world = world; } public boolean exists(int x, int z) throws IOException { final RegionFile region = this.cache.getRegionFileByChunk(x, z); final int regionX = x & REGION_MASK; final int regionZ = z & REGION_MASK; return region.hasChunk(regionX, regionZ); } @Override public boolean read(LanternChunk chunk) throws IOException { final int x = chunk.getX(); final int z = chunk.getZ(); final RegionFile region = this.cache.getRegionFileByChunk(x, z); final int regionX = x & REGION_MASK; final int regionZ = z & REGION_MASK; final DataInputStream is = region.getChunkDataInputStream(regionX, regionZ); if (is == null) { return false; } final DataView levelDataView; try (NbtDataContainerInputStream nbt = new NbtDataContainerInputStream(is)) { levelDataView = nbt.read().getView(LEVEL).get(); } // read the vertical sections final List<DataView> sectionList = levelDataView.getViewList(SECTIONS).get(); final ChunkSection[] sections = new ChunkSection[16]; //noinspection unchecked final Short2ObjectOpenHashMap<LanternTileEntity>[] tileEntitySections = new Short2ObjectOpenHashMap[sections.length]; for (DataView sectionTag : sectionList) { final int y = sectionTag.getInt(Y).get(); final byte[] rawTypes = (byte[]) sectionTag.get(BLOCKS).get(); final byte[] extTypes = sectionTag.contains(BLOCKS_EXTRA) ? (byte[]) sectionTag.get(BLOCKS_EXTRA).get() : null; final byte[] data = (byte[]) sectionTag.get(DATA).get(); final byte[] blockLight = (byte[]) sectionTag.get(BLOCK_LIGHT).get(); final byte[] skyLight = (byte[]) sectionTag.get(SKY_LIGHT).get(); final NibbleArray dataArray = new NibbleArray(rawTypes.length, data, true); final NibbleArray extTypesArray = extTypes == null ? null : new NibbleArray(rawTypes.length, extTypes, true); final short[] types = new short[rawTypes.length]; for (int i = 0; i < rawTypes.length; i++) { types[i] = (short) ((extTypesArray == null ? 0 : extTypesArray.get(i)) << 12 | ((rawTypes[i] & 0xff) << 4) | dataArray.get(i)); } tileEntitySections[y] = new Short2ObjectOpenHashMap<>(); sections[y] = new ChunkSection(types, new NibbleArray(rawTypes.length, skyLight, true), new NibbleArray(rawTypes.length, blockLight, true), tileEntitySections[y]); } levelDataView.getViewList(TILE_ENTITIES).ifPresent(tileEntityViews -> { final ObjectSerializer<LanternTileEntity> tileEntitySerializer = ObjectSerializerRegistry.get().get(LanternTileEntity.class).get(); for (DataView tileEntityView : tileEntityViews) { final int tileY = tileEntityView.getInt(TILE_ENTITY_Y).get(); final int section = tileY >> 4; if (tileEntitySections[section] == null) { continue; } final int tileZ = tileEntityView.getInt(TILE_ENTITY_Z).get(); final int tileX = tileEntityView.getInt(TILE_ENTITY_X).get(); try { final LanternTileEntity tileEntity = tileEntitySerializer.deserialize(tileEntityView); tileEntity.setLocation(new Location<>(this.world, tileX, tileY, tileZ)); tileEntity.setValid(true); tileEntitySections[section].put((short) ChunkSection.index(tileX & 0xf, tileY & 0xf, tileZ & 0xf), tileEntity); } catch (InvalidDataException e) { Lantern.getLogger().warn("Error loading tile entity at ({};{};{}) in the chunk ({},{}) in the world {}", tileX & 0xf, tileY & 0xf, tileZ & 0xf, x, z, this.getWorldProperties().getWorldName(), e); } } }); final DataView spongeDataView = levelDataView.getView(DataQueries.SPONGE_DATA).orElse(null); final List<DataView> trackerDataViews = spongeDataView == null ? null : levelDataView.getViewList(TRACKER_DATA_TABLE).orElse(null); //noinspection unchecked final Short2ObjectMap<LanternChunk.TrackerData>[] trackerData = chunk.getTrackerData().getRawObjects(); if (trackerDataViews != null) { for (DataView dataView : trackerDataViews) { final Optional<Short> optIndex = dataView.getShort(TRACKER_BLOCK_POS); if (!optIndex.isPresent()) { continue; } final int creatorId = dataView.getInt(TRACKER_ENTRY_CREATOR).orElse(-1); final int notifierId = dataView.getInt(TRACKER_ENTRY_NOTIFIER).orElse(-1); // index = z << 12 | y << 4 | x int index = optIndex.get() & 0xffff; final int section = (index >> 8) & 0xf; // Convert the index to the section based system // index = y << 8 | z << 4 | x index = ChunkSection.index(index & 0xf, (index >> 4) & 0xf, index >> 12); trackerData[section].put((short) index, new LanternChunk.TrackerData(creatorId, notifierId)); } } // initialize the chunk chunk.initializeSections(sections); chunk.setPopulated(levelDataView.getInt(TERRAIN_POPULATED).orElse(0) > 0); if (levelDataView.contains(BIOMES)) { final byte[] biomes = (byte[]) levelDataView.get(BIOMES).get(); final byte[] biomesExtra = (byte[]) (levelDataView.contains(BIOMES_EXTRA) ? levelDataView.get(BIOMES_EXTRA).get() : null); final short[] newBiomes = new short[biomes.length]; for (int i = 0; i < biomes.length; i++) { newBiomes[i] = (short) ((biomesExtra == null ? 0 : biomesExtra[i]) << 8 | biomes[i]); } chunk.initializeBiomes(newBiomes); } final Object heightMap; if (levelDataView.contains(HEIGHT_MAP) && (heightMap = levelDataView.get(HEIGHT_MAP).get()) instanceof int[]) { chunk.initializeHeightMap((int[]) heightMap); } else { chunk.initializeHeightMap(null); } levelDataView.getLong(INHABITED_TIME).ifPresent(time -> chunk.setInhabitedTime(time.intValue())); chunk.setLightPopulated(levelDataView.getInt(LIGHT_POPULATED).orElse(0) > 0); chunk.initializeLight(); levelDataView.getViewList(ENTITIES).ifPresent(entityViews -> { final ObjectSerializer<LanternEntity> entitySerializer = ObjectSerializerRegistry.get().get(LanternEntity.class).get(); for (DataView entityView : entityViews) { try { final LanternEntity entity = entitySerializer.deserialize(entityView); chunk.addEntity(entity, entity.getPosition().getFloorY() >> 4); } catch (InvalidDataException e) { Lantern.getLogger().warn("Error loading entity in the chunk ({},{}) in the world {}", x, z, this.getWorldProperties().getWorldName(), e); } } }); return true; } @Override public void write(LanternChunk chunk) throws IOException { final int x = chunk.getX(); final int z = chunk.getZ(); final RegionFile region = this.cache.getRegionFileByChunk(x, z); final int regionX = x & REGION_MASK; final int regionZ = z & REGION_MASK; final DataContainer rootView = new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED); final DataView levelDataView = rootView.createView(LEVEL); // Core properties levelDataView.set(VERSION, (byte) 1); levelDataView.set(X, chunk.getX()); levelDataView.set(Z, chunk.getZ()); levelDataView.set(TERRAIN_POPULATED, (byte) (chunk.isPopulated() ? 1 : 0)); levelDataView.set(LIGHT_POPULATED, (byte) (chunk.isLightPopulated() ? 1 : 0)); levelDataView.set(LAST_UPDATE, 0L); levelDataView.set(INHABITED_TIME, chunk.getInhabitedTime()); // Chunk sections final ChunkSectionSnapshot[] sections = chunk.getSectionSnapshots(true); final List<DataView> sectionDataViews = new ArrayList<>(); final List<DataView> tileEntityDataViews = new ArrayList<>(); for (byte i = 0; i < sections.length; ++i) { final ChunkSectionSnapshot section = sections[i]; if (section == null) { continue; } final DataContainer sectionDataView = new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED); sectionDataView.set(Y, i); final byte[] rawTypes = new byte[section.types.length]; final short[] types = section.types; NibbleArray extTypes = null; final NibbleArray data = new NibbleArray(rawTypes.length); for (int j = 0; j < rawTypes.length; j++) { rawTypes[j] = (byte) ((types[j] >> 4) & 0xff); byte extType = (byte) (types[j] >> 12); if (extType != 0) { if (extTypes == null) { extTypes = new NibbleArray(rawTypes.length); } extTypes.set(j, extType); } data.set(j, (byte) (types[j] & 0xf)); } sectionDataView.set(BLOCKS, rawTypes); if (extTypes != null) { sectionDataView.set(BLOCKS_EXTRA, extTypes.getPackedArray()); } sectionDataView.set(DATA, data.getPackedArray()); sectionDataView.set(BLOCK_LIGHT, section.lightFromBlock); final byte[] lightFromSky = section.lightFromSky; if (lightFromSky != null) { sectionDataView.set(SKY_LIGHT, lightFromSky); } sectionDataViews.add(sectionDataView); //noinspection unchecked final ObjectSerializer<LanternTileEntity> tileEntitySerializer = ObjectSerializerRegistry.get().get(LanternTileEntity.class).get(); // Serialize the tile entities for (Short2ObjectMap.Entry<LanternTileEntity> tileEntityEntry : section.tileEntities.short2ObjectEntrySet()) { if (!tileEntityEntry.getValue().isValid()) { continue; } final DataView dataView = tileEntitySerializer.serialize(tileEntityEntry.getValue()); final short pos = tileEntityEntry.getShortKey(); dataView.set(TILE_ENTITY_X, x * 16 + (pos & 0xf)); dataView.set(TILE_ENTITY_Y, (i << 4) | (pos >> 8)); dataView.set(TILE_ENTITY_Z, z * 16 + ((pos >> 4) & 0xf)); tileEntityDataViews.add(dataView); } } levelDataView.set(TILE_ENTITIES, tileEntityDataViews); levelDataView.set(SECTIONS, sectionDataViews); levelDataView.set(HEIGHT_MAP, chunk.getHeightMap()); //noinspection unchecked final Short2ObjectMap<LanternChunk.TrackerData>[] trackerData = chunk.getTrackerData().getRawObjects(); final List<DataView> trackerDataViews = new ArrayList<>(); for (int i = 0; i < trackerData.length; i++) { final Short2ObjectMap<LanternChunk.TrackerData> trackerDataSection = trackerData[i]; for (Short2ObjectMap.Entry<LanternChunk.TrackerData> entry : trackerDataSection.short2ObjectEntrySet()) { // index = y << 8 | z << 4 | x int index = entry.getShortKey() & 0xffff; // Convert the index to the column based system // index = z << 12 | y << 4 | x index = ((index >> 4) & 0xf) << 12 | i << 8 | (index >> 4) & 0xf0 | index & 0xf; final DataView trackerDataView = new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED); trackerDataView.set(TRACKER_BLOCK_POS, (short) index); trackerDataView.set(TRACKER_ENTRY_NOTIFIER, entry.getValue().getNotifierId()); trackerDataView.set(TRACKER_ENTRY_CREATOR, entry.getValue().getCreatorId()); trackerDataViews.add(trackerDataView); } } if (!trackerDataViews.isEmpty()) { levelDataView.createView(DataQueries.SPONGE_DATA).set(TRACKER_DATA_TABLE, trackerDataViews); } final short[] biomes = chunk.getBiomes(); final byte[] biomes0 = new byte[biomes.length]; byte[] biomes1 = null; for (int i = 0; i < biomes.length; i++) { biomes0[i] = (byte) (biomes[i] & 0xff); byte value = (byte) ((biomes[i] >> 4) & 0xff); if (value != 0) { if (biomes1 == null) { biomes1 = new byte[biomes0.length]; } biomes1[i] = value; } } levelDataView.set(BIOMES, biomes0); if (biomes1 != null) { levelDataView.set(BIOMES_EXTRA, biomes1); } //noinspection unchecked final List<LanternEntity> entities = new ArrayList(chunk.getEntities(entity -> !(entity instanceof Player))); final ObjectSerializer<LanternEntity> entitySerializer = ObjectSerializerRegistry.get().get(LanternEntity.class).get(); final List<DataView> entityViews = new ArrayList<>(); for (LanternEntity entity : entities) { if (entity.getRemoveState() == LanternEntity.RemoveState.DESTROYED) { continue; } final DataView entityView = entitySerializer.serialize(entity); entityViews.add(entityView); } levelDataView.set(ENTITIES, entityViews); try (NbtDataContainerOutputStream nbt = new NbtDataContainerOutputStream(region.getChunkDataOutputStream(regionX, regionZ))) { nbt.write(rootView); } } @Override public void unload() throws IOException { this.cache.clear(); } @Override public ChunkDataStream getGeneratedChunks() { return new ChunkDataStream() { // All the region files private Path[] paths; // The current region file that we opened @Nullable private RegionFile region; // The coordinates of the chunk inside the region private int chunkX; private int chunkZ; // The next index of the chunk in the region file private int regionChunkIndex; // The next index that we are in the file array private int regionFileIndex; // Whether the current fields are cached private boolean cached; // Done, no new chunks can be found private boolean done; { // Use the reset to initialize this.reset(); } @Override public DataContainer next() { if (!this.hasNext()) { throw new NoSuchElementException(); } try { final DataInputStream is = this.region.getChunkDataInputStream(this.chunkX, this.chunkZ); final DataContainer data; try (NbtDataContainerInputStream nbt = new NbtDataContainerInputStream(is)) { data = nbt.read(); } this.cached = false; return data; } catch (IOException e) { // This shouldn't happen throw new IllegalStateException(e); } } @Override public boolean hasNext() { // Fast fail if (this.done) { return false; } // Use the cached index if set if (this.cached) { return true; } // Try first to search for more chunks in the current region while (true) { if (this.region != null) { while (++this.regionChunkIndex < REGION_AREA) { this.chunkX = this.regionChunkIndex / REGION_SIZE; this.chunkZ = this.regionChunkIndex % REGION_SIZE; if (this.region.hasChunk(this.chunkX, this.chunkZ)) { this.cached = true; return true; } } } // There no chunk available in the current region, // reset the chunk index for the next one this.regionChunkIndex = -1; // There was no chunk present in the current region, // try the next region if (++this.regionFileIndex >= this.paths.length) { this.region = null; this.done = true; return false; } final Path nextRegionFile = this.paths[this.regionFileIndex]; if (Files.exists(nextRegionFile)) { Matcher matcher = cache.getFilePattern().matcher(nextRegionFile.getFileName().toString()); int regionX = Integer.parseInt(matcher.group(0)); int regionZ = Integer.parseInt(matcher.group(1)); try { this.region = cache.getRegionFile(regionX, regionZ); } catch (IOException e) { Lantern.getLogger().error("Failed to read the region file ({};{}) in the world folder {}", regionX, regionZ, baseDir.getFileName().toString(), e); this.region = null; } } else { this.region = null; } } } @Override public int available() { // TODO: Not sure how we will be able to do this without opening all // the region files throw new UnsupportedOperationException(); } @Override public void reset() { this.paths = cache.getRegionFiles(); this.regionFileIndex = -1; this.regionChunkIndex = -1; this.region = null; this.cached = false; this.done = false; } }; } @Override public CompletableFuture<Boolean> doesChunkExist(final Vector3i chunkCoords) { return Lantern.getScheduler().submitAsyncTask(() -> exists(chunkCoords.getX(), chunkCoords.getZ())); } @Override public CompletableFuture<Optional<DataContainer>> getChunkData(final Vector3i chunkCoords) { return Lantern.getScheduler().submitAsyncTask(() -> { int x = chunkCoords.getX(); int z = chunkCoords.getZ(); RegionFile region = cache.getRegionFileByChunk(x, z); int regionX = x & REGION_MASK; int regionZ = z & REGION_MASK; DataInputStream is = region.getChunkDataInputStream(regionX, regionZ); if (is == null) { return Optional.empty(); } DataContainer data; try (NbtDataContainerInputStream nbt = new NbtDataContainerInputStream(is)) { data = nbt.read(); } return Optional.of(data); }); } @Override public WorldProperties getWorldProperties() { return this.world.getProperties(); } }