439 lines
14 KiB
Java
439 lines
14 KiB
Java
/*
|
|
* This file is part of BlueMap, licensed under the MIT License (MIT).
|
|
*
|
|
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
|
|
* 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.
|
|
*/
|
|
package de.bluecolored.bluemap.core.mca;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.DataInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.RandomAccessFile;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.ExecutionException;
|
|
|
|
import com.flowpowered.math.vector.Vector2i;
|
|
import com.flowpowered.math.vector.Vector3i;
|
|
import com.google.common.cache.Cache;
|
|
import com.google.common.cache.CacheBuilder;
|
|
import com.google.common.collect.Multimap;
|
|
import com.google.common.collect.MultimapBuilder;
|
|
|
|
import de.bluecolored.bluemap.core.logger.Logger;
|
|
import de.bluecolored.bluemap.core.mca.extensions.BlockStateExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.DoorExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.DoublePlantExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.FireExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.GlassPaneConnectExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.NetherFenceConnectExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.RedstoneExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.SnowyExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.StairShapeExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.TripwireConnectExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.WallConnectExtension;
|
|
import de.bluecolored.bluemap.core.mca.extensions.WoodenFenceConnectExtension;
|
|
import de.bluecolored.bluemap.core.mca.mapping.BiomeIdMapper;
|
|
import de.bluecolored.bluemap.core.mca.mapping.BlockIdMapper;
|
|
import de.bluecolored.bluemap.core.mca.mapping.BlockProperties;
|
|
import de.bluecolored.bluemap.core.mca.mapping.BlockPropertyMapper;
|
|
import de.bluecolored.bluemap.core.mca.mapping.LightData;
|
|
import de.bluecolored.bluemap.core.util.AABB;
|
|
import de.bluecolored.bluemap.core.world.Block;
|
|
import de.bluecolored.bluemap.core.world.BlockState;
|
|
import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException;
|
|
import de.bluecolored.bluemap.core.world.World;
|
|
import de.bluecolored.bluemap.core.world.WorldChunk;
|
|
import net.querz.nbt.CompoundTag;
|
|
import net.querz.nbt.NBTUtil;
|
|
import net.querz.nbt.Tag;
|
|
import net.querz.nbt.mca.CompressionType;
|
|
import net.querz.nbt.mca.MCAUtil;
|
|
|
|
public class MCAWorld implements World {
|
|
|
|
private static final Cache<WorldChunkHash, Chunk> CHUNK_CACHE = CacheBuilder.newBuilder().maximumSize(500).build();
|
|
private static final Multimap<String, BlockStateExtension> BLOCK_STATE_EXTENSIONS = MultimapBuilder.hashKeys().arrayListValues().build();
|
|
|
|
public static final BlockIdMapper DEFAULT_BLOCK_ID_MAPPER;
|
|
public static final BlockPropertyMapper DEFAULT_BLOCK_PROPERTY_MAPPER;
|
|
public static final BiomeIdMapper DEFAULT_BIOME_ID_MAPPER;
|
|
|
|
static {
|
|
try {
|
|
DEFAULT_BLOCK_ID_MAPPER = BlockIdMapper.create();
|
|
DEFAULT_BLOCK_PROPERTY_MAPPER = BlockPropertyMapper.create();
|
|
DEFAULT_BIOME_ID_MAPPER = BiomeIdMapper.create();
|
|
} catch (IOException e) {
|
|
throw new RuntimeException("Failed to load essential resources!", e);
|
|
}
|
|
|
|
registerBlockStateExtension(new SnowyExtension());
|
|
registerBlockStateExtension(new StairShapeExtension());
|
|
registerBlockStateExtension(new FireExtension());
|
|
registerBlockStateExtension(new RedstoneExtension());
|
|
registerBlockStateExtension(new DoorExtension());
|
|
registerBlockStateExtension(new NetherFenceConnectExtension());
|
|
registerBlockStateExtension(new TripwireConnectExtension());
|
|
registerBlockStateExtension(new WallConnectExtension());
|
|
registerBlockStateExtension(new WoodenFenceConnectExtension());
|
|
registerBlockStateExtension(new GlassPaneConnectExtension());
|
|
registerBlockStateExtension(new DoublePlantExtension());
|
|
}
|
|
|
|
private final UUID uuid;
|
|
private final Path worldFolder;
|
|
private String name;
|
|
private AABB boundaries;
|
|
private int seaLevel;
|
|
private Vector3i spawnPoint;
|
|
|
|
private BlockIdMapper blockIdMapper;
|
|
private BlockPropertyMapper blockPropertyMapper;
|
|
private BiomeIdMapper biomeIdMapper;
|
|
|
|
private MCAWorld(
|
|
Path worldFolder,
|
|
UUID uuid,
|
|
String name,
|
|
int worldHeight,
|
|
int seaLevel,
|
|
Vector3i spawnPoint,
|
|
BlockIdMapper blockIdMapper,
|
|
BlockPropertyMapper blockPropertyMapper,
|
|
BiomeIdMapper biomeIdMapper
|
|
) {
|
|
this.uuid = uuid;
|
|
this.worldFolder = worldFolder;
|
|
this.name = name;
|
|
this.boundaries = new AABB(new Vector3i(-10000000, 0, -10000000), new Vector3i(10000000, worldHeight, 10000000));
|
|
this.seaLevel = seaLevel;
|
|
this.spawnPoint = spawnPoint;
|
|
|
|
this.blockIdMapper = blockIdMapper;
|
|
this.blockPropertyMapper = blockPropertyMapper;
|
|
this.biomeIdMapper = biomeIdMapper;
|
|
}
|
|
|
|
public BlockState getBlockState(Vector3i pos) {
|
|
try {
|
|
|
|
Vector2i chunkPos = blockToChunk(pos);
|
|
Chunk chunk = getChunk(chunkPos);
|
|
return chunk.getBlockState(pos);
|
|
|
|
} catch (Exception ex) {
|
|
return BlockState.AIR;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Block getBlock(Vector3i pos) throws ChunkNotGeneratedException {
|
|
try {
|
|
|
|
Vector2i chunkPos = blockToChunk(pos);
|
|
Chunk chunk = getChunk(chunkPos);
|
|
BlockState blockState = getExtendedBlockState(chunk, pos);
|
|
LightData lightData = chunk.getLightData(pos);
|
|
String biome = chunk.getBiomeId(pos);
|
|
BlockProperties properties = blockPropertyMapper.map(blockState);
|
|
return new MCABlock(this, blockState, lightData, biome, properties, pos);
|
|
|
|
} catch (IOException ex) {
|
|
throw new ChunkNotGeneratedException(ex); // to resolve the error, act like the chunk has not been generated yet
|
|
}
|
|
}
|
|
|
|
private BlockState getExtendedBlockState(Chunk chunk, Vector3i pos) throws ChunkNotGeneratedException {
|
|
BlockState blockState = chunk.getBlockState(pos);
|
|
|
|
for (BlockStateExtension ext : BLOCK_STATE_EXTENSIONS.get(blockState.getId())) {
|
|
blockState = ext.extend(this, pos, blockState);
|
|
}
|
|
|
|
return blockState;
|
|
}
|
|
|
|
@Override
|
|
public AABB getBoundaries() {
|
|
return boundaries;
|
|
}
|
|
|
|
@Override
|
|
public WorldChunk getWorldChunk(AABB boundaries) {
|
|
return new MCAWorldChunk(this, boundaries);
|
|
}
|
|
|
|
public Chunk getChunk(Vector2i chunkPos) throws IOException, ChunkNotGeneratedException {
|
|
try {
|
|
Chunk chunk = CHUNK_CACHE.get(new WorldChunkHash(this, chunkPos), () -> this.loadChunk(chunkPos));
|
|
if (!chunk.isGenerated()) throw new ChunkNotGeneratedException();
|
|
return chunk;
|
|
} catch (ExecutionException e) {
|
|
Throwable cause = e.getCause();
|
|
|
|
if (cause instanceof IOException) {
|
|
throw (IOException) cause;
|
|
}
|
|
|
|
else if (cause instanceof ChunkNotGeneratedException) {
|
|
throw (ChunkNotGeneratedException) cause;
|
|
}
|
|
|
|
else throw new IOException(cause);
|
|
}
|
|
}
|
|
|
|
private Chunk loadChunk(Vector2i chunkPos) throws IOException, ChunkNotGeneratedException {
|
|
Vector2i regionPos = chunkToRegion(chunkPos);
|
|
Path regionPath = getMCAFilePath(regionPos);
|
|
|
|
try (RandomAccessFile raf = new RandomAccessFile(regionPath.toFile(), "r")) {
|
|
|
|
int xzChunk = Math.floorMod(chunkPos.getY(), 32) * 32 + Math.floorMod(chunkPos.getX(), 32);
|
|
|
|
raf.seek(xzChunk * 4);
|
|
int offset = raf.read() << 16;
|
|
offset |= (raf.read() & 0xFF) << 8;
|
|
offset |= raf.read() & 0xFF;
|
|
offset *= 4096;
|
|
|
|
int size = raf.readByte() * 4096;
|
|
if (size == 0) throw new ChunkNotGeneratedException();
|
|
|
|
raf.seek(offset + 4); // +4 skip chunk size
|
|
|
|
byte compressionTypeByte = raf.readByte();
|
|
CompressionType compressionType = CompressionType.getFromID(compressionTypeByte);
|
|
if (compressionType == null) {
|
|
throw new IOException("invalid compression type " + compressionTypeByte);
|
|
}
|
|
|
|
DataInputStream dis = new DataInputStream(new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))));
|
|
Tag<?> tag = Tag.deserialize(dis, Tag.DEFAULT_MAX_DEPTH);
|
|
if (tag instanceof CompoundTag) {
|
|
return Chunk.create(this, (CompoundTag) tag);
|
|
} else {
|
|
throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName()));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public boolean isChunkGenerated(Vector2i chunkPos) {
|
|
try {
|
|
getChunk(chunkPos);
|
|
} catch (ChunkNotGeneratedException | IOException e) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public Collection<Vector2i> getChunkList(long modifiedSinceMillis){
|
|
List<Vector2i> chunks = new ArrayList<>(10000);
|
|
|
|
for (File file : getRegionFolder().toFile().listFiles()) {
|
|
if (!file.getName().endsWith(".mca")) continue;
|
|
|
|
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
|
|
|
|
String[] filenameParts = file.getName().split("\\.");
|
|
int rX = Integer.parseInt(filenameParts[1]);
|
|
int rZ = Integer.parseInt(filenameParts[2]);
|
|
|
|
for (int x = 0; x < 32; x++) {
|
|
for (int z = 0; z < 32; z++) {
|
|
int xzChunk = z * 32 + x;
|
|
|
|
raf.seek(xzChunk * 4 + 3);
|
|
int size = raf.readByte() * 4096;
|
|
|
|
if (size == 0) continue;
|
|
|
|
raf.seek(xzChunk * 4 + 4096);
|
|
int timestamp = raf.read() << 24;
|
|
timestamp |= (raf.read() & 0xFF) << 16;
|
|
timestamp |= (raf.read() & 0xFF) << 8;
|
|
timestamp |= raf.read() & 0xFF;
|
|
|
|
if (timestamp >= (modifiedSinceMillis / 1000)) chunks.add(new Vector2i(rX * 32 + x, rZ * 32 + z));
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
Logger.global.logWarning("Failed to read .mca file: " + file.getAbsolutePath() + " (" + ex.toString() + ")");
|
|
}
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return name;
|
|
}
|
|
|
|
@Override
|
|
public UUID getUUID() {
|
|
return uuid;
|
|
}
|
|
|
|
@Override
|
|
public int getSeaLevel() {
|
|
return seaLevel;
|
|
}
|
|
|
|
@Override
|
|
public Vector3i getSpawnPoint() {
|
|
return spawnPoint;
|
|
}
|
|
|
|
public BlockIdMapper getBlockIdMapper() {
|
|
return blockIdMapper;
|
|
}
|
|
|
|
public BlockPropertyMapper getBlockPropertyMapper() {
|
|
return blockPropertyMapper;
|
|
}
|
|
|
|
public BiomeIdMapper getBiomeIdMapper() {
|
|
return biomeIdMapper;
|
|
}
|
|
|
|
public void setBlockIdMapper(BlockIdMapper blockIdMapper) {
|
|
this.blockIdMapper = blockIdMapper;
|
|
}
|
|
|
|
public void setBlockPropertyMapper(BlockPropertyMapper blockPropertyMapper) {
|
|
this.blockPropertyMapper = blockPropertyMapper;
|
|
}
|
|
|
|
public void setBiomeIdMapper(BiomeIdMapper biomeIdMapper) {
|
|
this.biomeIdMapper = biomeIdMapper;
|
|
}
|
|
|
|
public Path getWorldFolder() {
|
|
return worldFolder;
|
|
}
|
|
|
|
private Path getRegionFolder() {
|
|
return worldFolder.resolve("region");
|
|
}
|
|
|
|
private Path getMCAFilePath(Vector2i region) {
|
|
return getRegionFolder().resolve(MCAUtil.createNameFromRegionLocation(region.getX(), region.getY()));
|
|
}
|
|
|
|
public static MCAWorld load(Path worldFolder, UUID uuid) throws IOException {
|
|
try {
|
|
CompoundTag level = (CompoundTag) NBTUtil.readTag(worldFolder.resolve("level.dat").toFile());
|
|
CompoundTag levelData = level.getCompoundTag("Data");
|
|
|
|
String name = levelData.getString("LevelName");
|
|
int worldHeight = 255;
|
|
int seaLevel = 63;
|
|
Vector3i spawnPoint = new Vector3i(
|
|
levelData.getInt("SpawnX"),
|
|
levelData.getInt("SpawnY"),
|
|
levelData.getInt("SpawnZ")
|
|
);
|
|
|
|
return new MCAWorld(
|
|
worldFolder,
|
|
uuid,
|
|
name,
|
|
worldHeight,
|
|
seaLevel,
|
|
spawnPoint,
|
|
DEFAULT_BLOCK_ID_MAPPER,
|
|
DEFAULT_BLOCK_PROPERTY_MAPPER,
|
|
DEFAULT_BIOME_ID_MAPPER
|
|
);
|
|
} catch (ClassCastException | NullPointerException ex) {
|
|
throw new IOException("Invaid level.dat format!", ex);
|
|
}
|
|
}
|
|
|
|
public static Vector2i blockToChunk(Vector3i pos) {
|
|
return new Vector2i(
|
|
MCAUtil.blockToChunk(pos.getX()),
|
|
MCAUtil.blockToChunk(pos.getZ())
|
|
);
|
|
}
|
|
|
|
public static Vector2i blockToRegion(Vector3i pos) {
|
|
return new Vector2i(
|
|
MCAUtil.blockToRegion(pos.getX()),
|
|
MCAUtil.blockToRegion(pos.getZ())
|
|
);
|
|
}
|
|
|
|
public static Vector2i chunkToRegion(Vector2i pos) {
|
|
return new Vector2i(
|
|
MCAUtil.chunkToRegion(pos.getX()),
|
|
MCAUtil.chunkToRegion(pos.getY())
|
|
);
|
|
}
|
|
|
|
public static void registerBlockStateExtension(BlockStateExtension extension) {
|
|
for (String id : extension.getAffectedBlockIds()) {
|
|
BLOCK_STATE_EXTENSIONS.put(id, extension);
|
|
}
|
|
}
|
|
|
|
private static class WorldChunkHash {
|
|
|
|
private final UUID world;
|
|
private final Vector2i chunk;
|
|
|
|
public WorldChunkHash(MCAWorld world, Vector2i chunk) {
|
|
this.world = world.getUUID();
|
|
this.chunk = chunk;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(world, chunk);
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
|
|
if (obj instanceof WorldChunkHash) {
|
|
WorldChunkHash other = (WorldChunkHash) obj;
|
|
return other.chunk.equals(chunk) && world.equals(other.world);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
}
|